Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ node_modules/
dist/
*.zip
.DS_Store

.env

# local working docs (tmp plan)
docs/
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2026 stefw
Copyright (c) 2026 40min (fork: OpenRouter provider, English UI)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
49 changes: 33 additions & 16 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ du texte. L'engagement bait, l'auto-promo, le hors-sujet et le sponsorisé sont
repliés en une barre d'une ligne, et chaque décision est expliquée directement sur
le post.

> Projet indépendant, sans lien avec LinkedIn ni TypeSafe AI. Il te faut ta propre
> clé API TypeSafe.
> **Note fork :** ce fork ajoute [OpenRouter](https://openrouter.ai) comme voie
> d'accès alternative à Jev — même modèle, facturé sur ton compte OpenRouter,
> sans compte TypeSafe.

> Projet indépendant, sans lien avec LinkedIn ni TypeSafe AI. Il te faut ta
> propre clé API pour le fournisseur choisi (TypeSafe direct, ou OpenRouter).

## Ce que ça fait

Expand All @@ -27,8 +31,9 @@ le post.
trop court pour le déclencheur « charger la suite » de LinkedIn ; lkclean le
relance sans déplacer ta position de lecture.
- **Rapide et pas cher** – un appel API par post, toutes les questions évaluées en
parallèle, verdicts mis en cache pour la session. Environ 0,042 $ par million de
tokens en entrée : une session de scroll coûte une fraction de centime.
parallèle, verdicts mis en cache pour la session. Environ 0,042 $ par million
de tokens en entrée (les tokens de sortie sont gratuits) : une session de
scroll coûte une fraction de centime, quel que soit le fournisseur.


<img width="502" height="383" alt="notok" src="https://github.com/user-attachments/assets/6c7a739f-a63e-4953-884e-6dff45736c45" />
Expand All @@ -43,9 +48,11 @@ le post.
LinkedIn (texte dans `[data-testid="expandable-text-box"]`), avec repli sur
l'ancien balisage `data-id="urn:li:activity:…"`. Le post est identifié par un
hash de son texte, LinkedIn n'exposant plus d'identifiant stable.
2. L'auteur et le texte partent au service worker, qui appelle
`POST https://api.typesafe.ai/v1/systemone` avec un *state*
`{ user_interests, post }` et plusieurs questions typées en une seule requête :
2. L'auteur et le texte partent au service worker, qui appelle l'endpoint System
One — `POST https://api.typesafe.ai/v1/systemone` (TypeSafe direct) ou
`POST https://openrouter.ai/api/v1/systemone` (via OpenRouter) — avec un
*state* `{ user_interests, post }` et plusieurs questions typées en une seule
requête :

| Question | Type | Sens |
| --- | --- | --- |
Expand Down Expand Up @@ -78,10 +85,14 @@ npm run build

Puis charge le dossier `dist/` de la même façon.

Clique sur l'icône de l'extension pour ouvrir les réglages : colle ta clé TypeSafe
(depuis [console.typesafe.ai](https://console.typesafe.ai)), liste tes centres
d'intérêt (un par ligne), éventuellement des sujets à bloquer, puis ajuste les
seuils. Recharge LinkedIn.
Clique sur l'icône de l'extension pour ouvrir les réglages : choisis le
fournisseur d'API (**TypeSafe direct** ou **OpenRouter**), colle la clé
correspondante (depuis [console.typesafe.ai](https://console.typesafe.ai) —
`ts-…` — ou [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) —
`sk-or-…`), liste tes centres d'intérêt (un par ligne), éventuellement des
sujets à bloquer, puis ajuste les seuils. Mets l'id du modèle en accord avec le
fournisseur : `jev-latest` (TypeSafe) ou `typesafe/jev-1.14` (OpenRouter).
Recharge LinkedIn.

Après chaque rebuild, clique sur ↻ sur la carte de l'extension dans
`chrome://extensions`, puis recharge l'onglet LinkedIn.
Expand All @@ -90,6 +101,7 @@ Après chaque rebuild, clique sur ↻ sur la carte de l'extension dans

| Réglage | Défaut | Effet |
| --- | --- | --- |
| Fournisseur d'API | TypeSafe direct | D'où Jev est appelé ; le champ de clé correspondant est utilisé |
| Centres d'intérêt | vide | Si vide, seul le bruit est filtré (bait, auto-promo, sponsorisé) |
| Sujets bloqués | vide | Toujours masqués quand Jev est sûr à ≥ 60 % |
| Seuil de bruit | 70 % | Masque si P(bait) ou P(auto-promo) le dépasse |
Expand All @@ -98,13 +110,17 @@ Après chaque rebuild, clique sur ↻ sur la carte de l'extension dans
| Longueur minimale | 40 caractères | Les posts plus courts ne sont jamais évalués |
| Expliquer les posts gardés | oui | La ligne « Gardé par Jev » |
| Pastille d'activité | oui | Le badge flottant Jev |
| Modèle | `jev-latest` | Épingle une version (ex. `jev-1.13.0`) une fois tes seuils réglés |
| Modèle | `jev-latest` | Défaut TypeSafe ; sur OpenRouter, `typesafe/jev-1.14`. Épingle une version une fois tes seuils réglés |

Aussi, l'interface (page de réglages) est en anglais dans ce fork : les libellés
du tableau ci-dessus décrivent l'effet, pas le libellé exact.

## Confidentialité

- Le nom de l'auteur et le texte de chaque post affiché dans ton fil sont envoyés à
l'API TypeSafe pour classification. Rien d'autre ne quitte ton navigateur, et
lkclean n'a ni serveur ni analytics.
- Le nom de l'auteur et le texte de chaque post affiché dans ton fil sont envoyés
au fournisseur Jev configuré (TypeSafe direct, ou OpenRouter) pour la
classification. Rien d'autre ne quitte ton navigateur, et lkclean n'a ni
serveur ni analytics.
- La clé API est stockée dans le stockage synchronisé de Chrome
(`chrome.storage.sync`).
- Les verdicts sont mis en cache dans `chrome.storage.session` et effacés à la
Expand All @@ -122,7 +138,8 @@ npm run typecheck # tsc --noEmit
| `src/content.ts` | détection des posts, extraction, masquage/annotation, relance du scroll infini |
| `src/sticker.ts` | la pastille d'activité Jev (Shadow DOM) |
| `src/background.ts` | questions, logique de décision, cache, appels API |
| `src/jev.ts` | client typé minimal pour l'endpoint System One |
| `src/jev.ts` | client typé minimal pour l'endpoint System One (TypeSafe direct ou via OpenRouter) |
| `src/types.ts` | déclarations pures : réglages, verdicts, messagerie, contrat de questions/réponses Jev |
| `src/options.ts`, `public/options.*` | page de réglages |

Pour changer ce qui est filtré, modifie `buildQuestions()` et `decide()` dans
Expand Down
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ Every post is sent to **Jev**, the typed classification model from
Engagement bait, self-promotion, off-topic and sponsored posts are collapsed into
a one-line bar, and every decision is explained right on the post.

> Not affiliated with LinkedIn or TypeSafe AI. You need your own TypeSafe API key.
> **Fork note:** this fork adds [OpenRouter](https://openrouter.ai) as an
> alternative way to reach Jev — same model, billed to your OpenRouter account,
> no TypeSafe account required.

> Not affiliated with LinkedIn or TypeSafe AI. You need your own API key for the
> provider you choose (TypeSafe direct, or OpenRouter).

## What it does

Expand All @@ -25,8 +30,9 @@ a one-line bar, and every decision is explained right on the post.
too short for LinkedIn's "load more" trigger; lkclean nudges it without moving
your reading position.
- **Cheap and fast** – one API call per post, all questions evaluated in parallel,
verdicts cached per session. Roughly $0.042 per million input tokens: a scrolling
session costs a fraction of a cent.
verdicts cached per session. Roughly $0.042 per million input tokens (output
tokens free): a scrolling session costs a fraction of a cent on either
provider.


<img width="502" height="383" alt="notok" src="https://github.com/user-attachments/assets/b9f92b7e-aad6-4287-830f-a625f586dd40" />
Expand All @@ -42,9 +48,11 @@ a one-line bar, and every decision is explained right on the post.
front end (text in `[data-testid="expandable-text-box"]`), with fallbacks for the
legacy `data-id="urn:li:activity:…"` markup. Posts are identified by a hash of
their text, since LinkedIn no longer exposes a stable id.
2. Author and text go to the service worker, which calls
`POST https://api.typesafe.ai/v1/systemone` with a *state*
`{ user_interests, post }` and several typed questions in a single request:
2. Author and text go to the service worker, which calls the System One
endpoint — `POST https://api.typesafe.ai/v1/systemone` (TypeSafe direct) or
`POST https://openrouter.ai/api/v1/systemone` (via OpenRouter) — with a
*state* `{ user_interests, post }` and several typed questions in a single
request:

| Question | Type | Meaning |
| --- | --- | --- |
Expand Down Expand Up @@ -77,10 +85,13 @@ npm run build

Then load the `dist/` folder the same way.

Click the extension icon to open the settings: paste your TypeSafe key
(from [console.typesafe.ai](https://console.typesafe.ai)), list your interests
(one per line), optionally list blocked topics, and adjust the thresholds.
Reload LinkedIn.
Click the extension icon to open the settings: pick your API provider
(**TypeSafe direct** or **OpenRouter**), paste the matching key (from
[console.typesafe.ai](https://console.typesafe.ai) — `ts-…` — or
[openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) — `sk-or-…`),
list your interests (one per line), optionally list blocked topics, and adjust
the thresholds. Set the model id to match the provider: `jev-latest` (TypeSafe)
or `typesafe/jev-1.14` (OpenRouter). Reload LinkedIn.

After each rebuild, press ↻ on the extension card in `chrome://extensions`, then
reload the LinkedIn tab.
Expand All @@ -89,6 +100,7 @@ reload the LinkedIn tab.

| Setting | Default | Effect |
| --- | --- | --- |
| API provider | TypeSafe direct | Where Jev is reached from; the matching key field is used |
| Interests | empty | If empty, only noise is filtered (bait, self-promo, sponsored) |
| Blocked topics | empty | Always hidden when Jev is ≥ 60 % confident |
| Noise threshold | 70 % | Hide when P(bait) or P(self-promo) is above it |
Expand All @@ -97,15 +109,14 @@ reload the LinkedIn tab.
| Minimum length | 40 chars | Shorter posts are never evaluated |
| Explain kept posts | on | The "Kept by Jev" line |
| Activity sticker | on | The floating Jev badge |
| Model | `jev-latest` | Pin a version such as `jev-1.13.0` once your thresholds are tuned |

The UI is currently in French.
| Model | `jev-latest` | TypeSafe default; on OpenRouter use `typesafe/jev-1.14`. Pin one version once your thresholds are tuned |

## Privacy

- The author name and text of each post displayed in your feed are sent to the
TypeSafe API for classification. Nothing else leaves your browser, and lkclean
has no server or analytics of its own.
configured Jev provider (TypeSafe direct, or OpenRouter) for classification.
Nothing else leaves your browser, and lkclean has no server or analytics of
its own.
- The API key is stored in Chrome's synced storage (`chrome.storage.sync`).
- Verdicts are cached in `chrome.storage.session` and cleared when the browser
closes or the settings change.
Expand All @@ -122,7 +133,8 @@ npm run typecheck # tsc --noEmit
| `src/content.ts` | post detection, extraction, hiding/annotating, infinite-scroll nudge |
| `src/sticker.ts` | the Jev activity sticker (Shadow DOM) |
| `src/background.ts` | questions, decision logic, cache, API calls |
| `src/jev.ts` | minimal typed client for the System One endpoint |
| `src/jev.ts` | minimal typed client for the System One endpoint (TypeSafe direct or via OpenRouter) |
| `src/types.ts` | pure declarations: settings, verdicts, messaging, Jev question/answer contract |
| `src/options.ts`, `public/options.*` | settings page |

To change what gets filtered, edit `buildQuestions()` and `decide()` in
Expand Down
Loading