@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
@@ -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 (fs.existsSync(resolved)) {
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 (fs.existsSync(resolved)) {
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 (fs.existsSync(resolved)) {
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 to artifact files in the session directory.
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
- * List available output IDs in artifacts directory.
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
- async function listAvailableOutputs(artifactsDir: string): Promise<string[]> {
29
- try {
30
- const files = await fs.readdir(artifactsDir);
31
- return files.filter(f => f.endsWith(".md")).map(f => f.replace(".md", ""));
32
- } catch {
33
- return [];
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
- // Load the output file
81
- const outputPath = path.join(artifactsDir, `${outputId}.md`);
82
- try {
83
- await fs.stat(outputPath);
84
- } catch (err) {
85
- if (isEnoent(err)) {
86
- const available = await listAvailableOutputs(artifactsDir);
87
- const availableStr = available.length > 0 ? available.join(", ") : "none";
88
- throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
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
- const rawContent = await Bun.file(outputPath).text();
94
- const notes: string[] = [];
101
+ if (!anyDirExists) {
102
+ throw new Error("No artifacts directory found");
103
+ }
95
104
 
96
- // Handle extraction
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: outputPath,
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 to files in the session artifacts directory.
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
- * List available artifact IDs in the directory.
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
- async function listAvailableArtifacts(artifactsDir: string): Promise<string[]> {
28
- try {
29
- const files = await fs.readdir(artifactsDir);
30
- return files
31
- .filter(f => /^\d+\./.test(f))
32
- .map(f => f.split(".")[0])
33
- .sort((a, b) => Number(a) - Number(b));
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
- // Check directory exists and find file matching ID prefix
69
- let files: string[];
70
- try {
71
- files = await fs.readdir(artifactsDir);
72
- } catch (err) {
73
- if (isEnoent(err)) {
74
- throw new Error("No artifacts directory found");
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
- const match = files.find(f => f.startsWith(`${id}.`));
79
+ if (!anyDirExists) {
80
+ throw new Error("No artifacts directory found");
81
+ }
80
82
 
81
- if (!match) {
82
- const available = await listAvailableArtifacts(artifactsDir);
83
- const availableStr = available.length > 0 ? available.join(", ") : "none";
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 filePath = path.join(artifactsDir, match);
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: filePath,
95
+ sourcePath: foundPath,
96
96
  };
97
97
  }
98
98
  }