syncstaff-mcp 0.2.3
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 +86 -0
- package/dist/lib/agent-state.js +119 -0
- package/dist/lib/blast.js +462 -0
- package/dist/lib/client-config.js +81 -0
- package/dist/lib/env-compat.js +66 -0
- package/dist/lib/globs.js +0 -0
- package/dist/lib/ids.js +24 -0
- package/dist/lib/index/aliases.js +244 -0
- package/dist/lib/index/call-sites.js +178 -0
- package/dist/lib/index/checker-resolver.js +257 -0
- package/dist/lib/index/context-card.js +140 -0
- package/dist/lib/index/coverage.js +218 -0
- package/dist/lib/index/delivery.js +66 -0
- package/dist/lib/index/discovery.js +90 -0
- package/dist/lib/index/embedding.js +110 -0
- package/dist/lib/index/file-index.js +222 -0
- package/dist/lib/index/fingerprint.js +0 -0
- package/dist/lib/index/git-history.js +136 -0
- package/dist/lib/index/graph.js +234 -0
- package/dist/lib/index/impact.js +174 -0
- package/dist/lib/index/incremental.js +332 -0
- package/dist/lib/index/lexical.js +462 -0
- package/dist/lib/index/order.js +43 -0
- package/dist/lib/index/pages.js +357 -0
- package/dist/lib/index/persistence.js +233 -0
- package/dist/lib/index/pipeline.js +527 -0
- package/dist/lib/index/registry.js +106 -0
- package/dist/lib/index/resolve.js +280 -0
- package/dist/lib/index/semantic.js +381 -0
- package/dist/lib/index/surfaces.js +27 -0
- package/dist/lib/index/symbols.js +426 -0
- package/dist/lib/index/transformers-embedder.js +73 -0
- package/dist/lib/index/typescript-parser.js +532 -0
- package/dist/lib/index/vector-cache.js +176 -0
- package/dist/lib/index/verification.js +58 -0
- package/dist/lib/mcp-compaction.js +241 -0
- package/dist/lib/model-roles.js +206 -0
- package/dist/lib/path-warnings.js +90 -0
- package/dist/lib/protocol.js +95 -0
- package/dist/lib/types.js +69 -0
- package/dist/lib/version.js +21 -0
- package/dist/lib/worktree.js +211 -0
- package/dist/mcp/approval.js +0 -0
- package/dist/mcp/cloud-connector.js +99 -0
- package/dist/mcp/daemon-client.js +156 -0
- package/dist/mcp/daemon-protocol.js +100 -0
- package/dist/mcp/escalation-waiter.js +183 -0
- package/dist/mcp/graph-ops.js +169 -0
- package/dist/mcp/index.js +1151 -0
- package/dist/mcp/login.js +169 -0
- package/dist/mcp/setup.js +90 -0
- package/package.json +42 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { repoRoot } from "../lib/agent-state.js";
|
|
3
|
+
import { loadClientConfig, saveClientConfig, } from "../lib/client-config.js";
|
|
4
|
+
import { adapterRequestHeaders } from "../lib/version.js";
|
|
5
|
+
function option(args, name) {
|
|
6
|
+
const exact = `--${name}`;
|
|
7
|
+
const index = args.indexOf(exact);
|
|
8
|
+
if (index !== -1)
|
|
9
|
+
return args[index + 1];
|
|
10
|
+
const prefix = `${exact}=`;
|
|
11
|
+
return args.find((value) => value.startsWith(prefix))?.slice(prefix.length);
|
|
12
|
+
}
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Where Keel is, when nobody said.
|
|
18
|
+
*
|
|
19
|
+
* This used to be `http://localhost:7377`, which is right for one developer
|
|
20
|
+
* running a server on their own machine and wrong for everyone else — and
|
|
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".
|
|
24
|
+
*
|
|
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`;
|
|
27
|
+
* self-hosters set it once at login and it is remembered in
|
|
28
|
+
* `.keel/config.json` thereafter.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_KEEL_SERVER = "https://openclaw212onubuntu-s-2vcpu-4gb-amd-nyc3-01.taild4ce34.ts.net";
|
|
31
|
+
function normalizeServer(value) {
|
|
32
|
+
let server;
|
|
33
|
+
try {
|
|
34
|
+
server = new URL(value.trim());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("keel login: --server must be an absolute HTTP(S) URL");
|
|
38
|
+
}
|
|
39
|
+
if (!["http:", "https:"].includes(server.protocol) ||
|
|
40
|
+
server.username ||
|
|
41
|
+
server.password ||
|
|
42
|
+
server.search ||
|
|
43
|
+
server.hash) {
|
|
44
|
+
throw new Error("keel login: --server must be an HTTP(S) URL without credentials, a query, or a fragment");
|
|
45
|
+
}
|
|
46
|
+
return server.toString().replace(/\/+$/, "");
|
|
47
|
+
}
|
|
48
|
+
function apiError(method, path, status, body) {
|
|
49
|
+
const detail = typeof body.error === "string" ? body.error : JSON.stringify(body);
|
|
50
|
+
return new Error(`${method} ${path} -> ${status}: ${detail}`);
|
|
51
|
+
}
|
|
52
|
+
/** Run the interactive device login used by both the public `keel` bin and the local hook CLI. */
|
|
53
|
+
export async function runLogin(args, deps = {}) {
|
|
54
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
55
|
+
const root = repoRoot(cwd);
|
|
56
|
+
const env = deps.env ?? process.env;
|
|
57
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
58
|
+
const write = deps.write ?? ((message) => process.stdout.write(`${message}\n`));
|
|
59
|
+
const wait = deps.wait ?? sleep;
|
|
60
|
+
const existing = loadClientConfig(root);
|
|
61
|
+
const chosenServer = option(args, "server") ?? env.KEEL_URL ?? env.CHARTER_URL ?? existing?.server;
|
|
62
|
+
// Keep the localhost fallback so a developer running a local server still
|
|
63
|
+
// needs no flags — but remember that nobody asked for it.
|
|
64
|
+
//
|
|
65
|
+
// Whoever set this project up has a `.keel/config.json` with the real
|
|
66
|
+
// server in it, so they never reach this line. The next person to clone
|
|
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;
|
|
69
|
+
// there is no principled reason these two behave differently, and the
|
|
70
|
+
// asymmetry is invisible to everyone who already has a config.
|
|
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;
|
|
74
|
+
if (!project?.trim()) {
|
|
75
|
+
throw new Error("keel login: --project <id|name> (or KEEL_PROJECT) is required");
|
|
76
|
+
}
|
|
77
|
+
const vendor = option(args, "vendor") ?? env.KEEL_VENDOR ?? env.CHARTER_VENDOR ?? existing?.vendor ?? "other";
|
|
78
|
+
const clientName = option(args, "name") ?? `${vendor} on ${hostname()}`;
|
|
79
|
+
const request = async (path, body) => {
|
|
80
|
+
const response = await fetchImpl(`${server}${path}`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json", ...adapterRequestHeaders() },
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
signal: AbortSignal.timeout(10_000),
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
status: response.status,
|
|
88
|
+
body: (await response.json().catch(() => ({}))),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/** Append the "you never chose this server" hint to any first-contact failure. */
|
|
92
|
+
const withServerHint = (error) => {
|
|
93
|
+
if (!serverWasDefaulted)
|
|
94
|
+
return error;
|
|
95
|
+
error.message +=
|
|
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.`;
|
|
98
|
+
return error;
|
|
99
|
+
};
|
|
100
|
+
let issued;
|
|
101
|
+
try {
|
|
102
|
+
issued = await request("/auth/device/code", {
|
|
103
|
+
project: project.trim(),
|
|
104
|
+
client_name: clientName,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// A connection refusal never reaches the status check below, and is
|
|
109
|
+
// exactly the case the hint exists for.
|
|
110
|
+
throw withServerHint(error instanceof Error ? error : new Error(String(error)));
|
|
111
|
+
}
|
|
112
|
+
if (issued.status !== 201) {
|
|
113
|
+
throw withServerHint(apiError("POST", "/auth/device/code", issued.status, issued.body));
|
|
114
|
+
}
|
|
115
|
+
if (typeof issued.body.device_code !== "string" ||
|
|
116
|
+
typeof issued.body.user_code !== "string" ||
|
|
117
|
+
typeof issued.body.verification_uri_complete !== "string") {
|
|
118
|
+
throw new Error("keel login: server returned an invalid device authorization response");
|
|
119
|
+
}
|
|
120
|
+
const verificationUrl = new URL(issued.body.verification_uri_complete, `${server}/`).toString();
|
|
121
|
+
write(`Open this link in your browser:\n${verificationUrl}`);
|
|
122
|
+
write(`Code: ${issued.body.user_code}`);
|
|
123
|
+
write("Waiting for approval…");
|
|
124
|
+
const expiresAt = typeof issued.body.expires_at === "string"
|
|
125
|
+
? Date.parse(issued.body.expires_at)
|
|
126
|
+
: Date.now() + Number(issued.body.expires_in_seconds ?? 600) * 1000;
|
|
127
|
+
if (!Number.isFinite(expiresAt)) {
|
|
128
|
+
throw new Error("keel login: server returned an invalid device authorization expiry");
|
|
129
|
+
}
|
|
130
|
+
const initialInterval = Number(issued.body.interval_seconds ?? 2);
|
|
131
|
+
let intervalSeconds = Number.isFinite(initialInterval) ? Math.max(0, initialInterval) : 2;
|
|
132
|
+
while (Date.now() < expiresAt) {
|
|
133
|
+
await wait(intervalSeconds * 1000);
|
|
134
|
+
const polled = await request("/auth/device/token", {
|
|
135
|
+
device_code: issued.body.device_code,
|
|
136
|
+
});
|
|
137
|
+
if (polled.status === 202 && polled.body.status === "authorization_pending") {
|
|
138
|
+
const nextInterval = Number(polled.body.interval_seconds ?? intervalSeconds);
|
|
139
|
+
if (Number.isFinite(nextInterval)) {
|
|
140
|
+
intervalSeconds = Math.max(intervalSeconds, Math.max(0, nextInterval));
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (polled.status !== 200) {
|
|
145
|
+
throw apiError("POST", "/auth/device/token", polled.status, polled.body);
|
|
146
|
+
}
|
|
147
|
+
for (const key of ["token", "team_id", "project_id", "project_name"]) {
|
|
148
|
+
if (typeof polled.body[key] !== "string" || !polled.body[key]) {
|
|
149
|
+
throw new Error(`keel login: token response omitted ${key}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const config = {
|
|
153
|
+
version: 1,
|
|
154
|
+
server,
|
|
155
|
+
project: polled.body.project_id,
|
|
156
|
+
token: polled.body.token,
|
|
157
|
+
vendor,
|
|
158
|
+
};
|
|
159
|
+
const configPath = saveClientConfig(root, config);
|
|
160
|
+
write(`Connected to ${polled.body.project_name}. Credentials saved to ${configPath}.`);
|
|
161
|
+
return {
|
|
162
|
+
configPath,
|
|
163
|
+
projectId: polled.body.project_id,
|
|
164
|
+
projectName: polled.body.project_name,
|
|
165
|
+
teamId: polled.body.team_id,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
throw new Error("keel login: device authorization expired before approval");
|
|
169
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { repoRoot } from "../lib/agent-state.js";
|
|
6
|
+
import { DEFAULT_KEEL_SERVER, runLogin } from "./login.js";
|
|
7
|
+
const PACKAGE_VERSION = "0.2.2";
|
|
8
|
+
function option(args, name) {
|
|
9
|
+
const flag = `--${name}`;
|
|
10
|
+
const index = args.indexOf(flag);
|
|
11
|
+
if (index !== -1)
|
|
12
|
+
return args[index + 1];
|
|
13
|
+
return args.find((value) => value.startsWith(`${flag}=`))?.slice(flag.length + 1);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The folder being set up, as a project name the server will accept.
|
|
17
|
+
*
|
|
18
|
+
* Server-side names are created verbatim, so normalise here rather than
|
|
19
|
+
* letting a directory like "My App" become a name nobody can retype.
|
|
20
|
+
*/
|
|
21
|
+
export function defaultProjectName(root) {
|
|
22
|
+
const base = root.split(/[\\/]/).filter(Boolean).pop() ?? "";
|
|
23
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
24
|
+
}
|
|
25
|
+
export function writeMcpConfig(root, packageVersion = PACKAGE_VERSION) {
|
|
26
|
+
const path = join(root, ".mcp.json");
|
|
27
|
+
let config = {};
|
|
28
|
+
if (existsSync(path)) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
32
|
+
config = parsed;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(`Syncstaff setup: ${path} exists but is not valid JSON`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
config.mcpServers = {
|
|
39
|
+
...(config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {}),
|
|
40
|
+
sync: {
|
|
41
|
+
command: "npx",
|
|
42
|
+
// The public Syncstaff adapter package. Keep generated client configs on
|
|
43
|
+
// the canonical product name so fresh installs never expose retired names.
|
|
44
|
+
args: ["-y", `syncstaff-mcp@${packageVersion}`],
|
|
45
|
+
// Bounds the adapter's V8 heap so several concurrent agent processes
|
|
46
|
+
// (one per coding session) don't each grow unbounded on a developer's
|
|
47
|
+
// machine — tsk_01M1MQ01E1D7JSEHFVE2RCK8DJ. 256MB is generous enough
|
|
48
|
+
// to avoid GC thrashing during multi-file AST traversal while still
|
|
49
|
+
// capping the worst case.
|
|
50
|
+
env: { NODE_OPTIONS: "--max-old-space-size=256" },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
export async function runSetup(args, deps = {}) {
|
|
57
|
+
const root = repoRoot(deps.cwd ?? process.cwd());
|
|
58
|
+
const env = deps.env ?? process.env;
|
|
59
|
+
const write = deps.write ?? ((message) => process.stdout.write(`${message}\n`));
|
|
60
|
+
const rl = createInterface({ input, output });
|
|
61
|
+
try {
|
|
62
|
+
const ask = async (label, current) => {
|
|
63
|
+
const supplied = current.trim();
|
|
64
|
+
if (supplied)
|
|
65
|
+
return supplied;
|
|
66
|
+
return (await rl.question(`${label}: `)).trim();
|
|
67
|
+
};
|
|
68
|
+
const server = await ask("Syncstaff server", option(args, "server") ?? env.SYNCSTAFF_URL ?? env.NAIB_URL ?? env.KEEL_URL ?? DEFAULT_KEEL_SERVER);
|
|
69
|
+
// Default to the directory being set up. Asking someone to name a project
|
|
70
|
+
// while they are standing inside it is asking them to read it off their
|
|
71
|
+
// own screen — and the name they invent instead is the one that then has
|
|
72
|
+
// 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");
|
|
75
|
+
if (!project)
|
|
76
|
+
throw new Error("Syncstaff setup: project is required");
|
|
77
|
+
const result = await runLogin(["--server", server, "--project", project, "--vendor", vendor], {
|
|
78
|
+
cwd: root,
|
|
79
|
+
env,
|
|
80
|
+
write,
|
|
81
|
+
});
|
|
82
|
+
const configPath = writeMcpConfig(root);
|
|
83
|
+
write(`Syncstaff is ready for ${result.projectName}.`);
|
|
84
|
+
write(`MCP configuration saved to ${configPath}`);
|
|
85
|
+
write("Restart your coding agent so it loads the Syncstaff MCP server as sync.");
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
rl.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "syncstaff-mcp",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "MCP adapter for Syncstaff cross-agent coordination",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"syncstaff-mcp": "dist/mcp/index.js",
|
|
8
|
+
"syncstaff": "dist/mcp/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "npm --prefix ../.. run build:mcp-package",
|
|
16
|
+
"smoke": "node ../../scripts/smoke-mcp-package.mjs",
|
|
17
|
+
"prepublishOnly": "npm run build && npm run smoke"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
21
|
+
"zod": "^4.2.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.5.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/jb63126/sync.git",
|
|
33
|
+
"directory": "keel/packages/mcp"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"syncstaff",
|
|
37
|
+
"mcp",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"multi-agent",
|
|
40
|
+
"coordination"
|
|
41
|
+
]
|
|
42
|
+
}
|