cuekit 0.0.12

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/src/doctor.ts ADDED
@@ -0,0 +1,438 @@
1
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { loadProjectConfig as loadProjectConfigFromDisk } from "@cuekit/project-config";
4
+ import {
5
+ DEFAULT_DB_PATH,
6
+ findInvalidTaskRows,
7
+ openDatabase,
8
+ repairTaskSqliteTimestamps,
9
+ } from "@cuekit/store";
10
+ import pkg from "../package.json" with { type: "json" };
11
+ import { formatCheckLine } from "./output.ts";
12
+
13
+ export type DoctorLevel = "ok" | "warn" | "fail";
14
+ export type DoctorCheck = { level: DoctorLevel; label: string; detail: string };
15
+ export type DoctorResult = {
16
+ exitCode: number;
17
+ checks: DoctorCheck[];
18
+ stdout: string;
19
+ stderr?: string;
20
+ };
21
+
22
+ export type DoctorExecResult =
23
+ | { ok: true; stdout: string; stderr?: string }
24
+ | { ok: false; stdout?: string; stderr: string };
25
+ export type DoctorExec = (command: string, args: string[]) => Promise<DoctorExecResult>;
26
+
27
+ export type WritableStateResult =
28
+ | { ok: true; path: string }
29
+ | { ok: false; path: string; reason: string };
30
+ export type DoctorProjectConfigResult =
31
+ | { ok: true; source: "config"; path: string }
32
+ | { ok: true; source: "git" | "cwd" }
33
+ | { ok: false; error: string };
34
+ export type LatestReleaseResult = { ok: true; tag: string } | { ok: false; reason: string };
35
+
36
+ export type RunDoctorOptions = {
37
+ cwd?: string;
38
+ env?: Record<string, string | undefined>;
39
+ fix?: boolean;
40
+ exec?: DoctorExec;
41
+ checkWritableState?: (env: Record<string, string | undefined>) => Promise<WritableStateResult>;
42
+ checkStateDbTaskRows?: (
43
+ env: Record<string, string | undefined>,
44
+ fix: boolean,
45
+ ) => Promise<DoctorCheck>;
46
+ loadProjectConfig?: (cwd: string) => DoctorProjectConfigResult;
47
+ getCurrentVersion?: () => string | undefined;
48
+ getLatestRelease?: () => Promise<LatestReleaseResult>;
49
+ };
50
+
51
+ type DoctorSpawnSync = (
52
+ command: string,
53
+ args: string[],
54
+ ) => {
55
+ success: boolean;
56
+ stdout: Uint8Array;
57
+ stderr: Uint8Array;
58
+ };
59
+
60
+ export function createDoctorExec(spawnSync: DoctorSpawnSync): DoctorExec {
61
+ return async (command, args) => {
62
+ try {
63
+ const proc = spawnSync(command, args);
64
+ const stdout = proc.stdout.toString();
65
+ const stderr = proc.stderr.toString();
66
+ return proc.success
67
+ ? { ok: true, stdout, stderr }
68
+ : { ok: false, stdout, stderr: stderr || "command failed" };
69
+ } catch (error) {
70
+ return { ok: false, stderr: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ };
73
+ }
74
+
75
+ export const defaultDoctorExec = createDoctorExec((command, args) =>
76
+ Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" }),
77
+ );
78
+
79
+ function findWritableExistingParent(path: string): string | undefined {
80
+ let current = path;
81
+ while (true) {
82
+ if (existsSync(current)) {
83
+ try {
84
+ if (statSync(current).isDirectory()) return current;
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ const parent = dirname(current);
90
+ if (parent === current) return undefined;
91
+ current = parent;
92
+ }
93
+ }
94
+
95
+ async function defaultCheckWritableState(
96
+ env: Record<string, string | undefined>,
97
+ ): Promise<WritableStateResult> {
98
+ const path =
99
+ env.CUEKIT_DB_PATH && env.CUEKIT_DB_PATH.length > 0 ? env.CUEKIT_DB_PATH : DEFAULT_DB_PATH;
100
+ if (path === ":memory:") return { ok: true, path };
101
+ const parent = findWritableExistingParent(dirname(path));
102
+ if (!parent) return { ok: false, path, reason: "no writable parent directory found" };
103
+ try {
104
+ accessSync(parent, constants.W_OK);
105
+ return { ok: true, path };
106
+ } catch (error) {
107
+ return { ok: false, path, reason: error instanceof Error ? error.message : String(error) };
108
+ }
109
+ }
110
+
111
+ async function defaultCheckStateDbTaskRows(
112
+ env: Record<string, string | undefined>,
113
+ fix: boolean,
114
+ ): Promise<DoctorCheck> {
115
+ const path =
116
+ env.CUEKIT_DB_PATH && env.CUEKIT_DB_PATH.length > 0 ? env.CUEKIT_DB_PATH : DEFAULT_DB_PATH;
117
+ if (path === ":memory:") {
118
+ return { level: "ok", label: "state db task rows", detail: "skipped (:memory:)" };
119
+ }
120
+ if (!existsSync(path)) {
121
+ return { level: "ok", label: "state db task rows", detail: "not created yet" };
122
+ }
123
+ let db: ReturnType<typeof openDatabase> | undefined;
124
+ try {
125
+ db = openDatabase({ path });
126
+ const repaired = fix ? repairTaskSqliteTimestamps(db) : 0;
127
+ const invalid = findInvalidTaskRows(db);
128
+ if (invalid.length === 0) {
129
+ return {
130
+ level: "ok",
131
+ label: "state db task rows",
132
+ detail: repaired > 0 ? `repaired ${repaired} SQLite timestamp(s)` : "healthy",
133
+ };
134
+ }
135
+ const examples = invalid
136
+ .slice(0, 3)
137
+ .map((row) => row.id)
138
+ .join(", ");
139
+ return {
140
+ level: "warn",
141
+ label: "state db task rows",
142
+ detail: `${invalid.length} invalid task row(s)${examples ? ` (${examples})` : ""}${
143
+ fix ? " after repair" : " (run cuekit doctor --fix)"
144
+ }`,
145
+ };
146
+ } catch (error) {
147
+ return {
148
+ level: "warn",
149
+ label: "state db task rows",
150
+ detail: `could not inspect (${error instanceof Error ? error.message : String(error)})`,
151
+ };
152
+ } finally {
153
+ try {
154
+ db?.close();
155
+ } catch {
156
+ // ignore close errors in diagnostics
157
+ }
158
+ }
159
+ }
160
+
161
+ function readMultiplexerSettings(cwd: string): {
162
+ requested: "tmux" | "zellij" | "herdr";
163
+ strict: boolean;
164
+ } {
165
+ try {
166
+ const loaded = loadProjectConfigFromDisk(cwd);
167
+ if (loaded.ok && loaded.discovery.source === "config") {
168
+ const configured = loaded.config.multiplexer;
169
+ return {
170
+ requested: typeof configured === "string" ? configured : (configured?.backend ?? "tmux"),
171
+ strict:
172
+ typeof configured === "object" && configured?.strict !== undefined
173
+ ? configured.strict
174
+ : loaded.config.multiplexer_strict === true,
175
+ };
176
+ }
177
+ } catch {
178
+ // fall through
179
+ }
180
+ return { requested: "tmux", strict: false };
181
+ }
182
+
183
+ function defaultLoadProjectConfig(cwd: string): DoctorProjectConfigResult {
184
+ const loaded = loadProjectConfigFromDisk(cwd);
185
+ if (!loaded.ok) return { ok: false, error: loaded.error };
186
+ if (loaded.discovery.source === "config" && loaded.discovery.configPath) {
187
+ return { ok: true, source: "config", path: loaded.discovery.configPath };
188
+ }
189
+ return { ok: true, source: loaded.discovery.source === "git" ? "git" : "cwd" };
190
+ }
191
+
192
+ async function defaultGetLatestRelease(): Promise<LatestReleaseResult> {
193
+ try {
194
+ const response = await fetch("https://api.github.com/repos/takemo101/cuekit/releases/latest", {
195
+ headers: { accept: "application/vnd.github+json" },
196
+ });
197
+ if (!response.ok) return { ok: false, reason: `HTTP ${response.status}` };
198
+ const body = (await response.json()) as { tag_name?: unknown };
199
+ return typeof body.tag_name === "string" && body.tag_name.length > 0
200
+ ? { ok: true, tag: body.tag_name }
201
+ : { ok: false, reason: "missing tag_name" };
202
+ } catch (error) {
203
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
204
+ }
205
+ }
206
+
207
+ function trimVersionOutput(value: string): string {
208
+ return value.trim().split(/\s+/).join(" ");
209
+ }
210
+
211
+ function normalizeReleaseVersion(value: string): string {
212
+ return value.trim().replace(/^v/, "");
213
+ }
214
+
215
+ const ADAPTER_EXECUTABLES = [
216
+ { kind: "claude-code", command: "claude" },
217
+ { kind: "pi", command: "pi" },
218
+ { kind: "opencode", command: "opencode" },
219
+ { kind: "jcode", command: "jcode" },
220
+ { kind: "gemini", command: "gemini" },
221
+ ] as const;
222
+
223
+ function renderDoctor(checks: DoctorCheck[]): string {
224
+ return [
225
+ "cuekit doctor",
226
+ "",
227
+ ...checks.map(formatCheckLine),
228
+ "",
229
+ "Next:",
230
+ " cuekit mcp config",
231
+ " cuekit update",
232
+ "",
233
+ ].join("\n");
234
+ }
235
+
236
+ export async function runDoctor(options: RunDoctorOptions = {}): Promise<DoctorResult> {
237
+ const env = options.env ?? process.env;
238
+ const fix = options.fix === true;
239
+ const exec = options.exec ?? defaultDoctorExec;
240
+ const checks: DoctorCheck[] = [];
241
+
242
+ const currentVersion = options.getCurrentVersion ? options.getCurrentVersion() : pkg.version;
243
+ checks.push(
244
+ currentVersion
245
+ ? { level: "ok", label: "cuekit", detail: currentVersion }
246
+ : { level: "warn", label: "cuekit", detail: "version unknown" },
247
+ );
248
+
249
+ const bunVersion = await exec("bun", ["--version"]);
250
+ checks.push(
251
+ bunVersion.ok
252
+ ? { level: "ok", label: "bun", detail: trimVersionOutput(bunVersion.stdout) }
253
+ : { level: "fail", label: "bun", detail: bunVersion.stderr || "not found" },
254
+ );
255
+
256
+ // Active multiplexer backend. Reads `multiplexer.backend` from project config
257
+ // (default tmux). When an optional backend is configured but its probe fails,
258
+ // the runtime factory soft-falls-back to tmux; doctor reports this so the
259
+ // operator can see at a glance whether they got the backend they asked
260
+ // for.
261
+ const cwdForConfig = options.cwd ?? process.cwd();
262
+ const { requested: requestedMultiplexer, strict: multiplexerStrict } =
263
+ readMultiplexerSettings(cwdForConfig);
264
+ const optionalBackendProbe =
265
+ requestedMultiplexer === "zellij"
266
+ ? await exec("zellij", ["--version"])
267
+ : requestedMultiplexer === "herdr"
268
+ ? await exec("herdr", ["--version"])
269
+ : null;
270
+ const fallbackApplied =
271
+ requestedMultiplexer !== "tmux" &&
272
+ optionalBackendProbe !== null &&
273
+ !optionalBackendProbe.ok &&
274
+ !multiplexerStrict;
275
+ const strictFailure =
276
+ requestedMultiplexer !== "tmux" &&
277
+ optionalBackendProbe !== null &&
278
+ !optionalBackendProbe.ok &&
279
+ multiplexerStrict;
280
+ const activeBackend = strictFailure
281
+ ? `${requestedMultiplexer} unavailable (strict mode)`
282
+ : fallbackApplied
283
+ ? `tmux (fallback from ${requestedMultiplexer})`
284
+ : requestedMultiplexer;
285
+ checks.push({
286
+ level: strictFailure ? "fail" : "ok",
287
+ label: "active backend",
288
+ detail: activeBackend,
289
+ });
290
+
291
+ if (optionalBackendProbe !== null && requestedMultiplexer !== "tmux") {
292
+ checks.push(
293
+ optionalBackendProbe.ok
294
+ ? {
295
+ level: "ok",
296
+ label: requestedMultiplexer,
297
+ detail: trimVersionOutput(optionalBackendProbe.stdout),
298
+ }
299
+ : {
300
+ level: multiplexerStrict ? "fail" : "warn",
301
+ label: requestedMultiplexer,
302
+ detail:
303
+ optionalBackendProbe.stderr ||
304
+ (multiplexerStrict ? "not found (strict mode)" : "not found (falling back to tmux)"),
305
+ },
306
+ );
307
+ }
308
+
309
+ const tmuxVersion = await exec("tmux", ["-V"]);
310
+ const tmuxRequired = requestedMultiplexer === "tmux" || fallbackApplied;
311
+ checks.push(
312
+ tmuxVersion.ok
313
+ ? { level: "ok", label: "tmux", detail: trimVersionOutput(tmuxVersion.stdout) }
314
+ : {
315
+ level: tmuxRequired ? "fail" : "warn",
316
+ label: "tmux",
317
+ detail:
318
+ tmuxVersion.stderr || (tmuxRequired ? "not found" : "not found (not active backend)"),
319
+ },
320
+ );
321
+
322
+ // Confirm `capture-pane` is recognised as a subcommand. cuekit's TUI
323
+ // reads live pane content through `tmux capture-pane -p -e -J ...`
324
+ // (#376), so a tmux without that subcommand would silently fall back
325
+ // to the file-tail and the operator would never know why the live
326
+ // view stayed stale. Probing against a guaranteed-missing target
327
+ // returns a 1 with "no such session" — that's the success path here.
328
+ if (tmuxVersion.ok) {
329
+ const capture = await exec("tmux", [
330
+ "capture-pane",
331
+ "-p",
332
+ "-t",
333
+ "cuekit-doctor-probe-no-such-session",
334
+ ]);
335
+ const stderr = (capture.stderr ?? "").toLowerCase();
336
+ const supportsCapture =
337
+ capture.ok || stderr.includes("session") || stderr.includes("can't find");
338
+ checks.push(
339
+ supportsCapture
340
+ ? {
341
+ level: "ok",
342
+ label: "tmux capture-pane",
343
+ detail: "supported",
344
+ }
345
+ : {
346
+ level: "warn",
347
+ label: "tmux capture-pane",
348
+ detail: capture.stderr || "subcommand not recognised",
349
+ },
350
+ );
351
+ }
352
+
353
+ const writableState = await (options.checkWritableState ?? defaultCheckWritableState)(env);
354
+ checks.push(
355
+ writableState.ok
356
+ ? { level: "ok", label: "state db", detail: `${writableState.path} writable` }
357
+ : {
358
+ level: "fail",
359
+ label: "state db",
360
+ detail: `${writableState.path}: ${writableState.reason}`,
361
+ },
362
+ );
363
+ checks.push(await (options.checkStateDbTaskRows ?? defaultCheckStateDbTaskRows)(env, fix));
364
+
365
+ const projectConfig = (options.loadProjectConfig ?? defaultLoadProjectConfig)(
366
+ options.cwd ?? process.cwd(),
367
+ );
368
+ if (!projectConfig.ok) {
369
+ checks.push({ level: "fail", label: "project config", detail: projectConfig.error });
370
+ } else if (projectConfig.source === "config") {
371
+ checks.push({ level: "ok", label: "project config", detail: projectConfig.path });
372
+ } else {
373
+ checks.push({
374
+ level: "warn",
375
+ label: "project config",
376
+ detail: `not found (using ${projectConfig.source} scope)`,
377
+ });
378
+ }
379
+
380
+ checks.push({ level: "ok", label: "MCP config helper", detail: "cuekit mcp config" });
381
+
382
+ // Parent PATH summary so operators can spot mismatches with pane
383
+ // environments (especially under herdr or nested tmux).
384
+ const parentPath = env.PATH ?? "";
385
+ checks.push({
386
+ level: "ok",
387
+ label: "parent PATH",
388
+ detail: parentPath ? `${parentPath.split(":").length} entries` : "not set",
389
+ });
390
+
391
+ for (const adapter of ADAPTER_EXECUTABLES) {
392
+ const result = await exec(adapter.command, ["--version"]);
393
+ checks.push(
394
+ result.ok
395
+ ? {
396
+ level: "ok",
397
+ label: `adapter ${adapter.kind}`,
398
+ detail: `${adapter.command} found`,
399
+ }
400
+ : {
401
+ level: "warn",
402
+ label: `adapter ${adapter.kind}`,
403
+ detail: `${adapter.command} not found`,
404
+ },
405
+ );
406
+ }
407
+
408
+ // Warn when adapters were found in the parent PATH but the active
409
+ // multiplexer backend runs tasks in panes that may inherit a different
410
+ // PATH. This is especially common with herdr or when cuekit itself is
411
+ // already running inside a multiplexer session.
412
+ const anyAdapterFound = checks.some(
413
+ (c) =>
414
+ c.label.startsWith("adapter ") && c.level === "ok" && c.detail.includes("found"),
415
+ );
416
+ if (anyAdapterFound && requestedMultiplexer !== "tmux") {
417
+ checks.push({
418
+ level: "warn",
419
+ label: "pane PATH",
420
+ detail: `${requestedMultiplexer} panes may have a different PATH; verify with 'echo $PATH' inside a fresh ${requestedMultiplexer} pane`,
421
+ });
422
+ }
423
+
424
+ const latest = await (options.getLatestRelease ?? defaultGetLatestRelease)();
425
+ if (!latest.ok) {
426
+ checks.push({ level: "warn", label: "update", detail: `skipped (${latest.reason})` });
427
+ } else if (
428
+ currentVersion &&
429
+ normalizeReleaseVersion(currentVersion) === normalizeReleaseVersion(latest.tag)
430
+ ) {
431
+ checks.push({ level: "ok", label: "update", detail: "up to date" });
432
+ } else {
433
+ checks.push({ level: "warn", label: "update", detail: `${latest.tag} available` });
434
+ }
435
+
436
+ const exitCode = checks.some((check) => check.level === "fail") ? 1 : 0;
437
+ return { exitCode, checks, stdout: renderDoctor(checks) };
438
+ }
@@ -0,0 +1,188 @@
1
+ import { registerPiMcpServer } from "@cuekit/mcp";
2
+ import { type ProjectConfigInitResult, runProjectConfigInit } from "@cuekit/project-config";
3
+ import { registerJcodeMcpServer } from "./jcode-mcp-config.ts";
4
+
5
+ export type HumanCommandResult = {
6
+ exitCode: number;
7
+ stdout: string;
8
+ stderr?: string;
9
+ };
10
+
11
+ export type RunInitDependencies = {
12
+ cwd?: string;
13
+ runProjectConfigInit?: typeof runProjectConfigInit;
14
+ };
15
+
16
+ export function printInitHelp(): string {
17
+ return [
18
+ "cuekit init — create safe project-local cuekit config",
19
+ "",
20
+ "Usage: cuekit init [--dry-run] [--force] [--no-gitignore] [--unsafe-bypass]",
21
+ "",
22
+ "Creates .cuekit.yaml in the current directory and adds .cuekit/tasks/ to .gitignore.",
23
+ "",
24
+ "Options:",
25
+ " --dry-run Show what would be written without changing files",
26
+ " --force Overwrite an existing .cuekit.yaml",
27
+ " --no-gitignore Do not create or update .gitignore",
28
+ " --unsafe-bypass Generate adapter permissions: bypass (unsafe; explicit opt-in)",
29
+ " -h, --help Show this help",
30
+ "",
31
+ ].join("\n");
32
+ }
33
+
34
+ function formatInitSummary(result: ProjectConfigInitResult): string {
35
+ const prefix = result.dryRun ? "dry-run: " : "";
36
+ const lines = [
37
+ `${prefix}cuekit init ${result.dryRun ? "would update" : "updated"} ${result.cwd}`,
38
+ ];
39
+ for (const path of result.created) lines.push(`${prefix}created ${path}`);
40
+ for (const path of result.updated) lines.push(`${prefix}updated ${path}`);
41
+ for (const path of result.skipped) lines.push(`${prefix}skipped ${path}`);
42
+ return `${lines.join("\n")}\n`;
43
+ }
44
+
45
+ export function runInitCommand(
46
+ argv: string[],
47
+ dependencies: RunInitDependencies = {},
48
+ ): HumanCommandResult {
49
+ if (argv.includes("--help") || argv.includes("-h")) {
50
+ return { exitCode: 0, stdout: printInitHelp() };
51
+ }
52
+ const unsafeBypass = argv.includes("--unsafe-bypass");
53
+ try {
54
+ const result = (dependencies.runProjectConfigInit ?? runProjectConfigInit)({
55
+ cwd: dependencies.cwd ?? process.cwd(),
56
+ dryRun: argv.includes("--dry-run"),
57
+ force: argv.includes("--force"),
58
+ gitignore: !argv.includes("--no-gitignore"),
59
+ unsafeBypass,
60
+ });
61
+ return {
62
+ exitCode: 0,
63
+ stdout: formatInitSummary(result),
64
+ ...(unsafeBypass
65
+ ? {
66
+ stderr:
67
+ "warning: --unsafe-bypass writes project-local adapter permissions: bypass; only use this for trusted repositories\n",
68
+ }
69
+ : {}),
70
+ };
71
+ } catch (err) {
72
+ return {
73
+ exitCode: 1,
74
+ stdout: "",
75
+ stderr: `${err instanceof Error ? err.message : String(err)}\n`,
76
+ };
77
+ }
78
+ }
79
+
80
+ export function printTuiHelp(): string {
81
+ return [
82
+ "cuekit tui — interactive task cockpit",
83
+ "",
84
+ "Usage: cuekit tui [--path] [--all]",
85
+ "",
86
+ "By default, uses .cuekit.yaml project scope when present, otherwise the current repository/worktree.",
87
+ "Use --path to ignore .cuekit.yaml identity and scope by the current path/Git root.",
88
+ "Use --all to show tasks across all projects for this invocation.",
89
+ "",
90
+ "Keys: ↑/↓ select, r refresh, a attach (returns after detach), t teams/tasks, s steer, c cancel, d delete, q quit",
91
+ "",
92
+ ].join("\n");
93
+ }
94
+
95
+ function splitAgentArgs(argv: string[], agent: string): { hasAgent: boolean; rest: string[] } {
96
+ const rest: string[] = [];
97
+ let hasAgent = false;
98
+ for (let i = 0; i < argv.length; i++) {
99
+ const arg = argv[i];
100
+ if (arg === undefined) continue;
101
+ if ((arg === "--agent" || arg === "-a") && argv[i + 1] === agent) {
102
+ hasAgent = true;
103
+ i++;
104
+ continue;
105
+ }
106
+ if (arg === `--agent=${agent}` || arg === `-a=${agent}`) {
107
+ hasAgent = true;
108
+ continue;
109
+ }
110
+ rest.push(arg);
111
+ }
112
+ return { hasAgent, rest };
113
+ }
114
+
115
+ export function splitPiAgentArgs(argv: string[]): { hasPi: boolean; rest: string[] } {
116
+ const result = splitAgentArgs(argv, "pi");
117
+ return { hasPi: result.hasAgent, rest: result.rest };
118
+ }
119
+
120
+ export function hasExplicitAgent(argv: string[]): boolean {
121
+ return argv.some(
122
+ (arg) =>
123
+ arg === "--agent" || arg === "-a" || arg.startsWith("--agent=") || arg.startsWith("-a="),
124
+ );
125
+ }
126
+
127
+ export function runPiMcpAddCommand(
128
+ argv: string[],
129
+ ): HumanCommandResult & { shouldDelegate: boolean; delegateArgv: string[] } {
130
+ const piAgents = splitPiAgentArgs(argv);
131
+ if (!piAgents.hasPi) {
132
+ return { exitCode: 0, stdout: "", shouldDelegate: true, delegateArgv: ["mcp", "add", ...argv] };
133
+ }
134
+
135
+ const result = registerPiMcpServer({
136
+ global: !piAgents.rest.includes("--no-global"),
137
+ });
138
+ return {
139
+ exitCode: 0,
140
+ stdout: `Registered MCP server '${result.serverName}' for Pi: ${result.path}\n`,
141
+ shouldDelegate: hasExplicitAgent(piAgents.rest),
142
+ delegateArgv: ["mcp", "add", ...piAgents.rest],
143
+ };
144
+ }
145
+
146
+ export type McpAddCommandResult = HumanCommandResult & {
147
+ shouldDelegate: boolean;
148
+ delegateArgv: string[];
149
+ };
150
+
151
+ export type RunJcodeMcpAddDependencies = {
152
+ cwd?: string;
153
+ home?: string;
154
+ jcodeHome?: string;
155
+ };
156
+
157
+ export function runJcodeMcpAddCommand(
158
+ argv: string[],
159
+ dependencies: RunJcodeMcpAddDependencies = {},
160
+ ): McpAddCommandResult {
161
+ const jcodeAgents = splitAgentArgs(argv, "jcode");
162
+ if (!jcodeAgents.hasAgent) {
163
+ return { exitCode: 0, stdout: "", shouldDelegate: true, delegateArgv: ["mcp", "add", ...argv] };
164
+ }
165
+
166
+ try {
167
+ const result = registerJcodeMcpServer({
168
+ global: !jcodeAgents.rest.includes("--no-global"),
169
+ cwd: dependencies.cwd,
170
+ home: dependencies.home,
171
+ jcodeHome: dependencies.jcodeHome,
172
+ });
173
+ return {
174
+ exitCode: 0,
175
+ stdout: `Registered MCP server '${result.serverName}' for jcode: ${result.path}\n`,
176
+ shouldDelegate: hasExplicitAgent(jcodeAgents.rest),
177
+ delegateArgv: ["mcp", "add", ...jcodeAgents.rest],
178
+ };
179
+ } catch (err) {
180
+ return {
181
+ exitCode: 1,
182
+ stdout: "",
183
+ stderr: `${err instanceof Error ? err.message : String(err)}\n`,
184
+ shouldDelegate: false,
185
+ delegateArgv: ["mcp", "add", ...jcodeAgents.rest],
186
+ };
187
+ }
188
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./dispatch.ts";
2
+ export * from "./doctor.ts";
3
+ export * from "./human-commands.ts";
4
+ export * from "./jcode-mcp-config.ts";
5
+ export * from "./output.ts";
6
+ export * from "./update.ts";