1lev1-mcp 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,12 +13,41 @@ By connecting, your AI agent gains "superpowers" within the 1lev1 system:
13
13
 
14
14
  ## Installation & Usage
15
15
 
16
- Once the package is published to NPM, no prior installation is needed. Simply run:
16
+ No prior installation is needed. Simply run:
17
17
 
18
18
  ```bash
19
19
  npx 1lev1-mcp
20
20
  ```
21
21
 
22
+ This does two things:
23
+
24
+ 1. **Connects** — opens 1lev1.com, you log in and approve, and the API key is
25
+ written into every AI agent you actually have installed.
26
+ 2. **Installs the skill** — the standing instructions that teach the agent what
27
+ a rikma is, that logged hours are ownership, and that it must never vote or
28
+ sign a profit-split on somebody else's behalf. The connection gives the agent
29
+ hands; the skill gives it judgement.
30
+
31
+ Restart your agent afterwards to pick up the new config.
32
+
33
+ To undo everything:
34
+
35
+ ```bash
36
+ npx 1lev1-mcp remove
37
+ ```
38
+
39
+ That removes the server entry and the skill from every agent config. The API key
40
+ itself stays valid until you revoke it at **1lev1.com → Settings → API keys**.
41
+
42
+ ### What it will not touch
43
+
44
+ - Config files that are not plain JSON (VS Code `settings.json` with comments,
45
+ for example) are **left alone** and reported, never rewritten.
46
+ - Agents you have not installed are skipped — no directories are created and no
47
+ API key is written for a tool you do not use.
48
+ - Every file it does modify is backed up once to `<file>.1lev1.bak` first, and
49
+ written atomically.
50
+
22
51
  ## Customization (for Developers)
23
52
 
24
53
  To use this tool for your own MCP project, follow these two simple steps:
