syncstaff-mcp 0.2.3 → 0.2.4

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
@@ -9,7 +9,7 @@ driver, and arbiter SDK are not part of this package.
9
9
  Run this from the repository you want your agent to edit:
10
10
 
11
11
  ```bash
12
- npx -y syncstaff-mcp@0.2.3 setup
12
+ npx -y syncstaff-mcp@0.2.4 setup
13
13
  ```
14
14
 
15
15
  It uses the folder name as the project and defaults to the hosted server, so
@@ -28,7 +28,7 @@ when it finishes.
28
28
  From the repository the agent will edit, start a device login:
29
29
 
30
30
  ```bash
31
- npx -y syncstaff-mcp@0.2.3 login \
31
+ npx -y syncstaff-mcp@0.2.4 login \
32
32
  --server https://api.syncstaff.ai \
33
33
  --project your-project \
34
34
  --vendor codex
@@ -36,8 +36,8 @@ npx -y syncstaff-mcp@0.2.3 login \
36
36
 
37
37
  Open the displayed link, approve the short code, and leave the command running
38
38
  until it confirms the connection. Syncstaff stores the returned credential in
39
- `.keel/config.json` with owner-only permissions and never prints the token.
40
- The repository's `.keel/` directory should remain ignored by Git.
39
+ `.syncstaff/config.json` with owner-only permissions and never prints the token.
40
+ The repository's `.syncstaff/` directory should remain ignored by Git.
41
41
 
42
42
  ## MCP configuration
43
43
 
@@ -49,7 +49,7 @@ that checkout for call sites and unleased writes:
49
49
  "mcpServers": {
50
50
  "sync": {
51
51
  "command": "npx",
52
- "args": ["-y", "syncstaff-mcp@0.2.3"]
52
+ "args": ["-y", "syncstaff-mcp@0.2.4"]
53
53
  }
54
54
  }
55
55
  }
@@ -75,7 +75,7 @@ so the server and adapter can be deployed in either order.
75
75
 
76
76
  ## Release verification
77
77
 
78
- From `keel/`, run:
78
+ From `sync/`, run:
79
79
 
80
80
  ```bash
81
81
  npm run test:mcp-package
@@ -1,7 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { keelEnv } from "./env-compat.js";
5
4
  /** Resolve the git root used by both the MCP process and lifecycle hooks. */
