@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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/CHANGELOG.md +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
|
@@ -66,3 +66,76 @@ it("agent:// resolves a depth-2 subagent's .md output while its session is live
|
|
|
66
66
|
const resource = await new AgentProtocolHandler().resolve(new URL(`agent://${grandchildId}`) as never);
|
|
67
67
|
expect(resource.content).toBe("full report content");
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("agent:// slash form resolves a nested subagent child (hierarchy separator)", async () => {
|
|
71
|
+
const root = tempDir.path();
|
|
72
|
+
const rootSessionFile = path.join(root, "slash-session.jsonl");
|
|
73
|
+
const rootArtifactsDir = rootSessionFile.slice(0, -6);
|
|
74
|
+
await fs.mkdir(rootArtifactsDir, { recursive: true });
|
|
75
|
+
const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
|
|
76
|
+
|
|
77
|
+
// Parent subagent adopts the root ArtifactManager; its own children are
|
|
78
|
+
// written one level deeper under its sessionFile-derived dir, dot-qualified.
|
|
79
|
+
const parentSessionFile = path.join(rootArtifactsDir, "Parent.jsonl");
|
|
80
|
+
const parentOwnDir = parentSessionFile.slice(0, -6);
|
|
81
|
+
await fs.mkdir(parentOwnDir, { recursive: true });
|
|
82
|
+
await fs.writeFile(path.join(parentOwnDir, "Parent.Child.md"), "child capsule");
|
|
83
|
+
// Parent output may be in the root dir; the nested child must still win.
|
|
84
|
+
await fs.writeFile(path.join(rootArtifactsDir, "Parent.md"), JSON.stringify({ Child: "wrong base output" }));
|
|
85
|
+
|
|
86
|
+
const fakeSession = {
|
|
87
|
+
sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
|
|
88
|
+
} as unknown as AgentSession;
|
|
89
|
+
const registry = AgentRegistry.global();
|
|
90
|
+
registry.register({
|
|
91
|
+
id: "Main",
|
|
92
|
+
displayName: "main",
|
|
93
|
+
kind: "main",
|
|
94
|
+
session: fakeSession,
|
|
95
|
+
sessionFile: rootSessionFile,
|
|
96
|
+
});
|
|
97
|
+
registry.register({
|
|
98
|
+
id: "Parent",
|
|
99
|
+
displayName: "sub",
|
|
100
|
+
kind: "sub",
|
|
101
|
+
parentId: "Main",
|
|
102
|
+
session: fakeSession,
|
|
103
|
+
sessionFile: parentSessionFile,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const handler = new AgentProtocolHandler();
|
|
107
|
+
// Slash form is a hierarchy hop, not a jq extraction.
|
|
108
|
+
const slash = await handler.resolve(new URL("agent://Parent/Child") as never);
|
|
109
|
+
expect(slash.content).toBe("child capsule");
|
|
110
|
+
expect(slash.contentType).toBe("text/markdown");
|
|
111
|
+
// The canonical dotted id resolves to the same output.
|
|
112
|
+
const dotted = await handler.resolve(new URL("agent://Parent.Child") as never);
|
|
113
|
+
expect(dotted.content).toBe("child capsule");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("agent:// path form falls back to JSON extraction when no nested output matches", async () => {
|
|
117
|
+
const root = tempDir.path();
|
|
118
|
+
const rootSessionFile = path.join(root, "json-session.jsonl");
|
|
119
|
+
const rootArtifactsDir = rootSessionFile.slice(0, -6);
|
|
120
|
+
await fs.mkdir(rootArtifactsDir, { recursive: true });
|
|
121
|
+
const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
|
|
122
|
+
await fs.writeFile(path.join(rootArtifactsDir, "Worker.md"), JSON.stringify({ result: { ok: true } }));
|
|
123
|
+
|
|
124
|
+
const fakeSession = {
|
|
125
|
+
sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
|
|
126
|
+
} as unknown as AgentSession;
|
|
127
|
+
const registry = AgentRegistry.global();
|
|
128
|
+
registry.register({
|
|
129
|
+
id: "Main",
|
|
130
|
+
displayName: "main",
|
|
131
|
+
kind: "main",
|
|
132
|
+
session: fakeSession,
|
|
133
|
+
sessionFile: rootSessionFile,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const handler = new AgentProtocolHandler();
|
|
137
|
+
// `result` names no nested output, so the path extracts JSON from Worker.md.
|
|
138
|
+
const extracted = await handler.resolve(new URL("agent://Worker/result") as never);
|
|
139
|
+
expect(extracted.contentType).toBe("application/json");
|
|
140
|
+
expect(JSON.parse(extracted.content)).toEqual({ ok: true });
|
|
141
|
+
});
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* URL forms:
|
|
10
10
|
* - agent://<id> - Full output content
|
|
11
|
-
* - agent://<id>/<
|
|
11
|
+
* - agent://<id>/<child> - Nested subagent output (hierarchy separator; the
|
|
12
|
+
* registry allocates a subagent's own children as dot-qualified ids, so
|
|
13
|
+
* `agent://Parent/Child` resolves `Parent.Child.md`)
|
|
14
|
+
* - agent://<id>/<path> - JSON extraction via path form (fallback when no
|
|
15
|
+
* nested output matches the path)
|
|
12
16
|
* - agent://<id>?q=<query> - JSON extraction via query form
|
|
13
17
|
*/
|
|
14
18
|
import * as fs from "node:fs/promises";
|
|
@@ -44,65 +48,56 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
const dirs = artifactsDirsFromRegistry();
|
|
47
|
-
|
|
48
51
|
if (dirs.length === 0) {
|
|
49
52
|
throw new Error("No session - agent outputs unavailable");
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
anyDirExists = true;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (isEnoent(err)) continue;
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
const candidate = path.join(dir, `${outputId}.md`);
|
|
55
|
+
// A subagent allocates its own children as dot-qualified ids
|
|
56
|
+
// (`Parent.Child`), so the slash path form is first tried as a hierarchy
|
|
57
|
+
// separator: `agent://Parent/Child` resolves `Parent.Child.md`. Only when
|
|
58
|
+
// no such nested output exists does the path fall back to jq-style JSON
|
|
59
|
+
// extraction on `<outputId>.md`. Query form (`?q=`) is always extraction.
|
|
60
|
+
const pathSegments = hasPathExtraction ? urlPath.split("/").filter(Boolean) : [];
|
|
61
|
+
const decodedSegments = pathSegments.map(segment => {
|
|
65
62
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (!isEnoent(err)) throw err;
|
|
71
|
-
try {
|
|
72
|
-
const files = await fs.readdir(dir);
|
|
73
|
-
for (const f of files) {
|
|
74
|
-
if (f.endsWith(".md")) availableIds.add(f.replace(/\.md$/, ""));
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// Listing failures are non-fatal; continue searching.
|
|
78
|
-
}
|
|
63
|
+
return decodeURIComponent(segment);
|
|
64
|
+
} catch {
|
|
65
|
+
return segment;
|
|
79
66
|
}
|
|
80
|
-
}
|
|
67
|
+
});
|
|
68
|
+
const nestedId =
|
|
69
|
+
decodedSegments.length > 0 && decodedSegments.every(segment => !segment.includes("."))
|
|
70
|
+
? [outputId, ...decodedSegments].join(".")
|
|
71
|
+
: undefined;
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
const scan = await this.#findOutput(dirs, nestedId ? [nestedId, outputId] : [outputId]);
|
|
74
|
+
if (!scan.anyDirExists) {
|
|
83
75
|
throw new Error("No artifacts directory found");
|
|
84
76
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const availableStr = availableIds.size > 0 ? [...availableIds].join(", ") : "none";
|
|
88
|
-
throw new Error(`Not found: ${
|
|
77
|
+
if (!scan.foundPath) {
|
|
78
|
+
const target = nestedId ?? outputId;
|
|
79
|
+
const availableStr = scan.availableIds.size > 0 ? [...scan.availableIds].join(", ") : "none";
|
|
80
|
+
throw new Error(`Not found: ${target}\nAvailable: ${availableStr}`);
|
|
89
81
|
}
|
|
90
82
|
|
|
91
|
-
const rawContent = await Bun.file(foundPath).text();
|
|
83
|
+
const rawContent = await Bun.file(scan.foundPath).text();
|
|
92
84
|
const notes: string[] = [];
|
|
93
85
|
let content = rawContent;
|
|
94
86
|
let contentType: InternalResource["contentType"] = "text/markdown";
|
|
95
87
|
|
|
96
|
-
|
|
88
|
+
// Extraction applies only when the URL did NOT resolve to a nested output
|
|
89
|
+
// (a slash that named a real child is a hierarchy hop, not a jq path).
|
|
90
|
+
const extract = hasQueryExtraction || (hasPathExtraction && scan.matchedId !== nestedId);
|
|
91
|
+
if (extract) {
|
|
97
92
|
let jsonValue: unknown;
|
|
98
93
|
try {
|
|
99
94
|
jsonValue = JSON.parse(rawContent);
|
|
100
95
|
} catch (err) {
|
|
101
96
|
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
-
throw new Error(`Output ${
|
|
97
|
+
throw new Error(`Output ${scan.matchedId} is not valid JSON: ${message}`);
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
const query =
|
|
100
|
+
const query = hasQueryExtraction ? queryParam! : pathToQuery(urlPath);
|
|
106
101
|
if (query) {
|
|
107
102
|
const extracted = applyQuery(jsonValue, query);
|
|
108
103
|
try {
|
|
@@ -122,11 +117,50 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
122
117
|
content,
|
|
123
118
|
contentType,
|
|
124
119
|
size: Buffer.byteLength(content, "utf-8"),
|
|
125
|
-
sourcePath: foundPath,
|
|
120
|
+
sourcePath: scan.foundPath,
|
|
126
121
|
notes,
|
|
127
122
|
};
|
|
128
123
|
}
|
|
129
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Scan every registered artifacts dir for the first `<id>.md` among
|
|
127
|
+
* `candidateIds` (tried in order, so a hierarchy match wins over the base
|
|
128
|
+
* id). Returns the resolved path and the id it matched, plus the set of
|
|
129
|
+
* available ids gathered from the scanned dirs for the not-found message.
|
|
130
|
+
*/
|
|
131
|
+
async #findOutput(
|
|
132
|
+
dirs: string[],
|
|
133
|
+
candidateIds: string[],
|
|
134
|
+
): Promise<{ foundPath?: string; matchedId?: string; anyDirExists: boolean; availableIds: Set<string> }> {
|
|
135
|
+
// Build a full id→path map across every registered dir before picking, so
|
|
136
|
+
// candidate priority is global: a nested id in a deeper dir must win over
|
|
137
|
+
// the base id even when the base id's dir is scanned first.
|
|
138
|
+
const byId = new Map<string, string>();
|
|
139
|
+
let anyDirExists = false;
|
|
140
|
+
for (const dir of dirs) {
|
|
141
|
+
let files: string[];
|
|
142
|
+
try {
|
|
143
|
+
files = await fs.readdir(dir);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (isEnoent(err)) continue;
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
anyDirExists = true;
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
if (!f.endsWith(".md")) continue;
|
|
151
|
+
const id = f.slice(0, -3);
|
|
152
|
+
if (!byId.has(id)) byId.set(id, path.join(dir, f));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const id of candidateIds) {
|
|
156
|
+
const foundPath = byId.get(id);
|
|
157
|
+
if (foundPath) {
|
|
158
|
+
return { foundPath, matchedId: id, anyDirExists, availableIds: new Set(byId.keys()) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { anyDirExists, availableIds: new Set(byId.keys()) };
|
|
162
|
+
}
|
|
163
|
+
|
|
130
164
|
async complete(): Promise<UrlCompletion[]> {
|
|
131
165
|
const ids = new Set<string>();
|
|
132
166
|
for (const dir of artifactsDirsFromRegistry()) {
|
package/src/irc/bus.ts
CHANGED
|
@@ -127,12 +127,31 @@ export class IrcBus {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// A `parked` recipient always needs the lifecycle to revive it — this is
|
|
131
|
+
// read from *this* bus's registry, so it holds for any registry. The
|
|
132
|
+
// mid-park / adopted checks below query the lifecycle's own state, which
|
|
133
|
+
// only describes the registry it manages: consult them only when the
|
|
134
|
+
// lifecycle owns this bus's registry, otherwise a custom-registry bus
|
|
135
|
+
// (fallen back to the global manager) would gate a live recipient on
|
|
136
|
+
// unrelated global park state. Main/non-adopted live peers skip the gate,
|
|
137
|
+
// and pending waiters still win without a session.
|
|
138
|
+
const lifecycle = this.#lifecycle();
|
|
139
|
+
const lifecycleOwnsRegistry = lifecycle.manages(this.#registry);
|
|
140
|
+
const needsLifecycleGate =
|
|
141
|
+
ref.status === "parked" ||
|
|
142
|
+
(lifecycleOwnsRegistry && (lifecycle.isParking(message.to) || lifecycle.has(message.to)));
|
|
143
|
+
|
|
144
|
+
const priorSession = ref.session;
|
|
130
145
|
let revived = false;
|
|
131
|
-
if (
|
|
146
|
+
if (needsLifecycleGate) {
|
|
132
147
|
try {
|
|
133
|
-
await
|
|
134
|
-
|
|
148
|
+
const liveSession = await lifecycle.ensureLive(message.to);
|
|
149
|
+
// Revival = we did not keep the same live instance (parked start, or
|
|
150
|
+
// park completed and a fresh session was rebuilt).
|
|
151
|
+
revived = !priorSession || liveSession !== priorSession;
|
|
135
152
|
} catch (error) {
|
|
153
|
+
// Not revivable / released / revive failed. Do not buffer: a permanent
|
|
154
|
+
// failure must not inflate unread counts or pretend delivery is pending.
|
|
136
155
|
return {
|
|
137
156
|
to: message.to,
|
|
138
157
|
outcome: "failed",
|
package/src/launch/broker.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as net from "node:net";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { isEexist, isEnoent, logger, postmortem, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
|
|
8
8
|
import { workerEnvFromParent } from "../subprocess/worker-client";
|
|
9
9
|
import { daemonBrokerEndpoint } from "./paths";
|
|
@@ -555,7 +555,8 @@ class DaemonBroker {
|
|
|
555
555
|
`printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
|
|
556
556
|
`exec ${argv.map(quoteShellArg).join(" ")}`,
|
|
557
557
|
].join("; ");
|
|
558
|
-
|
|
558
|
+
const shell = procmgr.getShellConfig().shell;
|
|
559
|
+
run = session.start({ command, shell, ...options }, onChunk);
|
|
559
560
|
}
|
|
560
561
|
void run
|
|
561
562
|
.then(result => this.#onPtyExit(record, generation, result))
|
package/src/launch/client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as net from "node:net";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { isEexist, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
|
|
7
7
|
import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
|
|
8
8
|
import {
|
|
@@ -49,7 +49,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
|
|
|
49
49
|
try {
|
|
50
50
|
return await fs.realpath(resolved);
|
|
51
51
|
} catch (error) {
|
|
52
|
-
if (isEnoent(error)) return resolved;
|
|
52
|
+
if (isEnoent(error) || isEisdir(error)) return resolved;
|
|
53
53
|
throw error;
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/launch/presence.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { daemonRuntimeDir } from "./paths";
|
|
5
5
|
|
|
6
6
|
const CLIENTS_DIR = "clients";
|
|
@@ -15,7 +15,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
|
|
|
15
15
|
try {
|
|
16
16
|
return await fs.realpath(resolved);
|
|
17
17
|
} catch (error) {
|
|
18
|
-
if (isEnoent(error)) return resolved;
|
|
18
|
+
if (isEnoent(error) || isEisdir(error)) return resolved;
|
|
19
19
|
throw error;
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/lib/xai-http.ts
CHANGED
|
@@ -16,7 +16,14 @@ export function ohMyPiXAIUserAgent(): string {
|
|
|
16
16
|
return "oh-my-pi/xai";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** xAI provider ids supported by shared HTTP tool transport resolution. */
|
|
20
|
+
export type XAIHttpProvider = "xai-oauth" | "xai";
|
|
21
|
+
|
|
22
|
+
/** Resolved endpoint and configured headers for an xAI HTTP tool request. */
|
|
23
|
+
export interface XAIHttpTransport {
|
|
24
|
+
baseURL: string;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Resolve the HTTP base URL for an xAI tool call.
|
|
@@ -48,7 +55,11 @@ type XAIProvider = "xai-oauth" | "xai";
|
|
|
48
55
|
* let xai-oauth entries hijack a xai tool call (or vice versa) when the
|
|
49
56
|
* same model id ships under both descriptors.
|
|
50
57
|
*/
|
|
51
|
-
function resolveXAIBaseURL(
|
|
58
|
+
function resolveXAIBaseURL(
|
|
59
|
+
modelRegistry: ModelRegistry,
|
|
60
|
+
provider: XAIHttpProvider,
|
|
61
|
+
modelId: string | undefined,
|
|
62
|
+
): string {
|
|
52
63
|
if (modelId) {
|
|
53
64
|
const merged = modelRegistry.getAll().find(m => m.id === modelId && m.provider === provider);
|
|
54
65
|
if (merged?.baseUrl) {
|
|
@@ -68,6 +79,21 @@ function resolveXAIBaseURL(modelRegistry: ModelRegistry, provider: XAIProvider,
|
|
|
68
79
|
}
|
|
69
80
|
return ($env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
70
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve an xAI tool endpoint and its provider/model header overrides.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveXAIHttpTransport(
|
|
86
|
+
modelRegistry: ModelRegistry,
|
|
87
|
+
provider: XAIHttpProvider,
|
|
88
|
+
modelId?: string,
|
|
89
|
+
): XAIHttpTransport {
|
|
90
|
+
return {
|
|
91
|
+
baseURL: resolveXAIBaseURL(modelRegistry, provider, modelId),
|
|
92
|
+
headers:
|
|
93
|
+
(modelId ? modelRegistry.find(provider, modelId)?.headers : undefined) ??
|
|
94
|
+
modelRegistry.getProviderHeaders(provider),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
71
97
|
|
|
72
98
|
/**
|
|
73
99
|
* Resolve xAI credentials for HTTP tool calls.
|
package/src/lsp/client.ts
CHANGED
|
@@ -428,7 +428,7 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
|
|
|
428
428
|
const items = params?.items ?? [];
|
|
429
429
|
const result = items.map(item => {
|
|
430
430
|
const section = item.section ?? "";
|
|
431
|
-
return client.config.settings?.[section] ??
|
|
431
|
+
return client.config.settings?.[section] ?? null;
|
|
432
432
|
});
|
|
433
433
|
await sendResponse(client, message.id, result, "workspace/configuration");
|
|
434
434
|
}
|
package/src/main.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
|
|
|
59
59
|
import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
|
|
60
60
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
61
61
|
import type { SubmittedUserInput } from "./modes/types";
|
|
62
|
+
import { createWarpEventBridgeExtension } from "./modes/warp-events";
|
|
62
63
|
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
|
|
63
64
|
import {
|
|
64
65
|
type CreateAgentSessionOptions,
|
|
@@ -76,7 +77,7 @@ import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
|
76
77
|
import { shouldShowStartupSplash } from "./startup-splash";
|
|
77
78
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
|
|
78
79
|
import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
|
|
79
|
-
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
80
|
+
import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
80
81
|
import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
|
|
81
82
|
import type { LspStartupServerInfo } from "./tools";
|
|
82
83
|
import {
|
|
@@ -1350,15 +1351,13 @@ export async function runRootCommand(
|
|
|
1350
1351
|
sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
|
|
1351
1352
|
sessionOptions.settings = settingsInstance;
|
|
1352
1353
|
|
|
1353
|
-
// OTEL: register
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1357
|
-
// is enough to enable telemetry — content capture is governed by the
|
|
1358
|
-
// standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
|
|
1354
|
+
// OTEL: register global OTLP exporters when an endpoint is configured via
|
|
1355
|
+
// env, then switch on the agent loop's telemetry hooks so traces, run-level
|
|
1356
|
+
// metrics, and structured logs have source events to export. Content capture
|
|
1357
|
+
// remains governed by OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.
|
|
1359
1358
|
await logger.time("initTelemetryExport", initTelemetryExport);
|
|
1360
1359
|
if (isTelemetryExportEnabled()) {
|
|
1361
|
-
sessionOptions.telemetry =
|
|
1360
|
+
sessionOptions.telemetry = createTelemetryExportConfig(sessionOptions.telemetry);
|
|
1362
1361
|
}
|
|
1363
1362
|
|
|
1364
1363
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
@@ -1407,6 +1406,10 @@ export async function runRootCommand(
|
|
|
1407
1406
|
// string-flag value such as `--target @notes.md` is the flag's value, not a
|
|
1408
1407
|
// file — and the same result is handed to createAgentSession via
|
|
1409
1408
|
// `preloadedExtensions` so the discovery work is not repeated.
|
|
1409
|
+
if (isInteractive) {
|
|
1410
|
+
sessionOptions.extensions = [...(sessionOptions.extensions ?? []), createWarpEventBridgeExtension()];
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1410
1413
|
const eventBus = new EventBus();
|
|
1411
1414
|
const extensionsResult = await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
|
|
1412
1415
|
const extensionFlagSink: ExtensionFlagSink = {
|
package/src/mcp/manager.ts
CHANGED
|
@@ -572,8 +572,14 @@ export class MCPManager {
|
|
|
572
572
|
};
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Ownership is matched via `mcpServerName`, never a `mcp__${name}_` name
|
|
577
|
+
* prefix: tool names are lossy-sanitized, so one server's sanitized name
|
|
578
|
+
* can prefix another's (`atlassian` vs `atlassian:atlassian`) and a name
|
|
579
|
+
* with sanitized characters never prefix-matches its own tools at all.
|
|
580
|
+
*/
|
|
575
581
|
#replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
|
|
576
|
-
this.#tools = this.#tools.filter(t =>
|
|
582
|
+
this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
|
|
577
583
|
this.#tools.push(...tools);
|
|
578
584
|
// Stable sort by name so reconnect order does not perturb the array.
|
|
579
585
|
// See `sortMCPToolsByName` for the cache-stability rationale.
|
|
@@ -762,8 +768,8 @@ export class MCPManager {
|
|
|
762
768
|
}
|
|
763
769
|
|
|
764
770
|
// Remove tools from this server and notify consumers
|
|
765
|
-
const hadTools = this.#tools.some(t => t.name
|
|
766
|
-
this.#tools = this.#tools.filter(t =>
|
|
771
|
+
const hadTools = this.#tools.some(t => t.mcpServerName === name);
|
|
772
|
+
this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
|
|
767
773
|
if (hadTools) this.#onToolsChanged?.(this.#tools);
|
|
768
774
|
|
|
769
775
|
// Notify prompt consumers so stale commands are cleared
|
|
@@ -54,6 +54,18 @@ export interface StdioSpawnCommand {
|
|
|
54
54
|
* grandchildren keep stdout routed through our pipe (#3544).
|
|
55
55
|
*/
|
|
56
56
|
detached: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Pass argv to `Bun.spawn` verbatim (Windows only), suppressing the
|
|
59
|
+
* default libuv backslash-quoting.
|
|
60
|
+
*
|
|
61
|
+
* Set when `cmd` already holds a `cmd.exe /d /e:ON /v:OFF /c "<line>"`
|
|
62
|
+
* command line escaped for `cmd.exe`'s parser (see `buildCmdExeArgv`).
|
|
63
|
+
* libuv's quoting targets `CommandLineToArgvW`, not `cmd.exe`, so letting
|
|
64
|
+
* it re-quote a batch launch would corrupt arguments and re-open the
|
|
65
|
+
* `%VAR%` / quote-injection holes the escaping closes (BatBadBut,
|
|
66
|
+
* CVE-2024-24576).
|
|
67
|
+
*/
|
|
68
|
+
windowsVerbatimArguments?: boolean;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** Inputs used to resolve platform-specific stdio spawn behavior. */
|
|
@@ -203,23 +215,6 @@ async function resolveWindowsNpmShimCommand(
|
|
|
203
215
|
};
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
function quoteCmdArg(value: string): string {
|
|
207
|
-
if (value.length === 0) return '""';
|
|
208
|
-
let result = '"';
|
|
209
|
-
for (const char of value) {
|
|
210
|
-
if (char === '"') {
|
|
211
|
-
result += '^"';
|
|
212
|
-
} else if (char === "^") {
|
|
213
|
-
result += "^^";
|
|
214
|
-
} else if (char === "%") {
|
|
215
|
-
result += "^%";
|
|
216
|
-
} else {
|
|
217
|
-
result += char;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return `${result}"`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
function isWindowsBatchCommand(command: string): boolean {
|
|
224
219
|
return WINDOWS_BATCH_EXTENSIONS.has(path.extname(command).toLowerCase());
|
|
225
220
|
}
|
|
@@ -229,10 +224,93 @@ function resolveComSpec(env: Record<string, string | undefined>): string {
|
|
|
229
224
|
return comspec && comspec.length > 0 ? comspec : "cmd.exe";
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
// Argument bytes cmd.exe delivers unchanged without quoting. Anything outside
|
|
228
|
+
// this set (spaces, quotes, `%`, shell metacharacters, non-ASCII) forces the
|
|
229
|
+
// quoted+escaped path below. Mirrors the fuzz-tested allow-list from Zig's
|
|
230
|
+
// BatBadBut mitigation.
|
|
231
|
+
const CMD_SAFE_ARG = /^[A-Za-z0-9#$*+\-./:?@\\_]+$/;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Escape the interior of a `cmd.exe`-quoted token: neutralize `%VAR%` expansion
|
|
235
|
+
* and double any backslash run that precedes a quote (including the caller's
|
|
236
|
+
* closing quote) so `CommandLineToArgvW` delivers the backslashes literally.
|
|
237
|
+
*
|
|
238
|
+
* `cmd.exe` re-parses the whole `/c` string and expands `%…%` *before* the
|
|
239
|
+
* batch shim's own argv split runs, so both the command path and every argument
|
|
240
|
+
* must pass through this. Percent → `%%cd:~,%` (which expands to nothing,
|
|
241
|
+
* leaving a literal `%`) and `"` → `""` are the documented BatBadBut mitigation
|
|
242
|
+
* (CVE-2024-24576). The caller supplies the surrounding double quotes.
|
|
243
|
+
*
|
|
244
|
+
* @see https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
|
|
245
|
+
*/
|
|
246
|
+
function escapeCmdQuotedInterior(value: string): string {
|
|
247
|
+
let out = "";
|
|
248
|
+
let backslashes = 0;
|
|
249
|
+
for (const ch of value) {
|
|
250
|
+
if (ch === "\\") {
|
|
251
|
+
backslashes += 1;
|
|
252
|
+
out += ch;
|
|
253
|
+
} else if (ch === '"') {
|
|
254
|
+
out += "\\".repeat(backslashes);
|
|
255
|
+
out += '""';
|
|
256
|
+
backslashes = 0;
|
|
257
|
+
} else if (ch === "%") {
|
|
258
|
+
out += "%%cd:~,%";
|
|
259
|
+
backslashes = 0;
|
|
260
|
+
} else {
|
|
261
|
+
backslashes = 0;
|
|
262
|
+
out += ch;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Double the trailing backslash run so it stays literal before the closing
|
|
266
|
+
// quote the caller appends.
|
|
267
|
+
out += "\\".repeat(backslashes);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Reject bytes that cannot round-trip through `cmd.exe`'s `/c` command line. */
|
|
272
|
+
function assertCmdBatchToken(value: string, kind: "command" | "argument"): void {
|
|
273
|
+
// NUL/LF act as an end-of-command marker and CR is stripped, so any of them
|
|
274
|
+
// would silently truncate or corrupt the launch.
|
|
275
|
+
if (/[\0\r\n]/.test(value)) {
|
|
276
|
+
throw new Error(`Windows batch MCP ${kind} cannot contain NUL, CR, or LF characters`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Escape one argument for `cmd.exe`'s command-line pre-parse so a `.cmd`/`.bat`
|
|
282
|
+
* shim receives it verbatim. Quotes only when the argument is empty, ends in a
|
|
283
|
+
* backslash, or holds a byte outside {@link CMD_SAFE_ARG}; the quoted body is
|
|
284
|
+
* escaped by {@link escapeCmdQuotedInterior}.
|
|
285
|
+
*
|
|
286
|
+
* @throws when the argument contains NUL, CR, or LF (see {@link assertCmdBatchToken}).
|
|
287
|
+
*/
|
|
288
|
+
function escapeCmdBatchArg(arg: string): string {
|
|
289
|
+
assertCmdBatchToken(arg, "argument");
|
|
290
|
+
const needsQuotes = arg.length === 0 || arg.endsWith("\\") || !CMD_SAFE_ARG.test(arg);
|
|
291
|
+
// An unquoted arg is pure allow-list bytes (no `%`, `"`, or trailing `\`), so
|
|
292
|
+
// it needs no interior escaping.
|
|
293
|
+
return needsQuotes ? `"${escapeCmdQuotedInterior(arg)}"` : arg;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Build the `cmd.exe` argv for a Windows `.cmd`/`.bat` (or unresolved bare)
|
|
298
|
+
* MCP command.
|
|
299
|
+
*
|
|
300
|
+
* The trailing element is a single `/c` string wrapped in an outer quote pair
|
|
301
|
+
* that `cmd.exe` strips (its opening-quote rule). The command token is always
|
|
302
|
+
* quoted and, like every argument, escaped so a `%` in the resolved path (e.g.
|
|
303
|
+
* `C:\work\%TOKEN%\server.cmd`) is not expanded before the shim launches.
|
|
304
|
+
* `/e:ON` keeps command extensions on (required for the `%%cd:~,%` trick) and
|
|
305
|
+
* `/v:OFF` disables delayed expansion. The result MUST be spawned with
|
|
306
|
+
* `windowsVerbatimArguments` so libuv passes it through unmodified.
|
|
307
|
+
*/
|
|
308
|
+
function buildCmdExeArgv(comspec: string, command: string, args: readonly string[]): string[] {
|
|
309
|
+
assertCmdBatchToken(command, "command");
|
|
310
|
+
let line = `""${escapeCmdQuotedInterior(command)}"`;
|
|
311
|
+
for (const arg of args) line += ` ${escapeCmdBatchArg(arg)}`;
|
|
312
|
+
line += '"';
|
|
313
|
+
return [comspec, "/d", "/e:ON", "/v:OFF", "/c", line];
|
|
236
314
|
}
|
|
237
315
|
|
|
238
316
|
/**
|
|
@@ -245,7 +323,7 @@ function buildCmdExeCommand(command: string, args: readonly string[]): string {
|
|
|
245
323
|
* only appends `.exe` for extensionless names — `.cmd`/`.bat` are never
|
|
246
324
|
* tried, so `npx` (which exists only as `npx.cmd` on Windows) crashes the
|
|
247
325
|
* subprocess immediately. When the resolver can't pin the command down,
|
|
248
|
-
* route through `cmd.exe
|
|
326
|
+
* route through `cmd.exe` so Windows's own PATHEXT lookup runs.
|
|
249
327
|
*/
|
|
250
328
|
export async function resolveStdioSpawnCommand(
|
|
251
329
|
config: MCPStdioServerConfig,
|
|
@@ -271,9 +349,10 @@ export async function resolveStdioSpawnCommand(
|
|
|
271
349
|
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide, detached };
|
|
272
350
|
|
|
273
351
|
return {
|
|
274
|
-
cmd:
|
|
352
|
+
cmd: buildCmdExeArgv(resolveComSpec(options.env), resolvedCommand, args),
|
|
275
353
|
windowsHide,
|
|
276
354
|
detached,
|
|
355
|
+
windowsVerbatimArguments: true,
|
|
277
356
|
};
|
|
278
357
|
}
|
|
279
358
|
|
|
@@ -388,6 +467,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
388
467
|
stderr: "pipe",
|
|
389
468
|
windowsHide: spawnCommand.windowsHide,
|
|
390
469
|
detached: spawnCommand.detached,
|
|
470
|
+
windowsVerbatimArguments: spawnCommand.windowsVerbatimArguments,
|
|
391
471
|
});
|
|
392
472
|
|
|
393
473
|
this.#connected = true;
|