@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
|
@@ -456,6 +456,10 @@
|
|
|
456
456
|
const content = truncate(normalize(extractContent(msg.content)));
|
|
457
457
|
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
|
458
458
|
}
|
|
459
|
+
if (msg.role === 'developer') {
|
|
460
|
+
const content = truncate(normalize(extractContent(msg.content)));
|
|
461
|
+
return labelHtml + `<span class="tree-role-developer">developer:</span> ${escapeHtml(content)}`;
|
|
462
|
+
}
|
|
459
463
|
if (msg.role === 'assistant') {
|
|
460
464
|
const textContent = truncate(normalize(extractContent(msg.content)));
|
|
461
465
|
if (textContent) {
|
|
@@ -1648,6 +1652,18 @@
|
|
|
1648
1652
|
return html;
|
|
1649
1653
|
}
|
|
1650
1654
|
|
|
1655
|
+
if (msg.role === 'developer') {
|
|
1656
|
+
let html = `<div class="user-message developer-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1657
|
+
const content = msg.content;
|
|
1658
|
+
const text = typeof content === 'string' ? content :
|
|
1659
|
+
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
1660
|
+
if (text.trim()) {
|
|
1661
|
+
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
|
1662
|
+
}
|
|
1663
|
+
html += '</div>';
|
|
1664
|
+
return html;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1651
1667
|
if (msg.role === 'assistant') {
|
|
1652
1668
|
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
1653
1669
|
|
|
@@ -1750,7 +1766,7 @@
|
|
|
1750
1766
|
// ============================================================
|
|
1751
1767
|
|
|
1752
1768
|
function computeStats(entryList) {
|
|
1753
|
-
let userMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1769
|
+
let userMessages = 0, developerMessages = 0, assistantMessages = 0, toolResults = 0;
|
|
1754
1770
|
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
|
|
1755
1771
|
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1756
1772
|
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
@@ -1760,6 +1776,7 @@
|
|
|
1760
1776
|
if (entry.type === 'message') {
|
|
1761
1777
|
const msg = entry.message;
|
|
1762
1778
|
if (msg.role === 'user') userMessages++;
|
|
1779
|
+
if (msg.role === 'developer') developerMessages++;
|
|
1763
1780
|
if (msg.role === 'assistant') {
|
|
1764
1781
|
assistantMessages++;
|
|
1765
1782
|
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
|
|
@@ -1787,7 +1804,7 @@
|
|
|
1787
1804
|
}
|
|
1788
1805
|
}
|
|
1789
1806
|
|
|
1790
|
-
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1807
|
+
return { userMessages, developerMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
|
1791
1808
|
}
|
|
1792
1809
|
|
|
1793
1810
|
const globalStats = computeStats(entries);
|
|
@@ -1803,6 +1820,7 @@
|
|
|
1803
1820
|
|
|
1804
1821
|
const msgParts = [];
|
|
1805
1822
|
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
|
|
1823
|
+
if (globalStats.developerMessages) msgParts.push(`${globalStats.developerMessages} developer`);
|
|
1806
1824
|
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
|
|
1807
1825
|
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
|
|
1808
1826
|
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
|
|
@@ -117,6 +117,31 @@ export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]>
|
|
|
117
117
|
// Path Resolution
|
|
118
118
|
// =============================================================================
|
|
119
119
|
|
|
120
|
+
const MANIFEST_ENTRY_INDEX_NAMES = ["index.ts", "index.js", "index.mjs", "index.cjs"];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a plugin manifest entry to a concrete loadable file path. Returns the
|
|
124
|
+
* file path itself when the entry points at a file, the matching index file when
|
|
125
|
+
* the entry points at a directory containing index.{ts,js,mjs,cjs}, and null
|
|
126
|
+
* when no entry exists at the joined path.
|
|
127
|
+
*/
|
|
128
|
+
function resolveManifestEntryFile(joined: string): string | null {
|
|
129
|
+
let stats: fs.Stats;
|
|
130
|
+
try {
|
|
131
|
+
stats = fs.statSync(joined);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (stats.isDirectory()) {
|
|
136
|
+
for (const name of MANIFEST_ENTRY_INDEX_NAMES) {
|
|
137
|
+
const candidate = path.join(joined, name);
|
|
138
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return joined;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
/**
|
|
121
146
|
* Generic path resolver for plugin manifest entries (tools, hooks, commands, extensions).
|
|
122
147
|
* Handles both single-string and string[] base entries, plus feature-specific entries.
|
|
@@ -130,8 +155,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
130
155
|
if (base) {
|
|
131
156
|
const entries = Array.isArray(base) ? base : [base];
|
|
132
157
|
for (const entry of entries) {
|
|
133
|
-
const resolved = path.join(plugin.path, entry);
|
|
134
|
-
if (
|
|
158
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
159
|
+
if (resolved) {
|
|
135
160
|
paths.push(resolved);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -146,8 +171,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
146
171
|
|
|
147
172
|
if (feat[key]) {
|
|
148
173
|
for (const entry of feat[key]) {
|
|
149
|
-
const resolved = path.join(plugin.path, entry);
|
|
150
|
-
if (
|
|
174
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
175
|
+
if (resolved) {
|
|
151
176
|
paths.push(resolved);
|
|
152
177
|
}
|
|
153
178
|
}
|
|
@@ -160,8 +185,8 @@ function resolvePluginPaths(plugin: InstalledPlugin, key: "tools" | "hooks" | "c
|
|
|
160
185
|
|
|
161
186
|
if (feat[key]) {
|
|
162
187
|
for (const entry of feat[key]) {
|
|
163
|
-
const resolved = path.join(plugin.path, entry);
|
|
164
|
-
if (
|
|
188
|
+
const resolved = resolveManifestEntryFile(path.join(plugin.path, entry));
|
|
189
|
+
if (resolved) {
|
|
165
190
|
paths.push(resolved);
|
|
166
191
|
}
|
|
167
192
|
}
|
|
@@ -28,6 +28,26 @@ export interface LoadSkillsResult {
|
|
|
28
28
|
warnings: SkillWarning[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
let activeSkills: readonly Skill[] = [];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Process-global snapshot of skills the active session loaded.
|
|
35
|
+
* Read by internal URL protocol handlers (skill://).
|
|
36
|
+
*/
|
|
37
|
+
export function getActiveSkills(): readonly Skill[] {
|
|
38
|
+
return activeSkills;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Replace the active skill snapshot. Called once per top-level session. */
|
|
42
|
+
export function setActiveSkills(value: readonly Skill[]): void {
|
|
43
|
+
activeSkills = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reset the active skill snapshot. Test-only. */
|
|
47
|
+
export function resetActiveSkillsForTests(): void {
|
|
48
|
+
activeSkills = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
export interface LoadSkillsFromDirOptions {
|
|
32
52
|
/** Directory to scan for skills */
|
|
33
53
|
dir: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for agent:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves agent output IDs
|
|
4
|
+
* Resolves agent output IDs against the artifacts directories of every active
|
|
5
|
+
* session. Parents and subagents share outputs via this registry: a subagent
|
|
6
|
+
* can read its parent's output IDs because both sessions are registered in
|
|
7
|
+
* the shared context.
|
|
5
8
|
*
|
|
6
9
|
* URL forms:
|
|
7
10
|
* - agent://<id> - Full output content
|
|
@@ -11,27 +14,27 @@
|
|
|
11
14
|
import * as fs from "node:fs/promises";
|
|
12
15
|
import * as path from "node:path";
|
|
13
16
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
14
18
|
import { applyQuery, pathToQuery } from "./json-query";
|
|
15
19
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
16
20
|
|
|
17
|
-
export interface AgentProtocolOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Returns the artifacts directory path, or null if no session.
|
|
20
|
-
* Artifacts directory is the session file path without .jsonl extension.
|
|
21
|
-
*/
|
|
22
|
-
getArtifactsDir: () => string | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
22
|
+
* Snapshot of artifacts dirs for every registered session, deduped.
|
|
23
|
+
*
|
|
24
|
+
* Prefers `sessionManager.getArtifactsDir()` because subagents adopt the
|
|
25
|
+
* parent's manager and report the parent's dir there; dedup then collapses
|
|
26
|
+
* the whole agent tree to one entry. Falls back to the raw session file
|
|
27
|
+
* when no live session reference is attached.
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
function artifactsDirsFromRegistry(): string[] {
|
|
30
|
+
const dirs: string[] = [];
|
|
31
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
32
|
+
const dir =
|
|
33
|
+
ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
|
|
34
|
+
if (!dir) continue;
|
|
35
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
34
36
|
}
|
|
37
|
+
return dirs;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -44,30 +47,12 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
44
47
|
readonly scheme = "agent";
|
|
45
48
|
readonly immutable = true;
|
|
46
49
|
|
|
47
|
-
constructor(private readonly options: AgentProtocolOptions) {}
|
|
48
|
-
|
|
49
50
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
50
|
-
const artifactsDir = this.options.getArtifactsDir();
|
|
51
|
-
if (!artifactsDir) {
|
|
52
|
-
throw new Error("No session - agent outputs unavailable");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
await fs.stat(artifactsDir);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (isEnoent(err)) {
|
|
59
|
-
throw new Error("No artifacts directory found");
|
|
60
|
-
}
|
|
61
|
-
throw err;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Extract output ID from host
|
|
65
51
|
const outputId = url.rawHost || url.hostname;
|
|
66
52
|
if (!outputId) {
|
|
67
53
|
throw new Error("agent:// URL requires an output ID: agent://<id>");
|
|
68
54
|
}
|
|
69
55
|
|
|
70
|
-
// Check for conflicting extraction methods
|
|
71
56
|
const urlPath = url.pathname;
|
|
72
57
|
const queryParam = url.searchParams.get("q");
|
|
73
58
|
const hasPathExtraction = urlPath && urlPath !== "/" && urlPath !== "";
|
|
@@ -77,28 +62,57 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
77
62
|
throw new Error("agent:// URL cannot combine path extraction with ?q=");
|
|
78
63
|
}
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
65
|
+
const dirs = artifactsDirsFromRegistry();
|
|
66
|
+
|
|
67
|
+
if (dirs.length === 0) {
|
|
68
|
+
throw new Error("No session - agent outputs unavailable");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let foundPath: string | undefined;
|
|
72
|
+
let anyDirExists = false;
|
|
73
|
+
const availableIds = new Set<string>();
|
|
74
|
+
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
try {
|
|
77
|
+
await fs.stat(dir);
|
|
78
|
+
anyDirExists = true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (isEnoent(err)) continue;
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
const candidate = path.join(dir, `${outputId}.md`);
|
|
84
|
+
try {
|
|
85
|
+
await fs.stat(candidate);
|
|
86
|
+
foundPath = candidate;
|
|
87
|
+
break;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!isEnoent(err)) throw err;
|
|
90
|
+
try {
|
|
91
|
+
const files = await fs.readdir(dir);
|
|
92
|
+
for (const f of files) {
|
|
93
|
+
if (f.endsWith(".md")) availableIds.add(f.replace(/\.md$/, ""));
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Listing failures are non-fatal; continue searching.
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
|
-
throw err;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
if (!anyDirExists) {
|
|
102
|
+
throw new Error("No artifacts directory found");
|
|
103
|
+
}
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
if (!foundPath) {
|
|
106
|
+
const availableStr = availableIds.size > 0 ? [...availableIds].join(", ") : "none";
|
|
107
|
+
throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rawContent = await Bun.file(foundPath).text();
|
|
111
|
+
const notes: string[] = [];
|
|
97
112
|
let content = rawContent;
|
|
98
113
|
let contentType: InternalResource["contentType"] = "text/markdown";
|
|
99
114
|
|
|
100
115
|
if (hasPathExtraction || hasQueryExtraction) {
|
|
101
|
-
// Parse JSON
|
|
102
116
|
let jsonValue: unknown;
|
|
103
117
|
try {
|
|
104
118
|
jsonValue = JSON.parse(rawContent);
|
|
@@ -107,9 +121,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
107
121
|
throw new Error(`Output ${outputId} is not valid JSON: ${message}`);
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
// Convert path to query if needed
|
|
111
124
|
const query = hasPathExtraction ? pathToQuery(urlPath) : queryParam!;
|
|
112
|
-
|
|
113
125
|
if (query) {
|
|
114
126
|
const extracted = applyQuery(jsonValue, query);
|
|
115
127
|
try {
|
|
@@ -119,7 +131,6 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
119
131
|
}
|
|
120
132
|
notes.push(`Extracted: ${query}`);
|
|
121
133
|
} else {
|
|
122
|
-
// Empty path/query means return full JSON
|
|
123
134
|
content = JSON.stringify(jsonValue, null, 2);
|
|
124
135
|
}
|
|
125
136
|
contentType = "application/json";
|
|
@@ -130,7 +141,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
|
|
|
130
141
|
content,
|
|
131
142
|
contentType,
|
|
132
143
|
size: Buffer.byteLength(content, "utf-8"),
|
|
133
|
-
sourcePath:
|
|
144
|
+
sourcePath: foundPath,
|
|
134
145
|
notes,
|
|
135
146
|
};
|
|
136
147
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol handler for artifact:// URLs.
|
|
3
3
|
*
|
|
4
|
-
* Resolves artifact IDs
|
|
5
|
-
* Unlike agent://, artifacts are raw text with no JSON extraction.
|
|
4
|
+
* Resolves artifact IDs against the artifacts directories of every active
|
|
5
|
+
* session. Unlike agent://, artifacts are raw text with no JSON extraction.
|
|
6
6
|
*
|
|
7
7
|
* URL form:
|
|
8
8
|
* - artifact://<id> - Full artifact content
|
|
@@ -12,87 +12,87 @@
|
|
|
12
12
|
import * as fs from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
15
16
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
16
17
|
|
|
17
|
-
export interface ArtifactProtocolOptions {
|
|
18
|
-
/**
|
|
19
|
-
* Returns the artifacts directory path, or null if no session.
|
|
20
|
-
*/
|
|
21
|
-
getArtifactsDir: () => string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
/**
|
|
25
|
-
*
|
|
19
|
+
* Snapshot of artifacts dirs across all registered sessions, deduped.
|
|
20
|
+
*
|
|
21
|
+
* Subagents adopt their parent's `ArtifactManager`, so their
|
|
22
|
+
* `sessionManager.getArtifactsDir()` returns the parent's dir; dedup
|
|
23
|
+
* collapses parent + N subagents to a single entry.
|
|
26
24
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} catch {
|
|
35
|
-
return [];
|
|
25
|
+
function artifactsDirsFromRegistry(): string[] {
|
|
26
|
+
const dirs: string[] = [];
|
|
27
|
+
for (const ref of AgentRegistry.global().list()) {
|
|
28
|
+
const dir =
|
|
29
|
+
ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
|
|
30
|
+
if (!dir) continue;
|
|
31
|
+
if (!dirs.includes(dir)) dirs.push(dir);
|
|
36
32
|
}
|
|
33
|
+
return dirs;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
/**
|
|
40
|
-
* Handler for artifact:// URLs.
|
|
41
|
-
*
|
|
42
|
-
* Resolves numeric artifact IDs to their text content.
|
|
43
|
-
* Artifacts are created by tools when output is truncated.
|
|
44
|
-
*/
|
|
45
36
|
export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
46
37
|
readonly scheme = "artifact";
|
|
47
38
|
readonly immutable = true;
|
|
48
39
|
|
|
49
|
-
constructor(private readonly options: ArtifactProtocolOptions) {}
|
|
50
|
-
|
|
51
40
|
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
52
|
-
const artifactsDir = this.options.getArtifactsDir();
|
|
53
|
-
if (!artifactsDir) {
|
|
54
|
-
throw new Error("No session - artifacts unavailable");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Extract artifact ID from host
|
|
58
41
|
const id = url.rawHost || url.hostname;
|
|
59
42
|
if (!id) {
|
|
60
43
|
throw new Error("artifact:// URL requires a numeric ID: artifact://0");
|
|
61
44
|
}
|
|
62
|
-
|
|
63
|
-
// Validate ID is numeric
|
|
64
45
|
if (!/^\d+$/.test(id)) {
|
|
65
46
|
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
const dirs = artifactsDirsFromRegistry();
|
|
50
|
+
|
|
51
|
+
if (dirs.length === 0) {
|
|
52
|
+
throw new Error("No session - artifacts unavailable");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let foundPath: string | undefined;
|
|
56
|
+
let anyDirExists = false;
|
|
57
|
+
const availableIds = new Set<string>();
|
|
58
|
+
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
let files: string[];
|
|
61
|
+
try {
|
|
62
|
+
files = await fs.readdir(dir);
|
|
63
|
+
anyDirExists = true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (isEnoent(err)) continue;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const match = files.find(f => f.startsWith(`${id}.`));
|
|
69
|
+
if (match) {
|
|
70
|
+
foundPath = path.join(dir, match);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
for (const f of files) {
|
|
74
|
+
const m = f.match(/^(\d+)\./);
|
|
75
|
+
if (m) availableIds.add(m[1]);
|
|
75
76
|
}
|
|
76
|
-
throw err;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
if (!anyDirExists) {
|
|
80
|
+
throw new Error("No artifacts directory found");
|
|
81
|
+
}
|
|
80
82
|
|
|
81
|
-
if (!
|
|
82
|
-
const
|
|
83
|
-
const availableStr =
|
|
83
|
+
if (!foundPath) {
|
|
84
|
+
const sorted = [...availableIds].sort((a, b) => Number(a) - Number(b));
|
|
85
|
+
const availableStr = sorted.length > 0 ? sorted.join(", ") : "none";
|
|
84
86
|
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const
|
|
88
|
-
const content = await Bun.file(filePath).text();
|
|
89
|
-
|
|
89
|
+
const content = await Bun.file(foundPath).text();
|
|
90
90
|
return {
|
|
91
91
|
url: url.href,
|
|
92
92
|
content,
|
|
93
93
|
contentType: "text/plain",
|
|
94
94
|
size: Buffer.byteLength(content, "utf-8"),
|
|
95
|
-
sourcePath:
|
|
95
|
+
sourcePath: foundPath,
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
}
|