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 +30 -1
- package/bin/cli.js +37 -20
- package/package.json +41 -32
- package/skills/1lev1-platform/SKILL.md +149 -0
- package/skills/1lev1-platform/references/concepts.md +42 -0
- package/skills/1lev1-platform/references/connect.md +62 -0
- package/skills/1lev1-platform/references/tools.md +77 -0
- package/src/auth-flow.js +42 -6
- package/src/config-writers.js +285 -214
- package/src/skill-writer.js +115 -0
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
|
-
|
|
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
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
program
|
|
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": "
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
package/src/config-writers.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
config.
|
|
116
|
-
config.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
config.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
config.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
config.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
}
|