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 +6 -6
- package/dist/lib/agent-state.js +7 -8
- package/dist/lib/blast.js +5 -5
- package/dist/lib/client-config.js +10 -10
- package/dist/lib/env-compat.js +12 -62
- package/dist/lib/index/aliases.js +2 -2
- package/dist/lib/index/call-sites.js +2 -2
- package/dist/lib/index/checker-resolver.js +3 -3
- package/dist/lib/index/lexical.js +5 -5
- package/dist/lib/index/pages.js +1 -1
- package/dist/lib/index/pipeline.js +5 -5
- package/dist/lib/index/registry.js +2 -2
- package/dist/lib/index/symbols.js +1 -1
- package/dist/lib/index/transformers-embedder.js +1 -1
- package/dist/lib/index/typescript-parser.js +2 -2
- package/dist/lib/index/vector-cache.js +1 -1
- package/dist/lib/mcp-compaction.js +1 -1
- package/dist/lib/model-roles.js +4 -11
- package/dist/lib/path-warnings.js +3 -3
- package/dist/lib/version.js +11 -8
- package/dist/lib/worktree.js +3 -3
- package/dist/mcp/daemon-client.js +2 -2
- package/dist/mcp/daemon-protocol.js +3 -3
- package/dist/mcp/graph-ops.js +4 -4
- package/dist/mcp/index.js +45 -45
- package/dist/mcp/login.js +21 -21
- package/dist/mcp/setup.js +13 -7
- package/package.json +2 -2
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.
|
|
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.
|
|
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
|
-
`.
|
|
40
|
-
The repository's `.
|
|
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.
|
|
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 `
|
|
78
|
+
From `sync/`, run:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
npm run test:mcp-package
|
package/dist/lib/agent-state.js
CHANGED
|
@@ -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 `
|
|
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.
|
|
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.
|
|
55
|
-
const explicit =
|
|
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, ".
|
|
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.
|
|
88
|
-
const explicit =
|
|
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", ".
|
|
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 — `
|
|
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. "
|
|
265
|
-
* and a union report is "
|
|
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", "
|
|
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, ".
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
}
|
package/dist/lib/env-compat.js
CHANGED
|
@@ -1,66 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Reject configuration from retired product eras before any module-scope
|
|
3
|
+
* environment read can silently fall back to a default.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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;
|
|
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: `
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 `
|
|
66
|
-
* and `
|
|
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.
|
|
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 };
|
package/dist/lib/index/pages.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
377
|
-
...(graphArtifacts && process.env.
|
|
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
|
|
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
|
-
*
|
|
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 `
|
|
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
|
|
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: "
|
|
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. `
|
|
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.
|
|
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
|
|
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
|
*
|
package/dist/lib/model-roles.js
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
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
|
-
* `
|
|
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 `
|
|
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
|
|
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 ->
|
|
26
|
-
// back as a warning — including `
|
|
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
|
//
|
package/dist/lib/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Release version shared by the server and the published MCP adapter. */
|
|
2
|
-
export const
|
|
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
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
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
|
-
[
|
|
19
|
-
[
|
|
21
|
+
[SYNCSTAFF_ADAPTER_VERSION_HEADER]: SYNCSTAFF_RELEASE_VERSION,
|
|
22
|
+
[SYNCSTAFF_ADAPTER_PROTOCOL_HEADER]: String(SYNCSTAFF_ADAPTER_PROTOCOL_VERSION),
|
|
20
23
|
};
|
|
21
24
|
}
|
package/dist/lib/worktree.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
".
|
|
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("
|
|
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.
|
|
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(), `
|
|
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(), `
|
|
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(), `
|
|
99
|
+
return join(baseDir(), `sync-daemon-${daemonInstanceId(root)}.pid`);
|
|
100
100
|
}
|
package/dist/mcp/graph-ops.js
CHANGED
|
@@ -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, ".
|
|
51
|
-
archive = new persistenceMod.IndexSnapshotArchive(join(root, ".
|
|
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("
|
|
154
|
+
queryRouting: retrievalMode !== "stable" && envFlag("SYNCSTAFF_RETRIEVAL_ROUTING", false),
|
|
155
155
|
coChangeIndex,
|
|
156
|
-
...(retrievalMode !== "stable" && envFlag("
|
|
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
|
-
*
|
|
3
|
+
* sync-mcp — MCP adapter exposing the coordination tool surface (§6.4).
|
|
4
4
|
*
|
|
5
|
-
* Connection settings come from
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
13
|
+
* SYNCSTAFF_RETRIEVAL_ROUTING=1 to test candidate-only intent-specific query
|
|
14
14
|
* routing; stable mode ignores it.
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
27
|
-
* and
|
|
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
|
|
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,
|
|
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.
|
|
67
|
-
const PROJECT = process.env.
|
|
68
|
-
const VENDOR = process.env.
|
|
69
|
-
const TOKEN = process.env.
|
|
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, "
|
|
82
|
-
join(root, "dist", "src", "cli", "
|
|
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,
|
|
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. `
|
|
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("
|
|
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
|
|
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
|
|
192
|
+
`Run: npm --prefix sync run login -- --server ${SERVER}` +
|
|
193
193
|
(PROJECT ? ` --project ${PROJECT}` : "") +
|
|
194
194
|
` --vendor ${VENDOR}\n` +
|
|
195
|
-
`Then run: node
|
|
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
|
-
// "
|
|
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("
|
|
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.
|
|
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("
|
|
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.
|
|
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:
|
|
472
|
-
protocolVersion: String(
|
|
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.
|
|
492
|
-
resolvedBy: process.env.
|
|
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:
|
|
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.
|
|
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 `
|
|
612
|
-
* `
|
|
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 (
|
|
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("
|
|
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("
|
|
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
|
|
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. `
|
|
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
|
|
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 "
|
|
23
|
-
* "you didn't say where
|
|
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 `
|
|
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
|
-
* `.
|
|
28
|
+
* `.syncstaff/config.json` thereafter.
|
|
29
29
|
*/
|
|
30
|
-
export const
|
|
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("
|
|
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("
|
|
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 `
|
|
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.
|
|
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 `.
|
|
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
|
-
// "
|
|
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 ??
|
|
73
|
-
const project = option(args, "project") ?? env.
|
|
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("
|
|
75
|
+
throw new Error("syncstaff login: --project <id|name> (or SYNCSTAFF_PROJECT) is required");
|
|
76
76
|
}
|
|
77
|
-
const vendor = option(args, "vendor") ?? env.
|
|
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
|
|
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("
|
|
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("
|
|
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(`
|
|
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("
|
|
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 {
|
|
7
|
-
|
|
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 =
|
|
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
|
-
...
|
|
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 ??
|
|
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 ??
|
|
74
|
-
const vendor = await ask("Agent vendor", option(args, "vendor") ?? env.SYNCSTAFF_VENDOR ??
|
|
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
|
+
"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": "
|
|
33
|
+
"directory": "sync/packages/mcp"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"syncstaff",
|