@oh-my-pi/pi-coding-agent 17.2.2 → 17.2.4
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 +55 -0
- package/dist/{CHANGELOG-xfpkakrn.md → CHANGELOG-bpmhv26t.md} +55 -0
- package/dist/cli.js +3294 -3294
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/paths.d.ts +2 -1
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +9 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/index.d.ts +6 -8
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +7 -0
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +11 -5
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/browser/launch.d.ts +30 -0
- package/dist/types/tools/browser/registry.d.ts +6 -1
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/collab/host.ts +1 -1
- package/src/config/keybindings.ts +14 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/launch/broker.ts +59 -41
- package/src/launch/paths.ts +2 -5
- package/src/main.ts +24 -22
- package/src/mcp/config.ts +1 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +26 -0
- package/src/modes/components/tool-execution.ts +77 -20
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +182 -10
- package/src/modes/controllers/selector-controller.ts +27 -6
- package/src/modes/types.ts +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +68 -29
- package/src/secrets/index.ts +15 -17
- package/src/session/agent-session.ts +11 -2
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/session-advisors.ts +19 -1
- package/src/session/session-maintenance.ts +5 -2
- package/src/session/session-manager.ts +128 -76
- package/src/session/session-tools.ts +25 -23
- package/src/slash-commands/builtin-registry.ts +2 -2
- package/src/system-prompt.ts +6 -1
- package/src/task/executor.ts +8 -1
- package/src/tiny/text.ts +12 -0
- package/src/tools/bash.ts +11 -5
- package/src/tools/browser/launch.ts +54 -9
- package/src/tools/browser/registry.ts +123 -30
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/index.ts +10 -6
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/codex.ts +31 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.2.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.2.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.2.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.2.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.2.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.2.
|
|
55
|
+
"@oh-my-pi/hashline": "17.2.4",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.2.4",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.2.4",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.2.4",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.2.4",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.2.4",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.2.4",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.2.4",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.2.4",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.2.4",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.2.4",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -142,7 +142,13 @@ export async function collectBundledPiEntries(): Promise<BundledPiEntry[]> {
|
|
|
142
142
|
|
|
143
143
|
const sourceDir = path.join(packageRoot, pattern.sourcePrefix);
|
|
144
144
|
try {
|
|
145
|
-
|
|
145
|
+
// Recursive on purpose: Node matches `*` in an `exports` pattern across
|
|
146
|
+
// `/`, so `./slash-commands/*` genuinely serves
|
|
147
|
+
// `slash-commands/helpers/active-oauth-account`. Enumerating only the
|
|
148
|
+
// top level left every nested key out of the compiled registry, where
|
|
149
|
+
// it fell through to `Bun.resolveSync` and died under bunfs — so such
|
|
150
|
+
// an import worked from source and failed inside a binary.
|
|
151
|
+
const glob = new Bun.Glob(`**/*${pattern.sourceSuffix}`);
|
|
146
152
|
const matches: string[] = [];
|
|
147
153
|
for await (const match of glob.scan({ cwd: sourceDir, onlyFiles: true })) {
|
|
148
154
|
matches.push(match);
|
|
@@ -151,7 +157,11 @@ export async function collectBundledPiEntries(): Promise<BundledPiEntry[]> {
|
|
|
151
157
|
for (const match of matches) {
|
|
152
158
|
if (!match.endsWith(pattern.sourceSuffix)) continue;
|
|
153
159
|
const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
|
|
154
|
-
|
|
160
|
+
const segments = basename.split("/");
|
|
161
|
+
// Every directory on the way has to be importable too: a private or
|
|
162
|
+
// hidden folder is no more exported than a private file.
|
|
163
|
+
if (segments.some(segment => segment.startsWith(".") || segment.startsWith("_"))) continue;
|
|
164
|
+
if (!isSafeWildcardBasename(segments.at(-1) ?? "")) continue;
|
|
155
165
|
const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
|
|
156
166
|
const key = `${manifest.name}/${subpath}`;
|
|
157
167
|
addEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
@@ -180,12 +180,23 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
|
|
|
180
180
|
* escalation: nit → concern → blocker), so an advisor cannot bypass dedupe
|
|
181
181
|
* by retagging the same text at a lower or equal severity. */
|
|
182
182
|
#deliveredNoteSeverities = new Map<string, number>();
|
|
183
|
+
#inProgressUpdate = false;
|
|
183
184
|
|
|
184
185
|
constructor(private readonly onAdvice: (note: string, severity?: AdviseDetails["severity"]) => void) {}
|
|
185
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Mark whether the next advisor prompt reviews an in-progress primary turn.
|
|
189
|
+
* Non-blockers are withheld until a completed update so partial work does
|
|
190
|
+
* not interrupt the primary before it can finish its planned steps.
|
|
191
|
+
*/
|
|
192
|
+
beginUpdate(inProgress: boolean): void {
|
|
193
|
+
this.#inProgressUpdate = inProgress;
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
/** Clear delivered-note memory when the advisor starts a fresh conversation. */
|
|
187
197
|
resetDeliveredNotes(): void {
|
|
188
198
|
this.#deliveredNoteSeverities.clear();
|
|
199
|
+
this.#inProgressUpdate = false;
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
async execute(
|
|
@@ -195,6 +206,13 @@ export class AdviseTool implements AgentTool<typeof adviseSchema, AdviseDetails>
|
|
|
195
206
|
_onUpdate?: AgentToolUpdateCallback<AdviseDetails>,
|
|
196
207
|
_context?: AgentToolContext,
|
|
197
208
|
): Promise<AgentToolResult<AdviseDetails>> {
|
|
209
|
+
if (this.#inProgressUpdate && args.severity !== "blocker") {
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text", text: "Recorded." }],
|
|
212
|
+
details: { note: args.note, severity: args.severity },
|
|
213
|
+
useless: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
198
216
|
const key = advisorNoteDedupeKey(args.note);
|
|
199
217
|
const rank = advisorSeverityRank(args.severity);
|
|
200
218
|
const previousRank = this.#deliveredNoteSeverities.get(key) ?? 0;
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -51,11 +51,11 @@ export interface AdvisorRuntimeHost {
|
|
|
51
51
|
maintainContext?(incomingTokens: number, signal: AbortSignal): Promise<boolean>;
|
|
52
52
|
/**
|
|
53
53
|
* Called immediately before each `agent.prompt(batch)` cycle. Lets the host
|
|
54
|
-
* clear per-update advisor state
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* clear per-update advisor state and apply the in-progress delivery policy.
|
|
55
|
+
* The host owns these gates because it routes `advise()` results back to the
|
|
56
|
+
* primary.
|
|
57
57
|
*/
|
|
58
|
-
beginAdvisorUpdate?(): void;
|
|
58
|
+
beginAdvisorUpdate?(inProgress: boolean): void;
|
|
59
59
|
/**
|
|
60
60
|
* Called with the error of every failed advisor turn, before the retry sleep
|
|
61
61
|
* or the dropped-after-3 path. Lets the host apply credential-level remedies
|
|
@@ -909,8 +909,8 @@ export class AdvisorRuntime {
|
|
|
909
909
|
const contextWasFresh = resetContext || recoveringOverflow || messageSnapshot === 0;
|
|
910
910
|
try {
|
|
911
911
|
// Reset the host's per-update advisor state (one-advise-per-update
|
|
912
|
-
// gate)
|
|
913
|
-
this.host.beginAdvisorUpdate?.();
|
|
912
|
+
// gate) and pass through whether this batch reviews partial work.
|
|
913
|
+
this.host.beginAdvisorUpdate?.(wip);
|
|
914
914
|
const prompt = this.agent.prompt(batch);
|
|
915
915
|
this.#promptInFlight = prompt;
|
|
916
916
|
try {
|
package/src/capability/mcp.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Canonical shape for MCP server configurations, regardless of source format.
|
|
5
5
|
* All providers translate their native format to this shape.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import type { MCPRequestIdFormat } from "../mcp/types";
|
|
7
9
|
import { defineCapability } from ".";
|
|
8
10
|
import type { SourceMeta } from "./types";
|
|
9
11
|
|
|
@@ -17,6 +19,8 @@ export interface MCPServer {
|
|
|
17
19
|
enabled?: boolean;
|
|
18
20
|
/** Connection timeout in milliseconds */
|
|
19
21
|
timeout?: number;
|
|
22
|
+
/** Encoding for outgoing JSON-RPC request ids (default: `"number"`) */
|
|
23
|
+
requestIdFormat?: MCPRequestIdFormat;
|
|
20
24
|
/** Command to run (for stdio transport) */
|
|
21
25
|
command?: string;
|
|
22
26
|
/** Command arguments */
|
|
@@ -56,6 +60,9 @@ export interface MCPServer {
|
|
|
56
60
|
/** Compare the transport inputs that determine which MCP endpoint gets connected. */
|
|
57
61
|
function isSameMCPConnection(left: MCPServer, right: MCPServer): boolean {
|
|
58
62
|
if (!Bun.deepEquals(left.auth, right.auth) || !Bun.deepEquals(left.oauth, right.oauth)) return false;
|
|
63
|
+
// Normalize against the allocator's own default so an explicit "number" is
|
|
64
|
+
// equivalent to leaving the option unset, not a distinct connection.
|
|
65
|
+
if ((left.requestIdFormat ?? "number") !== (right.requestIdFormat ?? "number")) return false;
|
|
59
66
|
|
|
60
67
|
const leftTransport = left.transport ?? (left.command ? "stdio" : left.url ? "http" : "stdio");
|
|
61
68
|
const rightTransport = right.transport ?? (right.command ? "stdio" : right.url ? "http" : "stdio");
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -17,12 +17,7 @@ import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { ModelRegistry } from "../config/model-registry";
|
|
19
19
|
import { Settings } from "../config/settings";
|
|
20
|
-
import {
|
|
21
|
-
discoverAndLoadExtensions,
|
|
22
|
-
ExtensionRunner,
|
|
23
|
-
emitSessionShutdownEvent,
|
|
24
|
-
loadExtensions,
|
|
25
|
-
} from "../extensibility/extensions";
|
|
20
|
+
import { discoverAndLoadExtensions, ExtensionRunner, emitSessionShutdownEvent } from "../extensibility/extensions";
|
|
26
21
|
import { discoverAuthStorage } from "../sdk";
|
|
27
22
|
import { SessionManager } from "../session/session-manager";
|
|
28
23
|
import { EventBus } from "../utils/event-bus";
|
|
@@ -283,7 +278,7 @@ export interface RunModelsListingOptions {
|
|
|
283
278
|
settingsExtensions?: string[];
|
|
284
279
|
/** Disabled extension ids from settings (`disabledExtensions`). */
|
|
285
280
|
disabledExtensionIds?: string[];
|
|
286
|
-
/** When true,
|
|
281
|
+
/** When true, exclude ambient factories and resolve only `additionalExtensionPaths`. */
|
|
287
282
|
disableExtensionDiscovery?: boolean;
|
|
288
283
|
}
|
|
289
284
|
|
|
@@ -301,14 +296,16 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
|
|
|
301
296
|
} = options;
|
|
302
297
|
|
|
303
298
|
const eventBus = new EventBus();
|
|
304
|
-
const
|
|
305
|
-
?
|
|
306
|
-
:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
299
|
+
const configuredPaths = disableExtensionDiscovery
|
|
300
|
+
? additionalExtensionPaths
|
|
301
|
+
: [...additionalExtensionPaths, ...settingsExtensions];
|
|
302
|
+
const extensionsResult = await discoverAndLoadExtensions(
|
|
303
|
+
configuredPaths,
|
|
304
|
+
cwd,
|
|
305
|
+
eventBus,
|
|
306
|
+
disableExtensionDiscovery ? undefined : disabledExtensionIds,
|
|
307
|
+
{ ambient: !disableExtensionDiscovery },
|
|
308
|
+
);
|
|
312
309
|
const extensionRunner =
|
|
313
310
|
extensionsResult.extensions.length > 0
|
|
314
311
|
? new ExtensionRunner(
|
|
@@ -370,7 +367,7 @@ export async function runModelsCommand(command: ModelsCommandArgs): Promise<void
|
|
|
370
367
|
}
|
|
371
368
|
await modelRegistry.refresh(action === "refresh" ? "online" : "online-if-uncached");
|
|
372
369
|
|
|
373
|
-
const cliExtensionPaths = command.flags.
|
|
370
|
+
const cliExtensionPaths = command.flags.extensions ?? [];
|
|
374
371
|
await runModelsListing({
|
|
375
372
|
modelRegistry,
|
|
376
373
|
cwd,
|
package/src/collab/host.ts
CHANGED
|
@@ -619,7 +619,7 @@ export class CollabHost {
|
|
|
619
619
|
if (ref.status === "running" && ref.session) {
|
|
620
620
|
await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
621
621
|
}
|
|
622
|
-
await AgentLifecycleManager.global().release(agentId, ref);
|
|
622
|
+
await AgentLifecycleManager.global().release(agentId, ref, { tombstone: true });
|
|
623
623
|
};
|
|
624
624
|
kill().catch(fail);
|
|
625
625
|
break;
|
|
@@ -140,7 +140,9 @@ export const KEYBINDINGS = {
|
|
|
140
140
|
description: "Retry last failed assistant turn",
|
|
141
141
|
},
|
|
142
142
|
"app.message.dequeue": {
|
|
143
|
-
|
|
143
|
+
// Shift+Up is listed alongside Alt+Up because macOS Terminal.app consumes Option
|
|
144
|
+
// for character composition, leaving Alt+Up unreachable there.
|
|
145
|
+
defaultKeys: ["alt+up", "shift+up"],
|
|
144
146
|
description: "Dequeue message",
|
|
145
147
|
},
|
|
146
148
|
"app.clipboard.pasteImage": {
|
|
@@ -516,6 +518,13 @@ function migrateKeybindingsConfigFile(agentDir: string): void {
|
|
|
516
518
|
|
|
517
519
|
const FOLLOW_UP_KEYBINDING: AppKeybinding = "app.message.followUp";
|
|
518
520
|
const WINDOWS_FOLLOW_UP_FALLBACK_KEY: KeyId = "ctrl+q";
|
|
521
|
+
const DEQUEUE_KEYBINDING: AppKeybinding = "app.message.dequeue";
|
|
522
|
+
const MACOS_DEQUEUE_FALLBACK_KEY: KeyId = "shift+up";
|
|
523
|
+
function getFallbackKey(keybinding: Keybinding): KeyId | undefined {
|
|
524
|
+
if (keybinding === FOLLOW_UP_KEYBINDING) return WINDOWS_FOLLOW_UP_FALLBACK_KEY;
|
|
525
|
+
if (keybinding === DEQUEUE_KEYBINDING) return MACOS_DEQUEUE_FALLBACK_KEY;
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
519
528
|
function keyListIncludes(keys: KeyId | KeyId[] | undefined, target: KeyId): boolean {
|
|
520
529
|
if (keys === undefined) return false;
|
|
521
530
|
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
@@ -600,14 +609,10 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
600
609
|
|
|
601
610
|
getKeys(keybinding: Keybinding): KeyId[] {
|
|
602
611
|
const keys = super.getKeys(keybinding);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
return removeKey(keys, WINDOWS_FOLLOW_UP_FALLBACK_KEY);
|
|
609
|
-
}
|
|
610
|
-
return keys;
|
|
612
|
+
const fallbackKey = getFallbackKey(keybinding);
|
|
613
|
+
if (fallbackKey === undefined || this.#userBindings[keybinding] !== undefined) return keys;
|
|
614
|
+
if (!userBindingClaimsKey(this.#userBindings, fallbackKey, keybinding)) return keys;
|
|
615
|
+
return removeKey(keys, fallbackKey);
|
|
611
616
|
}
|
|
612
617
|
|
|
613
618
|
getResolvedBindings(): KeybindingsConfig {
|
|
@@ -118,6 +118,11 @@
|
|
|
118
118
|
"minimum": 0,
|
|
119
119
|
"description": "MCP request timeout in milliseconds. Set to 0 to disable client-side MCP timeouts."
|
|
120
120
|
},
|
|
121
|
+
"requestIdFormat": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"enum": ["string", "number"],
|
|
124
|
+
"description": "Encoding for outgoing JSON-RPC request ids (default: \"number\"). Use \"string\" for servers that need collision-resistant snowflake string ids instead of per-transport integers. OMP-specific: set it in an OMP-owned config (`.omp/mcp.json`, `~/.omp/agent/mcp.json`, a project `mcp.json`/`.mcp.json`, or an OMP plugin). Servers imported from another tool's config (Claude, Cursor, VS Code, Gemini, OpenCode, Windsurf) ignore it, since that key is not part of those formats."
|
|
125
|
+
},
|
|
121
126
|
"auth": {
|
|
122
127
|
"$ref": "#/$defs/authConfig"
|
|
123
128
|
},
|
|
@@ -580,7 +580,13 @@ function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTr
|
|
|
580
580
|
result.headers = patch.headers;
|
|
581
581
|
compat = patch.compat;
|
|
582
582
|
}
|
|
583
|
-
|
|
583
|
+
const built = buildModel({ ...result, compat } as ModelSpec<Api>);
|
|
584
|
+
if (patch.thinking !== undefined && built.thinking !== undefined) {
|
|
585
|
+
// Config-authored capability metadata owns the explicit surface; build
|
|
586
|
+
// first so non-reasoning and wire-disabled models still suppress it.
|
|
587
|
+
built.thinking = patch.thinking;
|
|
588
|
+
}
|
|
589
|
+
return built;
|
|
584
590
|
}
|
|
585
591
|
|
|
586
592
|
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
@@ -4254,7 +4254,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4254
4254
|
group: "Discovery & MCP",
|
|
4255
4255
|
label: "xd:// Tools",
|
|
4256
4256
|
description:
|
|
4257
|
-
"Mount rarely-used (discoverable) tools under xd:// device URLs driven via read/write instead of shipping their schemas on every request. Disable to expose every enabled tool top-level.",
|
|
4257
|
+
"Mount rarely-used (discoverable) tools under xd:// device URLs driven via read/write instead of shipping their schemas on every request. Sessions without a granted write tool skip mounting and expose every tool top-level. Disable to expose every enabled tool top-level.",
|
|
4258
4258
|
},
|
|
4259
4259
|
},
|
|
4260
4260
|
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
expandEnvVarsDeep,
|
|
32
32
|
getExtensionNameFromPath,
|
|
33
33
|
loadFilesFromDir,
|
|
34
|
+
parseRequestIdFormat,
|
|
34
35
|
SOURCE_PATHS,
|
|
35
36
|
scanSkillsFromDir,
|
|
36
37
|
} from "./helpers";
|
|
@@ -154,10 +155,19 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
154
155
|
timeout = undefined;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Validate requestIdFormat: only the two documented encodings
|
|
159
|
+
const requestIdFormat = parseRequestIdFormat(serverConfig.requestIdFormat);
|
|
160
|
+
if (requestIdFormat === undefined && serverConfig.requestIdFormat != null) {
|
|
161
|
+
logger.warn(
|
|
162
|
+
`MCP server "${serverName}": invalid requestIdFormat ${JSON.stringify(serverConfig.requestIdFormat)}, ignoring`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
result.push({
|
|
158
167
|
name: serverName,
|
|
159
168
|
enabled,
|
|
160
169
|
timeout,
|
|
170
|
+
requestIdFormat,
|
|
161
171
|
command: serverConfig.command as string | undefined,
|
|
162
172
|
args: serverConfig.args as string[] | undefined,
|
|
163
173
|
env: serverConfig.env as Record<string, string> | undefined,
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../ca
|
|
|
16
16
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
17
17
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
18
18
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
19
|
+
import type { MCPRequestIdFormat } from "../mcp/types";
|
|
19
20
|
import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
|
|
20
21
|
import { normalizeToolNames } from "../tools/builtin-names";
|
|
21
22
|
|
|
@@ -140,6 +141,15 @@ export function parseBoolean(value: unknown): boolean | undefined {
|
|
|
140
141
|
return undefined;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Parse an MCP `requestIdFormat` value. Unrecognized values are dropped so a typo
|
|
146
|
+
* degrades to the default integer ids rather than reaching a transport.
|
|
147
|
+
*/
|
|
148
|
+
export function parseRequestIdFormat(value: unknown): MCPRequestIdFormat | undefined {
|
|
149
|
+
if (value === "string" || value === "number") return value;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
/**
|
|
144
154
|
* Parse a comma-separated string into an array of trimmed, non-empty strings.
|
|
145
155
|
*/
|
|
@@ -12,7 +12,7 @@ import { registerProvider } from "../capability";
|
|
|
12
12
|
import { readFile } from "../capability/fs";
|
|
13
13
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
14
14
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
15
|
-
import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
|
|
15
|
+
import { createSourceMeta, expandEnvVarsDeep, parseRequestIdFormat } from "./helpers";
|
|
16
16
|
|
|
17
17
|
const PROVIDER_ID = "mcp-json";
|
|
18
18
|
const DISPLAY_NAME = "MCP Config";
|
|
@@ -26,6 +26,7 @@ interface MCPConfigFile {
|
|
|
26
26
|
{
|
|
27
27
|
enabled?: boolean;
|
|
28
28
|
timeout?: number;
|
|
29
|
+
requestIdFormat?: "string" | "number";
|
|
29
30
|
command?: string;
|
|
30
31
|
args?: string[];
|
|
31
32
|
env?: Record<string, string>;
|
|
@@ -83,10 +84,19 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
const requestIdFormat = parseRequestIdFormat(serverConfig.requestIdFormat);
|
|
88
|
+
if (requestIdFormat === undefined && serverConfig.requestIdFormat !== undefined) {
|
|
89
|
+
logger.warn("MCP server has invalid 'requestIdFormat' value, ignoring", {
|
|
90
|
+
name,
|
|
91
|
+
value: serverConfig.requestIdFormat,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
const server: MCPServer = {
|
|
87
96
|
name,
|
|
88
97
|
enabled,
|
|
89
98
|
timeout,
|
|
99
|
+
requestIdFormat,
|
|
90
100
|
command: serverConfig.command,
|
|
91
101
|
args: serverConfig.args,
|
|
92
102
|
env: serverConfig.env,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @see ./omp-plugins.ts
|
|
16
16
|
* @see ./builtin.ts `loadExtensionModules`
|
|
17
17
|
*/
|
|
18
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
19
|
import * as fs from "node:fs/promises";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { getAgentDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
@@ -36,10 +37,48 @@ export interface OmpExtensionRoot {
|
|
|
36
37
|
|
|
37
38
|
interface InjectedRoot {
|
|
38
39
|
path: string;
|
|
40
|
+
/** Relative CLI spelling, rebound against the active project on discovery. */
|
|
41
|
+
relativePath?: string;
|
|
39
42
|
level: "user" | "project";
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
export type OmpExtensionRootMode = "merge" | "explicit-only";
|
|
46
|
+
|
|
47
|
+
interface InvocationRootScope {
|
|
48
|
+
/** Raw SDK spellings, resolved against the LoadContext that performs discovery. */
|
|
49
|
+
paths: readonly string[];
|
|
50
|
+
mode: OmpExtensionRootMode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const invocationRootScope = new AsyncLocalStorage<InvocationRootScope>();
|
|
54
|
+
|
|
42
55
|
let injectedCliRoots: InjectedRoot[] = [];
|
|
56
|
+
let injectedCliRootMode: OmpExtensionRootMode = "merge";
|
|
57
|
+
|
|
58
|
+
export interface InjectOmpExtensionCliRootOptions {
|
|
59
|
+
/**
|
|
60
|
+
* `explicit-only` exposes only roots named by this CLI invocation. Use it
|
|
61
|
+
* with `--no-extensions` so configured and installed packages cannot
|
|
62
|
+
* contribute sibling capabilities through the `omp-plugins` provider.
|
|
63
|
+
*/
|
|
64
|
+
mode?: OmpExtensionRootMode;
|
|
65
|
+
/** Replace roots from an earlier invocation instead of extending them. */
|
|
66
|
+
replace?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run one SDK invocation with its own extension-package roots. Async resources
|
|
71
|
+
* started inside `callback` retain this scope, including discovery deliberately
|
|
72
|
+
* deferred until the end of session startup. Raw relative paths are resolved by
|
|
73
|
+
* {@link listOmpExtensionRoots} against that invocation's active cwd.
|
|
74
|
+
*/
|
|
75
|
+
export function withOmpExtensionRootScope<T>(
|
|
76
|
+
paths: readonly string[],
|
|
77
|
+
mode: OmpExtensionRootMode,
|
|
78
|
+
callback: () => T,
|
|
79
|
+
): T {
|
|
80
|
+
return invocationRootScope.run({ paths: [...paths], mode }, callback);
|
|
81
|
+
}
|
|
43
82
|
|
|
44
83
|
/**
|
|
45
84
|
* Register CLI-provided extension package paths (e.g. from `--extension`/`-e`)
|
|
@@ -50,17 +89,27 @@ let injectedCliRoots: InjectedRoot[] = [];
|
|
|
50
89
|
* Call once during startup before any capability load. Repeated calls extend
|
|
51
90
|
* the registered set; {@link clearOmpExtensionCliRoots} resets for tests.
|
|
52
91
|
*/
|
|
53
|
-
export function injectOmpExtensionCliRoots(
|
|
92
|
+
export function injectOmpExtensionCliRoots(
|
|
93
|
+
paths: readonly string[],
|
|
94
|
+
home: string,
|
|
95
|
+
cwd: string,
|
|
96
|
+
options: InjectOmpExtensionCliRootOptions = {},
|
|
97
|
+
): void {
|
|
98
|
+
if (options.mode) injectedCliRootMode = options.mode;
|
|
99
|
+
if (options.replace) injectedCliRoots = [];
|
|
54
100
|
if (paths.length === 0) return;
|
|
55
101
|
const expanded = paths.map(raw => {
|
|
56
102
|
const tilde = expandTilde(raw, home);
|
|
57
|
-
return
|
|
103
|
+
return {
|
|
104
|
+
path: path.isAbsolute(tilde) ? tilde : path.resolve(cwd, tilde),
|
|
105
|
+
relativePath: path.isAbsolute(tilde) ? undefined : tilde,
|
|
106
|
+
};
|
|
58
107
|
});
|
|
59
108
|
const merged = new Map<string, InjectedRoot>();
|
|
60
109
|
for (const root of injectedCliRoots) merged.set(root.path, root);
|
|
61
|
-
for (const resolved of expanded) {
|
|
110
|
+
for (const { path: resolved, relativePath } of expanded) {
|
|
62
111
|
// CLI scope mirrors how `--extension` is treated elsewhere — user-level overrides win.
|
|
63
|
-
if (!merged.has(resolved)) merged.set(resolved, { path: resolved, level: "user" });
|
|
112
|
+
if (!merged.has(resolved)) merged.set(resolved, { path: resolved, relativePath, level: "user" });
|
|
64
113
|
}
|
|
65
114
|
injectedCliRoots = [...merged.values()];
|
|
66
115
|
}
|
|
@@ -68,6 +117,7 @@ export function injectOmpExtensionCliRoots(paths: readonly string[], home: strin
|
|
|
68
117
|
/** Drop every CLI-injected root. Tests use this between cases. */
|
|
69
118
|
export function clearOmpExtensionCliRoots(): void {
|
|
70
119
|
injectedCliRoots = [];
|
|
120
|
+
injectedCliRootMode = "merge";
|
|
71
121
|
}
|
|
72
122
|
|
|
73
123
|
/** Inspect currently-injected CLI roots (read-only). Exposed for diagnostics + tests. */
|
|
@@ -121,7 +171,8 @@ async function isDirectory(p: string): Promise<boolean> {
|
|
|
121
171
|
* Sources, in order of precedence (later entries with the same absolute path
|
|
122
172
|
* are dropped):
|
|
123
173
|
*
|
|
124
|
-
* 1.
|
|
174
|
+
* 1. Invocation-scoped SDK roots, when present; otherwise CLI roots injected
|
|
175
|
+
* via {@link injectOmpExtensionCliRoots}
|
|
125
176
|
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
|
126
177
|
* 3. User `~/.omp/agent/settings.json#extensions`
|
|
127
178
|
* 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
|
|
@@ -134,21 +185,29 @@ async function isDirectory(p: string): Promise<boolean> {
|
|
|
134
185
|
* other sources still surface.
|
|
135
186
|
*/
|
|
136
187
|
export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
const scopedRoots = invocationRootScope.getStore();
|
|
189
|
+
const rootMode = scopedRoots?.mode ?? injectedCliRootMode;
|
|
190
|
+
let candidates: InjectedRoot[] = scopedRoots
|
|
191
|
+
? scopedRoots.paths.map(raw => ({ path: resolveAgainst(raw, ctx), level: "user" }))
|
|
192
|
+
: injectedCliRoots.map(root =>
|
|
193
|
+
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
|
|
194
|
+
);
|
|
195
|
+
if (rootMode === "merge") {
|
|
196
|
+
const { project, user } = scopeDirs(ctx);
|
|
197
|
+
const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
|
|
198
|
+
readSettingsExtensions(path.join(project, "settings.json")),
|
|
199
|
+
readSettingsExtensions(path.join(user, "settings.json")),
|
|
200
|
+
listInstalledPluginRoots(ctx),
|
|
201
|
+
]);
|
|
202
|
+
candidates = [
|
|
203
|
+
...candidates,
|
|
204
|
+
...projectExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "project" })),
|
|
205
|
+
...userExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "user" })),
|
|
206
|
+
...installedPlugins,
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// First-seen-wins dedup preserves invocation/CLI > project-settings > user-settings > installed precedence.
|
|
152
211
|
const seen = new Set<string>();
|
|
153
212
|
const unique: InjectedRoot[] = [];
|
|
154
213
|
for (const candidate of candidates) {
|
|
@@ -28,7 +28,13 @@ import { type Skill, skillCapability } from "../capability/skill";
|
|
|
28
28
|
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
29
29
|
import { type CustomTool, toolCapability } from "../capability/tool";
|
|
30
30
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
buildRuleFromMarkdown,
|
|
33
|
+
createSourceMeta,
|
|
34
|
+
loadFilesFromDir,
|
|
35
|
+
parseRequestIdFormat,
|
|
36
|
+
scanSkillsFromDir,
|
|
37
|
+
} from "./helpers";
|
|
32
38
|
import { listOmpExtensionRoots, type OmpExtensionRoot } from "./omp-extension-roots";
|
|
33
39
|
import { resolvePluginStdioPaths } from "./substitute-plugin-root";
|
|
34
40
|
|
|
@@ -257,6 +263,7 @@ const MCP_FILENAMES = [".mcp.json", "mcp.json"] as const;
|
|
|
257
263
|
interface RawMcpServer {
|
|
258
264
|
enabled?: boolean;
|
|
259
265
|
timeout?: number;
|
|
266
|
+
requestIdFormat?: unknown;
|
|
260
267
|
command?: string;
|
|
261
268
|
args?: string[];
|
|
262
269
|
env?: Record<string, string>;
|
|
@@ -305,10 +312,12 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
305
312
|
// Root relative command/cwd at the plugin's config directory, not the
|
|
306
313
|
// session cwd (MCP stdio spawning resolves relative values there).
|
|
307
314
|
const rooted = resolvePluginStdioPaths({ command: cfg.command, cwd: cfg.cwd }, root.path);
|
|
315
|
+
const requestIdFormat = parseRequestIdFormat(cfg.requestIdFormat);
|
|
308
316
|
items.push({
|
|
309
317
|
name: serverName,
|
|
310
318
|
...(cfg.enabled !== undefined && { enabled: cfg.enabled }),
|
|
311
319
|
...(cfg.timeout !== undefined && { timeout: cfg.timeout }),
|
|
320
|
+
...(requestIdFormat !== undefined && { requestIdFormat }),
|
|
312
321
|
...(rooted.command !== undefined && { command: rooted.command }),
|
|
313
322
|
...(cfg.args !== undefined && { args: cfg.args }),
|
|
314
323
|
...(cfg.env !== undefined && { env: cfg.env }),
|