@dotlabshq/clave 0.0.0-stage → 0.2.1

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
@@ -1,3 +1,54 @@
1
- # Temporary Holding Version
1
+ # @dotlabshq/clave
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect a repository to [clave](https://clave.one), the plan and work tracker for people and AI agents, in one command.
4
+
5
+ ```bash
6
+ npx @dotlabshq/clave init
7
+ ```
8
+
9
+ It runs in your terminal, in the repository, the first time you set it up on this machine:
10
+
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** 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.
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
+ 6. **Adds a clave block** to `AGENTS.md` or `CLAUDE.md` (it asks first), so every agent session starts with `session_start`.
17
+
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`).
37
+
38
+ ## Options
39
+
40
+ | Option | |
41
+ |---|---|
42
+ | `--org <slug>` | Org to use (default: ask) |
43
+ | `--project <key>` | Existing project key (default: match this repository) |
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 |
47
+ | `--yes` | Accept the defaults without asking |
48
+ | `--no-open` | Print the sign-in link instead of opening the browser |
49
+ | `--no-claude` | Write `.mcp.json` instead of using the `claude` CLI (`.env.clave` is written either way) |
50
+ | `--url <site>` | clave site (default `https://clave.one`, or `CLAVE_URL`) |
51
+
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.
53
+
54
+ Guide for AI agents: https://clave.one/getting-started-ai.md
package/bin/clave.mjs ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
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.
4
+ import { readFileSync } from "node:fs";
5
+ import { add, explain, init } from "../src/init.mjs";
6
+
7
+ const args = process.argv.slice(2);
8
+ const flag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
9
+ const has = (name) => args.includes(`--${name}`);
10
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11
+
12
+ const help = `clave ${version}: connect this repository to clave (https://clave.one)
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)
18
+
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>}).
24
+
25
+ Options:
26
+ --org <slug> Org to use (default: ask)
27
+ --project <key> Existing project key (default: match this repository)
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
31
+ --yes Accept the defaults without asking
32
+ --no-open Don't open the browser; print the sign-in link only
33
+ --no-claude Don't use the claude CLI; write .mcp.json
34
+ --url <site> clave site (default: https://clave.one)
35
+ -h, --help This help
36
+ -v, --version The version`;
37
+
38
+ if (has("help") || args.includes("-h") || args.length === 0) { console.log(help); process.exit(0); }
39
+ if (has("version") || args.includes("-v")) { console.log(version); process.exit(0); }
40
+ const common = {
41
+ site: flag("url") ?? process.env.CLAVE_URL ?? "https://clave.one",
42
+ org: flag("org"), project: flag("project"),
43
+ yes: has("yes"), open: !has("no-open"), claude: has("no-claude") ? "never" : "auto",
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,6 +1,23 @@
1
1
  {
2
2
  "name": "@dotlabshq/clave",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "description": "Set up clave (clave.one) in a repository: sign in, project, agents and MCP config for Claude Code in one command.",
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
+ }
6
23
  }
@@ -0,0 +1,44 @@
1
+ // AGENTS.md / CLAUDE.md'ye clave bloğu: her oturum aynı başlasın. Varsa blok yenilenir.
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ const START = "<!-- clave:start -->", END = "<!-- clave:end -->";
6
+
7
+ export function block({ project, servers, site }) {
8
+ return `${START}
9
+ ## Task management: clave
10
+
11
+ - clave project: ${project}
12
+ - MCP server${servers.length > 1 ? "s" : ""}: ${servers.map((s) => `\`${s}\``).join(", ")} (${site}/v1/mcp). Setup guide: ${site}/getting-started-ai.md
13
+ - Start every session with \`session_start { project: "${project}" }\`: read the brief, the rules (binding) and the gotchas.
14
+ - Before starting work, find or create its item in clave; claim it, report progress, finish with evidence.
15
+ - Ask before risky steps with \`request_approval\` (production, deletes, external publishing). Leave what the next session needs with \`remember\`.
16
+ ${END}
17
+ `;
18
+ }
19
+
20
+ /** Hangi dosya: CLAUDE.md varsa o, yoksa AGENTS.md (yoksa oluşturulur). */
21
+ export function targetFile(cwd) {
22
+ for (const f of ["AGENTS.md", "CLAUDE.md"]) if (existsSync(join(cwd, f))) return join(cwd, f);
23
+ return join(cwd, "AGENTS.md");
24
+ }
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
+
38
+ export function upsert(path, text) {
39
+ const old = existsSync(path) ? readFileSync(path, "utf8") : "";
40
+ const i = old.indexOf(START), j = old.indexOf(END);
41
+ const next = i >= 0 && j > i ? old.slice(0, i) + text.trimEnd() + old.slice(j + END.length) : `${old}${old && !old.endsWith("\n\n") ? (old.endsWith("\n") ? "\n" : "\n\n") : ""}${text}`;
42
+ writeFileSync(path, next);
43
+ return old ? "updated" : "created";
44
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,57 @@
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
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, readFileSync, writeFileSync, chmodSync, appendFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ export const envName = (agent) => `CLAVE_TOKEN_${agent.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
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)}}`;
12
+
13
+ export function hasClaude(run = spawnSync) {
14
+ const r = run("claude", ["--version"], { stdio: "ignore" });
15
+ return !r.error && r.status === 0;
16
+ }
17
+
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 }) {
20
+ const name = serverName(agent);
21
+ run("claude", ["mcp", "remove", "--scope", "local", name], { cwd, stdio: "ignore" });
22
+ const r = run("claude", ["mcp", "add", "--scope", "local", "--transport", "http", name, mcpUrl, "--header", bearerRef(agent)], { cwd, stdio: "ignore" });
23
+ if (r.error || r.status !== 0) throw new Error(`claude mcp add ${name} failed`);
24
+ return name;
25
+ }
26
+
27
+ /** .mcp.json (değişken adı; commit edilebilir). Var olan başka sunuculara dokunmaz. */
28
+ export function writeMcpJson({ agents, mcpUrl, cwd }) {
29
+ const mcpPath = join(cwd, ".mcp.json");
30
+ const cfg = existsSync(mcpPath) ? JSON.parse(readFileSync(mcpPath, "utf8")) : {};
31
+ cfg.mcpServers ??= {};
32
+ for (const { agent } of agents) {
33
+ cfg.mcpServers[serverName(agent)] = { type: "http", url: mcpUrl, headers: { Authorization: `Bearer \${${envName(agent)}}` } };
34
+ }
35
+ writeFileSync(mcpPath, JSON.stringify(cfg, null, 2) + "\n");
36
+ return mcpPath;
37
+ }
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 }) {
41
+ const envPath = join(cwd, ".env.clave");
42
+ const old = existsSync(envPath) ? readFileSync(envPath, "utf8").split("\n").filter((l) => l && !agents.some((a) => l.startsWith(`${envName(a.agent)}=`))) : [];
43
+ writeFileSync(envPath, [...old, ...agents.map((a) => `${envName(a.agent)}=${a.secret}`)].join("\n") + "\n");
44
+ chmodSync(envPath, 0o600);
45
+
46
+ const gi = join(cwd, ".gitignore");
47
+ const lines = existsSync(gi) ? readFileSync(gi, "utf8").split("\n") : [];
48
+ if (!lines.includes(".env.clave")) appendFileSync(gi, `${lines.length && lines[lines.length - 1] !== "" ? "\n" : ""}.env.clave\n`);
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 []; }
57
+ }
package/src/http.mjs ADDED
@@ -0,0 +1,23 @@
1
+ // clave'in REST'i: site adresi + /v1/mcp (Flect önek siler; yerelde web aynı yolu API'ye geçirir).
2
+ // Kimlik cihaz akışından gelen kişi token'ı; org'a bağlı çağrılar X-Org-Id ile gider.
3
+
4
+ export class ApiError extends Error {
5
+ constructor(status, code, message) { super(message || code); this.status = status; this.code = code; }
6
+ }
7
+
8
+ export function urls(site) {
9
+ const base = site.replace(/\/+$/, "");
10
+ return { site: base, api: `${base}/v1/mcp`, auth: `${base}/v1/auth`, mcp: `${base}/v1/mcp` };
11
+ }
12
+
13
+ export function client({ api, token, org = null, fetchImpl = fetch }) {
14
+ return async function call(method, path, body) {
15
+ const headers = { authorization: `Bearer ${token}`, accept: "application/json" };
16
+ if (org) headers["x-org-id"] = org;
17
+ if (body !== undefined) headers["content-type"] = "application/json";
18
+ const r = await fetchImpl(`${api}${path}`, { method, headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}) });
19
+ const data = await r.json().catch(() => ({}));
20
+ if (!r.ok) throw new ApiError(r.status, data.error ?? `http_${r.status}`, data.message ?? data.error);
21
+ return data;
22
+ };
23
+ }
package/src/init.mjs ADDED
@@ -0,0 +1,202 @@
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.
5
+ import { spawnSync } from "node:child_process";
6
+ import { hostname } from "node:os";
7
+ import { basename } from "node:path";
8
+ import { ApiError, client, urls } from "./http.mjs";
9
+ import { deviceLogin, openBrowser } from "./login.mjs";
10
+ import { prompter } from "./prompt.mjs";
11
+ import { addToClaude, hasClaude, mcpJsonServers, serverName, writeEnv, writeMcpJson } from "./config.mjs";
12
+ import { block, readBlock, targetFile, upsert } from "./agentsmd.mjs";
13
+
14
+ const NAME = /^[a-z0-9][a-z0-9-]{1,31}$/;
15
+ const MANAGERS = new Set(["owner", "admin"]);
16
+
17
+ /** git remote → "github.com/owner/repo"; remote yoksa klasör adı. */
18
+ export function repoOf(cwd, run = spawnSync) {
19
+ const r = run("git", ["config", "--get", "remote.origin.url"], { cwd, encoding: "utf8" });
20
+ const raw = !r.error && r.status === 0 ? String(r.stdout).trim() : "";
21
+ const m = raw.match(/^(?:https?:\/\/|ssh:\/\/)?(?:[^@/]+@)?([^/:]+)[/:](.+?)(?:\.git)?\/?$/);
22
+ return { remote: m ? `${m[1]}/${m[2]}` : "", name: m ? basename(m[2]) : basename(cwd) };
23
+ }
24
+
25
+ export function matchProject(projects, repo) {
26
+ const live = projects.filter((p) => !p.archived);
27
+ const norm = (s) => String(s ?? "").toLowerCase().replace(/\.git$/, "").replace(/\/+$/, "");
28
+ return live.find((p) => repo.remote && p.repo && (norm(repo.remote).endsWith(norm(p.repo)) || norm(p.repo).endsWith(norm(repo.remote))))
29
+ ?? live.find((p) => p.key === repo.name.toLowerCase() || norm(p.name) === norm(repo.name)) ?? null;
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;
42
+ const u = urls(site);
43
+ const ask = opts.prompter ?? prompter({ yes });
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
+ }
59
+
60
+ /** Kişinin kendi agent'ları: kayıt (varsa kendi kaydı kullanılır) ve her birine bir token. Secret yalnız dönüşte. */
61
+ async function registerAgents(call, names, unit, log) {
62
+ const bad = names.find((n) => !NAME.test(n));
63
+ if (bad) throw new Error(`"${bad}" is not a valid agent name: 2–32 lowercase letters, digits, dashes.`);
64
+ const { agents: existing, me } = await call("GET", "/agents");
65
+ const issued = [];
66
+ for (const agent of names) {
67
+ let id = existing.find((a) => a.name === agent && (!a.owner || a.owner === me))?.id;
68
+ if (!id) { ({ id } = await call("POST", "/agents", { name: agent, unit })); log(`Agent registered: ${id}`); }
69
+ const { secret } = await call("POST", `/agents/${encodeURIComponent(id)}/tokens`, { label: hostname().slice(0, 30) });
70
+ issued.push({ agent, id, secret });
71
+ }
72
+ return issued;
73
+ }
74
+
75
+ /** MCP ayarı: secret .env.clave'ye, ayara değişken adı. Claude Code varsa local scope, yoksa .mcp.json. */
76
+ function configure({ issued, u, cwd, useClaude, run, log }) {
77
+ const claudeOk = useClaude === "never" ? false : useClaude === "always" ? true : hasClaude(run);
78
+ const envPath = writeEnv({ agents: issued, cwd });
79
+ let servers;
80
+ if (claudeOk) {
81
+ servers = issued.map(({ agent }) => addToClaude({ agent, mcpUrl: u.mcp, cwd, run }));
82
+ log(`Claude Code: added ${servers.join(", ")} for this project (local scope, by variable name; not in the repository).`);
83
+ } else {
84
+ const mcpPath = writeMcpJson({ agents: issued, mcpUrl: u.mcp, cwd });
85
+ servers = issued.map(({ agent }) => serverName(agent));
86
+ log(`Wrote ${basename(mcpPath)} (variable names only).`);
87
+ 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`);
88
+ }
89
+ 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).`);
90
+ issued.forEach((x) => { x.secret = ""; });
91
+ return { servers, claudeOk };
92
+ }
93
+
94
+ export async function init(opts) {
95
+ const { cwd = process.cwd(), claude: useClaude = "auto", run = spawnSync } = opts;
96
+ const done = existingSetup(cwd);
97
+ if (done) {
98
+ 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).`);
99
+ }
100
+ const { u, ask, log, org, call, manager } = await session(opts);
101
+ try {
102
+ // Proje: repo'dan eşleşen ya da yeni (yeni açmak sahip/yönetici işi).
103
+ const setup = await call("GET", "/setup");
104
+ const repo = repoOf(cwd, run);
105
+ let project = opts.project ? setup.projects.find((p) => p.key === opts.project) : matchProject(setup.projects, repo);
106
+ if (opts.project && !project) throw new Error(`No project "${opts.project}" in ${org.name}.`);
107
+ if (project && !opts.project && !(await ask.confirm(`Use project "${project.name}" (${project.key})?`, true))) project = null;
108
+ if (!project) {
109
+ 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}`);
110
+ const name = await ask.ask("New project name", repo.name);
111
+ const lines = setup.lines.filter((l) => !l.archived);
112
+ const { key } = await call("POST", "/setup/projects", { name, repo: repo.remote, line: lines[0]?.key ?? "" });
113
+ project = { key, name };
114
+ log(`Project created: ${name} (${key})`);
115
+ } else log(`Project: ${project.name} (${project.key})`);
116
+
117
+ // Birim: var olanlardan seç; yoksa sahip/yönetici açar.
118
+ const units = setup.units.filter((x) => !x.archived);
119
+ let unit;
120
+ if (units.length) {
121
+ const pickU = await ask.choose("Unit your agents work for", units.map((x) => ({ label: x.name, ...x })));
122
+ unit = pickU.key;
123
+ log(`Unit: ${pickU.name}`);
124
+ } else {
125
+ if (!manager) throw new Error(`${org.name} has no unit yet. Ask an owner or admin to run: clave add unit <name>`);
126
+ const name = await ask.ask("No units yet. Name the team that owns the work", "Core team");
127
+ unit = (await call("POST", "/setup/units", { name })).key;
128
+ log(`Unit created: ${name}`);
129
+ }
130
+ if (!setup.lines.some((l) => !l.archived) || !setup.goals.some((g) => !g.archived)) {
131
+ log(`Tip: the plan has no business line or goal yet. Finish it at ${u.site}/app/welcome`);
132
+ }
133
+
134
+ // Kişinin kendi agent'ları ve MCP ayarı
135
+ const wanted = (opts.agents ?? await ask.ask("Your agents to set up (comma-separated)", "coder")).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
136
+ const issued = await registerAgents(call, wanted, unit, log);
137
+ const { servers, claudeOk } = configure({ issued, u, cwd, useClaude, run, log });
138
+
139
+ // AGENTS.md
140
+ const file = targetFile(cwd);
141
+ if (await ask.confirm(`Add the clave block to ${basename(file)}?`, true)) {
142
+ const how = upsert(file, block({ project: project.key, servers, site: u.site }));
143
+ log(`${basename(file)} ${how}.`);
144
+ }
145
+
146
+ 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}" }.`);
147
+ return { org: org.slug, project: project.key, unit, agents: wanted, ids: issued.map((x) => x.id), servers, claude: claudeOk };
148
+ } finally {
149
+ ask.close?.();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * `clave add unit|project|agent <name>`: var olan kuruluma ekler.
155
+ * unit/project: sahip/yönetici (koordinatör, kişi token'ıyla). agent: kişinin kendisinin; MCP ayarı ve AGENTS.md bloğu güncellenir.
156
+ */
157
+ export async function add(kind, name, opts) {
158
+ const { cwd = process.cwd(), claude: useClaude = "auto", run = spawnSync } = opts;
159
+ if (!["unit", "project", "agent"].includes(kind)) throw new Error(`clave add: what to add is unit, project or agent (got "${kind ?? ""}")`);
160
+ if (!name) throw new Error(`clave add ${kind}: give it a name`);
161
+ const { u, ask, log, org, call } = await session(opts, { manage: kind !== "agent" });
162
+ try {
163
+ if (kind === "unit") {
164
+ const { key } = await call("POST", "/setup/units", { name });
165
+ log(`Unit created: ${name} (${key}). People run clave init in their repositories and pick it.`);
166
+ return { org: org.slug, unit: key };
167
+ }
168
+ if (kind === "project") {
169
+ const setup = await call("GET", "/setup");
170
+ const repo = repoOf(cwd, run);
171
+ const lines = setup.lines.filter((l) => !l.archived);
172
+ const { key } = await call("POST", "/setup/projects", { name, repo: opts.repo ?? repo.remote, line: lines[0]?.key ?? "" });
173
+ log(`Project created: ${name} (${key})${repo.remote ? ` for ${repo.remote}` : ""}. People run clave init in that repository.`);
174
+ return { org: org.slug, project: key };
175
+ }
176
+ // agent: birim kuruludan ya da --unit; kurulum yoksa da çalışır (yalnız MCP ayarı yazılır).
177
+ const done = existingSetup(cwd);
178
+ const setup = await call("GET", "/setup");
179
+ const units = setup.units.filter((x) => !x.archived);
180
+ let unit = opts.unit;
181
+ if (unit && !units.some((x) => x.key === unit)) throw new Error(`No unit "${unit}" in ${org.name}.`);
182
+ if (!unit && units.length) { const pickU = await ask.choose("Unit the agent works for", units.map((x) => ({ label: x.name, ...x }))); unit = pickU.key; }
183
+ const issued = await registerAgents(call, [name.toLowerCase()], unit ?? "", log);
184
+ const { servers, claudeOk } = configure({ issued, u, cwd, useClaude, run, log });
185
+ const project = opts.project ?? done?.project ?? "";
186
+ if (project) {
187
+ const all = [...new Set([...(done?.servers ?? []), ...servers])];
188
+ upsert(targetFile(cwd), block({ project, servers: all, site: u.site }));
189
+ log(`${basename(targetFile(cwd))} updated (${all.join(", ")}).`);
190
+ }
191
+ log(`\nDone. Open a new session to use ${servers[0]} (a running session does not pick up new servers).`);
192
+ return { org: org.slug, agent: issued[0].id, servers, claude: claudeOk };
193
+ } finally {
194
+ ask.close?.();
195
+ }
196
+ }
197
+
198
+ /** API hatasını kullanıcının diline çevirir (bin). */
199
+ export function explain(e) {
200
+ if (e instanceof ApiError && e.status === 403) return `${e.message} (this needs an owner or admin; a member manages only their own agents)`;
201
+ return e.message;
202
+ }
package/src/login.mjs ADDED
@@ -0,0 +1,32 @@
1
+ // Tarayıcıyla giriş: auth-service'in CLI cihaz akışı. /start bir onay adresi verir, kişi tarayıcıda clave'e
2
+ // girip onaylar, /poll kişi token'ını döner (24 saat). Token yalnız bellekte tutulur, diske yazılmaz.
3
+ import { spawn } from "node:child_process";
4
+
5
+ export function openBrowser(url) {
6
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
7
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
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; }
16
+ }
17
+
18
+ export async function deviceLogin({ auth, log, open = openBrowser, fetchImpl = fetch, interval = 2000, timeout = 600_000 }) {
19
+ const r = await fetchImpl(`${auth}/start`);
20
+ if (!r.ok) throw new Error(`Sign-in could not start (HTTP ${r.status})`);
21
+ const { state, url } = await r.json();
22
+ log(`Sign in to clave in your browser:\n ${url}`);
23
+ if (open) open(url);
24
+ const until = Date.now() + timeout;
25
+ while (Date.now() < until) {
26
+ await new Promise((res) => setTimeout(res, interval));
27
+ const p = await fetchImpl(`${auth}/poll/${encodeURIComponent(state)}`).then((x) => x.json()).catch(() => ({ status: "pending" }));
28
+ if (p.status === "done" && p.token) return p.token;
29
+ if (p.status === "expired") throw new Error("The sign-in link expired. Run the command again.");
30
+ }
31
+ throw new Error("Timed out waiting for the sign-in.");
32
+ }
package/src/prompt.mjs ADDED
@@ -0,0 +1,31 @@
1
+ // Soru-cevap: --yes ile varsayılanlar, yoksa terminalden.
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin, stdout } from "node:process";
4
+
5
+ export function prompter({ yes = false, input = stdin, output = stdout } = {}) {
6
+ let rl = null;
7
+ const line = async (q) => {
8
+ rl ??= createInterface({ input, output });
9
+ return (await rl.question(q)).trim();
10
+ };
11
+ return {
12
+ async ask(q, def = "") {
13
+ if (yes) return def;
14
+ const a = await line(def ? `${q} [${def}]: ` : `${q}: `);
15
+ return a || def;
16
+ },
17
+ async confirm(q, def = true) {
18
+ if (yes) return def;
19
+ const a = (await line(`${q} [${def ? "Y/n" : "y/N"}]: `)).toLowerCase();
20
+ return a ? a.startsWith("y") : def;
21
+ },
22
+ async choose(q, options, defIndex = 0) {
23
+ if (yes || options.length === 1) return options[defIndex];
24
+ options.forEach((o, i) => output.write(` ${i + 1}) ${o.label}\n`));
25
+ const a = await line(`${q} [${defIndex + 1}]: `);
26
+ const n = Number(a || defIndex + 1);
27
+ return options[Number.isInteger(n) && n >= 1 && n <= options.length ? n - 1 : defIndex];
28
+ },
29
+ close() { rl?.close(); },
30
+ };
31
+ }