@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
|
@@ -509,6 +509,26 @@ async function discoverExtensionsInDir(dir: string): Promise<string[]> {
|
|
|
509
509
|
|
|
510
510
|
return discovered;
|
|
511
511
|
}
|
|
512
|
+
async function discoverHooksInPackageRoot(root: string): Promise<string[]> {
|
|
513
|
+
const hooks: string[] = [];
|
|
514
|
+
for (const hookType of ["pre", "post"]) {
|
|
515
|
+
const hookDir = path.join(root, "hooks", hookType);
|
|
516
|
+
let entries: fs1.Dirent[];
|
|
517
|
+
try {
|
|
518
|
+
entries = await fs.readdir(hookDir, { withFileTypes: true });
|
|
519
|
+
} catch (err) {
|
|
520
|
+
if (isEnoent(err) || isEacces(err) || hasFsCode(err, "ENOTDIR") || hasFsCode(err, "EPERM")) continue;
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
for (const entry of entries) {
|
|
524
|
+
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
|
|
525
|
+
hooks.push(path.join(hookDir, entry.name));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return hooks;
|
|
530
|
+
}
|
|
531
|
+
|
|
512
532
|
/**
|
|
513
533
|
* Discover absolute paths of extensions to load, without importing or
|
|
514
534
|
* binding factories. Hot path on session startup — the scan walks native
|
|
@@ -522,10 +542,16 @@ async function discoverExtensionsInDir(dir: string): Promise<string[]> {
|
|
|
522
542
|
* `LoadExtensionsResult` directly would reuse handlers/tools/commands that
|
|
523
543
|
* closed over the parent's `cwd` and event bus.
|
|
524
544
|
*/
|
|
545
|
+
export interface DiscoverExtensionPathOptions {
|
|
546
|
+
/** Include ambient native extensions, hooks, and installed plugins. */
|
|
547
|
+
ambient?: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
525
550
|
export async function discoverExtensionPaths(
|
|
526
551
|
configuredPaths: string[],
|
|
527
552
|
cwd: string,
|
|
528
553
|
disabledExtensionIds?: string[],
|
|
554
|
+
options: DiscoverExtensionPathOptions = {},
|
|
529
555
|
): Promise<string[]> {
|
|
530
556
|
const allPaths: string[] = [];
|
|
531
557
|
const seen = new Set<string>();
|
|
@@ -549,33 +575,44 @@ export async function discoverExtensionPaths(
|
|
|
549
575
|
}
|
|
550
576
|
};
|
|
551
577
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
578
|
+
const ambient = options.ambient !== false;
|
|
579
|
+
if (ambient) {
|
|
580
|
+
// 1. Discover extension modules via capability API (native .omp/.pi only).
|
|
581
|
+
// Scope the load to the native provider — the extension-module capability
|
|
582
|
+
// also has claude/codex/gemini/opencode providers, and their items were
|
|
583
|
+
// discarded here anyway (see #4198). The provider filter skips the walk
|
|
584
|
+
// entirely instead of running four foreign directory scans and dropping
|
|
585
|
+
// the results.
|
|
586
|
+
const discovered = await loadCapability<ExtensionModule>(extensionModuleCapability.id, {
|
|
587
|
+
...loadOptions,
|
|
588
|
+
providers: ["native"],
|
|
589
|
+
});
|
|
590
|
+
for (const ext of discovered.items) {
|
|
591
|
+
addPath(ext.path);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 2. Discover JS/TS hook factories and bind them through the extension
|
|
596
|
+
// runner, which owns the current runtime event bus. Non-ambient discovery
|
|
597
|
+
// scans only this invocation's configured package roots; it must not consult
|
|
598
|
+
// settings, installed packages, or process-global CLI injection state.
|
|
599
|
+
if (ambient) {
|
|
600
|
+
const hooks = await loadCapability<Hook>(hookCapability.id, loadOptions);
|
|
601
|
+
for (const hookPath of hooks.items
|
|
602
|
+
.map(hook => hook.path)
|
|
603
|
+
.filter(hookPath => isExtensionFile(path.basename(hookPath)))) {
|
|
604
|
+
addPath(hookPath);
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
for (const configuredPath of configuredPaths) {
|
|
608
|
+
addPaths(await discoverHooksInPackageRoot(resolvePath(configuredPath, cwd)));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 3. Discover extension entry points from installed plugins.
|
|
613
|
+
if (ambient) {
|
|
614
|
+
addPaths(await getAllPluginExtensionPaths(cwd));
|
|
615
|
+
}
|
|
579
616
|
|
|
580
617
|
// 4. Explicitly configured paths
|
|
581
618
|
for (const configuredPath of configuredPaths) {
|
|
@@ -618,7 +655,8 @@ export async function discoverAndLoadExtensions(
|
|
|
618
655
|
cwd: string,
|
|
619
656
|
eventBus?: EventBus,
|
|
620
657
|
disabledExtensionIds?: string[],
|
|
658
|
+
options: DiscoverExtensionPathOptions = {},
|
|
621
659
|
): Promise<LoadExtensionsResult> {
|
|
622
|
-
const paths = await discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
|
|
660
|
+
const paths = await discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds, options);
|
|
623
661
|
return loadExtensions(paths, cwd, eventBus);
|
|
624
662
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Registry read/write operations for the marketplace plugin system.
|
|
3
3
|
*
|
|
4
4
|
* Two registries:
|
|
5
|
-
* - marketplaces.json
|
|
5
|
+
* - marketplaces.json at getMarketplacesRegistryPath() — which catalogs the user has added
|
|
6
6
|
* - installed_plugins.json under getPluginsDir() — which plugins are installed
|
|
7
7
|
*
|
|
8
8
|
* Read/write functions accept explicit file paths so callers control the
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import * as fs from "node:fs/promises";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getPluginsDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
19
|
+
|
|
20
|
+
export { getMarketplacesRegistryPath } from "@oh-my-pi/pi-utils";
|
|
19
21
|
|
|
20
22
|
import type {
|
|
21
23
|
InstalledPluginEntry,
|
|
@@ -24,12 +26,6 @@ import type {
|
|
|
24
26
|
MarketplacesRegistry,
|
|
25
27
|
} from "./types";
|
|
26
28
|
|
|
27
|
-
// ── Path helpers ─────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function getMarketplacesRegistryPath(): string {
|
|
30
|
-
return path.join(getConfigRootDir(), "marketplaces.json");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
export function getInstalledPluginsRegistryPath(): string {
|
|
34
30
|
return path.join(getPluginsDir(), "installed_plugins.json");
|
|
35
31
|
}
|
package/src/launch/broker.ts
CHANGED
|
@@ -342,6 +342,15 @@ class DaemonBroker {
|
|
|
342
342
|
readonly #token: string;
|
|
343
343
|
readonly #idleGraceMs: number;
|
|
344
344
|
readonly #records = new Map<string, ManagedDaemon>();
|
|
345
|
+
/**
|
|
346
|
+
* Names reserved by an in-flight `start` before its record lands in
|
|
347
|
+
* `#records`. Requests dispatch concurrently, and `#start` awaits (cwd stat,
|
|
348
|
+
* log open) between the duplicate check and the record insert; without a
|
|
349
|
+
* synchronous reservation two clients can both pass the check and spawn
|
|
350
|
+
* duplicate processes — one exits on a held resource (e.g. a Chromium
|
|
351
|
+
* profile lock) or keeps running untracked.
|
|
352
|
+
*/
|
|
353
|
+
readonly #startingNames = new Set<string>();
|
|
345
354
|
readonly #clients = new Set<net.Socket>();
|
|
346
355
|
readonly #finished = Promise.withResolvers<void>();
|
|
347
356
|
readonly #sockets = new Set<net.Socket>();
|
|
@@ -500,50 +509,59 @@ class DaemonBroker {
|
|
|
500
509
|
) {
|
|
501
510
|
throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
|
|
502
511
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (existing && !terminalState(existing.snapshot.state)) {
|
|
506
|
-
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
512
|
+
if (this.#startingNames.has(spec.name)) {
|
|
513
|
+
throw new Error(`Daemon ${spec.name} is already starting`);
|
|
507
514
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
515
|
+
this.#startingNames.add(spec.name);
|
|
516
|
+
let record: ManagedDaemon;
|
|
517
|
+
try {
|
|
518
|
+
const existing = this.#records.get(spec.name);
|
|
519
|
+
if (existing) await this.#refreshDetached(existing);
|
|
520
|
+
if (existing && !terminalState(existing.snapshot.state)) {
|
|
521
|
+
throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
|
|
522
|
+
}
|
|
523
|
+
if (spec.ready?.log) {
|
|
524
|
+
try {
|
|
525
|
+
new RegExp(spec.ready.log, "u");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
}
|
|
513
529
|
}
|
|
530
|
+
const stat = await fs.stat(spec.cwd);
|
|
531
|
+
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
532
|
+
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
record = {
|
|
535
|
+
spec,
|
|
536
|
+
snapshot: {
|
|
537
|
+
name: spec.name,
|
|
538
|
+
id: crypto.randomUUID(),
|
|
539
|
+
state: "starting",
|
|
540
|
+
createdAt: now,
|
|
541
|
+
startedAt: now,
|
|
542
|
+
restartCount: 0,
|
|
543
|
+
outputBytes: 0,
|
|
544
|
+
owner,
|
|
545
|
+
persist: spec.persist,
|
|
546
|
+
detached: spec.detached,
|
|
547
|
+
},
|
|
548
|
+
dir,
|
|
549
|
+
log: await DaemonLog.open(dir),
|
|
550
|
+
generation: 0,
|
|
551
|
+
stopRequested: false,
|
|
552
|
+
logReady: !spec.ready?.log,
|
|
553
|
+
portReady: spec.ready?.port === undefined,
|
|
554
|
+
readinessBuffer: "",
|
|
555
|
+
outputOffset: 0,
|
|
556
|
+
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
557
|
+
consecutiveFailures: 0,
|
|
558
|
+
persistQueue: Promise.resolve(),
|
|
559
|
+
};
|
|
560
|
+
syncReadyPending(record);
|
|
561
|
+
this.#records.set(spec.name, record);
|
|
562
|
+
} finally {
|
|
563
|
+
this.#startingNames.delete(spec.name);
|
|
514
564
|
}
|
|
515
|
-
const stat = await fs.stat(spec.cwd);
|
|
516
|
-
if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
|
|
517
|
-
const dir = path.join(this.#runtimeDir, "daemons", spec.name);
|
|
518
|
-
const now = Date.now();
|
|
519
|
-
const record: ManagedDaemon = {
|
|
520
|
-
spec,
|
|
521
|
-
snapshot: {
|
|
522
|
-
name: spec.name,
|
|
523
|
-
id: crypto.randomUUID(),
|
|
524
|
-
state: "starting",
|
|
525
|
-
createdAt: now,
|
|
526
|
-
startedAt: now,
|
|
527
|
-
restartCount: 0,
|
|
528
|
-
outputBytes: 0,
|
|
529
|
-
owner,
|
|
530
|
-
persist: spec.persist,
|
|
531
|
-
detached: spec.detached,
|
|
532
|
-
},
|
|
533
|
-
dir,
|
|
534
|
-
log: await DaemonLog.open(dir),
|
|
535
|
-
generation: 0,
|
|
536
|
-
stopRequested: false,
|
|
537
|
-
logReady: !spec.ready?.log,
|
|
538
|
-
portReady: spec.ready?.port === undefined,
|
|
539
|
-
readinessBuffer: "",
|
|
540
|
-
outputOffset: 0,
|
|
541
|
-
readyPattern: spec.ready?.log ? new RegExp(spec.ready.log, "u") : undefined,
|
|
542
|
-
consecutiveFailures: 0,
|
|
543
|
-
persistQueue: Promise.resolve(),
|
|
544
|
-
};
|
|
545
|
-
syncReadyPending(record);
|
|
546
|
-
this.#records.set(spec.name, record);
|
|
547
565
|
await this.#launch(record);
|
|
548
566
|
let readyTimedOut = false;
|
|
549
567
|
if (spec.ready && !terminalState(record.snapshot.state)) {
|
package/src/launch/paths.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { getDaemonRuntimeDir } from "@oh-my-pi/pi-utils";
|
|
3
3
|
|
|
4
4
|
/** Resolve the private runtime directory shared by omp processes in one project directory. */
|
|
5
|
-
export
|
|
6
|
-
const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0");
|
|
7
|
-
return path.join(configRoot, "run", "daemons", key);
|
|
8
|
-
}
|
|
5
|
+
export { getDaemonRuntimeDir as daemonRuntimeDir };
|
|
9
6
|
|
|
10
7
|
/** Resolve the Unix socket or Windows named pipe used by one project broker. */
|
|
11
8
|
export function daemonBrokerEndpoint(projectDir: string, runtimeDir: string): string {
|
package/src/main.ts
CHANGED
|
@@ -1110,14 +1110,13 @@ export async function buildSessionOptions(
|
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
1112
|
// Additional extension paths from CLI
|
|
1113
|
-
const cliExtensionPaths =
|
|
1113
|
+
const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
1114
1114
|
if (cliExtensionPaths.length > 0) {
|
|
1115
1115
|
options.additionalExtensionPaths = cliExtensionPaths;
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
1118
|
if (parsed.noExtensions) {
|
|
1119
1119
|
options.disableExtensionDiscovery = true;
|
|
1120
|
-
options.additionalExtensionPaths = [];
|
|
1121
1120
|
}
|
|
1122
1121
|
|
|
1123
1122
|
return options;
|
|
@@ -1151,10 +1150,6 @@ export async function runRootCommand(
|
|
|
1151
1150
|
|
|
1152
1151
|
const notifs: (InteractiveModeNotify | null)[] = [];
|
|
1153
1152
|
|
|
1154
|
-
// Create AuthStorage and ModelRegistry upfront
|
|
1155
|
-
const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
|
|
1156
|
-
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
1157
|
-
|
|
1158
1153
|
if (parsedArgs.version) {
|
|
1159
1154
|
writeStartupNotice(parsedArgs, `${VERSION}\n`);
|
|
1160
1155
|
process.exit(0);
|
|
@@ -1197,15 +1192,31 @@ export async function runRootCommand(
|
|
|
1197
1192
|
// Register CLI-provided extension package paths (`--extension`, `--hook`) so
|
|
1198
1193
|
// the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
|
|
1199
1194
|
// `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1195
|
+
// Explicit roots remain authorized under `--no-extensions`; only ambient
|
|
1196
|
+
// extension discovery is disabled.
|
|
1197
|
+
const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
|
|
1198
|
+
injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
|
|
1199
|
+
mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
|
|
1200
|
+
replace: true,
|
|
1201
|
+
});
|
|
1207
1202
|
|
|
1208
1203
|
let cwd = getProjectDir();
|
|
1204
|
+
// Classify the host before opening auth or settings storage so every
|
|
1205
|
+
// session-critical database connection picks the right busy timeout.
|
|
1206
|
+
// See getDbBusyTimeoutMs().
|
|
1207
|
+
const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
|
|
1208
|
+
// Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
|
|
1209
|
+
const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
|
|
1210
|
+
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
1211
|
+
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
1212
|
+
// Only the interactive host renders a focusable Agent Hub / subagent session
|
|
1213
|
+
// tree; declare it so headless subagent optimizations (e.g. skipping replan
|
|
1214
|
+
// title refresh) can tell a focusable process from a print/RPC/eval one.
|
|
1215
|
+
setInteractiveHost(isInteractive);
|
|
1216
|
+
// Create AuthStorage and ModelRegistry upfront
|
|
1217
|
+
const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
|
|
1218
|
+
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
1219
|
+
|
|
1209
1220
|
const settingsInstance =
|
|
1210
1221
|
deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd, configFiles: parsedArgs.config }));
|
|
1211
1222
|
if (parsedArgs.approvalMode) {
|
|
@@ -1228,15 +1239,6 @@ export async function runRootCommand(
|
|
|
1228
1239
|
if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
|
|
1229
1240
|
Bun.env.PI_NO_TITLE = "1";
|
|
1230
1241
|
}
|
|
1231
|
-
const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
|
|
1232
|
-
// Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
|
|
1233
|
-
const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
|
|
1234
|
-
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
1235
|
-
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
1236
|
-
// Only the interactive host renders a focusable Agent Hub / subagent session
|
|
1237
|
-
// tree; declare it so headless subagent optimizations (e.g. skipping replan
|
|
1238
|
-
// title refresh) can tell a focusable process from a print/RPC/eval one.
|
|
1239
|
-
setInteractiveHost(isInteractive);
|
|
1240
1242
|
|
|
1241
1243
|
// Initialize discovery system with settings for provider persistence
|
|
1242
1244
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
package/src/mcp/config.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import type { MCPRequestIdFormat } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-transport allocator for outgoing JSON-RPC request ids.
|
|
6
|
+
*
|
|
7
|
+
* JSON-RPC 2.0 permits String and Number ids equally, but the wider MCP
|
|
8
|
+
* ecosystem (reference SDKs included) issues incrementing integers, so servers
|
|
9
|
+
* are best tested against numbers — Apple's `xcrun mcpbridge` even logs
|
|
10
|
+
* `mcpbridge.DecodeError Code=1` and never answers a string id. Integers are
|
|
11
|
+
* therefore the default. `"string"` opts a server back into collision-resistant
|
|
12
|
+
* snowflake ids.
|
|
13
|
+
*
|
|
14
|
+
* The counter is per instance and never reset, so ids stay unique for the life
|
|
15
|
+
* of the transport even though a reconnect rejects and clears every pending
|
|
16
|
+
* request.
|
|
17
|
+
*/
|
|
18
|
+
export class RequestIdAllocator {
|
|
19
|
+
#previousNumeric = 0;
|
|
20
|
+
|
|
21
|
+
next(format: MCPRequestIdFormat | undefined): string | number {
|
|
22
|
+
return format === "string" ? Snowflake.next() : ++this.#previousNumeric;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
7
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
8
|
-
import { logger, readSseJson
|
|
8
|
+
import { logger, readSseJson } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type {
|
|
10
10
|
JsonRpcError,
|
|
11
11
|
JsonRpcMessage,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
MCPTransport,
|
|
18
18
|
} from "../../mcp/types";
|
|
19
19
|
import { toJsonRpcError } from "../../mcp/types";
|
|
20
|
+
import { RequestIdAllocator } from "../request-id";
|
|
20
21
|
import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
21
22
|
|
|
22
23
|
const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
|
|
@@ -42,6 +43,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
42
43
|
#connected = false;
|
|
43
44
|
#sessionId: string | null = null;
|
|
44
45
|
#sseConnection: AbortController | null = null;
|
|
46
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
45
47
|
|
|
46
48
|
onClose?: () => void;
|
|
47
49
|
onError?: (error: Error) => void;
|
|
@@ -209,7 +211,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
209
211
|
throw new Error("Transport not connected");
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
const id =
|
|
214
|
+
const id = this.#requestIds.next(this.config.requestIdFormat);
|
|
213
215
|
const body = {
|
|
214
216
|
jsonrpc: "2.0" as const,
|
|
215
217
|
id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
2
|
-
import { logger, readSseEvents
|
|
2
|
+
import { logger, readSseEvents } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type {
|
|
4
4
|
JsonRpcError,
|
|
5
5
|
JsonRpcMessage,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
MCPTransport,
|
|
11
11
|
} from "../../mcp/types";
|
|
12
12
|
import { toJsonRpcError } from "../../mcp/types";
|
|
13
|
+
import { RequestIdAllocator } from "../request-id";
|
|
13
14
|
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
14
15
|
|
|
15
16
|
interface MCPTimeoutOperation {
|
|
@@ -32,6 +33,7 @@ export class LegacySseTransport implements MCPTransport {
|
|
|
32
33
|
#sseConnection: AbortController | null = null;
|
|
33
34
|
#pending = new Map<string | number, PendingLegacySseRequest>();
|
|
34
35
|
#config: MCPSseServerConfig;
|
|
36
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
35
37
|
|
|
36
38
|
onClose?: () => void;
|
|
37
39
|
onError?: (error: Error) => void;
|
|
@@ -194,7 +196,7 @@ export class LegacySseTransport implements MCPTransport {
|
|
|
194
196
|
throw new Error("Transport not connected");
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
const id =
|
|
199
|
+
const id = this.#requestIds.next(this.#config.requestIdFormat);
|
|
198
200
|
const body = {
|
|
199
201
|
jsonrpc: "2.0" as const,
|
|
200
202
|
id,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import { getProjectDir, readJsonl
|
|
10
|
+
import { getProjectDir, readJsonl } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import type { Subprocess } from "bun";
|
|
12
12
|
import { hostHasInheritableConsole } from "../../eval/py/spawn-options";
|
|
13
13
|
import type {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
MCPTransport,
|
|
21
21
|
} from "../../mcp/types";
|
|
22
22
|
import { toJsonRpcError } from "../../mcp/types";
|
|
23
|
+
import { RequestIdAllocator } from "../request-id";
|
|
23
24
|
import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
24
25
|
|
|
25
26
|
/** Subprocess argv and platform-derived spawn flags for an MCP stdio server. */
|
|
@@ -551,6 +552,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
551
552
|
* actually spawned into its own session may target it.
|
|
552
553
|
*/
|
|
553
554
|
#detached = false;
|
|
555
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
554
556
|
|
|
555
557
|
onClose?: () => void;
|
|
556
558
|
onError?: (error: Error) => void;
|
|
@@ -734,7 +736,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
734
736
|
throw new Error("Transport not connected");
|
|
735
737
|
}
|
|
736
738
|
|
|
737
|
-
const id =
|
|
739
|
+
const id = this.#requestIds.next(this.config.requestIdFormat);
|
|
738
740
|
const request = {
|
|
739
741
|
jsonrpc: "2.0" as const,
|
|
740
742
|
id,
|
package/src/mcp/types.ts
CHANGED
|
@@ -59,12 +59,27 @@ export interface MCPAuthConfig {
|
|
|
59
59
|
resource?: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Encoding used for outgoing JSON-RPC request ids. */
|
|
63
|
+
export type MCPRequestIdFormat = "string" | "number";
|
|
64
|
+
|
|
62
65
|
/** Base server config with shared options */
|
|
63
66
|
interface MCPServerConfigBase {
|
|
64
67
|
/** Whether this server is enabled (default: true) */
|
|
65
68
|
enabled?: boolean;
|
|
66
69
|
/** MCP request timeout in milliseconds (default: 30000, 0 to disable) */
|
|
67
70
|
timeout?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Encoding for outgoing JSON-RPC request ids (default: `"number"`).
|
|
73
|
+
*
|
|
74
|
+
* Set `"string"` for servers that need collision-resistant snowflake string
|
|
75
|
+
* ids instead of per-transport integers. See `RequestIdAllocator` in
|
|
76
|
+
* `./request-id`.
|
|
77
|
+
*
|
|
78
|
+
* OMP-specific, so only the OMP-owned discovery providers parse it (native,
|
|
79
|
+
* standalone `mcp.json`, OMP plugins). Providers that translate another
|
|
80
|
+
* tool's config do not, since the key is not part of those formats.
|
|
81
|
+
*/
|
|
82
|
+
requestIdFormat?: MCPRequestIdFormat;
|
|
68
83
|
/** Authentication configuration (optional) */
|
|
69
84
|
auth?: MCPAuthConfig;
|
|
70
85
|
/** OAuth configuration for servers requiring explicit client credentials */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MemoryBackendId } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fallback text for `/memory stats` and `/memory diagnose` when the active
|
|
5
|
+
* backend's hook is missing or returns nothing.
|
|
6
|
+
*
|
|
7
|
+
* The `off` backend isn't a real backend a user picked among several
|
|
8
|
+
* stats-capable options — it's the no-op state memory falls back to by
|
|
9
|
+
* default — so it gets its own message instead of the generic
|
|
10
|
+
* "not available for the X backend" template, which would otherwise read as
|
|
11
|
+
* the self-contradictory "not available for the off backend".
|
|
12
|
+
*
|
|
13
|
+
* Shared by both the TUI (`CommandController.handleMemoryCommand`) and the
|
|
14
|
+
* ACP/RPC slash-command handler so the two surfaces stay consistent.
|
|
15
|
+
*/
|
|
16
|
+
export function memoryStatsUnavailableMessage(backendId: MemoryBackendId, action: "stats" | "diagnose"): string {
|
|
17
|
+
if (backendId === "off") return "Memory backend is off — there is nothing to show.";
|
|
18
|
+
return `Memory ${action} is not available for the ${backendId} backend.`;
|
|
19
|
+
}
|
|
@@ -636,7 +636,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
636
636
|
if (ref.status === "running" && ref.session) {
|
|
637
637
|
await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
638
638
|
}
|
|
639
|
-
await this.#lifecycle().release(ref.id, ref);
|
|
639
|
+
await this.#lifecycle().release(ref.id, ref, { tombstone: true });
|
|
640
640
|
} catch (error) {
|
|
641
641
|
logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
|
|
642
642
|
this.#notice = error instanceof Error ? error.message : String(error);
|