@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/package.json +7 -7
  3. package/src/async/job-manager.ts +66 -9
  4. package/src/capability/rule.ts +20 -0
  5. package/src/config/model-registry.ts +13 -0
  6. package/src/config/model-resolver.ts +8 -2
  7. package/src/config/settings-schema.ts +1 -1
  8. package/src/edit/index.ts +8 -0
  9. package/src/edit/renderer.ts +6 -1
  10. package/src/edit/streaming.ts +53 -2
  11. package/src/eval/js/context-manager.ts +1 -38
  12. package/src/eval/js/prelude.txt +0 -2
  13. package/src/eval/py/executor.ts +24 -8
  14. package/src/eval/py/index.ts +1 -0
  15. package/src/eval/py/prelude.py +11 -80
  16. package/src/export/html/template.css +12 -0
  17. package/src/export/html/template.generated.ts +1 -1
  18. package/src/export/html/template.js +20 -2
  19. package/src/extensibility/plugins/loader.ts +31 -6
  20. package/src/extensibility/skills.ts +20 -0
  21. package/src/internal-urls/agent-protocol.ts +63 -52
  22. package/src/internal-urls/artifact-protocol.ts +51 -51
  23. package/src/internal-urls/docs-index.generated.ts +33 -1
  24. package/src/internal-urls/index.ts +6 -19
  25. package/src/internal-urls/local-protocol.ts +49 -7
  26. package/src/internal-urls/mcp-protocol.ts +2 -8
  27. package/src/internal-urls/memory-protocol.ts +89 -59
  28. package/src/internal-urls/router.ts +38 -22
  29. package/src/internal-urls/rule-protocol.ts +2 -20
  30. package/src/internal-urls/skill-protocol.ts +4 -27
  31. package/src/main.ts +1 -1
  32. package/src/mcp/manager.ts +17 -0
  33. package/src/modes/components/session-observer-overlay.ts +2 -2
  34. package/src/modes/components/tool-execution.ts +6 -0
  35. package/src/modes/components/tree-selector.ts +4 -0
  36. package/src/modes/controllers/event-controller.ts +23 -2
  37. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  38. package/src/modes/interactive-mode.ts +2 -2
  39. package/src/modes/theme/theme.ts +27 -27
  40. package/src/modes/types.ts +1 -1
  41. package/src/modes/utils/ui-helpers.ts +14 -9
  42. package/src/prompts/commands/orchestrate.md +1 -0
  43. package/src/prompts/system/project-prompt.md +10 -2
  44. package/src/prompts/system/subagent-system-prompt.md +8 -8
  45. package/src/prompts/system/system-prompt.md +13 -7
  46. package/src/prompts/tools/ask.md +0 -1
  47. package/src/prompts/tools/bash.md +0 -10
  48. package/src/prompts/tools/eval.md +1 -3
  49. package/src/prompts/tools/github.md +6 -5
  50. package/src/prompts/tools/hashline.md +1 -0
  51. package/src/prompts/tools/job.md +14 -6
  52. package/src/prompts/tools/task.md +20 -3
  53. package/src/registry/agent-registry.ts +2 -1
  54. package/src/sdk.ts +87 -89
  55. package/src/session/agent-session.ts +58 -20
  56. package/src/session/artifacts.ts +7 -4
  57. package/src/session/session-manager.ts +30 -1
  58. package/src/ssh/connection-manager.ts +32 -16
  59. package/src/ssh/sshfs-mount.ts +10 -7
  60. package/src/system-prompt.ts +0 -5
  61. package/src/task/executor.ts +14 -2
  62. package/src/task/index.ts +19 -5
  63. package/src/tool-discovery/tool-index.ts +21 -8
  64. package/src/tools/ast-edit.ts +3 -2
  65. package/src/tools/ast-grep.ts +3 -2
  66. package/src/tools/bash.ts +15 -9
  67. package/src/tools/browser/tab-supervisor.ts +12 -2
  68. package/src/tools/eval.ts +48 -10
  69. package/src/tools/fetch.ts +1 -1
  70. package/src/tools/gh.ts +140 -4
  71. package/src/tools/index.ts +12 -11
  72. package/src/tools/job.ts +48 -12
  73. package/src/tools/read.ts +5 -4
  74. package/src/tools/search.ts +3 -2
  75. package/src/tools/todo-write.ts +1 -1
  76. package/src/web/scrapers/mastodon.ts +1 -1
  77. package/src/web/scrapers/repology.ts +7 -7
  78. package/src/internal-urls/jobs-protocol.ts +0 -120
  79. 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://, skill://, mcp://, and local://.
2
+ * Internal URL routing system for internal protocols like agent://, memory://,
3
+ * skill://, mcp://, and local://.
3
4
  *
4
- * This module provides a unified way to resolve internal URLs without
5
- * exposing filesystem paths to the agent.
6
- *
7
- * @example
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 all session local files
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
- constructor(private readonly options: LocalProtocolOptions) {}
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 localRoot = path.resolve(resolveLocalRoot(this.options));
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 type { MCPManager } from "../mcp/manager";
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 = this.options.getMcpManager();
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
- * Options for the memory:// URL protocol.
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
- export interface MemoryProtocolOptions {
14
- /**
15
- * Returns the absolute path to the current project's memory root.
16
- */
17
- getMemoryRoot: () => string;
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
- * URL forms:
68
- * - memory://root - Reads memory_summary.md
69
- * - memory://root/<path> - Reads a relative file under memory root
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 memoryRoot = path.resolve(this.options.getMemoryRoot());
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
- const parentDir = path.dirname(targetPath);
95
- try {
96
- const realParent = await fs.realpath(parentDir);
97
- ensureWithinRoot(realParent, resolvedRoot);
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 realTargetPath: string;
105
- try {
106
- realTargetPath = await fs.realpath(targetPath);
107
- } catch (error) {
108
- if (isEnoent(error)) {
109
- throw new Error(`Memory file not found: ${url.href}`);
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
- throw error;
152
+ const result = await tryResolveInRoot(url, root);
153
+ if (result) return result;
112
154
  }
113
155
 
114
- ensureWithinRoot(realTargetPath, resolvedRoot);
115
-
116
- const stat = await fs.stat(realTargetPath);
117
- if (!stat.isFile()) {
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
- const content = await Bun.file(realTargetPath).text();
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
- * Register a protocol handler.
18
- * @param handler Handler to register (uses handler.scheme as key)
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
- const scheme = match[1].toLowerCase();
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 type { Rule } from "../capability/rule";
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 = this.options.getRules();
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 type { Skill } from "../extensibility/skills";
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 = this.options.getSkills();
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
- // Read relative path within skill's baseDir
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 \u2014 /marketplace upgrade`);
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.
@@ -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 [/]/\u2190\u2192:cycle agents Esc/Ctrl+S:close g/G:top/bottom"),
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", "\u25B8")} ${theme.bold(theme.fg("muted", call.name))}${intentStr}`);
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));