@dotlabshq/clave 0.1.0 → 0.2.2
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 +29 -9
- package/bin/clave.mjs +23 -12
- package/package.json +22 -6
- package/src/agentsmd.mjs +12 -0
- package/src/config.mjs +22 -9
- package/src/init.mjs +154 -63
- package/src/login.mjs +8 -1
package/README.md
CHANGED
|
@@ -6,16 +6,34 @@ Connect a repository to [clave](https://clave.one), the plan and work tracker fo
|
|
|
6
6
|
npx @dotlabshq/clave init
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
It runs in your terminal, in the repository:
|
|
9
|
+
It runs in your terminal, in the repository, the first time you set it up on this machine:
|
|
10
10
|
|
|
11
11
|
1. **Signs you in** through the browser (no token to copy). The sign-in lasts for this run only and is not saved.
|
|
12
|
-
2. **Picks the org**
|
|
13
|
-
3. **Picks
|
|
14
|
-
4. **Registers your agents** (default `coder`)
|
|
15
|
-
5. **
|
|
12
|
+
2. **Picks the org** and **the project**: it matches this repository's git remote. A missing project or unit is created when you are owner or admin; otherwise it tells you what an owner should run.
|
|
13
|
+
3. **Picks the unit** (team) your agents work for; owners and admins can also create one from the list (`+ New unit…`).
|
|
14
|
+
4. **Registers your agents** (default `coder`). An agent belongs to you: its id is `name@your-handle` (e.g. `coder@ayse`), so the same name can exist for other people. One token per agent.
|
|
15
|
+
5. **Writes the MCP config by variable name**: `claude mcp add --scope local` with `Authorization: Bearer ${CLAVE_TOKEN_<AGENT>}` (stored on this machine, not in the repository); without the `claude` CLI it writes `.mcp.json`. The tokens themselves go to a git-ignored `.env.clave` (0600). Load it before starting your client: `set -a; . ./.env.clave; set +a`, or export those variables in your shell profile.
|
|
16
16
|
6. **Adds a clave block** to `AGENTS.md` or `CLAUDE.md` (it asks first), so every agent session starts with `session_start`.
|
|
17
17
|
|
|
18
|
-
Tokens are never printed, so they never reach the chat or your AI agent's context.
|
|
18
|
+
Tokens are never printed and never written into a config file, so they never reach the chat or your AI agent's context.
|
|
19
|
+
|
|
20
|
+
`init` refuses to run twice: when the repository already has the clave block or `clave-*` MCP servers it says so and points to `add`.
|
|
21
|
+
|
|
22
|
+
## Adding to an existing setup
|
|
23
|
+
|
|
24
|
+
Setup grows with the work; nothing has to be decided up front.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @dotlabshq/clave add unit <name> # a new team in the plan (owner or admin)
|
|
28
|
+
npx @dotlabshq/clave add project <name> # a new project; repo = this folder's git remote (owner or admin)
|
|
29
|
+
npx @dotlabshq/clave add agent <name> # another agent of your own: token, MCP entry, AGENTS.md block
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`add unit` and `add project` are the coordinator's, run with a person's token and the operator's approval; `add agent` is yours. After adding a server, open a **new** session: a running session does not pick up MCP servers added while it is open.
|
|
33
|
+
|
|
34
|
+
## Each person, each machine
|
|
35
|
+
|
|
36
|
+
Every person runs `init` in their own checkout with their own sign-in. The team, the project and the MCP server names (`clave-<agent>`) are the same everywhere; the token inside is the person's own agent token, so clave records who did what (`agent:coder@ayse`).
|
|
19
37
|
|
|
20
38
|
## Options
|
|
21
39
|
|
|
@@ -23,12 +41,14 @@ Tokens are never printed, so they never reach the chat or your AI agent's contex
|
|
|
23
41
|
|---|---|
|
|
24
42
|
| `--org <slug>` | Org to use (default: ask) |
|
|
25
43
|
| `--project <key>` | Existing project key (default: match this repository) |
|
|
26
|
-
| `--agents <a,b>` |
|
|
44
|
+
| `--agents <a,b>` | `init`: agent names, e.g. `coder,reviewer` (default: ask) |
|
|
45
|
+
| `--unit <key>` | `add agent`: the unit it works for (default: ask) |
|
|
46
|
+
| `--repo <remote>` | `add project`: repository instead of this folder's git remote |
|
|
27
47
|
| `--yes` | Accept the defaults without asking |
|
|
28
48
|
| `--no-open` | Print the sign-in link instead of opening the browser |
|
|
29
|
-
| `--no-claude` | Write `.mcp.json`
|
|
49
|
+
| `--no-claude` | Write `.mcp.json` instead of using the `claude` CLI (`.env.clave` is written either way) |
|
|
30
50
|
| `--url <site>` | clave site (default `https://clave.one`, or `CLAVE_URL`) |
|
|
31
51
|
|
|
32
|
-
Start Claude Code in the repository after `init`; servers added while a session is open load at the next session start.
|
|
52
|
+
Start Claude Code in the repository after `init` or `add agent`; servers added while a session is open load at the next session start.
|
|
33
53
|
|
|
34
54
|
Guide for AI agents: https://clave.one/getting-started-ai.md
|
package/bin/clave.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// clave CLI: `clave init` bu repoyu clave'e bağlar (giriş, proje, agent'
|
|
2
|
+
// clave CLI: `clave init` bu repoyu clave'e bağlar (giriş, proje, kendi agent'ların, MCP ayarı);
|
|
3
|
+
// `clave add unit|project|agent <ad>` var olan kuruluma ekler.
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { init } from "../src/init.mjs";
|
|
5
|
+
import { add, explain, init } from "../src/init.mjs";
|
|
5
6
|
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
|
|
@@ -11,28 +12,38 @@ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.u
|
|
|
11
12
|
const help = `clave ${version}: connect this repository to clave (https://clave.one)
|
|
12
13
|
|
|
13
14
|
Usage: npx @dotlabshq/clave init [options]
|
|
15
|
+
npx @dotlabshq/clave add unit <name> (owner or admin)
|
|
16
|
+
npx @dotlabshq/clave add project <name> (owner or admin; repo = this folder's git remote)
|
|
17
|
+
npx @dotlabshq/clave add agent <name> (your own agent)
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
agents, issues their tokens and adds them to Claude
|
|
17
|
-
|
|
19
|
+
init signs you in through the browser, picks the org and project, registers
|
|
20
|
+
your agents (id name@your-handle), issues their tokens and adds them to Claude
|
|
21
|
+
Code for this project. It stops if the repository is already set up: use add.
|
|
22
|
+
Tokens are never printed; they go to a git-ignored .env.clave and the MCP
|
|
23
|
+
config holds only the variable name (\${CLAVE_TOKEN_<AGENT>}).
|
|
18
24
|
|
|
19
25
|
Options:
|
|
20
26
|
--org <slug> Org to use (default: ask)
|
|
21
27
|
--project <key> Existing project key (default: match this repository)
|
|
22
|
-
--agents <a,b>
|
|
28
|
+
--agents <a,b> init: agent names (default: ask; "coder")
|
|
29
|
+
--unit <key> add agent: the unit it works for (default: ask)
|
|
30
|
+
--repo <remote> add project: repository instead of this folder's remote
|
|
23
31
|
--yes Accept the defaults without asking
|
|
24
32
|
--no-open Don't open the browser; print the sign-in link only
|
|
25
|
-
--no-claude Don't use the claude CLI; write .mcp.json
|
|
33
|
+
--no-claude Don't use the claude CLI; write .mcp.json
|
|
26
34
|
--url <site> clave site (default: https://clave.one)
|
|
27
35
|
-h, --help This help
|
|
28
36
|
-v, --version The version`;
|
|
29
37
|
|
|
30
38
|
if (has("help") || args.includes("-h") || args.length === 0) { console.log(help); process.exit(0); }
|
|
31
39
|
if (has("version") || args.includes("-v")) { console.log(version); process.exit(0); }
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
init({
|
|
40
|
+
const common = {
|
|
35
41
|
site: flag("url") ?? process.env.CLAVE_URL ?? "https://clave.one",
|
|
36
|
-
org: flag("org"), project: flag("project"),
|
|
42
|
+
org: flag("org"), project: flag("project"),
|
|
37
43
|
yes: has("yes"), open: !has("no-open"), claude: has("no-claude") ? "never" : "auto",
|
|
38
|
-
}
|
|
44
|
+
};
|
|
45
|
+
const run = args[0] === "init" ? init({ ...common, agents: flag("agents") })
|
|
46
|
+
: args[0] === "add" ? add(args[1], args[2], { ...common, unit: flag("unit"), repo: flag("repo") })
|
|
47
|
+
: null;
|
|
48
|
+
if (!run) { console.error(`Unknown command: ${args[0]}\n\n${help}`); process.exit(2); }
|
|
49
|
+
run.catch((e) => { console.error(`clave: ${explain(e)}`); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotlabshq/clave",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"type": "module",
|
|
2
5
|
"description": "Set up clave (clave.one) in a repository: sign in, project, agents and MCP config for Claude Code in one command.",
|
|
3
|
-
"license": "MIT",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://clave.one/docs/getting-started-ai",
|
|
8
|
+
"bin": {
|
|
9
|
+
"clave": "bin/clave.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test/*.test.mjs",
|
|
21
|
+
"typecheck": "node --check bin/clave.mjs && for f in src/*.mjs; do node --check $f || exit 1; done"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/agentsmd.mjs
CHANGED
|
@@ -23,6 +23,18 @@ export function targetFile(cwd) {
|
|
|
23
23
|
return join(cwd, "AGENTS.md");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Dosyadaki clave bloğu: proje anahtarı ve sunucu adları; blok yoksa null. `clave init` buradan "zaten kurulu" der. */
|
|
27
|
+
export function readBlock(path) {
|
|
28
|
+
if (!existsSync(path)) return null;
|
|
29
|
+
const s = readFileSync(path, "utf8");
|
|
30
|
+
const i = s.indexOf(START), j = s.indexOf(END);
|
|
31
|
+
if (i < 0 || j < i) return null;
|
|
32
|
+
const b = s.slice(i, j);
|
|
33
|
+
const project = b.match(/clave project: (\S+)/)?.[1] ?? "";
|
|
34
|
+
const servers = [...b.matchAll(/`(clave-[a-z0-9-]+)`/g)].map((m) => m[1]);
|
|
35
|
+
return { project, servers };
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
export function upsert(path, text) {
|
|
27
39
|
const old = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
28
40
|
const i = old.indexOf(START), j = old.indexOf(END);
|
package/src/config.mjs
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
// MCP ayarını yazar; secret ekrana ve modele hiç gelmez.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// MCP ayarını yazar; secret ekrana ve modele hiç gelmez, ayar dosyalarına da girmez.
|
|
2
|
+
// Secret her zaman git'e girmeyen .env.clave'de (0600); ayar yalnız değişken adını taşır: `${CLAVE_TOKEN_<AGENT>}`.
|
|
3
|
+
// Claude Code varsa `claude mcp add --scope local` (bu makinedeki proje ayarı, repoya girmez), yoksa .mcp.json.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, chmodSync, appendFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
8
|
export const envName = (agent) => `CLAVE_TOKEN_${agent.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
9
9
|
export const serverName = (agent) => `clave-${agent}`;
|
|
10
|
+
/** Ayara yazılan değer: değişken referansı, secret değil. */
|
|
11
|
+
export const bearerRef = (agent) => `Authorization: Bearer \${${envName(agent)}}`;
|
|
10
12
|
|
|
11
13
|
export function hasClaude(run = spawnSync) {
|
|
12
14
|
const r = run("claude", ["--version"], { stdio: "ignore" });
|
|
13
15
|
return !r.error && r.status === 0;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
/** Claude Code'a ekler; aynı adlı eski kayıt varsa önce siler. */
|
|
17
|
-
export function addToClaude({ agent, mcpUrl,
|
|
18
|
+
/** Claude Code'a değişken referansıyla ekler; aynı adlı eski kayıt varsa önce siler. */
|
|
19
|
+
export function addToClaude({ agent, mcpUrl, cwd, run = spawnSync }) {
|
|
18
20
|
const name = serverName(agent);
|
|
19
21
|
run("claude", ["mcp", "remove", "--scope", "local", name], { cwd, stdio: "ignore" });
|
|
20
|
-
const r = run("claude", ["mcp", "add", "--scope", "local", "--transport", "http", name, mcpUrl, "--header",
|
|
22
|
+
const r = run("claude", ["mcp", "add", "--scope", "local", "--transport", "http", name, mcpUrl, "--header", bearerRef(agent)], { cwd, stdio: "ignore" });
|
|
21
23
|
if (r.error || r.status !== 0) throw new Error(`claude mcp add ${name} failed`);
|
|
22
24
|
return name;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
/**
|
|
26
|
-
export function
|
|
27
|
+
/** .mcp.json (değişken adı; commit edilebilir). Var olan başka sunuculara dokunmaz. */
|
|
28
|
+
export function writeMcpJson({ agents, mcpUrl, cwd }) {
|
|
27
29
|
const mcpPath = join(cwd, ".mcp.json");
|
|
28
30
|
const cfg = existsSync(mcpPath) ? JSON.parse(readFileSync(mcpPath, "utf8")) : {};
|
|
29
31
|
cfg.mcpServers ??= {};
|
|
@@ -31,7 +33,11 @@ export function writeFiles({ agents, mcpUrl, cwd }) {
|
|
|
31
33
|
cfg.mcpServers[serverName(agent)] = { type: "http", url: mcpUrl, headers: { Authorization: `Bearer \${${envName(agent)}}` } };
|
|
32
34
|
}
|
|
33
35
|
writeFileSync(mcpPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
36
|
+
return mcpPath;
|
|
37
|
+
}
|
|
34
38
|
|
|
39
|
+
/** .env.clave (secret'lar, 0600) ve .gitignore'a .env.clave. Aynı agent'ın eski satırı yenilenir. */
|
|
40
|
+
export function writeEnv({ agents, cwd }) {
|
|
35
41
|
const envPath = join(cwd, ".env.clave");
|
|
36
42
|
const old = existsSync(envPath) ? readFileSync(envPath, "utf8").split("\n").filter((l) => l && !agents.some((a) => l.startsWith(`${envName(a.agent)}=`))) : [];
|
|
37
43
|
writeFileSync(envPath, [...old, ...agents.map((a) => `${envName(a.agent)}=${a.secret}`)].join("\n") + "\n");
|
|
@@ -40,5 +46,12 @@ export function writeFiles({ agents, mcpUrl, cwd }) {
|
|
|
40
46
|
const gi = join(cwd, ".gitignore");
|
|
41
47
|
const lines = existsSync(gi) ? readFileSync(gi, "utf8").split("\n") : [];
|
|
42
48
|
if (!lines.includes(".env.clave")) appendFileSync(gi, `${lines.length && lines[lines.length - 1] !== "" ? "\n" : ""}.env.clave\n`);
|
|
43
|
-
return
|
|
49
|
+
return envPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** .mcp.json'daki clave sunucuları (clave-*). */
|
|
53
|
+
export function mcpJsonServers(cwd) {
|
|
54
|
+
const p = join(cwd, ".mcp.json");
|
|
55
|
+
if (!existsSync(p)) return [];
|
|
56
|
+
try { return Object.keys(JSON.parse(readFileSync(p, "utf8")).mcpServers ?? {}).filter((s) => s.startsWith("clave-")); } catch { return []; }
|
|
44
57
|
}
|
package/src/init.mjs
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
// `clave init`: bu
|
|
2
|
-
// org → proje → birim → agent'
|
|
1
|
+
// `clave init`: bu repoyu bu makinede clave'e ilk kez bağlar, kişinin kendi kimliğiyle (cihaz girişi):
|
|
2
|
+
// org → proje → birim → kişinin kendi agent'ları ve token'ları → MCP ayarı → AGENTS.md. Token'lar ekrana basılmaz.
|
|
3
|
+
// Repo zaten bağlıysa durur ve `clave add`'e yollar. `clave add unit|project|agent`: var olan kuruluma parça ekler;
|
|
4
|
+
// unit ve project sahip/yönetici ister (koordinatör, kişi token'ıyla), agent kişinin kendisinindir.
|
|
3
5
|
import { spawnSync } from "node:child_process";
|
|
4
6
|
import { hostname } from "node:os";
|
|
5
7
|
import { basename } from "node:path";
|
|
6
|
-
import { client, urls } from "./http.mjs";
|
|
8
|
+
import { ApiError, client, urls } from "./http.mjs";
|
|
7
9
|
import { deviceLogin, openBrowser } from "./login.mjs";
|
|
8
10
|
import { prompter } from "./prompt.mjs";
|
|
9
|
-
import { addToClaude, hasClaude, serverName,
|
|
10
|
-
import { block, targetFile, upsert } from "./agentsmd.mjs";
|
|
11
|
+
import { addToClaude, hasClaude, mcpJsonServers, serverName, writeEnv, writeMcpJson } from "./config.mjs";
|
|
12
|
+
import { block, readBlock, targetFile, upsert } from "./agentsmd.mjs";
|
|
11
13
|
|
|
12
14
|
const NAME = /^[a-z0-9][a-z0-9-]{1,31}$/;
|
|
15
|
+
const MANAGERS = new Set(["owner", "admin"]);
|
|
13
16
|
|
|
14
17
|
/** git remote → "github.com/owner/repo"; remote yoksa klasör adı. */
|
|
15
18
|
export function repoOf(cwd, run = spawnSync) {
|
|
@@ -26,33 +29,103 @@ export function matchProject(projects, repo) {
|
|
|
26
29
|
?? live.find((p) => p.key === repo.name.toLowerCase() || norm(p.name) === norm(repo.name)) ?? null;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
/** Bu repo bu makinede kurulu mu: AGENTS.md/CLAUDE.md bloğu ya da .mcp.json'daki clave sunucuları. */
|
|
33
|
+
export function existingSetup(cwd) {
|
|
34
|
+
const fromBlock = readBlock(targetFile(cwd));
|
|
35
|
+
const servers = [...new Set([...(fromBlock?.servers ?? []), ...mcpJsonServers(cwd)])];
|
|
36
|
+
return fromBlock || servers.length ? { project: fromBlock?.project ?? "", servers } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Giriş ve org seçimi; her komutun ortak başı. `manage`: sahip/yönetici gerekir. */
|
|
40
|
+
async function session(opts, { manage = false } = {}) {
|
|
41
|
+
const { site = "https://clave.one", open = true, yes = false, log = (s) => console.log(s), fetchImpl = fetch, loginInterval } = opts;
|
|
32
42
|
const u = urls(site);
|
|
33
43
|
const ask = opts.prompter ?? prompter({ yes });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const token = opts.token ?? await deviceLogin({ auth: u.auth, log, open: open ? openBrowser : null, fetchImpl, ...(loginInterval ? { interval: loginInterval } : {}) });
|
|
45
|
+
const me = client({ api: u.api, token, fetchImpl });
|
|
46
|
+
const { orgs } = await me("GET", "/orgs");
|
|
47
|
+
const usable = orgs.filter((o) => o.role !== "viewer" && (!manage || MANAGERS.has(o.role)));
|
|
48
|
+
if (usable.length === 0) {
|
|
49
|
+
if (!orgs.length) throw new Error(`You have no org yet. Create one at ${u.site}/start, then run this again.`);
|
|
50
|
+
throw new Error(manage ? "This needs an owner or admin of the org; ask one to run it." : "You are a viewer in every org; ask an owner or admin for the member role.");
|
|
51
|
+
}
|
|
52
|
+
const pick = opts.org ? usable.find((o) => o.slug === opts.org || o.id === opts.org) : null;
|
|
53
|
+
if (opts.org && !pick) throw new Error(`No org "${opts.org}" where you are ${manage ? "owner or admin" : "a member"}.`);
|
|
54
|
+
const org = pick ?? await ask.choose("Org", usable.map((o) => ({ label: `${o.name} (${o.slug})`, ...o })));
|
|
55
|
+
const call = client({ api: u.api, token, org: org.slug, fetchImpl });
|
|
56
|
+
log(`Org: ${org.name}`);
|
|
57
|
+
return { u, ask, log, org, call, manager: MANAGERS.has(org.role) };
|
|
58
|
+
}
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Birim seçimi: var olanlardan biri ya da (sahip/yönetici) listenin sonundaki "+ New unit…" ile yeni birim.
|
|
62
|
+
* Hiç birim yoksa sahip/yönetici açar, üye sahibe yollanır. Dönüş: birim anahtarı.
|
|
63
|
+
*/
|
|
64
|
+
async function pickUnit({ setup, call, ask, log, org, manager, question }) {
|
|
65
|
+
const units = setup.units.filter((x) => !x.archived);
|
|
66
|
+
const NEW = { key: "", name: "+ New unit…", label: "+ New unit…" };
|
|
67
|
+
const options = [...units.map((x) => ({ label: x.name, ...x })), ...(manager ? [NEW] : [])];
|
|
68
|
+
let pick = null;
|
|
69
|
+
if (options.length) pick = await ask.choose(question, options);
|
|
70
|
+
if (pick && pick.key) { log(`Unit: ${pick.name}`); return pick.key; }
|
|
71
|
+
if (!manager) throw new Error(`${org.name} has no unit for this. Ask an owner or admin to run: clave add unit <name>`);
|
|
72
|
+
const name = await ask.ask(units.length ? "New unit name" : "No units yet. Name the team that owns the work", units.length ? "" : "Core team");
|
|
73
|
+
if (!name) throw new Error("A unit name is required.");
|
|
74
|
+
const { key } = await call("POST", "/setup/units", { name });
|
|
75
|
+
log(`Unit created: ${name} (${key})`);
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
48
78
|
|
|
49
|
-
|
|
79
|
+
/** Kişinin kendi agent'ları: kayıt (varsa kendi kaydı kullanılır) ve her birine bir token. Secret yalnız dönüşte. */
|
|
80
|
+
async function registerAgents(call, names, unit, log) {
|
|
81
|
+
const bad = names.find((n) => !NAME.test(n));
|
|
82
|
+
if (bad) throw new Error(`"${bad}" is not a valid agent name: 2–32 lowercase letters, digits, dashes.`);
|
|
83
|
+
const { agents: existing, me } = await call("GET", "/agents");
|
|
84
|
+
const issued = [];
|
|
85
|
+
for (const agent of names) {
|
|
86
|
+
let id = existing.find((a) => a.name === agent && (!a.owner || a.owner === me))?.id;
|
|
87
|
+
if (!id) { ({ id } = await call("POST", "/agents", { name: agent, unit })); log(`Agent registered: ${id}`); }
|
|
88
|
+
const { secret } = await call("POST", `/agents/${encodeURIComponent(id)}/tokens`, { label: hostname().slice(0, 30) });
|
|
89
|
+
issued.push({ agent, id, secret });
|
|
90
|
+
}
|
|
91
|
+
return issued;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** MCP ayarı: secret .env.clave'ye, ayara değişken adı. Claude Code varsa local scope, yoksa .mcp.json. */
|
|
95
|
+
function configure({ issued, u, cwd, useClaude, run, log }) {
|
|
96
|
+
const claudeOk = useClaude === "never" ? false : useClaude === "always" ? true : hasClaude(run);
|
|
97
|
+
const envPath = writeEnv({ agents: issued, cwd });
|
|
98
|
+
let servers;
|
|
99
|
+
if (claudeOk) {
|
|
100
|
+
servers = issued.map(({ agent }) => addToClaude({ agent, mcpUrl: u.mcp, cwd, run }));
|
|
101
|
+
log(`Claude Code: added ${servers.join(", ")} for this project (local scope, by variable name; not in the repository).`);
|
|
102
|
+
} else {
|
|
103
|
+
const mcpPath = writeMcpJson({ agents: issued, mcpUrl: u.mcp, cwd });
|
|
104
|
+
servers = issued.map(({ agent }) => serverName(agent));
|
|
105
|
+
log(`Wrote ${basename(mcpPath)} (variable names only).`);
|
|
106
|
+
log(`Codex: add [mcp_servers.${servers[0]}] url = "${u.mcp}", bearer_token_env_var = "CLAVE_TOKEN_${issued[0].agent.toUpperCase().replace(/[^A-Z0-9]/g, "_")}" to ~/.codex/config.toml`);
|
|
107
|
+
}
|
|
108
|
+
log(`Tokens are in ${basename(envPath)} (git-ignored, 0600). Load it before starting your client: set -a; . ./.env.clave; set +a (or export those variables in your shell profile).`);
|
|
109
|
+
issued.forEach((x) => { x.secret = ""; });
|
|
110
|
+
return { servers, claudeOk };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function init(opts) {
|
|
114
|
+
const { cwd = process.cwd(), claude: useClaude = "auto", run = spawnSync } = opts;
|
|
115
|
+
const done = existingSetup(cwd);
|
|
116
|
+
if (done) {
|
|
117
|
+
throw new Error(`This repository is already set up${done.project ? ` (project ${done.project}` : " ("}${done.servers.length ? `${done.project ? ", " : ""}servers ${done.servers.join(", ")}` : ""}). To add an agent: clave add agent <name>. To add a unit or project to the plan: clave add unit|project <name> (owner or admin).`);
|
|
118
|
+
}
|
|
119
|
+
const { u, ask, log, org, call, manager } = await session(opts);
|
|
120
|
+
try {
|
|
121
|
+
// Proje: repo'dan eşleşen ya da yeni (yeni açmak sahip/yönetici işi).
|
|
50
122
|
const setup = await call("GET", "/setup");
|
|
51
123
|
const repo = repoOf(cwd, run);
|
|
52
124
|
let project = opts.project ? setup.projects.find((p) => p.key === opts.project) : matchProject(setup.projects, repo);
|
|
53
125
|
if (opts.project && !project) throw new Error(`No project "${opts.project}" in ${org.name}.`);
|
|
54
126
|
if (project && !opts.project && !(await ask.confirm(`Use project "${project.name}" (${project.key})?`, true))) project = null;
|
|
55
127
|
if (!project) {
|
|
128
|
+
if (!manager) throw new Error(`No project for this repository in ${org.name}. Ask an owner or admin to run: clave add project ${repo.name}`);
|
|
56
129
|
const name = await ask.ask("New project name", repo.name);
|
|
57
130
|
const lines = setup.lines.filter((l) => !l.archived);
|
|
58
131
|
const { key } = await call("POST", "/setup/projects", { name, repo: repo.remote, line: lines[0]?.key ?? "" });
|
|
@@ -60,59 +133,77 @@ export async function init(opts) {
|
|
|
60
133
|
log(`Project created: ${name} (${key})`);
|
|
61
134
|
} else log(`Project: ${project.name} (${project.key})`);
|
|
62
135
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
let unit;
|
|
66
|
-
if (units.length) {
|
|
67
|
-
const pickU = await ask.choose("Unit the agents work for", units.map((x) => ({ label: x.name, ...x })));
|
|
68
|
-
unit = pickU.key;
|
|
69
|
-
log(`Unit: ${pickU.name}`);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const name = await ask.ask("No units yet. Name the team that owns the work", "Core team");
|
|
73
|
-
unit = (await call("POST", "/setup/units", { name })).key;
|
|
74
|
-
log(`Unit created: ${name}`);
|
|
75
|
-
}
|
|
136
|
+
// Birim: var olanlardan seç ya da (sahip/yönetici) yeni aç.
|
|
137
|
+
const unit = await pickUnit({ setup, call, ask, log, org, manager, question: "Unit your agents work for" });
|
|
76
138
|
if (!setup.lines.some((l) => !l.archived) || !setup.goals.some((g) => !g.archived)) {
|
|
77
139
|
log(`Tip: the plan has no business line or goal yet. Finish it at ${u.site}/app/welcome`);
|
|
78
140
|
}
|
|
79
141
|
|
|
80
|
-
//
|
|
81
|
-
const wanted = (opts.agents ?? await ask.ask("
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const { agents: existing } = await call("GET", "/agents");
|
|
85
|
-
const issued = [];
|
|
86
|
-
for (const agent of wanted) {
|
|
87
|
-
if (!existing.some((a) => a.name === agent)) { await call("POST", "/agents", { name: agent, unit }); log(`Agent registered: ${agent}`); }
|
|
88
|
-
const { secret } = await call("POST", `/agents/${encodeURIComponent(agent)}/tokens`, { label: hostname().slice(0, 30) });
|
|
89
|
-
issued.push({ agent, secret });
|
|
90
|
-
}
|
|
142
|
+
// Kişinin kendi agent'ları ve MCP ayarı
|
|
143
|
+
const wanted = (opts.agents ?? await ask.ask("Your agents to set up (comma-separated)", "coder")).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
144
|
+
const issued = await registerAgents(call, wanted, unit, log);
|
|
145
|
+
const { servers, claudeOk } = configure({ issued, u, cwd, useClaude, run, log });
|
|
91
146
|
|
|
92
|
-
//
|
|
93
|
-
const claudeOk = useClaude === "never" ? false : useClaude === "always" ? true : hasClaude(run);
|
|
94
|
-
let servers;
|
|
95
|
-
if (claudeOk) {
|
|
96
|
-
servers = issued.map(({ agent, secret }) => addToClaude({ agent, mcpUrl: u.mcp, secret, cwd, run }));
|
|
97
|
-
log(`Claude Code: added ${servers.join(", ")} for this project (local scope; not in the repository).`);
|
|
98
|
-
} else {
|
|
99
|
-
const { mcpPath, envPath } = writeFiles({ agents: issued, mcpUrl: u.mcp, cwd });
|
|
100
|
-
servers = issued.map(({ agent }) => serverName(agent));
|
|
101
|
-
log(`Wrote ${basename(mcpPath)} (variable names only) and ${basename(envPath)} (tokens, git-ignored). Load it before starting your client: set -a; . ./.env.clave; set +a`);
|
|
102
|
-
log(`Codex: add [mcp_servers.${servers[0]}] url = "${u.mcp}", bearer_token_env_var = "${envName(issued[0].agent)}" to ~/.codex/config.toml`);
|
|
103
|
-
}
|
|
104
|
-
issued.forEach((x) => { x.secret = ""; });
|
|
105
|
-
|
|
106
|
-
// 7. AGENTS.md
|
|
147
|
+
// AGENTS.md
|
|
107
148
|
const file = targetFile(cwd);
|
|
108
149
|
if (await ask.confirm(`Add the clave block to ${basename(file)}?`, true)) {
|
|
109
150
|
const how = upsert(file, block({ project: project.key, servers, site: u.site }));
|
|
110
151
|
log(`${basename(file)} ${how}.`);
|
|
111
152
|
}
|
|
112
153
|
|
|
113
|
-
log(`\nDone. Start your agent in this repository; its first call is session_start { project: "${project.key}" }.`);
|
|
114
|
-
return { org: org.slug, project: project.key, unit, agents: wanted, servers, claude: claudeOk };
|
|
154
|
+
log(`\nDone. Start your agent in this repository (a new session: servers load at session start); its first call is session_start { project: "${project.key}" }.`);
|
|
155
|
+
return { org: org.slug, project: project.key, unit, agents: wanted, ids: issued.map((x) => x.id), servers, claude: claudeOk };
|
|
115
156
|
} finally {
|
|
116
157
|
ask.close?.();
|
|
117
158
|
}
|
|
118
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `clave add unit|project|agent <name>`: var olan kuruluma ekler.
|
|
163
|
+
* unit/project: sahip/yönetici (koordinatör, kişi token'ıyla). agent: kişinin kendisinin; MCP ayarı ve AGENTS.md bloğu güncellenir.
|
|
164
|
+
*/
|
|
165
|
+
export async function add(kind, name, opts) {
|
|
166
|
+
const { cwd = process.cwd(), claude: useClaude = "auto", run = spawnSync } = opts;
|
|
167
|
+
if (!["unit", "project", "agent"].includes(kind)) throw new Error(`clave add: what to add is unit, project or agent (got "${kind ?? ""}")`);
|
|
168
|
+
if (!name) throw new Error(`clave add ${kind}: give it a name`);
|
|
169
|
+
const { u, ask, log, org, call, manager } = await session(opts, { manage: kind !== "agent" });
|
|
170
|
+
try {
|
|
171
|
+
if (kind === "unit") {
|
|
172
|
+
const { key } = await call("POST", "/setup/units", { name });
|
|
173
|
+
log(`Unit created: ${name} (${key}). People run clave init in their repositories and pick it.`);
|
|
174
|
+
return { org: org.slug, unit: key };
|
|
175
|
+
}
|
|
176
|
+
if (kind === "project") {
|
|
177
|
+
const setup = await call("GET", "/setup");
|
|
178
|
+
const repo = repoOf(cwd, run);
|
|
179
|
+
const lines = setup.lines.filter((l) => !l.archived);
|
|
180
|
+
const { key } = await call("POST", "/setup/projects", { name, repo: opts.repo ?? repo.remote, line: lines[0]?.key ?? "" });
|
|
181
|
+
log(`Project created: ${name} (${key})${repo.remote ? ` for ${repo.remote}` : ""}. People run clave init in that repository.`);
|
|
182
|
+
return { org: org.slug, project: key };
|
|
183
|
+
}
|
|
184
|
+
// agent: birim kuruludan ya da --unit; kurulum yoksa da çalışır (yalnız MCP ayarı yazılır).
|
|
185
|
+
const done = existingSetup(cwd);
|
|
186
|
+
const setup = await call("GET", "/setup");
|
|
187
|
+
let unit = opts.unit;
|
|
188
|
+
if (unit && !setup.units.some((x) => x.key === unit && !x.archived)) throw new Error(`No unit "${unit}" in ${org.name}.`);
|
|
189
|
+
if (!unit) unit = await pickUnit({ setup, call, ask, log, org, manager, question: "Unit the agent works for" });
|
|
190
|
+
const issued = await registerAgents(call, [name.toLowerCase()], unit, log);
|
|
191
|
+
const { servers, claudeOk } = configure({ issued, u, cwd, useClaude, run, log });
|
|
192
|
+
const project = opts.project ?? done?.project ?? "";
|
|
193
|
+
if (project) {
|
|
194
|
+
const all = [...new Set([...(done?.servers ?? []), ...servers])];
|
|
195
|
+
upsert(targetFile(cwd), block({ project, servers: all, site: u.site }));
|
|
196
|
+
log(`${basename(targetFile(cwd))} updated (${all.join(", ")}).`);
|
|
197
|
+
}
|
|
198
|
+
log(`\nDone. Open a new session to use ${servers[0]} (a running session does not pick up new servers).`);
|
|
199
|
+
return { org: org.slug, agent: issued[0].id, servers, claude: claudeOk };
|
|
200
|
+
} finally {
|
|
201
|
+
ask.close?.();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** API hatasını kullanıcının diline çevirir (bin). */
|
|
206
|
+
export function explain(e) {
|
|
207
|
+
if (e instanceof ApiError && e.status === 403) return `${e.message} (this needs an owner or admin; a member manages only their own agents)`;
|
|
208
|
+
return e.message;
|
|
209
|
+
}
|
package/src/login.mjs
CHANGED
|
@@ -5,7 +5,14 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
export function openBrowser(url) {
|
|
6
6
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
7
7
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
8
|
-
|
|
8
|
+
// Tarayıcı yoksa (container, SSH) spawn ENOENT'i senkron değil `error` olayıyla verir: dinlenmezse süreç düşer.
|
|
9
|
+
// Bağlantı zaten ekranda; açılamaması hata değildir.
|
|
10
|
+
try {
|
|
11
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
12
|
+
child.on("error", () => {});
|
|
13
|
+
child.unref();
|
|
14
|
+
return true;
|
|
15
|
+
} catch { return false; }
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export async function deviceLogin({ auth, log, open = openBrowser, fetchImpl = fetch, interval = 2000, timeout = 600_000 }) {
|