@ravefox/space 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +205 -0
- package/NOTICE +55 -0
- package/README.md +63 -2
- package/assets/webhook-sdk/python/bot.py +63 -0
- package/assets/webhook-sdk/python/commonly.py +232 -0
- package/node_modules/@commonlyai/seats/README.md +25 -0
- package/node_modules/@commonlyai/seats/package.json +22 -0
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +264 -0
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +24 -0
- package/node_modules/@commonlyai/seats/src/contract.js +48 -0
- package/node_modules/@commonlyai/seats/src/failures.js +44 -0
- package/node_modules/@commonlyai/seats/src/harness-plan.js +329 -0
- package/node_modules/@commonlyai/seats/src/index.js +22 -0
- package/node_modules/@commonlyai/seats/src/process-group.js +269 -0
- package/node_modules/@commonlyai/seats/src/record/format.js +737 -0
- package/node_modules/@commonlyai/seats/src/record/otel.js +238 -0
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +29 -0
- package/node_modules/@commonlyai/seats/src/seat-model.js +17 -0
- package/node_modules/@commonlyai/seats/src/supervisor.js +308 -0
- package/node_modules/@commonlyai/seats/src/trace/events.js +46 -0
- package/node_modules/@commonlyai/seats/src/trace/store.js +276 -0
- package/node_modules/@commonlyai/seats/src/turn-processes.js +226 -0
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +418 -0
- package/node_modules/@commonlyai/seats/src/valet/env.js +201 -0
- package/node_modules/@commonlyai/seats/src/valet/grant.js +123 -0
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +213 -0
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +529 -0
- package/package.json +63 -4
- package/skills/commonly/SKILL.md +233 -0
- package/src/commands/agent.js +6334 -0
- package/src/commands/daemon.js +933 -0
- package/src/commands/dev.js +153 -0
- package/src/commands/doctor.js +141 -0
- package/src/commands/inbox.js +330 -0
- package/src/commands/login.js +194 -0
- package/src/commands/mcp.js +56 -0
- package/src/commands/pod.js +120 -0
- package/src/commands/task.js +279 -0
- package/src/commands/verify.js +48 -0
- package/src/index.js +99 -0
- package/src/lib/adapter-login.js +76 -0
- package/src/lib/adapters/broker-bridge-entry.js +76 -0
- package/src/lib/adapters/claude.js +2207 -0
- package/src/lib/adapters/codex-broker-bridge.mjs +298 -0
- package/src/lib/adapters/codex.js +1898 -0
- package/src/lib/adapters/dsh.js +513 -0
- package/src/lib/adapters/goose.js +510 -0
- package/src/lib/adapters/hermes.js +18 -0
- package/src/lib/adapters/index.js +68 -0
- package/src/lib/adapters/opencode.js +1681 -0
- package/src/lib/adapters/pi-commonly-mcp.mjs +71 -0
- package/src/lib/adapters/pi-mcp-client.mjs +617 -0
- package/src/lib/adapters/pi-shell-env.mjs +41 -0
- package/src/lib/adapters/pi.js +1025 -0
- package/src/lib/adapters/qwen.js +523 -0
- package/src/lib/adapters/seat-model.js +8 -0
- package/src/lib/adapters/stub.js +1 -0
- package/src/lib/adapters/tested-version.js +106 -0
- package/src/lib/api.js +205 -0
- package/src/lib/claim-outcome.js +128 -0
- package/src/lib/claimed-work-gate.js +73 -0
- package/src/lib/cli-hint.js +33 -0
- package/src/lib/commit-attribution.js +391 -0
- package/src/lib/config.js +155 -0
- package/src/lib/content-refusal.js +94 -0
- package/src/lib/credential-file.js +208 -0
- package/src/lib/daemon-agent-states.js +95 -0
- package/src/lib/daemon-hint.js +18 -0
- package/src/lib/daemon-logs.js +43 -0
- package/src/lib/daemon-service-windows.js +263 -0
- package/src/lib/daemon-service.js +486 -0
- package/src/lib/daemon-state.js +84 -0
- package/src/lib/daemon-store.js +119 -0
- package/src/lib/daemon-supervisor.js +629 -0
- package/src/lib/daemon-watch.js +115 -0
- package/src/lib/declared-mcp-guard.js +284 -0
- package/src/lib/default-environment.js +207 -0
- package/src/lib/device-login.js +126 -0
- package/src/lib/digest-frame.js +151 -0
- package/src/lib/doctor.js +291 -0
- package/src/lib/egress-proxy.js +160 -0
- package/src/lib/enforcement.js +946 -0
- package/src/lib/environment.js +681 -0
- package/src/lib/first-output-watchdog.js +191 -0
- package/src/lib/git-readonly-server.js +138 -0
- package/src/lib/git-readonly.js +463 -0
- package/src/lib/git-remote-commonly.mjs +100 -0
- package/src/lib/grant-broker-guard.js +345 -0
- package/src/lib/hooks-config.js +238 -0
- package/src/lib/host-gh-broker.js +152 -0
- package/src/lib/host-gh-shim.mjs +98 -0
- package/src/lib/inbox.js +483 -0
- package/src/lib/instance-proxy.js +76 -0
- package/src/lib/machine-host.js +29 -0
- package/src/lib/mcp-credential-delivery.js +121 -0
- package/src/lib/mcp-first-run.js +50 -0
- package/src/lib/mcp-home.js +644 -0
- package/src/lib/mcp-ready.js +68 -0
- package/src/lib/mcp-server-version.js +123 -0
- package/src/lib/mcp-token.js +36 -0
- package/src/lib/mcp-warm-child.mjs +412 -0
- package/src/lib/memory-bridge.js +191 -0
- package/src/lib/memory-import.js +156 -0
- package/src/lib/missing-harness.js +31 -0
- package/src/lib/model-unavailable.js +154 -0
- package/src/lib/native-runtime-inventory.js +40 -0
- package/src/lib/outsider-grant-mcp.js +82 -0
- package/src/lib/own-key-launch.js +42 -0
- package/src/lib/pod-catch-up.js +55 -0
- package/src/lib/pod-focus.js +224 -0
- package/src/lib/pod-tail.js +68 -0
- package/src/lib/poll-liveness.js +103 -0
- package/src/lib/poll-retry.js +81 -0
- package/src/lib/poller.js +200 -0
- package/src/lib/provider-proxy.js +42 -0
- package/src/lib/public-seat-mcp-flag.js +36 -0
- package/src/lib/public-tool-error.js +60 -0
- package/src/lib/public-withheld-tools.js +48 -0
- package/src/lib/reply-receipt.js +55 -0
- package/src/lib/resolve-on-path.js +22 -0
- package/src/lib/runtime-inventory.js +56 -0
- package/src/lib/runtime-observation.js +37 -0
- package/src/lib/sandbox/bwrap.js +275 -0
- package/src/lib/sandbox/codex-perimeter.js +16 -0
- package/src/lib/sandbox/host-check.js +268 -0
- package/src/lib/sandbox/mode.js +49 -0
- package/src/lib/sandbox/opencode-loopback.mjs +68 -0
- package/src/lib/sandbox/seatbelt.js +684 -0
- package/src/lib/sandbox/turn-dir.js +56 -0
- package/src/lib/sandbox/windows-unsandboxed.js +183 -0
- package/src/lib/sandbox/workspace-git-config.js +243 -0
- package/src/lib/sandbox/workspace-pin.js +158 -0
- package/src/lib/seat-runtime-controls.js +61 -0
- package/src/lib/seat-stop.js +97 -0
- package/src/lib/server-skills.js +32 -0
- package/src/lib/session-recycle.js +219 -0
- package/src/lib/session-store.js +215 -0
- package/src/lib/skill-prompt.js +74 -0
- package/src/lib/skills-import.js +113 -0
- package/src/lib/spawn-credential.js +402 -0
- package/src/lib/spawn-retry.js +311 -0
- package/src/lib/stable-install.js +176 -0
- package/src/lib/supervised-turn.js +352 -0
- package/src/lib/task.js +235 -0
- package/src/lib/turn-failure-notice.js +177 -0
- package/src/lib/unconfined-seat-guard.js +94 -0
- package/src/lib/update-check.js +192 -0
- package/src/lib/upstream-refusal.js +1 -0
- package/src/lib/webhook-server.js +109 -0
- package/src/lib/windows-command.js +393 -0
- package/src/lib/workspace-git.js +87 -0
- package/src/lib/workspace-trust.js +162 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-output watchdog for a seat's model CLI spawn.
|
|
3
|
+
*
|
|
4
|
+
* On 2026-10-30 every launchd seat's `claude -p` turn parked while the Mac's
|
|
5
|
+
* console was locked: no transcript row, no stdout, no stderr, for the full
|
|
6
|
+
* 45-minute run timeout. Each one held its delivery the whole time, the retry
|
|
7
|
+
* respawned it into the same hang, and the log said only "claude timed out
|
|
8
|
+
* after 2700000ms", which names nothing.
|
|
9
|
+
*
|
|
10
|
+
* The signal is not stdout. A seat runs `claude -p --output-format text`,
|
|
11
|
+
* which writes stdout once, at the end, so a healthy 20-minute turn has zero
|
|
12
|
+
* stdout bytes for 20 minutes. What a healthy turn does write is its session
|
|
13
|
+
* transcript: measured on the dart seat, the first row landed 0.7 s after the
|
|
14
|
+
* spawn, and the hung spawns that night wrote no row at all. So a spawn counts
|
|
15
|
+
* as started when it writes any stdout or stderr byte OR its transcript grows.
|
|
16
|
+
* A spawn that does neither inside the window is killed with a reason. When
|
|
17
|
+
* the transcript is missing altogether, the path may simply be wrong, so the
|
|
18
|
+
* spawn is killed only if the console is also locked.
|
|
19
|
+
*
|
|
20
|
+
* That is the rule for a plain-text spawn. A stream-json spawn (TASK-710) counts
|
|
21
|
+
* only model events on stdout; see `modelStream` on watchFirstOutput.
|
|
22
|
+
*/
|
|
23
|
+
import { realpathSync, statSync } from 'fs';
|
|
24
|
+
import { readProcess } from '@commonlyai/seats/process-group';
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_FIRST_OUTPUT_WINDOW_MS = 180_000;
|
|
27
|
+
|
|
28
|
+
// 0 turns the watchdog off; anything unparseable keeps the default.
|
|
29
|
+
export const firstOutputWindowMs = (env = process.env) => {
|
|
30
|
+
const raw = env?.COMMONLY_FIRST_OUTPUT_WINDOW_MS;
|
|
31
|
+
if (raw === undefined || raw === '') return DEFAULT_FIRST_OUTPUT_WINDOW_MS;
|
|
32
|
+
const parsed = Number(raw);
|
|
33
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_FIRST_OUTPUT_WINDOW_MS;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const SPAWN_STALLED_CODE = 'spawn_stalled';
|
|
37
|
+
|
|
38
|
+
// The message carries the class word spawn-retry.js matches, so the stall gets
|
|
39
|
+
// its own backoff rather than RUNTIME's 5-second retry.
|
|
40
|
+
export const spawnStalledError = ({ label, windowMs, reason }) => {
|
|
41
|
+
const error = new Error(
|
|
42
|
+
`${label} produced no output in ${Math.round(windowMs / 1000)}s (${reason}); killed before the run timeout`,
|
|
43
|
+
);
|
|
44
|
+
error.code = SPAWN_STALLED_CODE;
|
|
45
|
+
error.stallReason = reason;
|
|
46
|
+
return error;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// `ioreg -n Root -d1` prints `"IOConsoleLocked" = Yes` while the screen is
|
|
50
|
+
// locked. True, false, or null when it cannot tell (not darwin, ioreg failed).
|
|
51
|
+
// child_process is loaded on demand: suites that mock it with only `spawn`
|
|
52
|
+
// would otherwise fail to import every adapter that uses this module.
|
|
53
|
+
export const consoleLocked = async ({ platform = process.platform, execFileImpl } = {}) => {
|
|
54
|
+
if (platform !== 'darwin') return null;
|
|
55
|
+
const run = execFileImpl || (await import('child_process')).execFile;
|
|
56
|
+
if (typeof run !== 'function') return null;
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
run('/usr/sbin/ioreg', ['-n', 'Root', '-d1'], { timeout: 5000 }, (error, stdout) => {
|
|
59
|
+
if (error) return resolve(null);
|
|
60
|
+
const match = String(stdout).match(/"IOConsoleLocked"\s*=\s*(Yes|No)/);
|
|
61
|
+
return resolve(match ? match[1] === 'Yes' : null);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Claude names its project folder after process.cwd(), which on macOS is the
|
|
67
|
+
// physical path (/tmp is /private/tmp), so the transcript is looked for there.
|
|
68
|
+
export const physicalPath = (dir) => {
|
|
69
|
+
if (!dir) return dir;
|
|
70
|
+
try {
|
|
71
|
+
return realpathSync(dir);
|
|
72
|
+
} catch {
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// How long a stalled spawn gets to exit after SIGTERM before SIGKILL.
|
|
78
|
+
export const STALL_KILL_GRACE_MS = 10_000;
|
|
79
|
+
|
|
80
|
+
// The boot line for a claude seat. This watchdog is the stall guard a claude
|
|
81
|
+
// turn runs: the seats supervisor's stall window is off for claude
|
|
82
|
+
// (STALL_DEFAULTS_MS in supervised-turn.js), and its package default of 120 s
|
|
83
|
+
// was once read as this one's (the G6 walk, 2026-11-01).
|
|
84
|
+
export const describeFirstOutputWatch = (env = process.env) => {
|
|
85
|
+
const windowMs = firstOutputWindowMs(env);
|
|
86
|
+
if (!windowMs) return 'first-output watchdog OFF for claude (COMMONLY_FIRST_OUTPUT_WINDOW_MS=0)';
|
|
87
|
+
return `first-output watchdog ON for claude: a turn with no model output in ${windowMs}ms is killed `
|
|
88
|
+
+ `(SIGTERM, then SIGKILL ${STALL_KILL_GRACE_MS}ms later if it is still alive)`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* SIGKILL each snapshotted descendant of a stalled spawn that is still alive
|
|
93
|
+
* once the spawn itself has exited. Its parent may have exited, so it may now
|
|
94
|
+
* belong to launchd: only the pid and start time are matched, which still
|
|
95
|
+
* rules out a reused pid. Returns the pids signalled.
|
|
96
|
+
*/
|
|
97
|
+
export const killStallSurvivors = (snapshot, { read = readProcess, kill = process.kill.bind(process) } = {}) => {
|
|
98
|
+
const killed = [];
|
|
99
|
+
for (const row of snapshot || []) {
|
|
100
|
+
const now = read(row.pid);
|
|
101
|
+
if (!now || now.started !== row.started) continue;
|
|
102
|
+
try { kill(row.pid, 'SIGKILL'); killed.push(row.pid); } catch { /* gone */ }
|
|
103
|
+
}
|
|
104
|
+
return killed;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sizeOf = (file) => {
|
|
108
|
+
if (!file) return -1;
|
|
109
|
+
try {
|
|
110
|
+
return statSync(file).size;
|
|
111
|
+
} catch {
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// A stream-json line is model progress when it is an event other than `system`.
|
|
117
|
+
// The `system` events are `init` (written before any model call) and
|
|
118
|
+
// `api_retry` (a provider failing and retrying). Counting either would keep a
|
|
119
|
+
// hung provider looking alive until the run timeout.
|
|
120
|
+
export const isModelProgressLine = (line) => {
|
|
121
|
+
try {
|
|
122
|
+
const event = JSON.parse(line);
|
|
123
|
+
return !!event && typeof event === 'object' && event.type !== 'system';
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Watch `proc` for its first sign of life. Returns `{ stop }`; call it when the
|
|
131
|
+
* process closes. `onStall(reason)` fires at most once, after the caller should
|
|
132
|
+
* kill the process.
|
|
133
|
+
*
|
|
134
|
+
* With `modelStream`, the spawn writes stream-json events. Only model output
|
|
135
|
+
* counts as life there. A transcript row does not, because claude writes its
|
|
136
|
+
* first row about a second after spawn, before any model reply. Stderr does
|
|
137
|
+
* not either: claude prints warnings at spawn (an unrecognised model, a
|
|
138
|
+
* connector notice) whatever the provider does next.
|
|
139
|
+
*/
|
|
140
|
+
export const watchFirstOutput = ({
|
|
141
|
+
proc,
|
|
142
|
+
windowMs,
|
|
143
|
+
transcriptPath = null,
|
|
144
|
+
modelStream = false,
|
|
145
|
+
onStall,
|
|
146
|
+
lockProbe = consoleLocked,
|
|
147
|
+
setTimeoutImpl = setTimeout,
|
|
148
|
+
clearTimeoutImpl = clearTimeout,
|
|
149
|
+
}) => {
|
|
150
|
+
if (!windowMs) return { stop: () => {} };
|
|
151
|
+
let started = false;
|
|
152
|
+
let stopped = false;
|
|
153
|
+
let partial = '';
|
|
154
|
+
const markStarted = () => { started = true; };
|
|
155
|
+
if (modelStream) {
|
|
156
|
+
proc.stdout?.on('data', (chunk) => {
|
|
157
|
+
const lines = (partial + chunk.toString()).split('\n');
|
|
158
|
+
partial = lines.pop();
|
|
159
|
+
if (lines.some(isModelProgressLine)) markStarted();
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
proc.stdout?.on('data', markStarted);
|
|
163
|
+
proc.stderr?.on('data', markStarted);
|
|
164
|
+
}
|
|
165
|
+
// A missing file counts as empty, so creating an empty one is not growth.
|
|
166
|
+
const baseline = Math.max(0, sizeOf(transcriptPath));
|
|
167
|
+
const timer = setTimeoutImpl(async () => {
|
|
168
|
+
const size = sizeOf(transcriptPath);
|
|
169
|
+
if (started || (!modelStream && size > baseline)) return;
|
|
170
|
+
const locked = await lockProbe().catch(() => null);
|
|
171
|
+
// Output can arrive while the probe runs (ps or ioreg may take a second).
|
|
172
|
+
if (stopped || started) return;
|
|
173
|
+
if (modelStream) {
|
|
174
|
+
// The model signal does not read the transcript, so a missing path cannot
|
|
175
|
+
// hide a hung provider here.
|
|
176
|
+
onStall(locked ? 'console locked' : 'stuck: no model output inside the window');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// No transcript where one was expected may only mean the path is wrong, so
|
|
180
|
+
// that alone never kills a turn: it takes a locked console as well.
|
|
181
|
+
if (size < 0 && locked !== true) return;
|
|
182
|
+
onStall(locked ? 'console locked' : 'no transcript, stdout or stderr');
|
|
183
|
+
}, windowMs);
|
|
184
|
+
timer?.unref?.();
|
|
185
|
+
return {
|
|
186
|
+
stop: () => {
|
|
187
|
+
stopped = true;
|
|
188
|
+
clearTimeoutImpl(timer);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `workspace_git` MCP server: one tool, `git`, for a workspace-trust seat
|
|
3
|
+
* (TASK-294). The claude adapter spawns it as
|
|
4
|
+
*
|
|
5
|
+
* node git-readonly-server.js --root <workspace> --git <binary>
|
|
6
|
+
*
|
|
7
|
+
* and the model sees it as `mcp__workspace_git__git`. It speaks the MCP stdio
|
|
8
|
+
* transport (newline-delimited JSON-RPC 2.0) and hands every call to
|
|
9
|
+
* runReadonlyGit, which refuses anything that is not read-only and runs the
|
|
10
|
+
* rest with execFile in the workspace.
|
|
11
|
+
*/
|
|
12
|
+
import { realpathSync, statSync } from 'fs';
|
|
13
|
+
import { isAbsolute } from 'path';
|
|
14
|
+
import { createInterface } from 'readline';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { READONLY_GIT_SUBCOMMANDS, runReadonlyGit } from './git-readonly.js';
|
|
17
|
+
|
|
18
|
+
export const SERVER_NAME = 'workspace_git';
|
|
19
|
+
export const TOOL_NAME = 'git';
|
|
20
|
+
const SERVER_VERSION = '1.0.0';
|
|
21
|
+
|
|
22
|
+
// The tool surface is the same in each of these, so the server answers in the
|
|
23
|
+
// version the client asked for when it is one of them.
|
|
24
|
+
const SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(['2024-11-05', '2025-03-26', '2025-06-18']);
|
|
25
|
+
|
|
26
|
+
export const GIT_TOOL = Object.freeze({
|
|
27
|
+
name: TOOL_NAME,
|
|
28
|
+
description: `Run read-only git in your workspace: ${READONLY_GIT_SUBCOMMANDS.join(', ')}. `
|
|
29
|
+
+ 'Pass the subcommand and its arguments separately, one argument per array entry, '
|
|
30
|
+
+ "e.g. { subcommand: 'log', args: ['-n', '5', '--oneline'] }. "
|
|
31
|
+
+ 'Options that write a file, run a program, or read outside the workspace are refused with the reason, '
|
|
32
|
+
+ 'and paths must stay inside the workspace. For log, diff and show, put paths after \'--\' '
|
|
33
|
+
+ "(e.g. ['-p', '--', 'src/app.js']). .env files are left out of every result. "
|
|
34
|
+
+ 'This is the only git available to you.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
subcommand: { type: 'string', enum: [...READONLY_GIT_SUBCOMMANDS] },
|
|
39
|
+
args: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: { type: 'string' },
|
|
42
|
+
description: 'Arguments after the subcommand, one per entry.',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['subcommand'],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const reply = (id, result) => ({ jsonrpc: '2.0', id, result });
|
|
51
|
+
const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A handler for one parsed JSON-RPC message. Resolves the response, or null
|
|
55
|
+
* for a notification (a message with no id), which gets none.
|
|
56
|
+
*/
|
|
57
|
+
export const createGitServerHandler = ({
|
|
58
|
+
root, git = 'git', baseEnv = process.env, run = runReadonlyGit,
|
|
59
|
+
}) => async (message) => {
|
|
60
|
+
if (!message || typeof message !== 'object' || Array.isArray(message) || typeof message.method !== 'string') {
|
|
61
|
+
return fail(message?.id ?? null, -32600, 'Invalid Request');
|
|
62
|
+
}
|
|
63
|
+
const { id, method, params } = message;
|
|
64
|
+
if (id === undefined) return null;
|
|
65
|
+
if (method === 'initialize') {
|
|
66
|
+
const asked = params?.protocolVersion;
|
|
67
|
+
return reply(id, {
|
|
68
|
+
protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(asked) ? asked : SUPPORTED_PROTOCOL_VERSIONS[0],
|
|
69
|
+
capabilities: { tools: {} },
|
|
70
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (method === 'ping') return reply(id, {});
|
|
74
|
+
if (method === 'tools/list') return reply(id, { tools: [GIT_TOOL] });
|
|
75
|
+
if (method === 'tools/call') {
|
|
76
|
+
if (params?.name !== TOOL_NAME) return fail(id, -32602, `Unknown tool: ${params?.name}`);
|
|
77
|
+
const { subcommand, args = [] } = params.arguments || {};
|
|
78
|
+
const { ok, text } = await run({
|
|
79
|
+
subcommand, args, root, git, baseEnv,
|
|
80
|
+
});
|
|
81
|
+
return reply(id, { content: [{ type: 'text', text }], isError: !ok });
|
|
82
|
+
}
|
|
83
|
+
return fail(id, -32601, `Method not found: ${method}`);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** `--root <dir> --git <binary>`; the root must be an existing directory. */
|
|
87
|
+
export const parseServerArgs = (argv) => {
|
|
88
|
+
const value = (flag) => {
|
|
89
|
+
const at = argv.indexOf(flag);
|
|
90
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
91
|
+
};
|
|
92
|
+
const root = value('--root');
|
|
93
|
+
if (typeof root !== 'string' || !isAbsolute(root)) throw new Error('--root <absolute workspace path> is required');
|
|
94
|
+
const real = realpathSync(root);
|
|
95
|
+
if (!statSync(real).isDirectory()) throw new Error(`--root ${root} is not a directory`);
|
|
96
|
+
return { root: real, git: value('--git') || 'git' };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const serve = ({
|
|
100
|
+
root, git, input = process.stdin, output = process.stdout, baseEnv = process.env,
|
|
101
|
+
}) => {
|
|
102
|
+
const handle = createGitServerHandler({ root, git, baseEnv });
|
|
103
|
+
// A closed pipe on the client side is the client leaving, not an error.
|
|
104
|
+
output.on('error', () => {});
|
|
105
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
106
|
+
lines.on('line', async (line) => {
|
|
107
|
+
if (!line.trim()) return;
|
|
108
|
+
let message;
|
|
109
|
+
try {
|
|
110
|
+
message = JSON.parse(line);
|
|
111
|
+
} catch {
|
|
112
|
+
output.write(`${JSON.stringify(fail(null, -32700, 'Parse error'))}\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const response = await handle(message);
|
|
116
|
+
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
117
|
+
});
|
|
118
|
+
return lines;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const runAsMain = () => {
|
|
122
|
+
try {
|
|
123
|
+
return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (runAsMain()) {
|
|
130
|
+
let options;
|
|
131
|
+
try {
|
|
132
|
+
options = parseServerArgs(process.argv.slice(2));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
process.stderr.write(`${SERVER_NAME}: ${err.message}\n`);
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
serve(options);
|
|
138
|
+
}
|