package/bin/cli.js CHANGED
@@ -1,20 +1,37 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { runSetup } from '../src/auth-flow.js';
4
- import { APP_CONFIG } from '../src/app-config.js';
5
-
6
- const program = new Command();
7
-
8
- program
9
- .name(APP_CONFIG.serviceName)
10
- .description('Connect your MCP server to AI coding agents')
11
- .version('1.0.0');
12
-
13
- program
14
- .command('setup', { isDefault: true })
15
- .description('Authenticate and configure all supported agents')
16
- .option('--url <url>', 'MCP server URL', APP_CONFIG.mcpUrl)
17
- .option('--port <port>', 'Local callback port', APP_CONFIG.defaultPort.toString())
18
- .action(runSetup);
19
-
20
- program.parse();
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command } from 'commander';
6
+ import { runSetup, runRemove } from '../src/auth-flow.js';
7
+ import { APP_CONFIG } from '../src/app-config.js';
8
+
9
+ // Read the real version from package.json rather than repeating it here — the
10
+ // hardcoded '1.0.0' had drifted two releases behind what npm was serving.
11
+ const pkg = JSON.parse(
12
+ fs.readFileSync(
13
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
14
+ 'utf-8'
15
+ )
16
+ );
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name(APP_CONFIG.serviceName)
22
+ .description('Connect 1lev1 to your AI coding agents')
23
+ .version(pkg.version);
24
+
25
+ program
26
+ .command('setup', { isDefault: true })
27
+ .description('Authenticate and configure all supported agents')
28
+ .option('--url <url>', 'MCP server URL', APP_CONFIG.mcpUrl)
29
+ .option('--port <port>', 'Local callback port', APP_CONFIG.defaultPort.toString())
30
+ .action(runSetup);
31
+
32
+ program
33
+ .command('remove')
34
+ .description('Remove the 1lev1 server and skill from every agent config')
35
+ .action(runRemove);
36
+
37
+ program.parse();
package/package.json CHANGED
@@ -1,32 +1,41 @@
1
- {
2
- "name": "1lev1-mcp",
3
- "version": "1.0.2",
4
- "description": "Connect 1lev1 to your AI coding agents",
5
- "type": "module",
6
- "bin": {
7
- "1lev1-mcp": "./bin/cli.js"
8
- },
9
- "files": [
10
- "bin/",
11
- "src/"
12
- ],
13
- "publishConfig": {
14
- "access": "public"
15
- },
16
- "keywords": [
17
- "mcp",
18
- "ai",
19
- "claude",
20
- "cursor"
21
- ],
22
- "license": "MIT",
23
- "dependencies": {
24
- "commander": "^12.0.0",
25
- "open": "^10.0.0",
26
- "chalk": "^5.0.0",
27
- "ora": "^8.0.0"
28
- },
29
- "engines": {
30
- "node": ">=18"
31
- }
32
- }
1
+ {
2
+ "name": "1lev1-mcp",
3
+ "version": "2.0.0",
4
+ "description": "Connect 1lev1 to your AI coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "1lev1-mcp": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "skills/"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "ai",
20
+ "claude",
21
+ "cursor"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "commander": "^12.0.0",
26
+ "open": "^10.0.0",
27
+ "chalk": "^5.0.0",
28
+ "ora": "^8.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Avi-ADAM/1lev1-mcp.git"
36
+ },
37
+ "homepage": "https://1lev1.com",
38
+ "scripts": {
39
+ "sync:skill": "node -e \"const fs=require('fs');const s=process.env.SKILL_SRC||'../1lev1-agent/plugins/1lev1/skills/1lev1-platform';fs.rmSync('skills/1lev1-platform',{recursive:true,force:true});fs.cpSync(s,'skills/1lev1-platform',{recursive:true});console.log('skill synced from '+s)\""
40
+ }
41
+ }
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: 1lev1-platform
3
+ description: Work with 1lev1.com partnerships ("rikmot") from the agent - list and search missions, start/stop mission timers, log and review work hours, see open votes and pending profit-splits, and prepare new partnerships, missions and tasks for human approval. Use whenever the user mentions 1lev1, a rikma/partnership, "my missions", mission timers, hour logging, consensus votes, halukot/profit-split, or asks to turn repo work (issues, PRs, TODOs) into shared, equity-bearing work.
4
+ ---
5
+
6
+ # 1lev1 platform
7
+
8
+ 1lev1 (1lev1.com) is a platform for **consent-based partnerships**. A partnership
9
+ is called a **rikma** (רקמה, "tissue/weave"). People contribute work to a rikma
10
+ through **missions**, the hours they log become their share of the rikma's value,
11
+ and money the rikma earns is split by **halukot** (profit-split agreements) that
12
+ every affected member signs.
13
+
14
+ This skill lets an agent operate a user's 1lev1 account through the 1lev1 MCP
15
+ server, and - crucially - operate it *the way the platform expects*: nothing that
16
+ affects another person happens without that person's explicit consent.
17
+
18
+ ## Before anything else: check the connection
19
+
20
+ The MCP server has two modes. Call `tools/list` (or just look at which `1lev1`
21
+ tools you have):
22
+
23
+ - **Only `getPlatformInfo` and `howToConnect` are present** -> the user is not
24
+ connected. Do not guess or fabricate data. Tell them, and offer the one-liner:
25
+
26
+ ```bash
27
+ npx 1lev1-mcp
28
+ ```
29
+
30
+ That opens 1lev1.com, has them log in (or register), asks them to approve the
31
+ connection, and writes the API key into their MCP config. Then the client must
32
+ be restarted for the authenticated tools to appear.
33
+
34
+ - **Mission/timer/project tools are present** -> the user is connected. Every
35
+ call is scoped to the API key's owner; you cannot see or touch anyone else's
36
+ data, and you should never claim otherwise.
37
+
38
+ If the user has never heard of 1lev1, call `getPlatformInfo` and explain it in
39
+ their language before pushing them to register.
40
+
41
+ ## The rule that governs everything: no unilateral writes
42
+
43
+ 1lev1's core principle is that **a decision that affects another person needs
44
+ that person's consent**, and that there is no hard "no" - the choices are
45
+ approve, discuss, or counter-propose. Two consequences for you:
46
+
47
+ 1. **Never** submit a vote, sign a haluka, accept an offer, or change another
48
+ member's standing on the user's behalf without the user explicitly asking for
49
+ that exact act in that exact turn.
50
+ 2. For anything that creates or changes a shared object, prefer the tools that
51
+ return a **prepared URL** over tools that write directly. Show the user the
52
+ link, let them review the filled-in form on the site, and let them press the
53
+ button. `createProjectTool` and `prepareMissionTool` are built for this.
54
+
55
+ When you are unsure whether an act is "yours to do", it is not. Describe it and
56
+ hand over the link.
57
+
58
+ ## Capability map
59
+
60
+ Discover exact tool names from `tools/list`; the groups below are stable.
61
+
62
+ | You want to | Use |
63
+ |---|---|
64
+ | Find the user's rikmot | `findUserProjectsTool` |
65
+ | Find a mission by name | `findMissionTool` |
66
+ | List the user's missions | `listUserMissionsTool` |
67
+ | Full detail on one mission | `getMissionDetailsTool` |
68
+ | Start / stop / edit a mission timer | `timerActionTool` |
69
+ | What is running right now | `getActiveTimersTool` |
70
+ | Past sessions on a mission | `getTimerHistoryTool` |
71
+ | Hours totals and trends | `getMissionStatsTool` |
72
+ | Who is in a rikma | `getProjectMembersTool` |
73
+ | What a given member is working on | `getMemberMissionsTool` |
74
+ | Draft a new rikma (returns a review URL) | `createProjectTool` |
75
+ | Draft a mission from a rough description | `prepareMissionTool` |
76
+ | Create a mission / task | `createMissionTool`, `createTaskTool` |
77
+ | Plan the next work in a rikma | `planProjectWorkTool`, `scanProjectDirectionsTool` |
78
+ | Where to send the user on the site | `getSitePagesTool`, `navigateToPageTool` |
79
+ | Explain what page they are on | `getPageContextTool` |
80
+
81
+ Read `references/tools.md` for argument shapes, gotchas and which of these
82
+ write versus only prepare.
83
+
84
+ ## Recipes
85
+
86
+ ### "What should I be doing?" (daily briefing)
87
+
88
+ 1. `findUserProjectsTool` -> the user's rikmot.
89
+ 2. `listUserMissionsTool` -> what is assigned and in progress.
90
+ 3. `getActiveTimersTool` -> is a timer still running from yesterday?
91
+ 4. Report grouped **by rikma**, newest commitment first, and end with anything
92
+ waiting on the user's consent (open votes, pending halukot) with a link.
93
+
94
+ Keep it short. A briefing longer than the work it describes is a failed briefing.
95
+
96
+ ### "Start working on X" / "I'm done"
97
+
98
+ 1. `findMissionTool` with the name the user used. If several match, list them and
99
+ ask - never guess which mission gets the hours; hours are equity.
100
+ 2. `timerActionTool` to start. On stop, report the session length and the new
101
+ total from `getMissionStatsTool`.
102
+ 3. If a timer was already running on a different mission, say so before starting
103
+ a new one instead of silently switching.
104
+
105
+ ### "Log the time I spent" (retroactive)
106
+
107
+ Use `timerActionTool`'s edit/manual path. Always echo back the exact interval you
108
+ are about to record and get a yes. Hours are the unit of ownership on this
109
+ platform - a wrong number is a wrong equity share, and correcting it later needs
110
+ another member's consent.
111
+
112
+ ### Turning repo work into a rikma (the developer path)
113
+
114
+ For a user with an open-source repo or a side project and collaborators:
115
+
116
+ 1. Read the repo yourself - README, contributors, open issues, roadmap.
117
+ 2. `createProjectTool` with a proposed name, description, values and roles ->
118
+ returns a prefilled URL. Send them there; they review and create.
119
+ 3. Once the rikma exists, use `prepareMissionTool` per meaningful workstream
120
+ (not per issue - a rikma of 200 one-line missions is unusable).
121
+ 4. Explain the payoff in one sentence: hours logged against those missions become
122
+ each contributor's documented share, so when the project earns anything the
123
+ split is already agreed rather than argued.
124
+
125
+ ### Planning
126
+
127
+ `planProjectWorkTool` and `scanProjectDirectionsTool` produce proposals, not
128
+ commitments. Present their output as a draft and route anything the user likes
129
+ through the prepare-then-approve path above.
130
+
131
+ ## Language and naming
132
+
133
+ - Answer in the user's language. Most of the platform's users write Hebrew; the
134
+ UI ships in he, en, ar, ru and es.
135
+ - Use the platform's own words with a gloss on first use: rikma (partnership),
136
+ mission (משימה), haluka (profit-split), moach (the rikma's management area),
137
+ lev (the personal home feed). See `references/concepts.md`.
138
+ - Never invent a Hebrew name for an entity. Echo names exactly as the tools
139
+ return them - Hebrew strings from some surfaces are stored pre-reversed for
140
+ rendering and must not be re-typed by hand.
141
+
142
+ ## Reference files
143
+
144
+ - `references/concepts.md` - domain glossary and the consent model. Read before
145
+ explaining anything about how ownership or decisions work.
146
+ - `references/tools.md` - per-tool argument shapes, read/write classification,
147
+ and known sharp edges.
148
+ - `references/connect.md` - connection, API keys, scopes, revocation and
149
+ troubleshooting when tools are missing or return 401.
@@ -0,0 +1,42 @@
1
+ # 1lev1 concepts
2
+
3
+ Use these words with the user, glossed once. Getting them wrong makes the agent
4
+ sound like it has never seen the platform.
5
+
6
+ ## Entities
7
+
8
+ | Term | Meaning |
9
+ |---|---|
10
+ | **rikma** (רקמה) | A partnership. The unit of collaboration - members, missions, resources, money. Sometimes rendered "project" or "embroidery" in older strings. |
11
+ | **mission** (משימה) | A piece of committed work inside a rikma. Hours are logged against it. `mesimabetahalich` = a mission in progress. |
12
+ | **task / act** (מטלה) | A smaller item hanging off a mission. Can arrive from an external system through the tasks API. |
13
+ | **haluka** (חלוקה) | A profit-split agreement: who receives what share of a given income, signed by everyone affected. |
14
+ | **sale** | Income recorded for a rikma. Only counts toward balances once the person holding the money has confirmed they hold it. |
15
+ | **moach** (מוח, "brain") | A rikma's management area on the site: members, missions, votes, money. |
16
+ | **lev** (לב, "heart") | The user's personal home: everything waiting on them across all their rikmot. |
17
+ | **consensus** | The separate discussion space where a contested decision gets talked through rather than voted down. |
18
+
19
+ ## The consent model
20
+
21
+ This is the part an agent most often gets wrong.
22
+
23
+ - **There is no unilateral "no".** A member facing a proposal may approve it,
24
+ open a discussion, or counter-propose - but not veto. "I got nothing" is
25
+ expressed as a counter-proposal of amount zero, not as a rejection.
26
+ - **Silence is consent, on a clock.** Every open proposal carries the rikma's
27
+ response time. If nobody answers within it, the standing version is approved
28
+ automatically. A counter-proposal restarts the clock.
29
+ - **Consent scope varies.** Most decisions are rikma-wide. Some are bilateral -
30
+ only the two people actually affected sign. Do not tell a user that "everyone
31
+ has to approve" without checking which kind of decision it is.
32
+ - **Hours are ownership.** Approved hours on a mission become the contributor's
33
+ share of the rikma's value. This is why a mistyped timer entry is not a
34
+ cosmetic error.
35
+ - **Money moves only when confirmed.** A sale whose holder has not confirmed, or
36
+ a payment not yet marked confirmed, does not count in anyone's balance.
37
+
38
+ ## What this means for you
39
+
40
+ When the user asks you to "just approve it", "vote yes for me", or "add Dana to
41
+ the split", you are being asked to act inside another person's consent. Prepare
42
+ it, show it, and let the human press the button on the site.
@@ -0,0 +1,62 @@
1
+ # Connecting to 1lev1
2
+
3
+ ## The flow
4
+
5
+ ```bash
6
+ npx 1lev1-mcp
7
+ ```
8
+
9
+ The CLI opens `https://1lev1.com/mcp-connect?callback=http://localhost:<port>`.
10
+ The user logs in (or registers), sees exactly what they are approving, and hits
11
+ approve. 1lev1 mints an API key and redirects it back to the CLI, which writes
12
+ the MCP server entry into the client config. **The client must be restarted**
13
+ before the authenticated tools appear.
14
+
15
+ Manual alternative: log in, go to Settings -> API keys, create a key named `MCP`,
16
+ then add to `.mcp.json`:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "1lev1": {
22
+ "type": "http",
23
+ "url": "https://api.1lev1.com/api/mcp",
24
+ "headers": { "Authorization": "Bearer 1lev1_..." }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ## Key facts
31
+
32
+ - Keys look like `1lev1_<base36 user id>_<48 hex chars>`. The user id is encoded
33
+ in the key; the server stores only an HMAC of it, never the key itself.
34
+ - One key per approval. Re-approving from `/mcp-connect` **replaces** the
35
+ previous MCP key and revokes the old one - warn the user before they re-run
36
+ the CLI on a machine where a working key already exists.
37
+ - Keys can carry scopes (a set of rikmot, a set of allowed operations). A scoped
38
+ key silently sees less; if a rikma the user expects is missing, check the key's
39
+ scopes before assuming a bug.
40
+ - Revoke from Settings -> API keys. Revocation is honoured on the server within
41
+ the key cache TTL (5 minutes).
42
+ - A key is a bearer credential for a real person's account. Never echo it into
43
+ chat, a commit, a log line, or a file the user did not ask for.
44
+
45
+ ## Troubleshooting
46
+
47
+ **Only `getPlatformInfo` and `howToConnect` are listed.**
48
+ The request reached the server without a valid key. Either no `Authorization`
49
+ header is being sent, or the key was rejected. Re-run `npx 1lev1-mcp`.
50
+
51
+ **401 on every call.**
52
+ The key is revoked, or it was minted against a different deployment. Keys are
53
+ HMAC'd with a per-environment secret, so a key created against one environment
54
+ will not verify against another. Mint a fresh key from the environment you are
55
+ actually pointing at.
56
+
57
+ **Tools appear but return empty lists.**
58
+ The account is real but has no rikmot yet, or the key is scoped to rikmot the
59
+ user is not in. Confirm with `findUserProjectsTool` before reporting a failure.
60
+
61
+ **Do not** work around auth problems by falling back to scraping 1lev1.com or by
62
+ inventing data. Say the connection is broken and stop.
@@ -0,0 +1,77 @@
1
+ # 1lev1 MCP tools
2
+
3
+ Names below are the tool names the server exposes. Always confirm against
4
+ `tools/list` - the set depends on the key's scopes.
5
+
6
+ ## Read
7
+
8
+ | Tool | Input | Notes |
9
+ |---|---|---|
10
+ | `findUserProjectsTool` | `query?` (and `userId?`, which you should omit) | Returns `{ id, idPr, name }[]`. `id` is the internal id; `idPr` is what site URLs use. Omit `userId` — it defaults to the key's owner, and naming anyone else is refused. |
11
+ | `findMissionTool` | `missionName` | Substring match across the user's missions. Returns `projectId` and `projectName` too - use it to disambiguate. |
12
+ | `listUserMissionsTool` | - | The caller's missions. |
13
+ | `getMissionDetailsTool` | mission id | Full record incl. skills, roles, hours. |
14
+ | `getActiveTimersTool` | - | Currently running timers. Check this before starting a new one. |
15
+ | `getTimerHistoryTool` | mission id | Past sessions. |
16
+ | `getMissionStatsTool` | mission id | Totals and trends. |
17
+ | `getProjectMembersTool` | `projectId`, `query?` | Returns `people[{id,username}]` **and** `roles[{id,roleDescription}]`. This is the only way to turn a name into an id. |
18
+ | `getMemberMissionsTool` | `projectId`, `userId` | Another member's in-progress missions in that rikma. |
19
+ | `getSitePagesTool` | - | The site's URL map. |
20
+ | `getPageContextTool` | path | What a given page is for. |
21
+ | `getPlatformInfo` | - | Public. Available even unauthenticated. |
22
+ | `howToConnect` | - | Public. The connect instructions. |
23
+
24
+ ## Prepare (safe - returns a URL, writes nothing)
25
+
26
+ | Tool | Input | Returns |
27
+ |---|---|---|
28
+ | `createProjectTool` | `name`, `desc?`, `details?` (HTML), `url?`, `vals?` (value names), `res?` (`feh` 48h / `sth` 72h / `nsh` 96h / `sevend` 1 week), `profit?`, `ont?` (continuous vs one-off) | A prefilled rikma-creation URL. **This is the preferred way to create a rikma.** |
29
+ | `prepareMissionTool` | `projectId`, `name`, `descrip?`, `skills?`, `roles?`, `workways?`, `nhours?`, `valph?` | A prefilled mission-creation URL. Prefer over `createMissionTool` unless the user asked for direct creation. |
30
+ | `navigateToPageTool` | page | A link to send the user to. |
31
+ | `planProjectWorkTool`, `scanProjectDirectionsTool` | project context | Drafts. Proposals, never commitments. |
32
+
33
+ ## Write (changes state - require an explicit ask)
34
+
35
+ `timerActionTool` is available to every key: it only ever touches the caller's
36
+ own timers and hours.
37
+
38
+ | Tool | Input | Notes |
39
+ |---|---|---|
40
+ | `timerActionTool` | `action`: `start` / `stop` / `pause` / `resume`, `missionId?` | Without `missionId` it acts on the currently active timer. **Always pass `missionId` on `start`** - an omitted id on an ambiguous account is how hours land on the wrong mission. |
41
+
42
+ ## Shared write (needs the `mcp:write` scope)
43
+
44
+ These create work and obligations for **other** members, so a default key does
45
+ not get them - they appear in `tools/list` only when the key was granted
46
+ `mcp:write`. If a user asks for one and you do not have the tool, say the key
47
+ needs that scope rather than improvising a workaround.
48
+
49
+ | Tool | Input | Notes |
50
+ |---|---|---|
51
+ | `createMissionTool` | `projectId`, `missionName`, plus `descrip?`, `skills?`, `roles?`, `workways?`, `nhours?`, `valph?`, `iskvua?` (recurring monthly), `dateStart?`, `dateEnd?`, `assignedUserId?`, `checklist?` | Omit `assignedUserId` to leave the mission open for anyone in the rikma to take. |
52
+ | `createTaskTool` | `projectId`, `name`, `description?`, `link?`, `missionId?`, `hashivut` (`white`/`green`/`yellow`/`red`), `dateS?`, `dateF?`, and **either** `assignedUserId` **or** `tafkidims` (role ids) - never both | Resolve names to ids with `getProjectMembersTool` first. Assigning to a role means "whoever holds it sees it". |
53
+
54
+ Prefer `prepareMissionTool` even when you do hold `mcp:write`: a prefilled form
55
+ the human approves is the platform's own pattern, and it costs one click.
56
+
57
+ ## Agent and workflow tools
58
+
59
+ Older deployments also exposed `ask_*` and `run_*` tools proxying 1lev1's own
60
+ in-app assistant. They are no longer served, and if you ever see one, do not use
61
+ it: calling it puts a second, less-informed agent inside your own loop. Use the
62
+ concrete tools above.
63
+
64
+ ## Sharp edges
65
+
66
+ - **`id` vs `idPr`.** Site URLs use `idPr`. Tool inputs want `id`. Mixing them
67
+ produces a 404 or an empty result rather than an error.
68
+ - **`createTaskTool` writes immediately** with a service token, not through the
69
+ user's session. Treat it as a real write: confirm the target rikma, mission and
70
+ assignee with the user before calling it.
71
+ - **Hebrew names may be stored pre-reversed** for canvas rendering on some
72
+ surfaces. Echo what the tool returned; do not retype or "fix" it.
73
+ - **A timer left running accrues hours.** If `getActiveTimersTool` shows one from
74
+ a previous day, surface it before doing anything else.
75
+ - **You cannot read another user's data.** Tools that take a `userId` refuse one
76
+ that is not the key's owner. If the user wants a teammate's status, use
77
+ `getProjectMembersTool` + `getMemberMissionsTool` within a shared rikma.
package/src/auth-flow.js CHANGED
@@ -2,9 +2,19 @@ import http from 'node:http';
2
2
  import open from 'open';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
- import { writeAllConfigs } from './config-writers.js';
5
+ import { writeAllConfigs, removeAllConfigs } from './config-writers.js';
6
+ import { writeAllSkills, removeAllSkills } from './skill-writer.js';
6
7
  import { APP_CONFIG } from './app-config.js';
7
8
 
9
+ /** Render one { agent: {success, skipped, reason, path} } map as aligned lines. */
10
+ function report(results) {
11
+ for (const [agent, status] of Object.entries(results)) {
12
+ const icon = status.success ? chalk.green('✓') : (status.skipped ? chalk.dim('–') : chalk.yellow('!'));
13
+ const msg = status.success ? chalk.dim(status.path) : chalk.dim(status.reason);
14
+ console.log(` ${icon} ${agent.padEnd(18)} ${msg}`);
15
+ }
16
+ }
17
+
8
18
  export async function runSetup({ url, port }) {
9
19
  console.log(chalk.bold('\n MCP Setup ' + url + ' ' + port + '\n'));
10
20
 
@@ -26,14 +36,40 @@ export async function runSetup({ url, port }) {
26
36
  const writeSpinner = ora('Writing agent configs...').start();
27
37
  const results = await writeAllConfigs({ mcpUrl: url, apiKey: key });
28
38
  writeSpinner.stop();
39
+ console.log(chalk.bold('\n Connection'));
40
+ report(results);
29
41
 
30
- for (const [agent, status] of Object.entries(results)) {
31
- const icon = status.success ? chalk.green('✓') : chalk.yellow('–');
32
- const msg = status.success ? chalk.dim(status.path) : chalk.dim(status.reason);
33
- console.log(` ${icon} ${agent.padEnd(18)} ${msg}`);
42
+ // The connection alone is not enough — install the skill that teaches the
43
+ // agent what a rikma is and what it must not do on somebody else's behalf.
44
+ const skillSpinner = ora('Installing 1lev1 skill...').start();
45
+ const skillResults = await writeAllSkills();
46
+ skillSpinner.stop();
47
+ console.log(chalk.bold('\n Skill'));
48
+ report(skillResults);
49
+
50
+ const wrote = Object.values(results).filter((r) => r.success).length;
51
+ if (wrote === 0) {
52
+ console.log('\n' + chalk.yellow(' No agent config was written.'));
53
+ console.log(chalk.dim(' Add the server manually:'));
54
+ console.log(chalk.dim(` { "mcpServers": { "${APP_CONFIG.serviceName}": { "type": "http", "url": "${url}", "headers": { "Authorization": "Bearer <your key>" } } } }`));
34
55
  }
35
56
 
36
- console.log('\n' + chalk.bold(' Done!') + ' Restart your agent to pick up the new config.\n');
57
+ console.log('\n' + chalk.bold(' Done!') + ' Restart your agent to pick up the new config.');
58
+ console.log(chalk.dim(' Undo any time with: 1lev1-mcp remove\n'));
59
+ }
60
+
61
+ /** Removes our entry from every agent config and deletes the installed skill. */
62
+ export async function runRemove() {
63
+ console.log(chalk.bold('\n Removing 1lev1 from your agents\n'));
64
+
65
+ console.log(chalk.bold(' Connection'));
66
+ report(await removeAllConfigs({}));
67
+
68
+ console.log(chalk.bold('\n Skill'));
69
+ report(await removeAllSkills());
70
+
71
+ console.log('\n' + chalk.dim(' The API key itself stays valid until you revoke it at'));
72
+ console.log(chalk.dim(' https://1lev1.com → Settings → API keys\n'));
37
73
  }
38
74
 
39
75
  // נפרד מ-waitForCallback — מחזיר { promise } בלי await
@@ -1,214 +1,285 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import { APP_CONFIG } from './app-config.js';
5
-
6
- const HOME = os.homedir();
7
-
8
- // כל agent — איפה הקונפיג שלו ואיך נראה הפורמט
9
- const AGENTS = {
10
- 'Claude Desktop': {
11
- paths: {
12
- darwin: path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json'),
13
- win32: path.join(process.env.APPDATA ?? '', 'Claude', 'claude_desktop_config.json'),
14
- linux: path.join(HOME, '.config/claude/claude_desktop_config.json'),
15
- },
16
- write: (config, { mcpUrl, apiKey, name }) => {
17
- config.mcpServers ??= {};
18
- config.mcpServers[name] = {
19
- url: mcpUrl,
20
- headers: { Authorization: `Bearer ${apiKey}` },
21
- };
22
- return config;
23
- },
24
- },
25
-
26
- 'Cursor': {
27
- paths: {
28
- darwin: path.join(HOME, '.cursor/mcp.json'),
29
- win32: path.join(HOME, '.cursor/mcp.json'),
30
- linux: path.join(HOME, '.cursor/mcp.json'),
31
- },
32
- write: (config, { mcpUrl, apiKey, name }) => {
33
- config.mcpServers ??= {};
34
- config.mcpServers[name] = {
35
- url: mcpUrl,
36
- headers: { Authorization: `Bearer ${apiKey}` },
37
- };
38
- return config;
39
- },
40
- },
41
-
42
- 'Windsurf': {
43
- paths: {
44
- darwin: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
45
- win32: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
46
- linux: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
47
- },
48
- write: (config, { mcpUrl, apiKey, name }) => {
49
- config.mcpServers ??= {};
50
- config.mcpServers[name] = {
51
- serverUrl: mcpUrl, // Windsurf משתמש ב-serverUrl
52
- headers: { Authorization: `Bearer ${apiKey}` },
53
- };
54
- return config;
55
- },
56
- },
57
-
58
- 'Claude Code': {
59
- // על פי הדוקומנטציה הרשמית, ההגדרות נשמרות בקובץ .claude.json בתיקיית הבית
60
- paths: {
61
- darwin: path.join(HOME, '.claude.json'),
62
- win32: path.join(HOME, '.claude.json'),
63
- linux: path.join(HOME, '.claude.json'),
64
- },
65
- write: (config, { mcpUrl, apiKey, name }) => {
66
- // אנחנו שומרים תחת אובייקט mcpServers הראשי (User Scope - גלובלי)
67
- config.mcpServers ??= {};
68
- config.mcpServers[name] = {
69
- type: 'http', // חובה ב-Claude Code להגדיר סוג חיבור
70
- url: mcpUrl,
71
- headers: { Authorization: `Bearer ${apiKey}` },
72
- };
73
- return config;
74
- },
75
- },
76
- 'Antigravity': {
77
- paths: {
78
- darwin: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
79
- win32: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
80
- linux: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
81
- },
82
- write: (config, { mcpUrl, apiKey, name }) => {
83
- config.mcpServers ??= {};
84
- config.mcpServers[name] = {
85
- serverUrl: mcpUrl, // Antigravity משתמש ב-serverUrl לחיבור מרוחק
86
- headers: { Authorization: `Bearer ${apiKey}` },
87
- };
88
- return config;
89
- },
90
- },
91
- 'VS Code': {
92
- // VS Code MCP כותב לworkspace — נכתוב ל-global settings
93
- paths: {
94
- darwin: path.join(HOME, 'Library/Application Support/Code/User/settings.json'),
95
- win32: path.join(process.env.APPDATA ?? '', 'Code', 'User', 'settings.json'),
96
- linux: path.join(HOME, '.config/Code/User/settings.json'),
97
- },
98
- write: (config, { mcpUrl, apiKey, name }) => {
99
- config['mcp.servers'] ??= {};
100
- config['mcp.servers'][name] = {
101
- url: mcpUrl,
102
- headers: { Authorization: `Bearer ${apiKey}` },
103
- };
104
- return config;
105
- },
106
- },
107
- 'OpenClaw': {
108
- paths: {
109
- darwin: path.join(HOME, '.openclaw/config.json'),
110
- win32: path.join(HOME, '.openclaw/config.json'), // בחלונות זה לרוב גם בתיקיית הבית
111
- linux: path.join(HOME, '.openclaw/config.json'),
112
- },
113
- write: (config, { mcpUrl, apiKey, name }) => {
114
- // האובייקט של OpenClaw בנוי בצורה מקוננת: config.mcp.servers
115
- config.mcp ??= {};
116
- config.mcp.servers ??= {};
117
-
118
- config.mcp.servers[name] = {
119
- url: mcpUrl,
120
- headers: { Authorization: `Bearer ${apiKey}` },
121
- };
122
- return config;
123
- },
124
- },
125
-
126
- 'Cline': {
127
- paths: {
128
- darwin: path.join(HOME, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
129
- win32: path.join(process.env.APPDATA ?? '', 'Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
130
- linux: path.join(HOME, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
131
- },
132
- write: (config, { mcpUrl, apiKey, name }) => {
133
- config.mcpServers ??= {};
134
- config.mcpServers[name] = {
135
- url: mcpUrl,
136
- headers: { Authorization: `Bearer ${apiKey}` },
137
- };
138
- return config;
139
- },
140
- },
141
-
142
- 'Roo Code': {
143
- paths: {
144
- darwin: path.join(HOME, 'Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
145
- win32: path.join(process.env.APPDATA ?? '', 'Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
146
- linux: path.join(HOME, '.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
147
- },
148
- write: (config, { mcpUrl, apiKey, name }) => {
149
- config.mcpServers ??= {};
150
- config.mcpServers[name] = {
151
- url: mcpUrl,
152
- headers: { Authorization: `Bearer ${apiKey}` },
153
- };
154
- return config;
155
- },
156
- },
157
-
158
- 'Continue': {
159
- paths: {
160
- darwin: path.join(HOME, '.continue/config.json'),
161
- win32: path.join(HOME, '.continue/config.json'),
162
- linux: path.join(HOME, '.continue/config.json'),
163
- },
164
- write: (config, { mcpUrl, apiKey, name }) => {
165
- config.mcpServers ??= {};
166
- // Continue.dev משתמש במבנה מעט שונה המגדיר transport
167
- config.mcpServers[name] = {
168
- transport: {
169
- type: "sse",
170
- url: mcpUrl,
171
- headers: { Authorization: `Bearer ${apiKey}` }
172
- }
173
- };
174
- return config;
175
- },
176
- },
177
-
178
- };
179
-
180
- export async function writeAllConfigs({ mcpUrl, apiKey, name = APP_CONFIG.serviceName }) {
181
- const platform = process.platform;
182
- const results = {};
183
-
184
- for (const [agentName, agent] of Object.entries(AGENTS)) {
185
- const filePath = agent.paths[platform] ?? agent.paths['linux'];
186
-
187
- try {
188
- // קורא קונפיג קיים (אם יש) או מתחיל ריק
189
- let config = {};
190
- if (fs.existsSync(filePath)) {
191
- const raw = fs.readFileSync(filePath, 'utf-8');
192
- try {
193
- config = JSON.parse(raw);
194
- } catch (parseError) {
195
- // טיפול למקרה שיש הערות בקובץ JSON (קורה הרבה ב-VS Code)
196
- console.warn(`[Warning] Could not parse JSON for ${agentName}. Creating new config. Error: ${parseError.message}`);
197
- }
198
- }
199
-
200
- // מעדכן
201
- config = agent.write(config, { mcpUrl, apiKey, name });
202
-
203
- // כותב בחזרה
204
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
205
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf-8');
206
-
207
- results[agentName] = { success: true, path: filePath };
208
- } catch (err) {
209
- results[agentName] = { success: false, reason: err.message };
210
- }
211
- }
212
-
213
- return results;
214
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { APP_CONFIG } from './app-config.js';
5
+
6
+ const HOME = os.homedir();
7
+ const APPDATA = process.env.APPDATA ?? '';
8
+
9
+ // כל agent — איפה הקונפיג שלו, איך נראה הפורמט, ואיך מזהים שהוא בכלל מותקן.
10
+ //
11
+ // `detect` הוא רשימת נתיבים שקיומם מעיד שהסוכן מותקן. אנחנו לא כותבים קונפיג
12
+ // לסוכן שהמשתמש לא התקין — אחרת כל הרצה של הכלי מפזרת תיקיות ומפתחות API
13
+ // לכלים שאיש לא ביקש.
14
+ const AGENTS = {
15
+ 'Claude Desktop': {
16
+ paths: {
17
+ darwin: path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json'),
18
+ win32: path.join(APPDATA, 'Claude', 'claude_desktop_config.json'),
19
+ linux: path.join(HOME, '.config/claude/claude_desktop_config.json'),
20
+ },
21
+ write: (config, { mcpUrl, apiKey, name }) => {
22
+ config.mcpServers ??= {};
23
+ config.mcpServers[name] = {
24
+ url: mcpUrl,
25
+ headers: { Authorization: `Bearer ${apiKey}` },
26
+ };
27
+ return config;
28
+ },
29
+ },
30
+
31
+ 'Cursor': {
32
+ paths: {
33
+ darwin: path.join(HOME, '.cursor/mcp.json'),
34
+ win32: path.join(HOME, '.cursor/mcp.json'),
35
+ linux: path.join(HOME, '.cursor/mcp.json'),
36
+ },
37
+ write: (config, { mcpUrl, apiKey, name }) => {
38
+ config.mcpServers ??= {};
39
+ config.mcpServers[name] = {
40
+ url: mcpUrl,
41
+ headers: { Authorization: `Bearer ${apiKey}` },
42
+ };
43
+ return config;
44
+ },
45
+ },
46
+
47
+ 'Windsurf': {
48
+ paths: {
49
+ darwin: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
50
+ win32: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
51
+ linux: path.join(HOME, '.codeium/windsurf/mcp_config.json'),
52
+ },
53
+ write: (config, { mcpUrl, apiKey, name }) => {
54
+ config.mcpServers ??= {};
55
+ config.mcpServers[name] = {
56
+ serverUrl: mcpUrl, // Windsurf משתמש ב-serverUrl
57
+ headers: { Authorization: `Bearer ${apiKey}` },
58
+ };
59
+ return config;
60
+ },
61
+ },
62
+
63
+ 'Claude Code': {
64
+ paths: {
65
+ darwin: path.join(HOME, '.claude.json'),
66
+ win32: path.join(HOME, '.claude.json'),
67
+ linux: path.join(HOME, '.claude.json'),
68
+ },
69
+ write: (config, { mcpUrl, apiKey, name }) => {
70
+ // User scope גלובלי
71
+ config.mcpServers ??= {};
72
+ config.mcpServers[name] = {
73
+ type: 'http', // חובה ב-Claude Code להגדיר סוג חיבור
74
+ url: mcpUrl,
75
+ headers: { Authorization: `Bearer ${apiKey}` },
76
+ };
77
+ return config;
78
+ },
79
+ },
80
+
81
+ 'Antigravity': {
82
+ paths: {
83
+ darwin: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
84
+ win32: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
85
+ linux: path.join(HOME, '.gemini/antigravity/mcp_config.json'),
86
+ },
87
+ write: (config, { mcpUrl, apiKey, name }) => {
88
+ config.mcpServers ??= {};
89
+ config.mcpServers[name] = {
90
+ serverUrl: mcpUrl,
91
+ headers: { Authorization: `Bearer ${apiKey}` },
92
+ };
93
+ return config;
94
+ },
95
+ },
96
+
97
+ 'VS Code': {
98
+ paths: {
99
+ darwin: path.join(HOME, 'Library/Application Support/Code/User/settings.json'),
100
+ win32: path.join(APPDATA, 'Code', 'User', 'settings.json'),
101
+ linux: path.join(HOME, '.config/Code/User/settings.json'),
102
+ },
103
+ write: (config, { mcpUrl, apiKey, name }) => {
104
+ config['mcp.servers'] ??= {};
105
+ config['mcp.servers'][name] = {
106
+ url: mcpUrl,
107
+ headers: { Authorization: `Bearer ${apiKey}` },
108
+ };
109
+ return config;
110
+ },
111
+ },
112
+
113
+ 'OpenClaw': {
114
+ paths: {
115
+ darwin: path.join(HOME, '.openclaw/config.json'),
116
+ win32: path.join(HOME, '.openclaw/config.json'),
117
+ linux: path.join(HOME, '.openclaw/config.json'),
118
+ },
119
+ write: (config, { mcpUrl, apiKey, name }) => {
120
+ config.mcp ??= {};
121
+ config.mcp.servers ??= {};
122
+ config.mcp.servers[name] = {
123
+ url: mcpUrl,
124
+ headers: { Authorization: `Bearer ${apiKey}` },
125
+ };
126
+ return config;
127
+ },
128
+ },
129
+
130
+ 'Cline': {
131
+ paths: {
132
+ darwin: path.join(HOME, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
133
+ win32: path.join(APPDATA, 'Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
134
+ linux: path.join(HOME, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'),
135
+ },
136
+ write: (config, { mcpUrl, apiKey, name }) => {
137
+ config.mcpServers ??= {};
138
+ config.mcpServers[name] = {
139
+ url: mcpUrl,
140
+ headers: { Authorization: `Bearer ${apiKey}` },
141
+ };
142
+ return config;
143
+ },
144
+ },
145
+
146
+ 'Roo Code': {
147
+ paths: {
148
+ darwin: path.join(HOME, 'Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
149
+ win32: path.join(APPDATA, 'Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
150
+ linux: path.join(HOME, '.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json'),
151
+ },
152
+ write: (config, { mcpUrl, apiKey, name }) => {
153
+ config.mcpServers ??= {};
154
+ config.mcpServers[name] = {
155
+ url: mcpUrl,
156
+ headers: { Authorization: `Bearer ${apiKey}` },
157
+ };
158
+ return config;
159
+ },
160
+ },
161
+
162
+ 'Continue': {
163
+ paths: {
164
+ darwin: path.join(HOME, '.continue/config.json'),
165
+ win32: path.join(HOME, '.continue/config.json'),
166
+ linux: path.join(HOME, '.continue/config.json'),
167
+ },
168
+ write: (config, { mcpUrl, apiKey, name }) => {
169
+ config.mcpServers ??= {};
170
+ // השרת הוא streamable HTTP, לא SSE.
171
+ config.mcpServers[name] = {
172
+ transport: {
173
+ type: 'streamable-http',
174
+ url: mcpUrl,
175
+ headers: { Authorization: `Bearer ${apiKey}` },
176
+ },
177
+ };
178
+ return config;
179
+ },
180
+ },
181
+ };
182
+
183
+ /** כותב JSON בצורה אטומית: קובץ זמני + rename, כדי שהפסקה באמצע לא תקטע קובץ. */
184
+ function writeJsonAtomic(filePath, data) {
185
+ const tmp = `${filePath}.1lev1-tmp-${process.pid}`;
186
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
187
+ fs.renameSync(tmp, filePath);
188
+ }
189
+
190
+ /**
191
+ * מעדכן קונפיג של סוכן אחד.
192
+ *
193
+ * שלושה כללי ברזל, שכל אחד מהם נולד מבאג אמיתי:
194
+ * 1. אם הקובץ קיים אבל לא נפרס כ-JSON — לא נוגעים בו. קובץ כזה הוא בדרך
195
+ * כלל JSONC עם הערות (settings.json של VS Code), והכתיבה הישנה דרסה אותו
196
+ * באובייקט חדש ומחקה את כל ההגדרות של המשתמש.
197
+ * 2. לא יוצרים קונפיג לסוכן שלא מותקן — קודם היינו יוצרים תיקיות לכלים
198
+ * שהמשתמש מעולם לא התקין, ומפזרים אליהם מפתח API.
199
+ * 3. גיבוי לפני דריסה, וכתיבה אטומית.
200
+ */
201
+ function updateAgentConfig(agentName, agent, { mcpUrl, apiKey, name }) {
202
+ const filePath = agent.paths[process.platform] ?? agent.paths['linux'];
203
+ const exists = fs.existsSync(filePath);
204
+
205
+ if (!exists && !fs.existsSync(path.dirname(filePath))) {
206
+ return { success: false, skipped: true, reason: 'not installed' };
207
+ }
208
+
209
+ let config = {};
210
+ if (exists) {
211
+ const raw = fs.readFileSync(filePath, 'utf-8');
212
+ if (raw.trim() !== '') {
213
+ try {
214
+ config = JSON.parse(raw);
215
+ } catch {
216
+ return {
217
+ success: false,
218
+ reason: 'not plain JSON (comments?) — left untouched, add the server manually',
219
+ path: filePath,
220
+ };
221
+ }
222
+ }
223
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
224
+ return { success: false, reason: 'unexpected config shape — left untouched', path: filePath };
225
+ }
226
+ const backup = `${filePath}.1lev1.bak`;
227
+ if (!fs.existsSync(backup)) fs.copyFileSync(filePath, backup);
228
+ }
229
+
230
+ config = agent.write(config, { mcpUrl, apiKey, name });
231
+ writeJsonAtomic(filePath, config);
232
+ return { success: true, path: filePath };
233
+ }
234
+
235
+ export async function writeAllConfigs({ mcpUrl, apiKey, name = APP_CONFIG.serviceName }) {
236
+ const results = {};
237
+ for (const [agentName, agent] of Object.entries(AGENTS)) {
238
+ try {
239
+ results[agentName] = updateAgentConfig(agentName, agent, { mcpUrl, apiKey, name });
240
+ } catch (err) {
241
+ results[agentName] = { success: false, reason: err.message };
242
+ }
243
+ }
244
+ return results;
245
+ }
246
+
247
+ /** מסיר את הרשומה שלנו מכל קונפיג — הבסיס ל-`1lev1-mcp remove`. */
248
+ export async function removeAllConfigs({ name = APP_CONFIG.serviceName } = {}) {
249
+ const results = {};
250
+ for (const [agentName, agent] of Object.entries(AGENTS)) {
251
+ const filePath = agent.paths[process.platform] ?? agent.paths['linux'];
252
+ try {
253
+ if (!fs.existsSync(filePath)) {
254
+ results[agentName] = { success: false, skipped: true, reason: 'no config' };
255
+ continue;
256
+ }
257
+ let config;
258
+ try {
259
+ config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
260
+ } catch {
261
+ results[agentName] = { success: false, reason: 'not plain JSON — left untouched', path: filePath };
262
+ continue;
263
+ }
264
+ const buckets = [config.mcpServers, config['mcp.servers'], config.mcp?.servers];
265
+ let removed = false;
266
+ for (const bucket of buckets) {
267
+ if (bucket && Object.prototype.hasOwnProperty.call(bucket, name)) {
268
+ delete bucket[name];
269
+ removed = true;
270
+ }
271
+ }
272
+ if (!removed) {
273
+ results[agentName] = { success: false, skipped: true, reason: 'nothing to remove' };
274
+ continue;
275
+ }
276
+ writeJsonAtomic(filePath, config);
277
+ results[agentName] = { success: true, path: filePath };
278
+ } catch (err) {
279
+ results[agentName] = { success: false, reason: err.message };
280
+ }
281
+ }
282
+ return results;
283
+ }
284
+
285
+ export { AGENTS };
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const HOME = os.homedir();
7
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const SKILL_SRC = path.join(PKG_ROOT, 'skills', '1lev1-platform');
9
+
10
+ /**
11
+ * Where each agent reads "skills" (standing instructions) from.
12
+ *
13
+ * The MCP server gives the agent hands; the skill gives it judgement. Without
14
+ * it an agent sees ~20 tools with a one-line description each and no idea that
15
+ * a rikma is a partnership, that logged hours are ownership, or that it must
16
+ * never vote on somebody else's behalf. Installing the connection without the
17
+ * skill is what produced most of the bad first impressions.
18
+ *
19
+ * Only targets whose parent directory already exists are written — same rule as
20
+ * the MCP config writer: we never create a tool's home directory for a tool the
21
+ * user has not installed.
22
+ */
23
+ const SKILL_TARGETS = {
24
+ 'Claude Code': {
25
+ dir: path.join(HOME, '.claude', 'skills', '1lev1-platform'),
26
+ /** Copy the skill folder verbatim (SKILL.md + references/). */
27
+ kind: 'folder',
28
+ },
29
+ 'Cursor': {
30
+ // Cursor reads project/user rules as .mdc files rather than skill folders.
31
+ dir: path.join(HOME, '.cursor', 'rules'),
32
+ file: '1lev1.mdc',
33
+ kind: 'flat',
34
+ },
35
+ };
36
+
37
+ /** Recursively copy a directory (Node 16.7+ has fs.cpSync, but keep it explicit). */
38
+ function copyDir(src, dest) {
39
+ fs.mkdirSync(dest, { recursive: true });
40
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
41
+ const s = path.join(src, entry.name);
42
+ const d = path.join(dest, entry.name);
43
+ if (entry.isDirectory()) copyDir(s, d);
44
+ else fs.copyFileSync(s, d);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Flatten SKILL.md + references into a single document, for agents that take
50
+ * one rules file rather than a skill folder.
51
+ */
52
+ function flattenSkill() {
53
+ let out = fs.readFileSync(path.join(SKILL_SRC, 'SKILL.md'), 'utf-8');
54
+ const refDir = path.join(SKILL_SRC, 'references');
55
+ if (fs.existsSync(refDir)) {
56
+ for (const name of fs.readdirSync(refDir).sort()) {
57
+ out += `\n\n---\n\n# reference: ${name}\n\n`;
58
+ out += fs.readFileSync(path.join(refDir, name), 'utf-8');
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export async function writeAllSkills() {
65
+ const results = {};
66
+
67
+ if (!fs.existsSync(SKILL_SRC)) {
68
+ return { skill: { success: false, reason: 'skill files missing from package' } };
69
+ }
70
+
71
+ for (const [agentName, target] of Object.entries(SKILL_TARGETS)) {
72
+ try {
73
+ // The agent's root config dir must already exist (.claude / .cursor).
74
+ const agentRoot = target.kind === 'folder'
75
+ ? path.dirname(path.dirname(target.dir)) // ~/.claude
76
+ : path.dirname(target.dir); // ~/.cursor
77
+ if (!fs.existsSync(agentRoot)) {
78
+ results[agentName] = { success: false, skipped: true, reason: 'not installed' };
79
+ continue;
80
+ }
81
+
82
+ if (target.kind === 'folder') {
83
+ copyDir(SKILL_SRC, target.dir);
84
+ results[agentName] = { success: true, path: target.dir };
85
+ } else {
86
+ fs.mkdirSync(target.dir, { recursive: true });
87
+ const dest = path.join(target.dir, target.file);
88
+ fs.writeFileSync(dest, flattenSkill(), 'utf-8');
89
+ results[agentName] = { success: true, path: dest };
90
+ }
91
+ } catch (err) {
92
+ results[agentName] = { success: false, reason: err.message };
93
+ }
94
+ }
95
+
96
+ return results;
97
+ }
98
+
99
+ export async function removeAllSkills() {
100
+ const results = {};
101
+ for (const [agentName, target] of Object.entries(SKILL_TARGETS)) {
102
+ try {
103
+ const dest = target.kind === 'folder' ? target.dir : path.join(target.dir, target.file);
104
+ if (!fs.existsSync(dest)) {
105
+ results[agentName] = { success: false, skipped: true, reason: 'nothing to remove' };
106
+ continue;
107
+ }
108
+ fs.rmSync(dest, { recursive: true, force: true });
109
+ results[agentName] = { success: true, path: dest };
110
+ } catch (err) {
111
+ results[agentName] = { success: false, reason: err.message };
112
+ }
113
+ }
114
+ return results;
115
+ }