6
5
  export function repoRoot(cwd = process.cwd()) {
7
6
  try {
@@ -22,14 +21,14 @@ const slug = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 160);
22
21
  * Every supported client exports one to its child processes, which is what
23
22
  * lets a hook process and an MCP process for the *same* session agree on an
24
23
  * identity without either being told: they read the same variable. The
25
- * generic `CHARTER_SESSION_ID` wins so a harness can pin one explicitly.
24
+ * generic `SYNCSTAFF_SESSION_ID` wins so a harness can pin one explicitly.
26
25
  *
27
26
  * This must be consulted for **every** vendor. It previously read only
28
27
  * `CODEX_THREAD_ID`, which is why Codex was the only client whose parallel
29
28
  * sessions were ever isolated — see the note on `stateFile`.
30
29
  */
31
30
  export function vendorSessionId() {
32
- return (process.env.CHARTER_SESSION_ID ||
31
+ return (process.env.SYNCSTAFF_SESSION_ID ||
33
32
  process.env.CODEX_THREAD_ID ||
34
33
  process.env.CLAUDE_CODE_SESSION_ID ||
35
34
  undefined);
@@ -51,8 +50,8 @@ export function vendorSessionId() {
51
50
  * null or undefined is what that bug looked like, so prefer
52
51
  * :func:`vendorSessionId` over passing nothing.
53
52
  */
54
- export function stateFile(root = repoRoot(), sessionId = vendorSessionId(), vendor = process.env.CHARTER_VENDOR) {
55
- const explicit = keelEnv("STATE_FILE");
53
+ export function stateFile(root = repoRoot(), sessionId = vendorSessionId(), vendor = process.env.SYNCSTAFF_VENDOR) {
54
+ const explicit = process.env.SYNCSTAFF_STATE_FILE;
56
55
  if (explicit)
57
56
  return explicit;
58
57
  const parts = ["state"];
@@ -60,7 +59,7 @@ export function stateFile(root = repoRoot(), sessionId = vendorSessionId(), vend
60
59
  parts.push(slug(vendor));
61
60
  if (sessionId)
62
61
  parts.push(slug(sessionId));
63
- return join(root, ".keel", `${parts.join("-")}.json`);
62
+ return join(root, ".syncstaff", `${parts.join("-")}.json`);
64
63
  }
65
64
  /**
66
65
  * The state file to *read*, tolerating identities written before sessions were
@@ -84,8 +83,8 @@ export function stateFile(root = repoRoot(), sessionId = vendorSessionId(), vend
84
83
  * prevent. A missing scoped file now simply means "not registered under this
85
84
  * session yet"; the caller re-registers, which is cheap and correct.
86
85
  */
87
- export function resolveStateFile(root = repoRoot(), sessionId = vendorSessionId(), vendor = process.env.CHARTER_VENDOR) {
88
- const explicit = keelEnv("STATE_FILE");
86
+ export function resolveStateFile(root = repoRoot(), sessionId = vendorSessionId(), vendor = process.env.SYNCSTAFF_VENDOR) {
87
+ const explicit = process.env.SYNCSTAFF_STATE_FILE;
89
88
  if (explicit)
90
89
  return explicit;
91
90
  const scoped = stateFile(root, sessionId, vendor);
package/dist/lib/blast.js CHANGED
@@ -14,7 +14,7 @@ const MAX_REPORTED_SITES = 20_000;
14
14
  const MAX_CONTEXT_CHARS = 200;
15
15
  const SKIP_DIRS = new Set([
16
16
  "node_modules", ".git", "dist", "build", "out", ".venv", "venv",
17
- "__pycache__", ".next", ".turbo", "coverage", ".keel",
17
+ "__pycache__", ".next", ".turbo", "coverage", ".syncstaff",
18
18
  ]);
19
19
  /**
20
20
  * Files git is tracking, for resolving call sites against.
@@ -221,7 +221,7 @@ export function attachClientCallSiteReports(repoPath, interfaces, options = {})
221
221
  // now resolve through one function (R12.0), because two analyzers that
222
222
  // answer "what references this symbol" differently is how an agent learns
223
223
  // to trust neither. Before this, declaring an intent over a constant found
224
- // only what grep found — `KEEL_RELEASE_VERSION(` appears nowhere, so a
224
+ // only what grep found — `SYNCSTAFF_RELEASE_VERSION(` appears nowhere, so a
225
225
  // change to a constant four modules read produced an empty confirmed set.
226
226
  //
227
227
  // Import lines are the one kind left out, and the reason is not tidiness.
@@ -261,12 +261,12 @@ export function attachClientCallSiteReports(repoPath, interfaces, options = {})
261
261
  /**
262
262
  * Analyzers whose reports this server will read.
263
263
  *
264
- * "grep-v1" is the original. "keel-graph-v1(...)" carries a parser signature,
265
- * and a union report is "keel-graph-v1(...)+grep-v1". Matching by prefix means
264
+ * "grep-v1" is the original. "syncstaff-graph-v1(...)" carries a parser signature,
265
+ * and a union report is "syncstaff-graph-v1(...)+grep-v1". Matching by prefix means
266
266
  * a client with an upgraded parser is not rejected by an older server, which
267
267
  * is the failure mode that makes an agent's analysis silently disappear.
268
268
  */
269
- const KNOWN_ANALYZER_PREFIXES = ["grep-v1", "keel-graph-v1"];
269
+ const KNOWN_ANALYZER_PREFIXES = ["grep-v1", "syncstaff-graph-v1"];
270
270
  function isKnownAnalyzer(analyzer) {
271
271
  return (typeof analyzer === "string" &&
272
272
  KNOWN_ANALYZER_PREFIXES.some((prefix) => analyzer.startsWith(prefix)));
@@ -1,19 +1,19 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  function configPath(root) {
4
- return join(root, ".keel", "config.json");
4
+ return join(root, ".syncstaff", "config.json");
5
5
  }
6
6
  function validate(value, path) {
7
7
  if (!value || typeof value !== "object" || Array.isArray(value)) {
8
- throw new Error(`invalid Keel client config at ${path}: expected an object`);
8
+ throw new Error(`invalid Syncstaff client config at ${path}: expected an object`);
9
9
  }
10
10
  const candidate = value;
11
11
  if (candidate.version !== 1) {
12
- throw new Error(`invalid Keel client config at ${path}: unsupported version`);
12
+ throw new Error(`invalid Syncstaff client config at ${path}: unsupported version`);
13
13
  }
14
14
  for (const key of ["server", "project", "token", "vendor"]) {
15
15
  if (typeof candidate[key] !== "string" || !candidate[key].trim()) {
16
- throw new Error(`invalid Keel client config at ${path}: ${key} is required`);
16
+ throw new Error(`invalid Syncstaff client config at ${path}: ${key} is required`);
17
17
  }
18
18
  }
19
19
  let server;
@@ -21,16 +21,16 @@ function validate(value, path) {
21
21
  server = new URL(candidate.server);
22
22
  }
23
23
  catch {
24
- throw new Error(`invalid Keel client config at ${path}: server must be an absolute URL`);
24
+ throw new Error(`invalid Syncstaff client config at ${path}: server must be an absolute URL`);
25
25
  }
26
26
  if (!["http:", "https:"].includes(server.protocol) || server.username || server.password) {
27
- throw new Error(`invalid Keel client config at ${path}: server must be an HTTP(S) URL without credentials`);
27
+ throw new Error(`invalid Syncstaff client config at ${path}: server must be an HTTP(S) URL without credentials`);
28
28
  }
29
29
  if (server.search || server.hash) {
30
- throw new Error(`invalid Keel client config at ${path}: server must not contain a query or fragment`);
30
+ throw new Error(`invalid Syncstaff client config at ${path}: server must not contain a query or fragment`);
31
31
  }
32
32
  if (!/^ch_[A-Za-z0-9_-]+$/.test(candidate.token)) {
33
- throw new Error(`invalid Keel client config at ${path}: token has an unexpected format`);
33
+ throw new Error(`invalid Syncstaff client config at ${path}: token has an unexpected format`);
34
34
  }
35
35
  return {
36
36
  version: 1,
@@ -40,7 +40,7 @@ function validate(value, path) {
40
40
  vendor: candidate.vendor.trim(),
41
41
  };
42
42
  }
43
- /** Load repo-local adapter credentials, or null before `keel login`. */
43
+ /** Load repo-local adapter credentials, or null before `syncstaff login`. */
44
44
  export function loadClientConfig(root) {
45
45
  const path = configPath(root);
46
46
  if (!existsSync(path))
@@ -50,7 +50,7 @@ export function loadClientConfig(root) {
50
50
  parsed = JSON.parse(readFileSync(path, "utf8"));
51
51
  }
52
52
  catch {
53
- throw new Error(`invalid Keel client config at ${path}: file is not valid JSON`);
53
+ throw new Error(`invalid Syncstaff client config at ${path}: file is not valid JSON`);
54
54
  }
55
55
  return validate(parsed, path);
56
56
  }
@@ -1,66 +1,16 @@
1
1
  /**
2
- * KEEL_* CHARTER_* environment aliasing.
2
+ * Reject configuration from retired product eras before any module-scope
3
+ * environment read can silently fall back to a default.
3
4
  *
4
- * The service is renamed Charter Keel. Twenty-six environment variables
5
- * carry the old prefix, and they are read from ~30 files, three agent hook
6
- * configs, two MCP configs, a CI workflow, and a systemd unit on a host this
7
- * repo does not control. Renaming all of that in one commit means every one
8
- * of those has to land simultaneously, and anything that restarts in between
9
- * comes up misconfigured.
10
- *
11
- * That is not a hypothetical here. `src/mcp/index.ts` was switched to KEEL_*
12
- * before the configs that feed it were, so the adapter would have thrown
13
- * "KEEL_PROJECT env var is required" on the first tool call after any agent
14
- * restarted — while `dist/` still held a pre-rename build, so the breakage was
15
- * invisible until the next `npm run build`. A test run would have armed it.
16
- *
17
- * So instead: alias both directions, once, at process start. Every variable
18
- * answers to both names, the running fleet keeps working on CHARTER_*, new
19
- * code can read KEEL_*, and the configs migrate one at a time instead of all
20
- * at once. This module is the reason the rename does not need a window.
21
- *
22
- * Remove it only when nothing sets CHARTER_* anywhere — including the VPS
23
- * systemd unit and every developer's shell profile, which is the part nobody
24
- * remembers.
25
- */
26
- const PREFIXES = ["SYNCSTAFF_", "NAIB_", "KEEL_", "CHARTER_"];
27
- /**
28
- * Mirror SYNCSTAFF_x ⇄ NAIB_x ⇄ KEEL_x ⇄ CHARTER_x across all naming eras.
29
- *
30
- * An explicitly set value always wins: if multiple names are present with
31
- * different values, neither is overwritten.
32
- *
33
- * Idempotent, so entry points may call it more than once.
34
- */
35
- export function aliasEnv(env = process.env) {
36
- for (const [key, value] of Object.entries(env)) {
37
- if (value === undefined)
38
- continue;
39
- for (const sourcePrefix of PREFIXES) {
40
- if (key.startsWith(sourcePrefix)) {
41
- const suffix = key.slice(sourcePrefix.length);
42
- for (const targetPrefix of PREFIXES) {
43
- if (targetPrefix !== sourcePrefix) {
44
- const renamed = targetPrefix + suffix;
45
- if (env[renamed] === undefined)
46
- env[renamed] = value;
47
- }
48
- }
49
- break;
50
- }
51
- }
52
- }
53
- }
54
- /**
55
- * Read a variable by its suffix under NAIB_, KEEL_, or CHARTER_ prefix (newest first).
5
+ * Every process entry point imports this module first. Syncstaff is a hard
6
+ * cutover: old prefixes are errors with an exact replacement, not aliases.
56
7
  */
57
- export function naibEnv(suffix) {
58
- return process.env["SYNCSTAFF_" + suffix] ?? process.env["NAIB_" + suffix] ?? process.env["KEEL_" + suffix] ?? process.env["CHARTER_" + suffix];
8
+ const RETIRED_PREFIXES = ["KE" + "EL_", "NA" + "IB_", "CHAR" + "TER_"];
9
+ for (const key of Object.keys(process.env)) {
10
+ const prefix = RETIRED_PREFIXES.find((candidate) => key.startsWith(candidate));
11
+ if (!prefix)
12
+ continue;
13
+ const replacement = `SYNCSTAFF_${key.slice(prefix.length)}`;
14
+ throw new Error(`Retired environment variable ${key} is not supported. Rename it to ${replacement}.`);
59
15
  }
60
- export const keelEnv = naibEnv;
61
- // Aliasing on import is deliberate. Several modules read process.env at module
62
- // scope (`const SERVER = process.env.KEEL_URL ?? …`), which runs before any
63
- // entry point's first statement. A function an entry point must remember to
64
- // call would therefore be called too late, and the failure would be a silent
65
- // fallback to a default rather than an error.
66
- aliasEnv();
16
+ export {};
@@ -9,7 +9,7 @@
9
9
  * That was true of this repository until now. web/tsconfig.json declares
10
10
  * `"@/*": ["./src/*"]`, six files import through it, and the graph saw none of
11
11
  * those edges. The real-repo tests passed only because they were scoped to
12
- * keel/src, which uses relative imports throughout — the blind spot was hiding
12
+ * sync/src, which uses relative imports throughout — the blind spot was hiding
13
13
  * behind the choice of test corpus, which is the most comfortable place for a
14
14
  * blind spot to hide.
15
15
  *
@@ -17,7 +17,7 @@
17
17
  * implementation is worse:
18
18
  *
19
19
  * SCOPE. A monorepo has several tsconfigs. web/tsconfig.json's aliases apply
20
- * to web/, not to keel/. Applying every alias everywhere would invent edges
20
+ * to web/, not to sync/. Applying every alias everywhere would invent edges
21
21
  * across package boundaries — the same class of error as matching a name in an
22
22
  * unrelated language. Each mapping records the directory it governs, and the
23
23
  * nearest governing config wins.
@@ -91,7 +91,7 @@ export function buildGraphCallSiteIndex(repoPath, loadParsers, options = {}) {
91
91
  * then pools them back together, undoing the resolution that just happened.
92
92
  *
93
93
  * What that looked like here: `now`, exported by lib/ids.ts, collected the
94
- * two reads of a `const now` local to lib/slack.ts; `keelEnv`, exported by
94
+ * two reads of a `const now` local to lib/slack.ts; a retired environment helper exported by
95
95
  * lib/env-compat.ts, collected five calls to a same-named local in a script
96
96
  * that does not import it. Both were labelled "confirmed" — the label that
97
97
  * tells a human these are the trustworthy ones. Across this repository 268
@@ -173,6 +173,6 @@ export function buildGraphCallSiteIndex(repoPath, loadParsers, options = {}) {
173
173
  blindSpots: index.blindSpots.length,
174
174
  unresolvedCalls: calls.unresolved.length,
175
175
  unresolvedReferences: references.unresolved.length,
176
- analyzer: `keel-graph-v1(${registry.signature()})`,
176
+ analyzer: `syncstaff-graph-v1(${registry.signature()})`,
177
177
  };
178
178
  }
@@ -8,7 +8,7 @@
8
8
  * precision has held at 1.00 against the compiler. The cost of that refusal
9
9
  * was never measured until now, and it is much larger than assumed:
10
10
  *
11
- * keel (plain functions) 470 edges found, checker finds 695 — 64% recall
11
+ * sync (plain functions) 470 edges found, checker finds 695 — 64% recall
12
12
  * zod (fluent API) 722 edges found, checker finds 3178 — 14% recall
13
13
  *
14
14
  * `z.string()` is 2,591 calls in zod that the syntactic pass cannot see, and
@@ -18,7 +18,7 @@
18
18
  * inferred.
19
19
  *
20
20
  * WHAT THIS IS NOT. It does not replace the syntactic backend. It is slower by
21
- * 6-7x (4.1s on keel, 11.2s on zod, against 0.7s and 1.6s), it needs a
21
+ * 6-7x (4.1s on sync, 11.2s on zod, against 0.7s and 1.6s), it needs a
22
22
  * tsconfig, and it wants node_modules present to resolve external types. The
23
23
  * syntactic pass still answers when those are missing, which is the ordinary
24
24
  * case in a fresh checkout. Callers choose, and the result says which backend
@@ -77,7 +77,7 @@ export function findProjectConfigs(ts, root) {
77
77
  for (const candidate of NAMES)
78
78
  visit(join(root, candidate), 0);
79
79
  // Monorepos keep the tsconfig one level down. `repoRoot()` returns the git
80
- // root, so asking about a symbol in keel/ resolved to /home/.../sync, which
80
+ // root, so asking about a symbol in sync/ resolved to the monorepo root, which
81
81
  // has no tsconfig — findProjectConfigs returned nothing, the checker backend
82
82
  // reported "cannot run here", and the hook fell back to the parser while
83
83
  // still claiming it had been asked for the checker. A silent downgrade is
@@ -8,7 +8,7 @@ const tokenise = (value) => value
8
8
  .filter(Boolean) ?? [];
9
9
  const fields = (page) => [page.title, page.content, page.summary, page.target_path, page.body ?? "", page.lexical_vocabulary ?? ""].join(" ");
10
10
  /** Parse the candidate-only vocabulary weight; malformed values fail closed. */
11
- export function vocabularyWeightFromEnv(raw = process.env.KEEL_RETRIEVAL_VOCABULARY) {
11
+ export function vocabularyWeightFromEnv(raw = process.env.SYNCSTAFF_RETRIEVAL_VOCABULARY) {
12
12
  if (!raw?.trim())
13
13
  return 0;
14
14
  const value = Number(raw.trim());
@@ -45,7 +45,7 @@ export const IDENTITY_FIELD_WEIGHTS = { path: 1, facts: 1, summary: 1, body: 1,
45
45
  *
46
46
  * A degenerate baseline (rule 2) makes the confounder concrete. Serving the ten
47
47
  * largest files in the corpus, ignoring the query completely, scores recall@10
48
- * of 0.1596 on keel200 and 0.0793 on pw1200 — far below real retrieval, but
48
+ * of 0.1596 on private200 and 0.0793 on pw1200 — far below real retrieval, but
49
49
  * three to twenty-five times a same-sized random pick. File size genuinely
50
50
  * correlates with being edited, so any mechanism whose gain arrives together
51
51
  * with a file-size increase has to be assumed guilty until separated.
@@ -62,14 +62,14 @@ const FIELD_ORDER = ["path", "facts", "summary", "body"];
62
62
  * Parse `path=4,facts=3,summary=1,body=1` from the environment.
63
63
  *
64
64
  * One build serves every weighting, so a sweep cannot be confounded with
65
- * anything else that changed between two builds — the same reason `KEEL_CHUNK`
66
- * and `KEEL_NO_GRAPH` are switches rather than branches.
65
+ * anything else that changed between two builds — the same reason `SYNCSTAFF_CHUNK`
66
+ * and `SYNCSTAFF_NO_GRAPH` are switches rather than branches.
67
67
  *
68
68
  * Anything unparseable falls back to the identity weighting rather than
69
69
  * throwing. A malformed weight string in a benchmark harness should produce the
70
70
  * documented default, not abort a run that has already paid for its index.
71
71
  */
72
- export function fieldWeightsFromEnv(raw = process.env.KEEL_FIELD_WEIGHTS ?? process.env.CHARTER_FIELD_WEIGHTS) {
72
+ export function fieldWeightsFromEnv(raw = process.env.SYNCSTAFF_FIELD_WEIGHTS) {
73
73
  if (!raw)
74
74
  return IDENTITY_FIELD_WEIGHTS;
75
75
  const weights = { ...IDENTITY_FIELD_WEIGHTS };
@@ -234,7 +234,7 @@ function chunkPages(file, page) {
234
234
  // rejected is the economic case for chunking on top of it. Re-open this only
235
235
  // with a measurement that beats the per-instance figure above, not a pooled
236
236
  // one.
237
- if (process.env.KEEL_CHUNK !== "1" && process.env.CHARTER_CHUNK !== "1")
237
+ if (process.env.SYNCSTAFF_CHUNK !== "1")
238
238
  return [];
239
239
  const original = page.body ?? "";
240
240
  const body = distillBody(original);
@@ -8,7 +8,7 @@ import { buildDependencyGraph } from "./graph.js";
8
8
  import { buildSymbolTable, resolveCallEdges } from "./symbols.js";
9
9
  import { byCodeUnit } from "./order.js";
10
10
  /** Parse the local rollout switch. Unknown values fail closed to stable. */
11
- export function retrievalModeFromEnv(value = process.env.KEEL_RETRIEVAL_MODE) {
11
+ export function retrievalModeFromEnv(value = process.env.SYNCSTAFF_RETRIEVAL_MODE) {
12
12
  const normalized = value?.trim().toLowerCase();
13
13
  return normalized === "candidate" || normalized === "shadow" ? normalized : "stable";
14
14
  }
@@ -225,9 +225,9 @@ export async function retrieve(pages, query, options = {}) {
225
225
  // The default model is also opt-in even when the caller expresses no
226
226
  // preference: loading ~200-300MB of ONNX weights on every retrieve() call
227
227
  // is exactly the duplicate-memory cost tsk_01M1MQ01E1D7JSEHFVE2RCK8DJ
228
- // flagged across concurrent agent processes. KEEL_RETRIEVAL_SEMANTIC=1
228
+ // flagged across concurrent agent processes. SYNCSTAFF_RETRIEVAL_SEMANTIC=1
229
229
  // opts back in; lexical + symbol stays the default high-precision path.
230
- const semanticOptIn = process.env.KEEL_RETRIEVAL_SEMANTIC === "1";
230
+ const semanticOptIn = process.env.SYNCSTAFF_RETRIEVAL_SEMANTIC === "1";
231
231
  const embedder = options.embedder === undefined
232
232
  ? (semanticOptIn ? await createTransformersEmbedder() : null)
233
233
  : options.embedder;
@@ -373,8 +373,8 @@ export async function retrieve(pages, query, options = {}) {
373
373
  // Ablation switch. A corpus measured for the first time has no pre-graph
374
374
  // baseline to compare against, and producing one from an older build would
375
375
  // confound the comparison with everything else that changed in between —
376
- // the same reason KEEL_CHUNK exists.
377
- ...(graphArtifacts && process.env.KEEL_NO_GRAPH !== "1" && process.env.CHARTER_NO_GRAPH !== "1"
376
+ // the same reason SYNCSTAFF_CHUNK exists.
377
+ ...(graphArtifacts && process.env.SYNCSTAFF_NO_GRAPH !== "1"
378
378
  ? { expandFromSeeds: expandFromSeeds(graphArtifacts.graph, graphArtifacts.calls, pagesByPath, graphRankablePaths) }
379
379
  : {}),
380
380
  ...(cochangeExpander ? { expandFromCoChange: cochangeExpander } : {}),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The language/parser contract for Keel's local index.
2
+ * The language/parser contract for Syncstaff's local index.
3
3
  *
4
4
  * Everything here runs on the agent's machine, beside the checkout, per
5
5
  * inv_01KZWPAM9QCZ0DD7789N55X647 and ADR 002: the central server routes
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * A confident wrong answer is worse than an admitted gap.
11
11
  *
12
- * Keel already has the counter-example in production. `grep-v1` resolves call
12
+ * Syncstaff already has the counter-example in production. `grep-v1` resolves call
13
13
  * sites by matching a bare symbol name across the repository, so declaring a
14
14
  * TypeScript method named `check` matched a Python comment reading
15
15
  * "# side of the ORPHAN check" and sixteen other files in an unrelated tree.
@@ -160,7 +160,7 @@ export function resolveCallEdges(index, table) {
160
160
  * This is the answer to "what would break if I changed this symbol", and it is
161
161
  * the single path both blast-radius surfaces read. Before it existed, the graph
162
162
  * could name every caller of a function and not one user of a constant: the
163
- * extractor only fired inside `isCallExpression`, so `KEEL_RELEASE_VERSION`,
163
+ * extractor only fired inside `isCallExpression`, so `SYNCSTAFF_RELEASE_VERSION`,
164
164
  * read in four files, resolved to silence.
165
165
  *
166
166
  * Deterministic, for the same reason as resolveCallEdges: two machines with the
@@ -23,7 +23,7 @@ export class TransformersEmbedder {
23
23
  if (!this.pipelinePromise) {
24
24
  this.pipelinePromise = (async () => {
25
25
  try {
26
- // Dynamic import allows Keel to run if the dependency is missing (R4.2)
26
+ // Dynamic import allows Syncstaff to run if the dependency is missing (R4.2)
27
27
  const { pipeline } = await import("@xenova/transformers");
28
28
  return await pipeline("feature-extraction", this.modelId, {
29
29
  quantized: true // Use INT8 quantization for smaller size and faster CPU inference
@@ -9,7 +9,7 @@ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"
9
9
  const PARSER_VERSION = "4";
10
10
  export function createTypeScriptParser(tsApi) {
11
11
  return {
12
- id: "keel-typescript",
12
+ id: "syncstaff-typescript",
13
13
  version: PARSER_VERSION,
14
14
  language: "typescript",
15
15
  extensions: EXTENSIONS,
@@ -385,7 +385,7 @@ function parseSource(tsApi, source, path) {
385
385
  }
386
386
  }
387
387
  // ---- plain reads -------------------------------------------------------
388
- // The branch that was missing entirely. `KEEL_RELEASE_VERSION` is read in
388
+ // The branch that was missing entirely. `SYNCSTAFF_RELEASE_VERSION` is read in
389
389
  // four files and invoked in none, so every extractor above walks straight
390
390
  // past it and the graph reports that nothing uses a constant three modules
391
391
  // depend on. Everything already handled above is excluded here rather than
@@ -35,7 +35,7 @@ export function embedderIdentity(embedder) {
35
35
  * keeps the representations in separate cache entries. `facts` is the
36
36
  * narrower candidate; `1` additionally includes the bounded vocabulary.
37
37
  */
38
- const vectorDocumentFactsMode = () => process.env.KEEL_VECTOR_DOCUMENT_FACTS ?? "";
38
+ const vectorDocumentFactsMode = () => process.env.SYNCSTAFF_VECTOR_DOCUMENT_FACTS ?? "";
39
39
  /**
40
40
  * The exact text sent to the embedder for one page.
41
41
  *
@@ -4,7 +4,7 @@
4
4
  * Every function here is pure, and that is the point: the MCP adapter is a
5
5
  * process that connects on import, so nothing inside it can be unit tested.
6
6
  * These were measured against 40 sessions of real transcripts — 960,307
7
- * response tokens across 1,837 keel tool calls, against roughly 6,000 tokens
7
+ * response tokens across 1,837 Sync tool calls, against roughly 6,000 tokens
8
8
  * per session of static tool schemas and protocol instructions. Responses
9
9
  * outweigh definitions about four to one, so this is where the budget is.
10
10
  *
@@ -65,9 +65,7 @@ const inferProvider = (model) => /^claude[-.]/i.test(model) ? "anthropic" : "ope
65
65
  * environment is per server and set by whoever runs it; the default is what
66
66
  * ships.
67
67
  *
68
- * Environment names are `NAIB_MODEL_<ROLE>_MODEL` and friends, with the
69
- * `KEEL_`/`CHARTER_` aliases honoured directly so this works whether or not
70
- * `aliasEnv` has run.
68
+ * Environment names are `SYNCSTAFF_MODEL_<ROLE>_MODEL` and friends.
71
69
  *
72
70
  * Never throws. A misconfiguration disables one role and explains itself; the
73
71
  * caller degrades per `whenUnavailable` rather than failing.
@@ -79,17 +77,12 @@ export function resolveRole(role, settings, env = {}) {
79
77
  }
80
78
  const upper = role.toUpperCase();
81
79
  const scoped = (scope, suffix) => {
82
- for (const prefix of ["NAIB_", "KEEL_", "CHARTER_"]) {
83
- const v = env[`${prefix}MODEL_${scope}_${suffix}`]?.trim();
84
- if (v)
85
- return v;
86
- }
87
- return undefined;
80
+ return env[`SYNCSTAFF_MODEL_${scope}_${suffix}`]?.trim() || undefined;
88
81
  };
89
82
  /**
90
83
  * Per-role first, then the server-wide default.
91
84
  *
92
- * `NAIB_MODEL_DEFAULT_*` exists because "plug whatever model in" should not
85
+ * `SYNCSTAFF_MODEL_DEFAULT_*` exists because "plug whatever model in" should not
93
86
  * mean repeating the same endpoint and key variable once per role, and
94
87
  * repeating them again when a role is added.
95
88
  */
@@ -105,7 +98,7 @@ export function resolveRole(role, settings, env = {}) {
105
98
  * The default's endpoint and key belong to the default's *model*, and must
106
99
  * not follow a role that points somewhere else.
107
100
  *
108
- * Setting `NAIB_MODEL_DEFAULT_*` to GLM and then overriding one role to a
101
+ * Setting `SYNCSTAFF_MODEL_DEFAULT_*` to GLM and then overriding one role to a
109
102
  * Claude model used to leave that role resolving to Anthropic while still
110
103
  * inheriting `GLM_API_KEY`, so every request came back
111
104
  * `401 invalid x-api-key` — naming a key the operator had never pointed at
@@ -6,7 +6,7 @@
6
6
  * copy of the repository on the server's own disk, at whatever commit it last
7
7
  * pulled, which is nobody's working tree. That made the same declaration
8
8
  * warn on one instance and stay silent on another, and it was the last thing
9
- * keeping a filesystem dependency inside a Keel process
9
+ * keeping a filesystem dependency inside a Syncstaff process
10
10
  * (inv_01KZWPAT93H32HB3X48FNPTA1E).
11
11
  *
12
12
  * Only filenames are read, never file contents, so this never breached the
@@ -22,8 +22,8 @@ export function declaredPathWarnings(paths, files) {
22
22
  // An EMPTY file list is "could not look", not "the repository is empty",
23
23
  // and conflating them turns this into a machine for crying wolf. That is not
24
24
  // hypothetical: `repo_path` still said /opt/charter/sync after the
25
- // Charter -> Keel rename, the walk returned [], and every declared path came
26
- // back as a warning — including `keel/src/server/app.ts`, which obviously
25
+ // Charter -> Syncstaff rename, the walk returned [], and every declared path came
26
+ // back as a warning — including `sync/src/server/app.ts`, which obviously
27
27
  // exists. A check that fires on everything is worse than no check, because
28
28
  // it teaches the reader to skip the one that matters.
29
29
  //
@@ -1,5 +1,5 @@
1
1
  /** Release version shared by the server and the published MCP adapter. */
2
- export const KEEL_RELEASE_VERSION = "0.2.3";
2
+ export const SYNCSTAFF_RELEASE_VERSION = "0.2.4";
3
3
  /**
4
4
  * Adapter/API compatibility level.
5
5
  *
@@ -7,15 +7,18 @@ export const KEEL_RELEASE_VERSION = "0.2.3";
7
7
  * the server. It is deliberately separate from the npm package version so a
8
8
  * bug-fix release does not manufacture a protocol break.
9
9
  */
10
- export const KEEL_ADAPTER_PROTOCOL_VERSION = 1;
11
- export const KEEL_ADAPTER_VERSION_HEADER = "x-keel-adapter-version";
12
- export const KEEL_ADAPTER_PROTOCOL_HEADER = "x-keel-adapter-protocol";
13
- export const KEEL_SERVER_VERSION_HEADER = "x-keel-server-version";
14
- export const KEEL_SUPPORTED_ADAPTER_PROTOCOL_HEADER = "x-keel-supported-adapter-protocol";
10
+ export const SYNCSTAFF_ADAPTER_PROTOCOL_VERSION = 2;
11
+ export const SYNCSTAFF_ADAPTER_VERSION_HEADER = "x-syncstaff-adapter-version";
12
+ export const SYNCSTAFF_ADAPTER_PROTOCOL_HEADER = "x-syncstaff-adapter-protocol";
13
+ export const SYNCSTAFF_SERVER_VERSION_HEADER = "x-syncstaff-server-version";
14
+ export const SYNCSTAFF_SUPPORTED_ADAPTER_PROTOCOL_HEADER = "x-syncstaff-supported-adapter-protocol";
15
+ const RETIRED_PRODUCT_SLUG = ["ke", "el"].join("");
16
+ export const RETIRED_ADAPTER_VERSION_HEADER = `x-${RETIRED_PRODUCT_SLUG}-adapter-version`;
17
+ export const RETIRED_ADAPTER_PROTOCOL_HEADER = `x-${RETIRED_PRODUCT_SLUG}-adapter-protocol`;
15
18
  /** Headers attached to every adapter-to-server request. */
16
19
  export function adapterRequestHeaders() {
17
20
  return {
18
- [KEEL_ADAPTER_VERSION_HEADER]: KEEL_RELEASE_VERSION,
19
- [KEEL_ADAPTER_PROTOCOL_HEADER]: String(KEEL_ADAPTER_PROTOCOL_VERSION),
21
+ [SYNCSTAFF_ADAPTER_VERSION_HEADER]: SYNCSTAFF_RELEASE_VERSION,
22
+ [SYNCSTAFF_ADAPTER_PROTOCOL_HEADER]: String(SYNCSTAFF_ADAPTER_PROTOCOL_VERSION),
20
23
  };
21
24
  }
@@ -101,7 +101,7 @@ export function upstreamLandingReport(repoRoot) {
101
101
  return {
102
102
  upstream_status: "unknown",
103
103
  ahead_count: null,
104
- warning: "Intent completed, but Keel could not verify whether HEAD is published to an " +
104
+ warning: "Intent completed, but Syncstaff could not verify whether HEAD is published to an " +
105
105
  "upstream branch. Configure a tracking branch or verify and push manually.",
106
106
  };
107
107
  }
@@ -122,7 +122,7 @@ export function listWorktreeChanges(repoRoot) {
122
122
  /**
123
123
  * Changed paths not covered by any of `leasedPaths`.
124
124
  *
125
- * `ignore` drops paths that are never coordination-relevant (Keel's own
125
+ * `ignore` drops paths that are never coordination-relevant (Syncstaff's own
126
126
  * per-session state, build output, dependencies). Directories must be given
127
127
  * with a trailing slash.
128
128
  */
@@ -196,7 +196,7 @@ export function formatUnleasedWriteAlert(report, writes) {
196
196
  return lines.join("\n");
197
197
  }
198
198
  export const DEFAULT_IGNORES = [
199
- ".keel/",
199
+ ".syncstaff/",
200
200
  "node_modules/",
201
201
  "dist/",
202
202
  "build/",
@@ -19,11 +19,11 @@ function envSeconds(name, fallback) {
19
19
  return Number.isFinite(value) && value >= 0 ? value : fallback;
20
20
  }
21
21
  /** 0 disables the daemon path entirely — every call falls back to local computation, same as before this module existed. */
22
- const REQUEST_TIMEOUT_MS = envSeconds("KEEL_MCP_DAEMON_TIMEOUT_SECONDS", 20) * 1000;
22
+ const REQUEST_TIMEOUT_MS = envSeconds("SYNCSTAFF_MCP_DAEMON_TIMEOUT_SECONDS", 20) * 1000;
23
23
  const CONNECT_TIMEOUT_MS = 500;
24
24
  /** Opt-in: off by default, so existing behavior is unchanged unless a caller sets this. */
25
25
  export function daemonEnabled() {
26
- const v = process.env.KEEL_MCP_DAEMON?.trim().toLowerCase();
26
+ const v = process.env.SYNCSTAFF_MCP_DAEMON?.trim().toLowerCase();
27
27
  return v === "1" || v === "true" || v === "on";
28
28
  }
29
29
  function connectTo(target) {
@@ -88,13 +88,13 @@ export function daemonInstanceId(root) {
88
88
  * daemon's socket happened to still exist.
89
89
  */
90
90
  export function socketPathFor(root) {
91
- return join(baseDir(), `keel-daemon-${daemonInstanceId(root)}.sock`);
91
+ return join(baseDir(), `sync-daemon-${daemonInstanceId(root)}.sock`);
92
92
  }
93
93
  /** Where the TCP fallback (used when the socket path can't be bound — see daemon.ts) records its port. */
94
94
  export function tcpPortFileFor(root) {
95
- return join(baseDir(), `keel-daemon-${daemonInstanceId(root)}.port`);
95
+ return join(baseDir(), `sync-daemon-${daemonInstanceId(root)}.port`);
96
96
  }
97
97
  /** The pidfile a running daemon maintains so a stale one can be told apart from a live one before connecting. */
98
98
  export function pidFileFor(root) {
99
- return join(baseDir(), `keel-daemon-${daemonInstanceId(root)}.pid`);
99
+ return join(baseDir(), `sync-daemon-${daemonInstanceId(root)}.pid`);
100
100
  }
@@ -47,8 +47,8 @@ export async function buildLocalGraph(root) {
47
47
  let keepArchiveOpen = false;
48
48
  let indexed;
49
49
  try {
50
- mkdirSync(join(root, ".keel"), { recursive: true });
51
- archive = new persistenceMod.IndexSnapshotArchive(join(root, ".keel", "index.sqlite"));
50
+ mkdirSync(join(root, ".syncstaff"), { recursive: true });
51
+ archive = new persistenceMod.IndexSnapshotArchive(join(root, ".syncstaff", "index.sqlite"));
52
52
  vectorCache = new vectorCacheMod.VectorCache(archive);
53
53
  indexed = incrementalMod.loadOrUpdateFileIndex(root, registry, {
54
54
  archive,
@@ -151,9 +151,9 @@ export async function runRetrieve(local, args) {
151
151
  vectorCache: vectorCache ?? undefined,
152
152
  retrievalMode,
153
153
  lexicalVocabularyWeight: vocabularyWeightFromEnv(),
154
- queryRouting: retrievalMode !== "stable" && envFlag("KEEL_RETRIEVAL_ROUTING", false),
154
+ queryRouting: retrievalMode !== "stable" && envFlag("SYNCSTAFF_RETRIEVAL_ROUTING", false),
155
155
  coChangeIndex,
156
- ...(retrievalMode !== "stable" && envFlag("KEEL_RETRIEVAL_VERIFY", false) ? { verify: { root } } : {}),
156
+ ...(retrievalMode !== "stable" && envFlag("SYNCSTAFF_RETRIEVAL_VERIFY", false) ? { verify: { root } } : {}),
157
157
  });
158
158
  return {
159
159
  ...answer,
package/dist/mcp/index.js CHANGED
@@ -1,36 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * keel-mcp — MCP adapter exposing the coordination tool surface (§6.4).
3
+ * sync-mcp — MCP adapter exposing the coordination tool surface (§6.4).
4
4
  *
5
- * Connection settings come from KEEL_URL / KEEL_PROJECT / KEEL_VENDOR /
6
- * KEEL_TOKEN first, then the repo-local config written by `keel login`.
7
- * KEEL_URL defaults to http://localhost:7377. Set
8
- * KEEL_BLAST_INCLUDE_CONTEXT=0 to omit source snippets from client-side
5
+ * Connection settings come from SYNCSTAFF_URL / SYNCSTAFF_PROJECT / SYNCSTAFF_VENDOR /
6
+ * SYNCSTAFF_TOKEN first, then the repo-local config written by `sync login`.
7
+ * SYNCSTAFF_URL defaults to http://localhost:7377. Set
8
+ * SYNCSTAFF_BLAST_INCLUDE_CONTEXT=0 to omit source snippets from client-side
9
9
  * call-site reports. Retrieval rollout is local and fail-closed: use
10
- * KEEL_RETRIEVAL_MODE=stable (default), candidate, or shadow. Set
11
- * KEEL_RETRIEVAL_VOCABULARY=1 only for candidate/shadow testing of the
10
+ * SYNCSTAFF_RETRIEVAL_MODE=stable (default), candidate, or shadow. Set
11
+ * SYNCSTAFF_RETRIEVAL_VOCABULARY=1 only for candidate/shadow testing of the
12
12
  * bounded parser-derived lexical vocabulary; stable mode ignores it. Set
13
- * KEEL_RETRIEVAL_ROUTING=1 to test candidate-only intent-specific query
13
+ * SYNCSTAFF_RETRIEVAL_ROUTING=1 to test candidate-only intent-specific query
14
14
  * routing; stable mode ignores it.
15
- * KEEL_RETRIEVAL_VERIFY=1 to run the local TypeScript checker against
15
+ * SYNCSTAFF_RETRIEVAL_VERIFY=1 to run the local TypeScript checker against
16
16
  * candidate-mode results and promote only candidates with call-site evidence;
17
17
  * stable mode ignores it. Verification is budgeted and degrades to the
18
18
  * unverified ranking when the checkout cannot be checked.
19
- * KEEL_API_TIMEOUT_SECONDS (default 45, 0 disables) bounds every HTTP call
19
+ * SYNCSTAFF_API_TIMEOUT_SECONDS (default 45, 0 disables) bounds every HTTP call
20
20
  * this adapter makes to the Sync server, so a stalled request fails with a
21
21
  * clear error instead of hanging for as long as the calling harness allows.
22
- * KEEL_MCP_DAEMON=1 shares the local graph index (sync_impact /
22
+ * SYNCSTAFF_MCP_DAEMON=1 shares the local graph index (sync_impact /
23
23
  * sync_context_card / sync_retrieve) across every agent CLI on this checkout
24
24
  * through one background process instead of one per adapter process. Off by
25
25
  * default — unset, this adapter's behavior is unchanged from before daemon
26
- * mode existed. See daemon.ts for KEEL_MCP_DAEMON_IDLE_MINUTES (default 15)
27
- * and KEEL_MCP_DAEMON_TIMEOUT_SECONDS (default 20).
26
+ * mode existed. See daemon.ts for SYNCSTAFF_MCP_DAEMON_IDLE_MINUTES (default 15)
27
+ * and SYNCSTAFF_MCP_DAEMON_TIMEOUT_SECONDS (default 20).
28
28
  *
29
29
  * The agent is registered lazily on the first tool call. When a lifecycle
30
30
  * hook registered the session first, the MCP process adopts that same id so
31
31
  * lease checks and lease acquisition refer to one agent.
32
32
  */
33
- // FIRST import, deliberately: it aliases KEEL_* ⇄ CHARTER_* on load, and the
33
+ // FIRST import, deliberately: it aliases SYNCSTAFF_* ⇄ SYNCSTAFF_* on load, and the
34
34
  // module-scope reads below (and in agent-state) would otherwise run first and
35
35
  // see whichever prefix the caller happened not to set.
36
36
  import "../lib/env-compat.js";
@@ -49,7 +49,7 @@ import { PROTOCOL_INSTRUCTIONS } from "../lib/protocol.js";
49
49
  import { globMatches, isGlob, pathsOverlap } from "../lib/globs.js";
50
50
  import { declaredPathWarnings } from "../lib/path-warnings.js";
51
51
  import { BOARD_KINDS, BOARD_STATUSES } from "../lib/types.js";
52
- import { adapterRequestHeaders, KEEL_ADAPTER_PROTOCOL_VERSION, KEEL_RELEASE_VERSION, } from "../lib/version.js";
52
+ import { adapterRequestHeaders, SYNCSTAFF_ADAPTER_PROTOCOL_VERSION, SYNCSTAFF_RELEASE_VERSION, } from "../lib/version.js";
53
53
  import { DEFAULT_IGNORES, findUnleasedWrites, fingerprintWrites, listWorktreeChanges, listWorktreeChangesStrict, upstreamLandingReport, } from "../lib/worktree.js";
54
54
  import { createApprovalGate } from "./approval.js";
55
55
  import { runLogin } from "./login.js";
@@ -63,10 +63,10 @@ const LOGIN_MODE = process.argv[2] === "login";
63
63
  const SETUP_MODE = process.argv[2] === "setup";
64
64
  const TEST_IMPORT_MODE = process.argv[2] === "__test_import__";
65
65
  const CLIENT_CONFIG = LOGIN_MODE ? null : loadClientConfig(repoRoot());
66
- const SERVER = process.env.KEEL_URL ?? CLIENT_CONFIG?.server ?? "http://localhost:7377";
67
- const PROJECT = process.env.KEEL_PROJECT ?? CLIENT_CONFIG?.project;
68
- const VENDOR = process.env.KEEL_VENDOR ?? CLIENT_CONFIG?.vendor ?? "other";
69
- const TOKEN = process.env.KEEL_TOKEN?.trim() ||
66
+ const SERVER = process.env.SYNCSTAFF_URL ?? CLIENT_CONFIG?.server ?? "http://localhost:7377";
67
+ const PROJECT = process.env.SYNCSTAFF_PROJECT ?? CLIENT_CONFIG?.project;
68
+ const VENDOR = process.env.SYNCSTAFF_VENDOR ?? CLIENT_CONFIG?.vendor ?? "other";
69
+ const TOKEN = process.env.SYNCSTAFF_TOKEN?.trim() ||
70
70
  (CLIENT_CONFIG?.server.replace(/\/+$/, "") === SERVER.replace(/\/+$/, "")
71
71
  ? CLIENT_CONFIG.token
72
72
  : undefined);
@@ -78,8 +78,8 @@ const TOKEN = process.env.KEEL_TOKEN?.trim() ||
78
78
  */
79
79
  export function localAutoRunnerCommand(root, statePath) {
80
80
  const candidates = [
81
- join(root, "keel", "dist", "src", "cli", "keel-cli.js"),
82
- join(root, "dist", "src", "cli", "keel-cli.js"),
81
+ join(root, "sync", "dist", "src", "cli", "syncstaff-cli.js"),
82
+ join(root, "dist", "src", "cli", "syncstaff-cli.js"),
83
83
  ];
84
84
  const cli = candidates.find((candidate) => existsSync(candidate));
85
85
  return cli ? { command: process.execPath, args: [cli, "auto-run", "--state-file", statePath] } : null;
@@ -93,7 +93,7 @@ function ensureLocalAutoRunner(root, statePath) {
93
93
  detached: true,
94
94
  stdio: "ignore",
95
95
  windowsHide: true,
96
- env: { ...process.env, KEEL_VENDOR: VENDOR },
96
+ env: { ...process.env, SYNCSTAFF_VENDOR: VENDOR },
97
97
  });
98
98
  child.unref();
99
99
  return true;
@@ -146,10 +146,10 @@ function apiHeaders() {
146
146
  * half of the same fix, one level up the call chain.
147
147
  *
148
148
  * A generous default: legitimate calls finish in low seconds even with the
149
- * arbiter in the loop. `KEEL_API_TIMEOUT_SECONDS=0` disables it for a slow
149
+ * arbiter in the loop. `SYNCSTAFF_API_TIMEOUT_SECONDS=0` disables it for a slow
150
150
  * link where 45s is provably too tight, rather than editing this file.
151
151
  */
152
- const API_TIMEOUT_MS = envSeconds("KEEL_API_TIMEOUT_SECONDS", 45) * 1000;
152
+ const API_TIMEOUT_MS = envSeconds("SYNCSTAFF_API_TIMEOUT_SECONDS", 45) * 1000;
153
153
  async function api(method, path, body, extraHeaders = {}) {
154
154
  const controller = new AbortController();
155
155
  const timer = API_TIMEOUT_MS > 0 ? setTimeout(() => controller.abort(), API_TIMEOUT_MS) : undefined;
@@ -172,7 +172,7 @@ async function api(method, path, body, extraHeaders = {}) {
172
172
  // where the tool accepts one rather than declaring blind.
173
173
  throw new Error(`${method} ${path} timed out after ${API_TIMEOUT_MS / 1000}s with no response. ` +
174
174
  `It may still have landed server-side — check before retrying blind ` +
175
- `(e.g. intent_list_active for an intents call). Raise KEEL_API_TIMEOUT_SECONDS ` +
175
+ `(e.g. intent_list_active for an intents call). Raise SYNCSTAFF_API_TIMEOUT_SECONDS ` +
176
176
  `if this route is legitimately slower than that.`);
177
177
  }
178
178
  throw e;
@@ -189,10 +189,10 @@ async function api(method, path, body, extraHeaders = {}) {
189
189
  // on. Everything else keeps the literal response.
190
190
  if (res.status === 401) {
191
191
  throw new Error(`${method} ${path} -> 401: this machine has no valid Sync credential.\n` +
192
- `Run: npm --prefix keel run login -- --server ${SERVER}` +
192
+ `Run: npm --prefix sync run login -- --server ${SERVER}` +
193
193
  (PROJECT ? ` --project ${PROJECT}` : "") +
194
194
  ` --vendor ${VENDOR}\n` +
195
- `Then run: node keel/dist/src/cli/keel-cli.js doctor`);
195
+ `Then run: node sync/dist/src/cli/syncstaff-cli.js doctor`);
196
196
  }
197
197
  throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(data)}`);
198
198
  }
@@ -261,10 +261,10 @@ function hookBindingRequired(root) {
261
261
  try {
262
262
  const config = readFileSync(join(root, relative), "utf8");
263
263
  // Either binary name. The rename briefly broke this: the check looked for
264
- // "keel-cli" while the config still said "charter-cli", so it returned
264
+ // "syncstaff-cli" while the config still said "charter-cli", so it returned
265
265
  // false and Codex's session-binding requirement silently stopped being
266
266
  // enforced — a guard that fails open reads exactly like a guard that passes.
267
- const wired = config.includes("keel-cli") || config.includes("charter-cli");
267
+ const wired = config.includes("syncstaff-cli") || config.includes("charter-cli");
268
268
  if (wired && config.includes("preflight"))
269
269
  return true;
270
270
  }
@@ -346,7 +346,7 @@ const interfaceSchema = z.object({
346
346
  change: z.enum(["none", "signature", "added", "removed", "behavior"]),
347
347
  });
348
348
  function withLocalCallSites(subjects, analyzeChanges) {
349
- const contextSetting = process.env.KEEL_BLAST_INCLUDE_CONTEXT?.toLowerCase();
349
+ const contextSetting = process.env.SYNCSTAFF_BLAST_INCLUDE_CONTEXT?.toLowerCase();
350
350
  const includeContext = !["0", "false", "off", "no"].includes(contextSetting ?? "");
351
351
  return attachClientCallSiteReports(repoRoot(), subjects, {
352
352
  includeContext,
@@ -356,7 +356,7 @@ function withLocalCallSites(subjects, analyzeChanges) {
356
356
  export function createSyncMcpServer(requestContext) {
357
357
  async function ensureAgent(extra, sessionIdFromArg) {
358
358
  if (!PROJECT)
359
- throw new Error("KEEL_PROJECT env var is required");
359
+ throw new Error("SYNCSTAFF_PROJECT env var is required");
360
360
  const root = repoRoot();
361
361
  if (!requestContext)
362
362
  requestContext = {};
@@ -419,7 +419,7 @@ export function createSyncMcpServer(requestContext) {
419
419
  project_id: projectId,
420
420
  vendor: VENDOR,
421
421
  vendor_session_id: requestedSessionId,
422
- human_owner: process.env.KEEL_HUMAN ?? process.env.USER ?? "unknown",
422
+ human_owner: process.env.SYNCSTAFF_HUMAN ?? process.env.USER ?? "unknown",
423
423
  machine_id: hostname(),
424
424
  checkout_path: root,
425
425
  enforcement_level: "advisory",
@@ -468,8 +468,8 @@ export function createSyncMcpServer(requestContext) {
468
468
  projectId,
469
469
  machineId: hostname(),
470
470
  displayName: hostname(),
471
- clientVersion: KEEL_RELEASE_VERSION,
472
- protocolVersion: String(KEEL_ADAPTER_PROTOCOL_VERSION),
471
+ clientVersion: SYNCSTAFF_RELEASE_VERSION,
472
+ protocolVersion: String(SYNCSTAFF_ADAPTER_PROTOCOL_VERSION),
473
473
  platform: `${process.platform}-${process.arch}`,
474
474
  });
475
475
  }
@@ -488,15 +488,15 @@ export function createSyncMcpServer(requestContext) {
488
488
  project: PROJECT ?? "",
489
489
  repoRoot: repoRoot(),
490
490
  token: TOKEN,
491
- key: process.env.KEEL_REQUEST_STATE_KEY,
492
- resolvedBy: process.env.KEEL_HUMAN ?? process.env.USER ?? "unknown",
491
+ key: process.env.SYNCSTAFF_REQUEST_STATE_KEY,
492
+ resolvedBy: process.env.SYNCSTAFF_HUMAN ?? process.env.USER ?? "unknown",
493
493
  // The era belongs to the connection, and `serveStdio` puts it on the
494
494
  // context it builds this server with — there is no accessor on the server
495
495
  // itself because a modern instance and a legacy one are different
496
496
  // instances, not two modes of one.
497
497
  era: () => requestContext?.era,
498
498
  });
499
- const mcp = new McpServer({ name: "sync", version: KEEL_RELEASE_VERSION }, {
499
+ const mcp = new McpServer({ name: "sync", version: SYNCSTAFF_RELEASE_VERSION }, {
500
500
  instructions: PROTOCOL_INSTRUCTIONS,
501
501
  requestState: { verify: approval.verify },
502
502
  });
@@ -527,7 +527,7 @@ export function createSyncMcpServer(requestContext) {
527
527
  project_id: projectId,
528
528
  repo_root: repoRoot(),
529
529
  vendor: VENDOR,
530
- human_owner: process.env.NAIB_HUMAN ?? process.env.KEEL_HUMAN ?? process.env.USER ?? "unknown",
530
+ human_owner: process.env.SYNCSTAFF_HUMAN ?? process.env.USER ?? "unknown",
531
531
  path_convention: "All paths are relative to repo_root, never to your cwd.",
532
532
  });
533
533
  });
@@ -608,8 +608,8 @@ export function createSyncMcpServer(requestContext) {
608
608
  * Do the declared paths exist? Answered here, in the checkout being edited.
609
609
  *
610
610
  * inv_01KYRCZX78VK0YYMHC4FYEHR5W says every path is git-root-relative, and
611
- * nothing enforced it: a lease on `keel/src/server/db.ts` when the file is at
612
- * `keel/src/lib/db.ts` is granted, renewed and released without complaint
611
+ * nothing enforced it: a lease on `sync/src/server/db.ts` when the file is at
612
+ * `sync/src/lib/db.ts` is granted, renewed and released without complaint
613
613
  * while the agent edits a file no lease covers.
614
614
  *
615
615
  * The server used to answer this from its own `repo_path`, which is a
@@ -634,7 +634,7 @@ export function createSyncMcpServer(requestContext) {
634
634
  return null;
635
635
  }
636
636
  }
637
- mcp.registerTool("intent_declare", { description: "Declare what you are about to do BEFORE editing any file: a one-sentence summary, the paths you will touch, and every interface you will add/remove/change. Returns an arbiter verdict and blast radius. If the coordinator escalates the declaration, this call waits briefly (KEEL_ESCALATION_WAIT_SECONDS, default 60) in case the decision is fast or already automated, then returns either way with the escalation_id. On a timeout the escalation is already durable — run `node keel/scripts/await-escalation.mjs <escalation_id>` in the background to keep watching it while you do something else, rather than re-declaring or retrying this call.", inputSchema: z.object({
637
+ mcp.registerTool("intent_declare", { description: "Declare what you are about to do BEFORE editing any file: a one-sentence summary, the paths you will touch, and every interface you will add/remove/change. Returns an arbiter verdict and blast radius. If the coordinator escalates the declaration, this call waits briefly (SYNCSTAFF_ESCALATION_WAIT_SECONDS, default 60) in case the decision is fast or already automated, then returns either way with the escalation_id. On a timeout the escalation is already durable — run `node sync/scripts/await-escalation.mjs <escalation_id>` in the background to keep watching it while you do something else, rather than re-declaring or retrying this call.", inputSchema: z.object({
638
638
  summary: z.string(),
639
639
  paths: z.array(z.string()),
640
640
  interfaces_touched: z.array(interfaceSchema).default([]),
@@ -671,7 +671,7 @@ export function createSyncMcpServer(requestContext) {
671
671
  `${foreignWrites.slice(0, 8).join(", ")}${foreignWrites.length > 8 ? `, +${foreignWrites.length - 8} more` : ""}. ` +
672
672
  `Only stage and commit the paths you actually declared.`;
673
673
  }
674
- if (!declared.escalation_id || !envFlag("KEEL_AUTO_WAIT_ESCALATIONS", true)) {
674
+ if (!declared.escalation_id || !envFlag("SYNCSTAFF_AUTO_WAIT_ESCALATIONS", true)) {
675
675
  return text(compactDeclaration(declared));
676
676
  }
677
677
  const waited = await waitForEscalation({
@@ -687,14 +687,14 @@ export function createSyncMcpServer(requestContext) {
687
687
  // automated resolution inline; anything slower should be watched with
688
688
  // scripts/await-escalation.mjs instead of held open here. See
689
689
  // tsk_01M17NJ3KKTEVTYEVGPCN957D7.
690
- timeoutMs: envSeconds("KEEL_ESCALATION_WAIT_SECONDS", 60) * 1000,
690
+ timeoutMs: envSeconds("SYNCSTAFF_ESCALATION_WAIT_SECONDS", 60) * 1000,
691
691
  });
692
692
  return text({
693
693
  ...compactDeclaration(declared),
694
694
  escalation_wait: waited,
695
695
  next_step: waited.status === "resolved"
696
696
  ? "The durable human decision is applied. Continue based on escalation_wait.resolution and escalation_wait.changes."
697
- : `The human decision did not arrive within the wait. The escalation is durable — run in the background: node keel/scripts/await-escalation.mjs ${declared.escalation_id}. Do not re-declare the same intent to keep waiting; that creates a duplicate.`,
697
+ : `The human decision did not arrive within the wait. The escalation is durable — run in the background: node sync/scripts/await-escalation.mjs ${declared.escalation_id}. Do not re-declare the same intent to keep waiting; that creates a duplicate.`,
698
698
  });
699
699
  });
700
700
  /**
@@ -1034,7 +1034,7 @@ export function createSyncMcpServer(requestContext) {
1034
1034
  /**
1035
1035
  * One index per process, keyed by root. Rebuilding per call would be absurd
1036
1036
  * — and so, at N agent CLIs sharing one checkout, is N processes each
1037
- * holding their own copy. `KEEL_MCP_DAEMON=1` moves that copy into one
1037
+ * holding their own copy. `SYNCSTAFF_MCP_DAEMON=1` moves that copy into one
1038
1038
  * shared background process (daemon.ts); `daemonEnabled()` off (the
1039
1039
  * default) or the daemon being unreachable both fall through to exactly
1040
1040
  * this process's own build, unchanged from before daemon mode existed.
package/dist/mcp/login.js CHANGED
@@ -14,34 +14,34 @@ function sleep(ms) {
14
14
  return new Promise((resolve) => setTimeout(resolve, ms));
15
15
  }
16
16
  /**
17
- * Where Keel is, when nobody said.
17
+ * Where Syncstaff is, when nobody said.
18
18
  *
19
19
  * This used to be `http://localhost:7377`, which is right for one developer
20
20
  * running a server on their own machine and wrong for everyone else — and
21
21
  * "everyone else" is who runs this command. They got a connection refused
22
- * against a host they never chose, which reads as "Keel is down" rather than
23
- * "you didn't say where Keel is".
22
+ * against a host they never chose, which reads as "Syncstaff is down" rather than
23
+ * "you didn't say where Syncstaff is".
24
24
  *
25
25
  * A hosted default is the honest one for a package whose whole purpose is
26
- * connecting to a hosted server. Override with `--server` or `KEEL_URL`;
26
+ * connecting to a hosted server. Override with `--server` or `SYNCSTAFF_URL`;
27
27
  * self-hosters set it once at login and it is remembered in
28
- * `.keel/config.json` thereafter.
28
+ * `.syncstaff/config.json` thereafter.
29
29
  */
30
- export const DEFAULT_KEEL_SERVER = "https://openclaw212onubuntu-s-2vcpu-4gb-amd-nyc3-01.taild4ce34.ts.net";
30
+ export const DEFAULT_SYNCSTAFF_SERVER = "https://openclaw212onubuntu-s-2vcpu-4gb-amd-nyc3-01.taild4ce34.ts.net";
31
31
  function normalizeServer(value) {
32
32
  let server;
33
33
  try {
34
34
  server = new URL(value.trim());
35
35
  }
36
36
  catch {
37
- throw new Error("keel login: --server must be an absolute HTTP(S) URL");
37
+ throw new Error("syncstaff login: --server must be an absolute HTTP(S) URL");
38
38
  }
39
39
  if (!["http:", "https:"].includes(server.protocol) ||
40
40
  server.username ||
41
41
  server.password ||
42
42
  server.search ||
43
43
  server.hash) {
44
- throw new Error("keel login: --server must be an HTTP(S) URL without credentials, a query, or a fragment");
44
+ throw new Error("syncstaff login: --server must be an HTTP(S) URL without credentials, a query, or a fragment");
45
45
  }
46
46
  return server.toString().replace(/\/+$/, "");
47
47
  }
@@ -49,7 +49,7 @@ function apiError(method, path, status, body) {
49
49
  const detail = typeof body.error === "string" ? body.error : JSON.stringify(body);
50
50
  return new Error(`${method} ${path} -> ${status}: ${detail}`);
51
51
  }
52
- /** Run the interactive device login used by both the public `keel` bin and the local hook CLI. */
52
+ /** Run the interactive device login used by both the public `syncstaff` bin and the local hook CLI. */
53
53
  export async function runLogin(args, deps = {}) {
54
54
  const cwd = deps.cwd ?? process.cwd();
55
55
  const root = repoRoot(cwd);
@@ -58,23 +58,23 @@ export async function runLogin(args, deps = {}) {
58
58
  const write = deps.write ?? ((message) => process.stdout.write(`${message}\n`));
59
59
  const wait = deps.wait ?? sleep;
60
60
  const existing = loadClientConfig(root);
61
- const chosenServer = option(args, "server") ?? env.KEEL_URL ?? env.CHARTER_URL ?? existing?.server;
61
+ const chosenServer = option(args, "server") ?? env.SYNCSTAFF_URL ?? existing?.server;
62
62
  // Keep the localhost fallback so a developer running a local server still
63
63
  // needs no flags — but remember that nobody asked for it.
64
64
  //
65
- // Whoever set this project up has a `.keel/config.json` with the real
65
+ // Whoever set this project up has a `.syncstaff/config.json` with the real
66
66
  // server in it, so they never reach this line. The next person to clone
67
67
  // does, gets ECONNREFUSED against a host they never chose, and reads it as
68
- // "Keel is down". Three lines below, `--project` refuses to guess at all;
68
+ // "Syncstaff is down". Three lines below, `--project` refuses to guess at all;
69
69
  // there is no principled reason these two behave differently, and the
70
70
  // asymmetry is invisible to everyone who already has a config.
71
71
  const serverWasDefaulted = !chosenServer;
72
- const server = normalizeServer(chosenServer ?? DEFAULT_KEEL_SERVER);
73
- const project = option(args, "project") ?? env.KEEL_PROJECT ?? env.CHARTER_PROJECT ?? existing?.project;
72
+ const server = normalizeServer(chosenServer ?? DEFAULT_SYNCSTAFF_SERVER);
73
+ const project = option(args, "project") ?? env.SYNCSTAFF_PROJECT ?? existing?.project;
74
74
  if (!project?.trim()) {
75
- throw new Error("keel login: --project <id|name> (or KEEL_PROJECT) is required");
75
+ throw new Error("syncstaff login: --project <id|name> (or SYNCSTAFF_PROJECT) is required");
76
76
  }
77
- const vendor = option(args, "vendor") ?? env.KEEL_VENDOR ?? env.CHARTER_VENDOR ?? existing?.vendor ?? "other";
77
+ const vendor = option(args, "vendor") ?? env.SYNCSTAFF_VENDOR ?? existing?.vendor ?? "other";
78
78
  const clientName = option(args, "name") ?? `${vendor} on ${hostname()}`;
79
79
  const request = async (path, body) => {
80
80
  const response = await fetchImpl(`${server}${path}`, {
@@ -94,7 +94,7 @@ export async function runLogin(args, deps = {}) {
94
94
  return error;
95
95
  error.message +=
96
96
  `\n\nNo --server was given, so this defaulted to ${server}.` +
97
- `\nIf you meant a hosted Keel, pass --server <url> or set KEEL_URL.`;
97
+ `\nIf you meant hosted Syncstaff, pass --server <url> or set SYNCSTAFF_URL.`;
98
98
  return error;
99
99
  };
100
100
  let issued;
@@ -115,7 +115,7 @@ export async function runLogin(args, deps = {}) {
115
115
  if (typeof issued.body.device_code !== "string" ||
116
116
  typeof issued.body.user_code !== "string" ||
117
117
  typeof issued.body.verification_uri_complete !== "string") {
118
- throw new Error("keel login: server returned an invalid device authorization response");
118
+ throw new Error("syncstaff login: server returned an invalid device authorization response");
119
119
  }
120
120
  const verificationUrl = new URL(issued.body.verification_uri_complete, `${server}/`).toString();
121
121
  write(`Open this link in your browser:\n${verificationUrl}`);
@@ -125,7 +125,7 @@ export async function runLogin(args, deps = {}) {
125
125
  ? Date.parse(issued.body.expires_at)
126
126
  : Date.now() + Number(issued.body.expires_in_seconds ?? 600) * 1000;
127
127
  if (!Number.isFinite(expiresAt)) {
128
- throw new Error("keel login: server returned an invalid device authorization expiry");
128
+ throw new Error("syncstaff login: server returned an invalid device authorization expiry");
129
129
  }
130
130
  const initialInterval = Number(issued.body.interval_seconds ?? 2);
131
131
  let intervalSeconds = Number.isFinite(initialInterval) ? Math.max(0, initialInterval) : 2;
@@ -146,7 +146,7 @@ export async function runLogin(args, deps = {}) {
146
146
  }
147
147
  for (const key of ["token", "team_id", "project_id", "project_name"]) {
148
148
  if (typeof polled.body[key] !== "string" || !polled.body[key]) {
149
- throw new Error(`keel login: token response omitted ${key}`);
149
+ throw new Error(`syncstaff login: token response omitted ${key}`);
150
150
  }
151
151
  }
152
152
  const config = {
@@ -165,5 +165,5 @@ export async function runLogin(args, deps = {}) {
165
165
  teamId: polled.body.team_id,
166
166
  };
167
167
  }
168
- throw new Error("keel login: device authorization expired before approval");
168
+ throw new Error("syncstaff login: device authorization expired before approval");
169
169
  }
package/dist/mcp/setup.js CHANGED
@@ -3,8 +3,8 @@ import { join } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  import { repoRoot } from "../lib/agent-state.js";
6
- import { DEFAULT_KEEL_SERVER, runLogin } from "./login.js";
7
- const PACKAGE_VERSION = "0.2.2";
6
+ import { SYNCSTAFF_RELEASE_VERSION } from "../lib/version.js";
7
+ import { DEFAULT_SYNCSTAFF_SERVER, runLogin } from "./login.js";
8
8
  function option(args, name) {
9
9
  const flag = `--${name}`;
10
10
  const index = args.indexOf(flag);
@@ -22,7 +22,7 @@ export function defaultProjectName(root) {
22
22
  const base = root.split(/[\\/]/).filter(Boolean).pop() ?? "";
23
23
  return base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24
24
  }
25
- export function writeMcpConfig(root, packageVersion = PACKAGE_VERSION) {
25
+ export function writeMcpConfig(root, packageVersion = SYNCSTAFF_RELEASE_VERSION) {
26
26
  const path = join(root, ".mcp.json");
27
27
  let config = {};
28
28
  if (existsSync(path)) {
@@ -35,8 +35,14 @@ export function writeMcpConfig(root, packageVersion = PACKAGE_VERSION) {
35
35
  throw new Error(`Syncstaff setup: ${path} exists but is not valid JSON`);
36
36
  }
37
37
  }
38
+ const servers = config.mcpServers && typeof config.mcpServers === "object"
39
+ ? { ...config.mcpServers }
40
+ : {};
41
+ for (const retired of [["ke", "el"].join(""), ["na", "ib"].join(""), "charter"]) {
42
+ delete servers[retired];
43
+ }
38
44
  config.mcpServers = {
39
- ...(config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {}),
45
+ ...servers,
40
46
  sync: {
41
47
  command: "npx",
42
48
  // The public Syncstaff adapter package. Keep generated client configs on
@@ -65,13 +71,13 @@ export async function runSetup(args, deps = {}) {
65
71
  return supplied;
66
72
  return (await rl.question(`${label}: `)).trim();
67
73
  };
68
- const server = await ask("Syncstaff server", option(args, "server") ?? env.SYNCSTAFF_URL ?? env.NAIB_URL ?? env.KEEL_URL ?? DEFAULT_KEEL_SERVER);
74
+ const server = await ask("Syncstaff server", option(args, "server") ?? env.SYNCSTAFF_URL ?? DEFAULT_SYNCSTAFF_SERVER);
69
75
  // Default to the directory being set up. Asking someone to name a project
70
76
  // while they are standing inside it is asking them to read it off their
71
77
  // own screen — and the name they invent instead is the one that then has
72
78
  // to match at approval time.
73
- const project = await ask("Project id or name", option(args, "project") ?? env.SYNCSTAFF_PROJECT ?? env.NAIB_PROJECT ?? env.KEEL_PROJECT ?? defaultProjectName(root));
74
- const vendor = await ask("Agent vendor", option(args, "vendor") ?? env.SYNCSTAFF_VENDOR ?? env.NAIB_VENDOR ?? env.KEEL_VENDOR ?? "other");
79
+ const project = await ask("Project id or name", option(args, "project") ?? env.SYNCSTAFF_PROJECT ?? defaultProjectName(root));
80
+ const vendor = await ask("Agent vendor", option(args, "vendor") ?? env.SYNCSTAFF_VENDOR ?? "other");
75
81
  if (!project)
76
82
  throw new Error("Syncstaff setup: project is required");
77
83
  const result = await runLogin(["--server", server, "--project", project, "--vendor", vendor], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syncstaff-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "MCP adapter for Syncstaff cross-agent coordination",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "repository": {
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/jb63126/sync.git",
33
- "directory": "keel/packages/mcp"
33
+ "directory": "sync/packages/mcp"
34
34
  },
35
35
  "keywords": [
36
36
  "syncstaff",