@oh-my-pi/pi-coding-agent 14.9.3 → 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 +44 -0
- package/package.json +7 -7
- 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/settings-schema.ts +1 -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/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- 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/export/html/template.css +12 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +20 -2
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/internal-urls/agent-protocol.ts +63 -52
- package/src/internal-urls/artifact-protocol.ts +51 -51
- package/src/internal-urls/docs-index.generated.ts +33 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +49 -7
- package/src/internal-urls/mcp-protocol.ts +2 -8
- package/src/internal-urls/memory-protocol.ts +89 -59
- package/src/internal-urls/router.ts +38 -22
- package/src/internal-urls/rule-protocol.ts +2 -20
- package/src/internal-urls/skill-protocol.ts +4 -27
- 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/project-prompt.md +10 -2
- package/src/prompts/system/subagent-system-prompt.md +8 -8
- package/src/prompts/system/system-prompt.md +13 -7
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -10
- package/src/prompts/tools/eval.md +1 -3
- 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 +58 -20
- 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 +0 -5
- package/src/task/executor.ts +14 -2
- package/src/task/index.ts +19 -5
- 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 +15 -9
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +48 -10
- 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/read.ts +5 -4
- package/src/tools/search.ts +3 -2
- package/src/tools/todo-write.ts +1 -1
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -120
- package/src/prompts/system/now-prompt.md +0 -7
|
@@ -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,17 +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";
|
|
144
145
|
readonly immutable = false;
|
|
145
146
|
|
|
146
|
-
|
|
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
|
+
}
|
|
147
186
|
|
|
148
187
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
149
|
-
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));
|
|
150
194
|
await fs.mkdir(localRoot, { recursive: true });
|
|
151
195
|
|
|
152
196
|
let resolvedRoot: string;
|
|
@@ -172,9 +216,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
172
216
|
const realParent = await fs.realpath(parentDir);
|
|
173
217
|
ensureWithinRoot(realParent, resolvedRoot);
|
|
174
218
|
} catch (error) {
|
|
175
|
-
if (!isEnoent(error))
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
219
|
+
if (!isEnoent(error)) throw error;
|
|
178
220
|
}
|
|
179
221
|
|
|
180
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
|
}
|
|
@@ -108,10 +104,8 @@ export class McpProtocolHandler implements ProtocolHandler {
|
|
|
108
104
|
readonly scheme = "mcp";
|
|
109
105
|
readonly immutable = true;
|
|
110
106
|
|
|
111
|
-
constructor(private readonly options: McpProtocolOptions) {}
|
|
112
|
-
|
|
113
107
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
114
|
-
const mcpManager =
|
|
108
|
+
const mcpManager = MCPManager.instance();
|
|
115
109
|
if (!mcpManager) {
|
|
116
110
|
throw new Error("No MCP manager available. MCP servers may not be configured.");
|
|
117
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,74 +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
132
|
readonly immutable = true;
|
|
74
133
|
|
|
75
|
-
constructor(private readonly options: MemoryProtocolOptions) {}
|
|
76
|
-
|
|
77
134
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
78
|
-
const
|
|
79
|
-
let resolvedRoot: string;
|
|
80
|
-
try {
|
|
81
|
-
resolvedRoot = await fs.realpath(memoryRoot);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (isEnoent(error)) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const targetPath = resolveMemoryUrlToPath(url, resolvedRoot);
|
|
92
|
-
ensureWithinRoot(targetPath, resolvedRoot);
|
|
135
|
+
const roots = memoryRootsFromRegistry();
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (!isEnoent(error)) {
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
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
|
+
);
|
|
102
141
|
}
|
|
103
142
|
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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;
|
|
110
151
|
}
|
|
111
|
-
|
|
152
|
+
const result = await tryResolveInRoot(url, root);
|
|
153
|
+
if (result) return result;
|
|
112
154
|
}
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
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
|
+
);
|
|
119
160
|
}
|
|
120
161
|
|
|
121
|
-
|
|
122
|
-
const ext = path.extname(realTargetPath).toLowerCase();
|
|
123
|
-
const contentType: InternalResource["contentType"] = ext === ".md" ? "text/markdown" : "text/plain";
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
url: url.href,
|
|
127
|
-
content,
|
|
128
|
-
contentType,
|
|
129
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
130
|
-
sourcePath: realTargetPath,
|
|
131
|
-
notes: [],
|
|
132
|
-
};
|
|
162
|
+
throw new Error(`Memory file not found: ${url.href}`);
|
|
133
163
|
}
|
|
134
164
|
}
|
|
@@ -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();
|
|
@@ -1,42 +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
12
|
readonly immutable = true;
|
|
27
13
|
|
|
28
|
-
constructor(private readonly options: RuleProtocolOptions) {}
|
|
29
|
-
|
|
30
14
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
31
|
-
const rules =
|
|
15
|
+
const rules = getActiveRules();
|
|
32
16
|
|
|
33
|
-
// Extract rule name from host
|
|
34
17
|
const ruleName = url.rawHost || url.hostname;
|
|
35
18
|
if (!ruleName) {
|
|
36
19
|
throw new Error("rule:// URL requires a rule name: rule://<name>");
|
|
37
20
|
}
|
|
38
21
|
|
|
39
|
-
// Find the rule
|
|
40
22
|
const rule = rules.find(r => r.name === ruleName);
|
|
41
23
|
if (!rule) {
|
|
42
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,25 +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
39
|
readonly immutable = true;
|
|
52
40
|
|
|
53
|
-
constructor(private readonly options: SkillProtocolOptions) {}
|
|
54
|
-
|
|
55
41
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
56
|
-
const skills =
|
|
42
|
+
const skills = getActiveSkills();
|
|
57
43
|
|
|
58
|
-
// Extract skill name from host
|
|
59
44
|
const skillName = url.rawHost || url.hostname;
|
|
60
45
|
if (!skillName) {
|
|
61
46
|
throw new Error("skill:// URL requires a skill name: skill://<name>");
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
// Find the skill
|
|
65
49
|
const skill = skills.find(s => s.name === skillName);
|
|
66
50
|
if (!skill) {
|
|
67
51
|
const available = skills.map(s => s.name);
|
|
@@ -69,41 +53,34 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
69
53
|
throw new Error(`Unknown skill: ${skillName}\nAvailable: ${availableStr}`);
|
|
70
54
|
}
|
|
71
55
|
|
|
72
|
-
// Determine the file to read
|
|
73
56
|
let targetPath: string;
|
|
74
57
|
const urlPath = url.pathname;
|
|
75
58
|
const hasRelativePath = urlPath && urlPath !== "/" && urlPath !== "";
|
|
76
59
|
|
|
77
60
|
if (hasRelativePath) {
|
|
78
|
-
|
|
79
|
-
const relativePath = decodeURIComponent(urlPath.slice(1)); // Remove leading /
|
|
61
|
+
const relativePath = decodeURIComponent(urlPath.slice(1));
|
|
80
62
|
validateRelativePath(relativePath);
|
|
81
63
|
targetPath = path.join(skill.baseDir, relativePath);
|
|
82
64
|
|
|
83
|
-
// Verify the resolved path is still within baseDir
|
|
84
65
|
const resolvedPath = path.resolve(targetPath);
|
|
85
66
|
const resolvedBaseDir = path.resolve(skill.baseDir);
|
|
86
67
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
87
68
|
throw new Error("Path traversal is not allowed");
|
|
88
69
|
}
|
|
89
70
|
} else {
|
|
90
|
-
// Read SKILL.md
|
|
91
71
|
targetPath = skill.filePath;
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
// Read the file
|
|
95
74
|
const file = Bun.file(targetPath);
|
|
96
75
|
if (!(await file.exists())) {
|
|
97
76
|
throw new Error(`File not found: ${targetPath}`);
|
|
98
77
|
}
|
|
99
78
|
|
|
100
79
|
const content = await file.text();
|
|
101
|
-
const contentType = getContentType(targetPath);
|
|
102
|
-
|
|
103
80
|
return {
|
|
104
81
|
url: url.href,
|
|
105
82
|
content,
|
|
106
|
-
contentType,
|
|
83
|
+
contentType: getContentType(targetPath),
|
|
107
84
|
size: Buffer.byteLength(content, "utf-8"),
|
|
108
85
|
sourcePath: targetPath,
|
|
109
86
|
notes: [],
|
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
|
|
|
@@ -539,6 +539,10 @@ class TreeList implements Component {
|
|
|
539
539
|
const msgWithContent = msg as { content?: unknown };
|
|
540
540
|
const content = normalize(this.#extractContent(msgWithContent.content));
|
|
541
541
|
result = theme.fg("accent", "user: ") + content;
|
|
542
|
+
} else if (role === "developer") {
|
|
543
|
+
const msgWithContent = msg as { content?: unknown };
|
|
544
|
+
const content = normalize(this.#extractContent(msgWithContent.content));
|
|
545
|
+
result = theme.fg("dim", "developer: ") + theme.fg("muted", content);
|
|
542
546
|
} else if (role === "assistant") {
|
|
543
547
|
const msgWithContent = msg as { content?: unknown; stopReason?: string; errorMessage?: string };
|
|
544
548
|
const textContent = normalize(this.#extractContent(msgWithContent.content));
|