@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.5
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 +89 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/parse.ts +156 -255
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +50 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +229 -17
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +64 -52
- package/src/internal-urls/artifact-protocol.ts +52 -51
- package/src/internal-urls/docs-index.generated.ts +34 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +50 -7
- package/src/internal-urls/mcp-protocol.ts +3 -8
- package/src/internal-urls/memory-protocol.ts +90 -59
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +40 -23
- package/src/internal-urls/rule-protocol.ts +3 -20
- package/src/internal-urls/skill-protocol.ts +5 -27
- package/src/internal-urls/types.ts +18 -2
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/project-prompt.md +10 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +159 -232
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -34
- package/src/prompts/tools/eval.md +27 -16
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +107 -37
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +3 -9
- package/src/task/executor.ts +23 -7
- package/src/task/index.ts +57 -36
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +30 -50
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +59 -44
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +74 -31
- package/src/tools/search.ts +16 -3
- package/src/tools/todo-write.ts +1 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -119
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal URL routing system for internal protocols like agent://, memory://,
|
|
2
|
+
* Internal URL routing system for internal protocols like agent://, memory://,
|
|
3
|
+
* skill://, mcp://, and local://.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { InternalUrlRouter, AgentProtocolHandler, MemoryProtocolHandler, SkillProtocolHandler } from './internal-urls';
|
|
10
|
-
*
|
|
11
|
-
* const router = new InternalUrlRouter();
|
|
12
|
-
* router.register(new AgentProtocolHandler({ getArtifactsDir: () => sessionDir }));
|
|
13
|
-
* router.register(new MemoryProtocolHandler({ getMemoryRoot: () => memoryRoot }));
|
|
14
|
-
* router.register(new SkillProtocolHandler({ getSkills: () => skills }));
|
|
15
|
-
*
|
|
16
|
-
* if (router.canHandle('agent://reviewer_0')) {
|
|
17
|
-
* const resource = await router.resolve('agent://reviewer_0');
|
|
18
|
-
* console.log(resource.content);
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
5
|
+
* One process-global `InternalUrlRouter` is shared across sessions. Handlers
|
|
6
|
+
* are stateless; they pull whatever they need (active skills/rules, active
|
|
7
|
+
* MCP/async managers, AgentRegistry-listed sessions) from the owning module
|
|
8
|
+
* on each resolve call.
|
|
21
9
|
*/
|
|
22
10
|
|
|
23
11
|
export * from "./agent-protocol";
|
|
24
12
|
export * from "./artifact-protocol";
|
|
25
|
-
export * from "./jobs-protocol";
|
|
26
13
|
export * from "./json-query";
|
|
27
14
|
export * from "./local-protocol";
|
|
28
15
|
export * from "./mcp-protocol";
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
5
6
|
import { parseInternalUrl } from "./parse";
|
|
6
7
|
import { validateRelativePath } from "./skill-protocol";
|
|
7
8
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
@@ -136,16 +137,60 @@ export function resolveLocalUrlToPath(input: string | InternalUrl, options: Loca
|
|
|
136
137
|
* Protocol handler for local:// URLs.
|
|
137
138
|
*
|
|
138
139
|
* URL forms:
|
|
139
|
-
* - local:// - Lists
|
|
140
|
-
* - local://<path> - Reads a file under session local root
|
|
140
|
+
* - local:// - Lists files at the session local root
|
|
141
|
+
* - local://<path> - Reads a file under the session local root
|
|
141
142
|
*/
|
|
142
143
|
export class LocalProtocolHandler implements ProtocolHandler {
|
|
143
144
|
readonly scheme = "local";
|
|
145
|
+
readonly immutable = false;
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
static #override: LocalProtocolOptions | undefined;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Install a process-global override that wins over the AgentRegistry-based
|
|
151
|
+
* derivation. Used by SDK consumers that wire `localProtocolOptions` on
|
|
152
|
+
* `createAgentSession` and by subagents that share their parent's root.
|
|
153
|
+
*/
|
|
154
|
+
static setOverride(value: LocalProtocolOptions | undefined): void {
|
|
155
|
+
LocalProtocolHandler.#override = value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Reset the process-global override. Test-only. */
|
|
159
|
+
static resetOverrideForTests(): void {
|
|
160
|
+
LocalProtocolHandler.#override = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns the active local-protocol options.
|
|
165
|
+
*
|
|
166
|
+
* Resolution order:
|
|
167
|
+
* 1. Explicit override installed via {@link setOverride} (used by subagents
|
|
168
|
+
* that share their parent's root and by SDK consumers with a custom
|
|
169
|
+
* artifacts/session id mapping).
|
|
170
|
+
* 2. The main session in `AgentRegistry.global()`. Its `SessionManager`
|
|
171
|
+
* supplies both `getArtifactsDir` and `getSessionId`.
|
|
172
|
+
*/
|
|
173
|
+
static resolveOptions(): LocalProtocolOptions | undefined {
|
|
174
|
+
const override = LocalProtocolHandler.#override;
|
|
175
|
+
if (override) return override;
|
|
176
|
+
const main = AgentRegistry.global()
|
|
177
|
+
.list()
|
|
178
|
+
.find(ref => ref.kind === "main");
|
|
179
|
+
const sessionManager = main?.session?.sessionManager;
|
|
180
|
+
if (!sessionManager) return undefined;
|
|
181
|
+
return {
|
|
182
|
+
getArtifactsDir: () => sessionManager.getArtifactsDir(),
|
|
183
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
146
186
|
|
|
147
187
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
148
|
-
const
|
|
188
|
+
const opts = LocalProtocolHandler.resolveOptions();
|
|
189
|
+
if (!opts) {
|
|
190
|
+
throw new Error("No session - local:// unavailable");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const localRoot = path.resolve(resolveLocalRoot(opts));
|
|
149
194
|
await fs.mkdir(localRoot, { recursive: true });
|
|
150
195
|
|
|
151
196
|
let resolvedRoot: string;
|
|
@@ -171,9 +216,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
171
216
|
const realParent = await fs.realpath(parentDir);
|
|
172
217
|
ensureWithinRoot(realParent, resolvedRoot);
|
|
173
218
|
} catch (error) {
|
|
174
|
-
if (!isEnoent(error))
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
219
|
+
if (!isEnoent(error)) throw error;
|
|
177
220
|
}
|
|
178
221
|
|
|
179
222
|
let realTargetPath: string;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MCPManager } from "../mcp/manager";
|
|
2
2
|
import type { MCPResourceReadResult } from "../mcp/types";
|
|
3
3
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
4
4
|
|
|
5
|
-
export interface McpProtocolOptions {
|
|
6
|
-
getMcpManager: () => MCPManager | undefined;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
function escapeRegex(text: string): string {
|
|
10
6
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
7
|
}
|
|
@@ -106,11 +102,10 @@ function formatAvailableResources(mcpManager: MCPManager): string {
|
|
|
106
102
|
*/
|
|
107
103
|
export class McpProtocolHandler implements ProtocolHandler {
|
|
108
104
|
readonly scheme = "mcp";
|
|
109
|
-
|
|
110
|
-
constructor(private readonly options: McpProtocolOptions) {}
|
|
105
|
+
readonly immutable = true;
|
|
111
106
|
|
|
112
107
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
113
|
-
const mcpManager =
|
|
108
|
+
const mcpManager = MCPManager.instance();
|
|
114
109
|
if (!mcpManager) {
|
|
115
110
|
throw new Error("No MCP manager available. MCP servers may not be configured.");
|
|
116
111
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { getMemoryRoot } from "../memories";
|
|
5
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
4
6
|
import { validateRelativePath } from "./skill-protocol";
|
|
5
7
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
6
8
|
|
|
@@ -8,13 +10,20 @@ const DEFAULT_MEMORY_FILE = "memory_summary.md";
|
|
|
8
10
|
const MEMORY_NAMESPACE = "root";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
13
|
+
* Snapshot of memory roots for every registered session, deduped.
|
|
14
|
+
* Each session has its own cwd (possibly a worktree), so subagents and main
|
|
15
|
+
* may see different roots.
|
|
12
16
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
function memoryRootsFromRegistry(): string[] {
|
|
18
|
+
const agentDir = getAgentDir();
|
|
19
|
+
const roots: string[] = [];
|
|
20
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
21
|
+
const sm = ref.session?.sessionManager;
|
|
22
|
+
if (!sm) continue;
|
|
23
|
+
const root = getMemoryRoot(agentDir, sm.getCwd());
|
|
24
|
+
if (root && !roots.includes(root)) roots.push(root);
|
|
25
|
+
}
|
|
26
|
+
return roots;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
function ensureWithinRoot(targetPath: string, rootPath: string): void {
|
|
@@ -61,73 +70,95 @@ export function resolveMemoryUrlToPath(url: InternalUrl, memoryRoot: string): st
|
|
|
61
70
|
return path.resolve(memoryRoot, relativePath);
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
async function tryResolveInRoot(url: InternalUrl, memoryRoot: string): Promise<InternalResource | undefined> {
|
|
74
|
+
const resolved = path.resolve(memoryRoot);
|
|
75
|
+
let resolvedRoot: string;
|
|
76
|
+
try {
|
|
77
|
+
resolvedRoot = await fs.realpath(resolved);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isEnoent(error)) return undefined;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const targetPath = resolveMemoryUrlToPath(url, resolvedRoot);
|
|
84
|
+
ensureWithinRoot(targetPath, resolvedRoot);
|
|
85
|
+
|
|
86
|
+
const parentDir = path.dirname(targetPath);
|
|
87
|
+
try {
|
|
88
|
+
const realParent = await fs.realpath(parentDir);
|
|
89
|
+
ensureWithinRoot(realParent, resolvedRoot);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!isEnoent(error)) throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let realTargetPath: string;
|
|
95
|
+
try {
|
|
96
|
+
realTargetPath = await fs.realpath(targetPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (isEnoent(error)) return undefined;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ensureWithinRoot(realTargetPath, resolvedRoot);
|
|
103
|
+
|
|
104
|
+
const stat = await fs.stat(realTargetPath);
|
|
105
|
+
if (!stat.isFile()) {
|
|
106
|
+
throw new Error(`memory:// URL must resolve to a file: ${url.href}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = await Bun.file(realTargetPath).text();
|
|
110
|
+
const ext = path.extname(realTargetPath).toLowerCase();
|
|
111
|
+
const contentType: InternalResource["contentType"] = ext === ".md" ? "text/markdown" : "text/plain";
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
url: url.href,
|
|
115
|
+
content,
|
|
116
|
+
contentType,
|
|
117
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
118
|
+
sourcePath: realTargetPath,
|
|
119
|
+
notes: [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
64
123
|
/**
|
|
65
124
|
* Protocol handler for memory:// URLs.
|
|
66
125
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
126
|
+
* Walks every active session's memory root. Worktree-based subagents have
|
|
127
|
+
* their own root; first one containing the file wins. Parent and subagent
|
|
128
|
+
* sharing a cwd see the same file regardless of order.
|
|
70
129
|
*/
|
|
71
130
|
export class MemoryProtocolHandler implements ProtocolHandler {
|
|
72
131
|
readonly scheme = "memory";
|
|
73
|
-
|
|
74
|
-
constructor(private readonly options: MemoryProtocolOptions) {}
|
|
132
|
+
readonly immutable = true;
|
|
75
133
|
|
|
76
134
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
77
|
-
const
|
|
78
|
-
let resolvedRoot: string;
|
|
79
|
-
try {
|
|
80
|
-
resolvedRoot = await fs.realpath(memoryRoot);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
if (isEnoent(error)) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const targetPath = resolveMemoryUrlToPath(url, resolvedRoot);
|
|
91
|
-
ensureWithinRoot(targetPath, resolvedRoot);
|
|
135
|
+
const roots = memoryRootsFromRegistry();
|
|
92
136
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (!isEnoent(error)) {
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
137
|
+
if (roots.length === 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
140
|
+
);
|
|
101
141
|
}
|
|
102
142
|
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
let anyExists = false;
|
|
144
|
+
for (const root of roots) {
|
|
145
|
+
try {
|
|
146
|
+
await fs.stat(root);
|
|
147
|
+
anyExists = true;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (isEnoent(error)) continue;
|
|
150
|
+
throw error;
|
|
109
151
|
}
|
|
110
|
-
|
|
152
|
+
const result = await tryResolveInRoot(url, root);
|
|
153
|
+
if (result) return result;
|
|
111
154
|
}
|
|
112
155
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw new Error(`memory:// URL must resolve to a file: ${url.href}`);
|
|
156
|
+
if (!anyExists) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
159
|
+
);
|
|
118
160
|
}
|
|
119
161
|
|
|
120
|
-
|
|
121
|
-
const ext = path.extname(realTargetPath).toLowerCase();
|
|
122
|
-
const contentType: InternalResource["contentType"] = ext === ".md" ? "text/markdown" : "text/plain";
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
url: url.href,
|
|
126
|
-
content,
|
|
127
|
-
contentType,
|
|
128
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
129
|
-
sourcePath: realTargetPath,
|
|
130
|
-
notes: [],
|
|
131
|
-
};
|
|
162
|
+
throw new Error(`Memory file not found: ${url.href}`);
|
|
132
163
|
}
|
|
133
164
|
}
|
|
@@ -18,6 +18,7 @@ import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
|
18
18
|
*/
|
|
19
19
|
export class PiProtocolHandler implements ProtocolHandler {
|
|
20
20
|
readonly scheme = "pi";
|
|
21
|
+
readonly immutable = true;
|
|
21
22
|
|
|
22
23
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
23
24
|
// Extract filename from host + path
|
|
@@ -1,42 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, pi://, local://).
|
|
3
|
+
*
|
|
4
|
+
* One process-global router with one handler per scheme. Access via
|
|
5
|
+
* `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
|
|
6
|
+
* shared state lives in `./state.ts`.
|
|
3
7
|
*/
|
|
8
|
+
import { AgentProtocolHandler } from "./agent-protocol";
|
|
9
|
+
import { ArtifactProtocolHandler } from "./artifact-protocol";
|
|
10
|
+
import { LocalProtocolHandler } from "./local-protocol";
|
|
11
|
+
import { McpProtocolHandler } from "./mcp-protocol";
|
|
12
|
+
import { MemoryProtocolHandler } from "./memory-protocol";
|
|
4
13
|
import { parseInternalUrl } from "./parse";
|
|
14
|
+
import { PiProtocolHandler } from "./pi-protocol";
|
|
15
|
+
import { RuleProtocolHandler } from "./rule-protocol";
|
|
16
|
+
import { SkillProtocolHandler } from "./skill-protocol";
|
|
5
17
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
6
18
|
|
|
7
|
-
/**
|
|
8
|
-
* Router for internal URL schemes.
|
|
9
|
-
*
|
|
10
|
-
* Dispatches URLs like `agent://output_id` or `memory://root/memory_summary.md` to
|
|
11
|
-
* registered protocol handlers.
|
|
12
|
-
*/
|
|
13
19
|
export class InternalUrlRouter {
|
|
20
|
+
static #instance: InternalUrlRouter | undefined;
|
|
21
|
+
|
|
14
22
|
#handlers = new Map<string, ProtocolHandler>();
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
constructor() {
|
|
25
|
+
this.register(new PiProtocolHandler());
|
|
26
|
+
this.register(new AgentProtocolHandler());
|
|
27
|
+
this.register(new ArtifactProtocolHandler());
|
|
28
|
+
this.register(new MemoryProtocolHandler());
|
|
29
|
+
this.register(new LocalProtocolHandler());
|
|
30
|
+
this.register(new SkillProtocolHandler());
|
|
31
|
+
this.register(new RuleProtocolHandler());
|
|
32
|
+
this.register(new McpProtocolHandler());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Process-global router instance. */
|
|
36
|
+
static instance(): InternalUrlRouter {
|
|
37
|
+
InternalUrlRouter.#instance ??= new InternalUrlRouter();
|
|
38
|
+
return InternalUrlRouter.#instance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Reset the global instance in tests. */
|
|
42
|
+
static resetForTests(): void {
|
|
43
|
+
InternalUrlRouter.#instance = undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
register(handler: ProtocolHandler): void {
|
|
21
|
-
this.#handlers.set(handler.scheme, handler);
|
|
47
|
+
this.#handlers.set(handler.scheme.toLowerCase(), handler);
|
|
22
48
|
}
|
|
23
49
|
|
|
24
|
-
/**
|
|
25
|
-
* Check if the router can handle a URL.
|
|
26
|
-
* @param input URL string to check
|
|
27
|
-
*/
|
|
28
50
|
canHandle(input: string): boolean {
|
|
29
51
|
const match = input.match(/^([a-z][a-z0-9+.-]*):\/\//i);
|
|
30
52
|
if (!match) return false;
|
|
31
|
-
|
|
32
|
-
return this.#handlers.has(scheme);
|
|
53
|
+
return this.#handlers.has(match[1].toLowerCase());
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
/**
|
|
36
|
-
* Resolve an internal URL to its content.
|
|
37
|
-
* @param input URL string (e.g., "agent://reviewer_0", "skill://notion-pages")
|
|
38
|
-
* @throws Error if scheme is not registered or resolution fails
|
|
39
|
-
*/
|
|
40
56
|
async resolve(input: string): Promise<InternalResource> {
|
|
41
57
|
const parsed = parseInternalUrl(input);
|
|
42
58
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
@@ -49,6 +65,7 @@ export class InternalUrlRouter {
|
|
|
49
65
|
throw new Error(`Unknown protocol: ${scheme}://\nSupported: ${available || "none"}`);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
const resource = await handler.resolve(parsed as InternalUrl);
|
|
69
|
+
return { ...resource, immutable: resource.immutable ?? handler.immutable };
|
|
53
70
|
}
|
|
54
71
|
}
|
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for rule:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves rule names to their content files.
|
|
5
|
-
*
|
|
6
4
|
* URL forms:
|
|
7
5
|
* - rule://<name> - Reads rule content
|
|
8
6
|
*/
|
|
9
|
-
import
|
|
7
|
+
import { getActiveRules } from "../capability/rule";
|
|
10
8
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
11
9
|
|
|
12
|
-
export interface RuleProtocolOptions {
|
|
13
|
-
/**
|
|
14
|
-
* Returns the currently loaded rules.
|
|
15
|
-
*/
|
|
16
|
-
getRules: () => readonly Rule[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Handler for rule:// URLs.
|
|
21
|
-
*
|
|
22
|
-
* Resolves rule names to their content.
|
|
23
|
-
*/
|
|
24
10
|
export class RuleProtocolHandler implements ProtocolHandler {
|
|
25
11
|
readonly scheme = "rule";
|
|
26
|
-
|
|
27
|
-
constructor(private readonly options: RuleProtocolOptions) {}
|
|
12
|
+
readonly immutable = true;
|
|
28
13
|
|
|
29
14
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
30
|
-
const rules =
|
|
15
|
+
const rules = getActiveRules();
|
|
31
16
|
|
|
32
|
-
// Extract rule name from host
|
|
33
17
|
const ruleName = url.rawHost || url.hostname;
|
|
34
18
|
if (!ruleName) {
|
|
35
19
|
throw new Error("rule:// URL requires a rule name: rule://<name>");
|
|
36
20
|
}
|
|
37
21
|
|
|
38
|
-
// Find the rule
|
|
39
22
|
const rule = rules.find(r => r.name === ruleName);
|
|
40
23
|
if (!rule) {
|
|
41
24
|
const available = rules.map(r => r.name);
|
|
@@ -8,19 +8,9 @@
|
|
|
8
8
|
* - skill://<name>/<path> - Reads relative path within skill's baseDir
|
|
9
9
|
*/
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
import
|
|
11
|
+
import { getActiveSkills } from "../extensibility/skills";
|
|
12
12
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
13
13
|
|
|
14
|
-
export interface SkillProtocolOptions {
|
|
15
|
-
/**
|
|
16
|
-
* Returns the currently loaded skills.
|
|
17
|
-
*/
|
|
18
|
-
getSkills: () => readonly Skill[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get content type based on file extension.
|
|
23
|
-
*/
|
|
24
14
|
function getContentType(filePath: string): InternalResource["contentType"] {
|
|
25
15
|
const ext = path.extname(filePath).toLowerCase();
|
|
26
16
|
if (ext === ".md") return "text/markdown";
|
|
@@ -43,24 +33,19 @@ export function validateRelativePath(relativePath: string): void {
|
|
|
43
33
|
|
|
44
34
|
/**
|
|
45
35
|
* Handler for skill:// URLs.
|
|
46
|
-
*
|
|
47
|
-
* Resolves skill names to their content files.
|
|
48
36
|
*/
|
|
49
37
|
export class SkillProtocolHandler implements ProtocolHandler {
|
|
50
38
|
readonly scheme = "skill";
|
|
51
|
-
|
|
52
|
-
constructor(private readonly options: SkillProtocolOptions) {}
|
|
39
|
+
readonly immutable = true;
|
|
53
40
|
|
|
54
41
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
55
|
-
const skills =
|
|
42
|
+
const skills = getActiveSkills();
|
|
56
43
|
|
|
57
|
-
// Extract skill name from host
|
|
58
44
|
const skillName = url.rawHost || url.hostname;
|
|
59
45
|
if (!skillName) {
|
|
60
46
|
throw new Error("skill:// URL requires a skill name: skill://<name>");
|
|
61
47
|
}
|
|
62
48
|
|
|
63
|
-
// Find the skill
|
|
64
49
|
const skill = skills.find(s => s.name === skillName);
|
|
65
50
|
if (!skill) {
|
|
66
51
|
const available = skills.map(s => s.name);
|
|
@@ -68,41 +53,34 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
68
53
|
throw new Error(`Unknown skill: ${skillName}\nAvailable: ${availableStr}`);
|
|
69
54
|
}
|
|
70
55
|
|
|
71
|
-
// Determine the file to read
|
|
72
56
|
let targetPath: string;
|
|
73
57
|
const urlPath = url.pathname;
|
|
74
58
|
const hasRelativePath = urlPath && urlPath !== "/" && urlPath !== "";
|
|
75
59
|
|
|
76
60
|
if (hasRelativePath) {
|
|
77
|
-
|
|
78
|
-
const relativePath = decodeURIComponent(urlPath.slice(1)); // Remove leading /
|
|
61
|
+
const relativePath = decodeURIComponent(urlPath.slice(1));
|
|
79
62
|
validateRelativePath(relativePath);
|
|
80
63
|
targetPath = path.join(skill.baseDir, relativePath);
|
|
81
64
|
|
|
82
|
-
// Verify the resolved path is still within baseDir
|
|
83
65
|
const resolvedPath = path.resolve(targetPath);
|
|
84
66
|
const resolvedBaseDir = path.resolve(skill.baseDir);
|
|
85
67
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
86
68
|
throw new Error("Path traversal is not allowed");
|
|
87
69
|
}
|
|
88
70
|
} else {
|
|
89
|
-
// Read SKILL.md
|
|
90
71
|
targetPath = skill.filePath;
|
|
91
72
|
}
|
|
92
73
|
|
|
93
|
-
// Read the file
|
|
94
74
|
const file = Bun.file(targetPath);
|
|
95
75
|
if (!(await file.exists())) {
|
|
96
76
|
throw new Error(`File not found: ${targetPath}`);
|
|
97
77
|
}
|
|
98
78
|
|
|
99
79
|
const content = await file.text();
|
|
100
|
-
const contentType = getContentType(targetPath);
|
|
101
|
-
|
|
102
80
|
return {
|
|
103
81
|
url: url.href,
|
|
104
82
|
content,
|
|
105
|
-
contentType,
|
|
83
|
+
contentType: getContentType(targetPath),
|
|
106
84
|
size: Buffer.byteLength(content, "utf-8"),
|
|
107
85
|
sourcePath: targetPath,
|
|
108
86
|
notes: [],
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Raw resource payload returned by protocol handlers. The `immutable` flag is
|
|
10
|
+
* applied by the router from {@link ProtocolHandler.immutable}, so handlers do
|
|
11
|
+
* not need to set it themselves.
|
|
10
12
|
*/
|
|
11
13
|
export interface InternalResource {
|
|
12
14
|
/** Canonical URL that was resolved */
|
|
@@ -21,6 +23,13 @@ export interface InternalResource {
|
|
|
21
23
|
sourcePath?: string;
|
|
22
24
|
/** Additional notes about resolution */
|
|
23
25
|
notes?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* True when the resolved content cannot be edited by the agent (e.g. sealed
|
|
28
|
+
* artifacts, harness docs, machine-generated memory summaries). Hashline
|
|
29
|
+
* anchors and similar edit affordances are suppressed for immutable
|
|
30
|
+
* resources. Mutable resources (e.g. local://) behave like editable files.
|
|
31
|
+
*/
|
|
32
|
+
immutable?: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -44,7 +53,14 @@ export interface ProtocolHandler {
|
|
|
44
53
|
/** The scheme this handler processes (without trailing ://) */
|
|
45
54
|
readonly scheme: string;
|
|
46
55
|
/**
|
|
47
|
-
*
|
|
56
|
+
* Whether resources produced by this handler are immutable (cannot be
|
|
57
|
+
* edited by the agent). When true, callers suppress hashline anchors and
|
|
58
|
+
* other edit affordances. When false, resources behave like editable files.
|
|
59
|
+
*/
|
|
60
|
+
readonly immutable: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an internal URL to its content. The router stamps the
|
|
63
|
+
* {@link InternalResource.immutable} flag from {@link ProtocolHandler.immutable}.
|
|
48
64
|
* @param url Parsed URL object
|
|
49
65
|
* @throws Error with user-friendly message if resolution fails
|
|
50
66
|
*/
|
package/src/main.ts
CHANGED
|
@@ -765,7 +765,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
765
765
|
await mgr.upgradeAllPlugins();
|
|
766
766
|
logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
|
|
767
767
|
} else {
|
|
768
|
-
logger.debug(`${updates.length} marketplace plugin update(s) available
|
|
768
|
+
logger.debug(`${updates.length} marketplace plugin update(s) available — /marketplace upgrade`);
|
|
769
769
|
}
|
|
770
770
|
} catch {
|
|
771
771
|
// Silently ignore — network failure, corrupt data, offline.
|
package/src/mcp/manager.ts
CHANGED
|
@@ -132,6 +132,23 @@ export interface MCPDiscoverOptions {
|
|
|
132
132
|
* Manages connections to MCP servers and provides tools to the agent.
|
|
133
133
|
*/
|
|
134
134
|
export class MCPManager {
|
|
135
|
+
static #instance: MCPManager | undefined;
|
|
136
|
+
|
|
137
|
+
/** Process-global instance shared by internal URL protocol handlers and tools. */
|
|
138
|
+
static instance(): MCPManager | undefined {
|
|
139
|
+
return MCPManager.#instance;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Install or clear the process-global instance. */
|
|
143
|
+
static setInstance(value: MCPManager | undefined): void {
|
|
144
|
+
MCPManager.#instance = value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reset the process-global instance. Test-only. */
|
|
148
|
+
static resetForTests(): void {
|
|
149
|
+
MCPManager.#instance = undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
135
152
|
#connections = new Map<string, MCPServerConnection>();
|
|
136
153
|
#tools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
137
154
|
#pendingConnections = new Map<string, Promise<MCPServerConnection>>();
|
|
@@ -192,7 +192,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
192
192
|
const statsLine = this.#buildStatsLine(session);
|
|
193
193
|
if (statsLine) this.#viewerFooterLines.push(statsLine);
|
|
194
194
|
this.#viewerFooterLines.push(
|
|
195
|
-
theme.fg("dim", "j/k:scroll Enter:expand [/]
|
|
195
|
+
theme.fg("dim", "j/k:scroll Enter:expand [/]/←→:cycle agents Esc/Ctrl+S:close g/G:top/bottom"),
|
|
196
196
|
);
|
|
197
197
|
|
|
198
198
|
// Auto-scroll to bottom if we were at bottom
|
|
@@ -452,7 +452,7 @@ export class SessionObserverOverlayComponent extends Container {
|
|
|
452
452
|
|
|
453
453
|
// Tool call header
|
|
454
454
|
const intentStr = call.intent ? theme.fg("dim", ` ${sanitizeLine(call.intent, TRUNCATE_LENGTHS.SHORT)}`) : "";
|
|
455
|
-
lines.push(`${cursor} ${theme.fg("accent", "
|
|
455
|
+
lines.push(`${cursor} ${theme.fg("accent", "▸")} ${theme.bold(theme.fg("muted", call.name))}${intentStr}`);
|
|
456
456
|
|
|
457
457
|
// Key arguments
|
|
458
458
|
const argSummary = this.#formatToolArgs(call.name, call.arguments);
|
|
@@ -665,6 +665,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
665
665
|
context.perFileDiffPreview = previews;
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
|
+
if (!previews?.some(preview => preview.diff)) {
|
|
669
|
+
const editMode = this.#editMode;
|
|
670
|
+
const strategy = editMode ? EDIT_MODE_STRATEGIES[editMode] : undefined;
|
|
671
|
+
const fallback = strategy?.renderStreamingFallback(this.#args, theme);
|
|
672
|
+
if (fallback) context.editStreamingFallback = fallback;
|
|
673
|
+
}
|
|
668
674
|
context.renderDiff = renderDiff;
|
|
669
675
|
}
|
|
670
676
|
|