@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.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/CHANGELOG.md +162 -40
- package/dist/cli.js +4559 -4543
- 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/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- 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/eval/agent-bridge.d.ts +7 -19
- 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 +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -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/print-mode.d.ts +1 -1
- 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/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- 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/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- 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 +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- 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 +149 -8
- 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/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/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -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 +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- 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/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
|
@@ -44,7 +44,7 @@ export function createNoOpUIContext(): HookUIContext {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Raised by {@link
|
|
47
|
+
* Raised by {@link withHostGuard} when a guarded callback synchronously
|
|
48
48
|
* attempts to terminate the host process. Callers catch this like any other
|
|
49
49
|
* load-time failure so the extension/hook is skipped with a logged error
|
|
50
50
|
* instead of taking the CLI down with it.
|
|
@@ -66,22 +66,46 @@ export class ExtensionExitError extends Error {
|
|
|
66
66
|
|
|
67
67
|
type ExitAliasName = "process.exit" | "process.reallyExit";
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
/**
|
|
70
|
+
* stdin events a loaded module must not be allowed to leave hijacked. A
|
|
71
|
+
* top-level `new StdioServerTransport()` (or a bare `process.stdin.resume()`)
|
|
72
|
+
* inside a `~/.claude/tools` MCP server attaches a `data` consumer and puts the
|
|
73
|
+
* shared stdin into flowing mode; Bun delivers one `data` event to that
|
|
74
|
+
* consumer and the TUI's own listener (attached later in `terminal.start()`)
|
|
75
|
+
* then never re-arms — every keypress after the first is swallowed (#5618).
|
|
76
|
+
*/
|
|
77
|
+
const HOST_GUARD_STDIN_EVENTS = ["data", "readable", "end", "close", "error"] as const;
|
|
78
|
+
type StdinGuardEvent = (typeof HOST_GUARD_STDIN_EVENTS)[number];
|
|
79
|
+
type StdinGuardListener = (...args: unknown[]) => void;
|
|
80
|
+
|
|
81
|
+
let hostGuardDepth = 0;
|
|
82
|
+
let hostGuardOriginalProcessExit: typeof process.exit | null = null;
|
|
83
|
+
let hostGuardOriginalReallyExit: typeof process.reallyExit | null = null;
|
|
84
|
+
let hostGuardStdinListeners: Record<StdinGuardEvent, StdinGuardListener[]> | null = null;
|
|
85
|
+
let hostGuardStdinWasPaused = false;
|
|
86
|
+
let hostGuardStdinWasRaw = false;
|
|
72
87
|
|
|
73
88
|
/**
|
|
74
|
-
* Run `fn` with
|
|
75
|
-
*
|
|
89
|
+
* Run `fn` with host-owned process state fenced off from third-party module
|
|
90
|
+
* evaluation, restored in `finally`. Guards the dynamic-import and
|
|
91
|
+
* factory-invocation sites that load extension / hook / tool / plugin modules
|
|
92
|
+
* from user directories (including Claude Code's `~/.claude/tools`, which OMP
|
|
93
|
+
* slurps wholesale). Two hazards are neutralized:
|
|
76
94
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
95
|
+
* - **Hard exit.** `process.exit(0)` / `process.reallyExit(0)` in a stranger's
|
|
96
|
+
* script (e.g. a CLI-shaped module with `main()` at the bottom) would kill
|
|
97
|
+
* OMP during startup with no error surface, since `try/catch` cannot
|
|
98
|
+
* intercept a synchronous exit. Both are patched to throw
|
|
99
|
+
* {@link ExtensionExitError} instead.
|
|
100
|
+
* - **stdin hijack.** A module that attaches a stdin consumer at evaluation
|
|
101
|
+
* time (an MCP `StdioServerTransport`, or a bare `resume()`) steals Bun's
|
|
102
|
+
* single stdin reader, so the TUI goes permanently deaf after one keypress
|
|
103
|
+
* (#5618). Any `data`/`readable`/`end`/`close`/`error` listener the module
|
|
104
|
+
* adds is removed, and the stream's paused and raw-mode state is restored to
|
|
105
|
+
* the pre-load snapshot.
|
|
82
106
|
*
|
|
83
107
|
* Nested and concurrent guard windows are safe: only the outermost guard
|
|
84
|
-
*
|
|
108
|
+
* snapshots and restores host state.
|
|
85
109
|
*/
|
|
86
110
|
function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
|
|
87
111
|
return (code?: number | string): never => {
|
|
@@ -89,29 +113,71 @@ function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
|
|
|
89
113
|
};
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
export async function
|
|
93
|
-
if (
|
|
94
|
-
|
|
116
|
+
export async function withHostGuard<T>(fn: () => Promise<T>): Promise<T> {
|
|
117
|
+
if (hostGuardDepth === 0) {
|
|
118
|
+
hostGuardOriginalProcessExit = process.exit;
|
|
95
119
|
process.exit = guardedExit("process.exit") as typeof process.exit;
|
|
96
120
|
|
|
97
121
|
if (typeof process.reallyExit === "function") {
|
|
98
|
-
|
|
122
|
+
hostGuardOriginalReallyExit = process.reallyExit;
|
|
99
123
|
process.reallyExit = guardedExit("process.reallyExit") as typeof process.reallyExit;
|
|
100
124
|
}
|
|
125
|
+
|
|
126
|
+
const stdin = process.stdin;
|
|
127
|
+
hostGuardStdinWasPaused = stdin.isPaused();
|
|
128
|
+
hostGuardStdinWasRaw = stdin.isRaw ?? false;
|
|
129
|
+
const snapshot = {} as Record<StdinGuardEvent, StdinGuardListener[]>;
|
|
130
|
+
for (const event of HOST_GUARD_STDIN_EVENTS) {
|
|
131
|
+
snapshot[event] = stdin.rawListeners(event) as StdinGuardListener[];
|
|
132
|
+
}
|
|
133
|
+
hostGuardStdinListeners = snapshot;
|
|
101
134
|
}
|
|
102
|
-
|
|
135
|
+
hostGuardDepth++;
|
|
103
136
|
try {
|
|
104
137
|
return await fn();
|
|
105
138
|
} finally {
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
process.exit =
|
|
110
|
-
|
|
139
|
+
hostGuardDepth--;
|
|
140
|
+
if (hostGuardDepth === 0) {
|
|
141
|
+
if (hostGuardOriginalProcessExit) {
|
|
142
|
+
process.exit = hostGuardOriginalProcessExit;
|
|
143
|
+
hostGuardOriginalProcessExit = null;
|
|
144
|
+
}
|
|
145
|
+
if (hostGuardOriginalReallyExit) {
|
|
146
|
+
process.reallyExit = hostGuardOriginalReallyExit;
|
|
147
|
+
hostGuardOriginalReallyExit = null;
|
|
111
148
|
}
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
if (hostGuardStdinListeners) {
|
|
150
|
+
const stdin = process.stdin;
|
|
151
|
+
for (const event of HOST_GUARD_STDIN_EVENTS) {
|
|
152
|
+
const before = hostGuardStdinListeners[event];
|
|
153
|
+
// Reconcile the stream back to the pre-load snapshot: drop any
|
|
154
|
+
// listener the module added, and reinstate any snapshot listener
|
|
155
|
+
// it removed (e.g. a factory calling `removeAllListeners("data")`
|
|
156
|
+
// would otherwise permanently strip ProcessTerminal's input
|
|
157
|
+
// handler, leaving the parent TUI deaf). removeAllListeners then
|
|
158
|
+
// re-adding in snapshot order restores both membership and order.
|
|
159
|
+
const current = stdin.rawListeners(event) as StdinGuardListener[];
|
|
160
|
+
const differs =
|
|
161
|
+
current.length !== before.length || current.some((listener, index) => listener !== before[index]);
|
|
162
|
+
if (!differs) continue;
|
|
163
|
+
stdin.removeAllListeners(event);
|
|
164
|
+
for (const listener of before) {
|
|
165
|
+
stdin.on(event, listener);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
stdin.isTTY &&
|
|
170
|
+
typeof stdin.setRawMode === "function" &&
|
|
171
|
+
(stdin.isRaw ?? false) !== hostGuardStdinWasRaw
|
|
172
|
+
) {
|
|
173
|
+
stdin.setRawMode(hostGuardStdinWasRaw);
|
|
174
|
+
}
|
|
175
|
+
if (hostGuardStdinWasPaused && !stdin.isPaused()) {
|
|
176
|
+
stdin.pause();
|
|
177
|
+
} else if (!hostGuardStdinWasPaused && stdin.isPaused()) {
|
|
178
|
+
stdin.resume();
|
|
179
|
+
}
|
|
180
|
+
hostGuardStdinListeners = null;
|
|
115
181
|
}
|
|
116
182
|
}
|
|
117
183
|
}
|
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/lsp/client.ts
CHANGED
|
@@ -142,6 +142,9 @@ const CLIENT_CAPABILITIES = {
|
|
|
142
142
|
codeDescriptionSupport: true,
|
|
143
143
|
dataSupport: true,
|
|
144
144
|
},
|
|
145
|
+
diagnostic: {
|
|
146
|
+
dynamicRegistration: true,
|
|
147
|
+
},
|
|
145
148
|
},
|
|
146
149
|
window: {
|
|
147
150
|
workDoneProgress: true,
|
|
@@ -428,7 +431,7 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
|
|
|
428
431
|
const items = params?.items ?? [];
|
|
429
432
|
const result = items.map(item => {
|
|
430
433
|
const section = item.section ?? "";
|
|
431
|
-
return client.config.settings?.[section] ??
|
|
434
|
+
return client.config.settings?.[section] ?? null;
|
|
432
435
|
});
|
|
433
436
|
await sendResponse(client, message.id, result, "workspace/configuration");
|
|
434
437
|
}
|
|
@@ -456,6 +459,58 @@ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequ
|
|
|
456
459
|
}
|
|
457
460
|
}
|
|
458
461
|
|
|
462
|
+
interface DynamicCapabilityRegistration {
|
|
463
|
+
id?: unknown;
|
|
464
|
+
method?: unknown;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
interface DynamicCapabilityParams {
|
|
468
|
+
registrations?: DynamicCapabilityRegistration[];
|
|
469
|
+
unregisterations?: DynamicCapabilityRegistration[];
|
|
470
|
+
unregistrations?: DynamicCapabilityRegistration[];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function updateDynamicCapabilities(client: LspClient, message: LspJsonRpcRequest): void {
|
|
474
|
+
const params = message.params as DynamicCapabilityParams;
|
|
475
|
+
if (message.method === "client/registerCapability") {
|
|
476
|
+
if (!Array.isArray(params.registrations)) return;
|
|
477
|
+
let registrations = client.dynamicCapabilityRegistrations;
|
|
478
|
+
if (!registrations) {
|
|
479
|
+
registrations = new Map();
|
|
480
|
+
client.dynamicCapabilityRegistrations = registrations;
|
|
481
|
+
}
|
|
482
|
+
for (const registration of params.registrations) {
|
|
483
|
+
if (typeof registration.id === "string" && typeof registration.method === "string") {
|
|
484
|
+
registrations.set(registration.id, registration.method);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const registrations = client.dynamicCapabilityRegistrations;
|
|
491
|
+
if (!registrations) return;
|
|
492
|
+
const unregistrations = params.unregisterations ?? params.unregistrations;
|
|
493
|
+
if (!Array.isArray(unregistrations)) return;
|
|
494
|
+
for (const registration of unregistrations) {
|
|
495
|
+
if (typeof registration.id === "string") {
|
|
496
|
+
registrations.delete(registration.id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Whether the server advertised LSP 3.17 document diagnostic pulls statically or through registration. */
|
|
502
|
+
export function supportsDocumentDiagnostics(client: LspClient): boolean {
|
|
503
|
+
const staticProvider = client.serverCapabilities?.diagnosticProvider;
|
|
504
|
+
if (staticProvider) return true;
|
|
505
|
+
|
|
506
|
+
const registrations = client.dynamicCapabilityRegistrations;
|
|
507
|
+
if (!registrations) return false;
|
|
508
|
+
for (const method of registrations.values()) {
|
|
509
|
+
if (method === "textDocument/diagnostic") return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
459
514
|
/**
|
|
460
515
|
* Respond to a server-initiated request.
|
|
461
516
|
*/
|
|
@@ -478,6 +533,7 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
|
|
478
533
|
return;
|
|
479
534
|
}
|
|
480
535
|
if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
|
|
536
|
+
updateDynamicCapabilities(client, message);
|
|
481
537
|
// Some servers block semantic requests until dynamic registration succeeds.
|
|
482
538
|
await sendResponse(client, message.id, null, message.method);
|
|
483
539
|
return;
|
|
@@ -685,6 +741,7 @@ export async function getOrCreateClient(
|
|
|
685
741
|
requestId: 0,
|
|
686
742
|
diagnostics: new Map(),
|
|
687
743
|
diagnosticsVersion: 0,
|
|
744
|
+
dynamicCapabilityRegistrations: new Map(),
|
|
688
745
|
openFiles: new Map(),
|
|
689
746
|
pendingRequests: new Map(),
|
|
690
747
|
messageBuffer: new Uint8Array(0),
|
package/src/lsp/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
sendNotification,
|
|
29
29
|
sendRequest,
|
|
30
30
|
setIdleTimeout,
|
|
31
|
+
supportsDocumentDiagnostics,
|
|
31
32
|
syncContent,
|
|
32
33
|
WARMUP_TIMEOUT_MS,
|
|
33
34
|
waitForProjectLoaded,
|
|
@@ -530,17 +531,49 @@ interface WaitForDiagnosticsOptions {
|
|
|
530
531
|
settleMs?: number;
|
|
531
532
|
}
|
|
532
533
|
|
|
534
|
+
function requestDocumentDiagnostics(
|
|
535
|
+
client: LspClient,
|
|
536
|
+
uri: string,
|
|
537
|
+
signal: AbortSignal | undefined,
|
|
538
|
+
timeoutMs: number,
|
|
539
|
+
): Promise<Diagnostic[] | undefined> {
|
|
540
|
+
return sendRequest(client, "textDocument/diagnostic", { textDocument: { uri } }, signal, timeoutMs)
|
|
541
|
+
.then(report => {
|
|
542
|
+
if (!report || typeof report !== "object" || !("kind" in report) || report.kind !== "full") {
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
if (!("items" in report) || !Array.isArray(report.items)) return undefined;
|
|
546
|
+
return report.items;
|
|
547
|
+
})
|
|
548
|
+
.catch(err => {
|
|
549
|
+
if (!signal?.aborted) {
|
|
550
|
+
logger.debug("LSP document diagnostic pull failed", { server: client.name, uri, error: String(err) });
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
533
556
|
async function waitForDiagnostics(
|
|
534
557
|
client: LspClient,
|
|
535
558
|
uri: string,
|
|
536
559
|
options: WaitForDiagnosticsOptions = {},
|
|
537
560
|
): Promise<Diagnostic[]> {
|
|
538
561
|
const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
|
|
539
|
-
const
|
|
562
|
+
const deadline = Date.now() + timeoutMs;
|
|
563
|
+
let pullAttempted = false;
|
|
564
|
+
let pullResultPromise: Promise<{ diagnostics: Diagnostic[] | undefined }> | undefined;
|
|
565
|
+
let pulled: Diagnostic[] | undefined;
|
|
540
566
|
let settledRef: PublishedDiagnostics | undefined;
|
|
541
567
|
let settledAt = 0;
|
|
542
|
-
while (Date.now()
|
|
568
|
+
while (Date.now() < deadline) {
|
|
543
569
|
throwIfAborted(signal);
|
|
570
|
+
if (!pullAttempted && supportsDocumentDiagnostics(client)) {
|
|
571
|
+
pullAttempted = true;
|
|
572
|
+
pullResultPromise = requestDocumentDiagnostics(client, uri, signal, Math.max(1, deadline - Date.now())).then(
|
|
573
|
+
diagnostics => ({ diagnostics }),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
544
577
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
545
578
|
const published = client.diagnostics.get(uri);
|
|
546
579
|
if (published && versionOk) {
|
|
@@ -557,13 +590,36 @@ async function waitForDiagnostics(
|
|
|
557
590
|
return published.diagnostics;
|
|
558
591
|
}
|
|
559
592
|
}
|
|
560
|
-
|
|
593
|
+
|
|
594
|
+
const pollMs = Math.min(DIAGNOSTICS_POLL_MS, Math.max(0, deadline - Date.now()));
|
|
595
|
+
if (!pullResultPromise) {
|
|
596
|
+
await Bun.sleep(pollMs);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const pullResult = await Promise.race([pullResultPromise, Bun.sleep(pollMs).then(() => undefined)]);
|
|
600
|
+
if (pullResult) {
|
|
601
|
+
pullResultPromise = undefined;
|
|
602
|
+
pulled = pullResult.diagnostics;
|
|
603
|
+
if (pulled !== undefined) break;
|
|
604
|
+
}
|
|
561
605
|
}
|
|
606
|
+
|
|
562
607
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
563
|
-
|
|
564
|
-
|
|
608
|
+
const published = client.diagnostics.get(uri);
|
|
609
|
+
if (published && versionOk) {
|
|
610
|
+
return published.diagnostics;
|
|
611
|
+
}
|
|
612
|
+
if (pullResultPromise) {
|
|
613
|
+
pulled = (await pullResultPromise).diagnostics;
|
|
565
614
|
}
|
|
566
|
-
|
|
615
|
+
throwIfAborted(signal);
|
|
616
|
+
if (pulled === undefined) return [];
|
|
617
|
+
client.diagnostics.set(uri, {
|
|
618
|
+
diagnostics: pulled,
|
|
619
|
+
version: expectedDocumentVersion ?? client.openFiles.get(uri)?.version ?? null,
|
|
620
|
+
});
|
|
621
|
+
client.diagnosticsVersion += 1;
|
|
622
|
+
return pulled;
|
|
567
623
|
}
|
|
568
624
|
|
|
569
625
|
/** Project type detection result */
|
package/src/lsp/types.ts
CHANGED
|
@@ -387,6 +387,7 @@ export interface LspServerCapabilities {
|
|
|
387
387
|
referencesProvider?: boolean;
|
|
388
388
|
documentSymbolProvider?: boolean;
|
|
389
389
|
workspaceSymbolProvider?: boolean;
|
|
390
|
+
diagnosticProvider?: boolean | Record<string, unknown>;
|
|
390
391
|
[key: string]: unknown;
|
|
391
392
|
}
|
|
392
393
|
|
|
@@ -398,6 +399,8 @@ export interface LspClient {
|
|
|
398
399
|
requestId: number;
|
|
399
400
|
diagnostics: Map<string, PublishedDiagnostics>;
|
|
400
401
|
diagnosticsVersion: number;
|
|
402
|
+
/** Dynamic capability registrations keyed by the server-provided registration ID. */
|
|
403
|
+
dynamicCapabilityRegistrations?: Map<string, string>;
|
|
401
404
|
openFiles: Map<string, OpenFile>;
|
|
402
405
|
pendingRequests: Map<number | string, PendingRequest>;
|
|
403
406
|
messageBuffer: Uint8Array;
|
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
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -385,6 +385,13 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
385
385
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
386
386
|
if (!this.#resolvedClientId) {
|
|
387
387
|
await this.#tryRegisterClient(redirectUri);
|
|
388
|
+
// `unapproved_client` explicitly establishes that registration cannot
|
|
389
|
+
// produce the required client id. Other DCR failures stay on the
|
|
390
|
+
// clientless probe path because they may be transient or caused by
|
|
391
|
+
// unrelated registration metadata.
|
|
392
|
+
if (!this.#resolvedClientId && this.#isDefinitiveRegistrationRejection()) {
|
|
393
|
+
throw this.#missingClientIdError();
|
|
394
|
+
}
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
const authUrl = new URL(this.config.authorizationUrl);
|
|
@@ -691,6 +698,19 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
691
698
|
}
|
|
692
699
|
}
|
|
693
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Whether the provider explicitly rejected this client as unapproved.
|
|
703
|
+
*
|
|
704
|
+
* HTTP status alone is insufficient: payload errors such as
|
|
705
|
+
* `invalid_client_metadata` and `invalid_redirect_uri` do not establish that
|
|
706
|
+
* the authorization endpoint requires a client id. Keep those on the
|
|
707
|
+
* clientless probe path.
|
|
708
|
+
*/
|
|
709
|
+
#isDefinitiveRegistrationRejection(): boolean {
|
|
710
|
+
const failure = this.#registrationFailure;
|
|
711
|
+
return failure?.status === 403 && /\bunapproved_client\b/i.test(failure.detail ?? "");
|
|
712
|
+
}
|
|
713
|
+
|
|
694
714
|
/**
|
|
695
715
|
* Build the error thrown when the authorize probe confirms the provider
|
|
696
716
|
* demands a `client_id`. When dynamic client registration was attempted and
|