@nghyane/arcane 0.1.25 → 0.1.27

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 CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.27] - 2026-03-08
6
+
7
+ ### Fixed
8
+
9
+ - Fix GitHub tool: typed API interfaces replacing `any`, cache key collision with different media types, 403 rate limit not retried, `retry-after` NaN crash, raw file detection returning `[object Object]` for JSON files
10
+ - Fix bash timeout misreported as "Command aborted" instead of "Command timed out"
11
+ - Fix unhandled promise rejection crash in interactive bash PTY finalization
12
+ - Reorder HTML render pipeline to native-first, avoiding unnecessary network calls to jina.ai
13
+
14
+ ### Changed
15
+
16
+ - Add GitHub tool guidance to system prompt
17
+ - Remove dead code: `normalizeBashCommand`, `expandSkillUrls`, `BashToolOptions`, `isInteractiveResult`
18
+
19
+ ## [0.1.26] - 2026-03-08
20
+
21
+ ### Fixed
22
+
23
+ - Improve session search by indexing all user messages instead of only the first
24
+ - Fix quoted phrase parsing in find_thread search queries
25
+ - Sort search results by relevance instead of date
26
+
27
+ ### Removed
28
+
29
+ - Remove gemini-image and save-memory tools
30
+
31
+ ### Changed
32
+
33
+ - Rename reviewer-tool.ts to reviewer.ts, subagent-tool.ts to subagent.ts
34
+ - RenderMermaid tool now displays diagram directly in TUI output
35
+
5
36
  ## [0.1.25] - 2026-03-08
6
37
 
7
38
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@nghyane/arcane",
4
- "version": "0.1.25",
4
+ "version": "0.1.27",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/nghyane/arcane",
7
7
  "author": "Can Bölük",
@@ -27,6 +27,7 @@ export interface BashResult {
27
27
  output: string;
28
28
  exitCode: number | undefined;
29
29
  cancelled: boolean;
30
+ timedOut: boolean;
30
31
  truncated: boolean;
31
32
  totalLines: number;
32
33
  totalBytes: number;
@@ -44,11 +45,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
44
45
  const { shell, env: shellEnv, prefix } = settings.getShellConfig();
45
46
  const snapshotPath = shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
46
47
 
47
- // Apply command prefix if configured
48
48
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
49
- const finalCommand = prefixedCommand;
50
49
 
51
- // Create output sink for truncation and artifact handling
52
50
  const sink = new OutputSink({
53
51
  onChunk: options?.onChunk,
54
52
  artifactPath: options?.artifactPath,
@@ -64,6 +62,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
64
62
  return {
65
63
  exitCode: undefined,
66
64
  cancelled: true,
65
+ timedOut: false,
67
66
  ...(await sink.dump("Command cancelled")),
68
67
  };
69
68
  }
@@ -96,7 +95,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
96
95
  try {
97
96
  const runPromise = shellSession.run(
98
97
  {
99
- command: finalCommand,
98
+ command: prefixedCommand,
100
99
  cwd: options?.cwd,
101
100
  env: options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV,
102
101
  timeoutMs: options?.timeout,
@@ -121,11 +120,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
121
120
  return {
122
121
  exitCode: undefined,
123
122
  cancelled: true,
123
+ timedOut: true,
124
124
  ...(await sink.dump(`Command exceeded hard timeout after ${Math.round(hardTimeoutMs / 1000)} seconds`)),
125
125
  };
126
126
  }
127
127
 
128
- // Handle timeout
129
128
  if (winner.result.timedOut) {
130
129
  const annotation = options?.timeout
131
130
  ? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
@@ -133,25 +132,26 @@ export async function executeBash(command: string, options?: BashExecutorOptions
133
132
  resetSession = true;
134
133
  return {
135
134
  exitCode: undefined,
136
- cancelled: true,
135
+ cancelled: false,
136
+ timedOut: true,
137
137
  ...(await sink.dump(annotation)),
138
138
  };
139
139
  }
140
140
 
141
- // Handle cancellation
142
141
  if (winner.result.cancelled) {
143
142
  resetSession = true;
144
143
  return {
145
144
  exitCode: undefined,
146
145
  cancelled: true,
146
+ timedOut: false,
147
147
  ...(await sink.dump("Command cancelled")),
148
148
  };
149
149
  }
150
150
 
151
- // Normal completion
152
151
  return {
153
152
  exitCode: winner.result.exitCode,
154
153
  cancelled: false,
154
+ timedOut: false,
155
155
  ...(await sink.dump()),
156
156
  };
157
157
  } catch (err) {
@@ -415,9 +415,7 @@ export class CommandController {
415
415
  }
416
416
 
417
417
  async handleMemoryCommand(_text: string): Promise<void> {
418
- this.ctx.showWarning(
419
- "The /memory command has been removed. Use save_memory tool or add facts to AGENTS.md directly.",
420
- );
418
+ this.ctx.showWarning("The /memory command has been removed. Add facts to AGENTS.md directly.");
421
419
  }
422
420
 
423
421
  async handleClearCommand(): Promise<void> {
@@ -18,7 +18,7 @@ import {
18
18
  setTheme,
19
19
  theme,
20
20
  } from "../../theme/theme";
21
- import { setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
21
+ import { setPreferredSearchProvider } from "../../tools";
22
22
  import { AssistantMessageComponent } from "../components/assistant-message";
23
23
  import { ExtensionDashboard } from "../components/extensions";
24
24
  import { HistorySearchComponent } from "../components/history-search";
@@ -289,9 +289,6 @@ export class SelectorController {
289
289
  value as "auto" | "exa" | "jina" | "zai" | "perplexity" | "anthropic" | "gemini" | "codex",
290
290
  );
291
291
  break;
292
- case "imageProvider":
293
- setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
294
- break;
295
292
 
296
293
  // All other settings are handled by the definitions (get/set on SettingsManager)
297
294
  // No additional side effects needed
@@ -171,7 +171,7 @@ Best practices:
171
171
 
172
172
  ### Cross-session Knowledge
173
173
 
174
- Tools: `find_thread`, `read_thread`, `save_memory`
174
+ Tools: `find_thread`, `read_thread`
175
175
  **Proactive search triggers** — use `find_thread` when:
176
176
  - User mentions past work: "we did this before", "last time", "in a previous session"
177
177
  - User asks "what did we do about X" or "how did we solve Y"
@@ -181,7 +181,16 @@ Tools: `find_thread`, `read_thread`, `save_memory`
181
181
  - Question is about current session context
182
182
  - Generic coding question with no project-specific history
183
183
  - User explicitly provides all needed context
184
- **save_memory**: only when user says "remember this" or states a clear preference. If unsure, ask.
184
+
185
+ {{#has tools "github"}}
186
+ ### GitHub
187
+
188
+ Tool: `github`
189
+ Use for **remote** GitHub API calls — repos, issues, PRs, commits, file contents, tree listings. Never use for the local repo; use read/grep/explore for local files.
190
+ - **Use `github`** for: reading issues/PRs, listing commits, fetching files from remote repos, searching repos, getting repo metadata.
191
+ - **Use `librarian`** for: cross-repo exploration, understanding architectural patterns, tracing how other projects solve problems.
192
+ - **Avoid `bash gh`** when `github` covers the action — the tool has caching, pagination, and structured output.
193
+ {{/has}}
185
194
 
186
195
  ### Verification
187
196
  Work incrementally. Make a small change, verify it works, then continue. Prefer a sequence of small, validated edits over one large change. Use commands from AGENTS.md or the project's config to verify. Address all errors caused by your changes before yielding.
package/src/sdk.ts CHANGED
@@ -71,7 +71,6 @@ import {
71
71
  PythonTool,
72
72
  ReadTool,
73
73
  type SubagentContext,
74
- setPreferredImageProvider,
75
74
  setPreferredSearchProvider,
76
75
  type Tool,
77
76
  type ToolSession,
@@ -79,7 +78,6 @@ import {
79
78
  warmupLspServers,
80
79
  } from "./tools";
81
80
  import { ToolContextStore } from "./tools/context";
82
- import { getGeminiImageTools } from "./tools/gemini-image";
83
81
  import { EventBus } from "./utils/event-bus";
84
82
  import { time } from "./utils/timings";
85
83
 
@@ -559,7 +557,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
559
557
 
560
558
  // Initialize provider preferences from settings
561
559
  setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
562
- setPreferredImageProvider(settings.get("providers.image") ?? "auto");
563
560
 
564
561
  const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
565
562
  time("sessionManager");
@@ -803,13 +800,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
803
800
  }
804
801
  }
805
802
 
806
- // Add Gemini image tools if GEMINI_AARCANE_KEY (or GOOGLE_AARCANE_KEY) is available
807
- const geminiImageTools = await getGeminiImageTools();
808
- time("getGeminiImageTools");
809
- if (geminiImageTools.length > 0) {
810
- customTools.push(...(geminiImageTools as unknown as CustomTool[]));
811
- }
812
-
813
803
  // Add specialized Exa web search tools if EXA_AARCANE_KEY is available
814
804
  const exaSettings = settings.getGroup("exa");
815
805
  if (exaSettings.enabled && exaSettings.enableSearch) {
@@ -7,8 +7,7 @@ import { getAgentDir } from "@nghyane/arcane-utils/dirs";
7
7
  export interface SessionIndexEntry {
8
8
  sessionId: string;
9
9
  title: string;
10
- firstMessage: string;
11
- files: string;
10
+ content: string;
12
11
  cwd: string;
13
12
  createdAt: number;
14
13
  messageCount: number;
@@ -30,6 +29,7 @@ interface SearchRow {
30
29
  snippet: string;
31
30
  }
32
31
 
32
+ const CONTENT_MAX_LENGTH = 5000;
33
33
  const FILE_PATH_PARAMS = new Set(["path", "file", "filePath", "glob", "pattern", "command_working_directory"]);
34
34
 
35
35
  export class SessionIndex {
@@ -49,46 +49,68 @@ export class SessionIndex {
49
49
  PRAGMA journal_mode=WAL;
50
50
  PRAGMA synchronous=NORMAL;
51
51
  PRAGMA busy_timeout=5000;
52
+ `);
53
+
54
+ this.#migrateIfNeeded();
52
55
 
56
+ this.#db.exec(`
53
57
  CREATE TABLE IF NOT EXISTS session_index (
54
58
  session_id TEXT PRIMARY KEY,
55
59
  title TEXT,
56
- first_message TEXT,
57
- files TEXT,
60
+ content TEXT,
58
61
  cwd TEXT,
59
62
  created_at INTEGER,
60
63
  message_count INTEGER
61
64
  );
62
65
 
63
66
  CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
64
- title, first_message, files,
67
+ title, content,
65
68
  content='session_index', content_rowid='rowid'
66
69
  );
67
70
 
68
71
  CREATE TRIGGER IF NOT EXISTS session_index_ai AFTER INSERT ON session_index BEGIN
69
- INSERT INTO session_fts(rowid, title, first_message, files)
70
- VALUES (new.rowid, new.title, new.first_message, new.files);
72
+ INSERT INTO session_fts(rowid, title, content)
73
+ VALUES (new.rowid, new.title, new.content);
71
74
  END;
72
75
 
73
76
  CREATE TRIGGER IF NOT EXISTS session_index_ad AFTER DELETE ON session_index BEGIN
74
- INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
75
- VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
77
+ INSERT INTO session_fts(session_fts, rowid, title, content)
78
+ VALUES ('delete', old.rowid, old.title, old.content);
76
79
  END;
77
80
 
78
81
  CREATE TRIGGER IF NOT EXISTS session_index_au AFTER UPDATE ON session_index BEGIN
79
- INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
80
- VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
81
- INSERT INTO session_fts(rowid, title, first_message, files)
82
- VALUES (new.rowid, new.title, new.first_message, new.files);
82
+ INSERT INTO session_fts(session_fts, rowid, title, content)
83
+ VALUES ('delete', old.rowid, old.title, old.content);
84
+ INSERT INTO session_fts(rowid, title, content)
85
+ VALUES (new.rowid, new.title, new.content);
83
86
  END;
84
87
  `);
85
88
 
86
89
  this.#upsertStmt = this.#db.prepare(
87
- "INSERT OR REPLACE INTO session_index (session_id, title, first_message, files, cwd, created_at, message_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
90
+ "INSERT OR REPLACE INTO session_index (session_id, title, content, cwd, created_at, message_count) VALUES (?, ?, ?, ?, ?, ?)",
88
91
  );
89
92
  this.#hasStmt = this.#db.prepare("SELECT 1 FROM session_index WHERE session_id = ?");
90
93
  }
91
94
 
95
+ #migrateIfNeeded(): void {
96
+ try {
97
+ const row = this.#db.prepare("PRAGMA table_info(session_index)").all() as Array<{ name: string }>;
98
+ const columns = new Set(row.map(r => r.name));
99
+ if (columns.has("first_message") || !columns.has("content")) {
100
+ this.#db.exec(`
101
+ DROP TRIGGER IF EXISTS session_index_ai;
102
+ DROP TRIGGER IF EXISTS session_index_ad;
103
+ DROP TRIGGER IF EXISTS session_index_au;
104
+ DROP TABLE IF EXISTS session_fts;
105
+ DROP TABLE IF EXISTS session_index;
106
+ `);
107
+ logger.debug("SessionIndex: migrated to schema v2 (dropped old tables)");
108
+ }
109
+ } catch (error) {
110
+ logger.warn("SessionIndex migration check failed", { error: String(error) });
111
+ }
112
+ }
113
+
92
114
  static open(dbPath: string = path.join(getAgentDir(), "session-index.db")): SessionIndex {
93
115
  if (!SessionIndex.#instance) {
94
116
  SessionIndex.#instance = new SessionIndex(dbPath);
@@ -101,8 +123,7 @@ END;
101
123
  this.#upsertStmt.run(
102
124
  entry.sessionId,
103
125
  entry.title,
104
- entry.firstMessage,
105
- entry.files,
126
+ entry.content,
106
127
  entry.cwd,
107
128
  entry.createdAt,
108
129
  entry.messageCount,
@@ -139,7 +160,7 @@ END;
139
160
  FROM session_fts f
140
161
  JOIN session_index si ON si.rowid = f.rowid
141
162
  WHERE ${conditions.join(" AND ")}
142
- ORDER BY si.created_at DESC
163
+ ORDER BY rank
143
164
  LIMIT ?`;
144
165
 
145
166
  try {
@@ -159,8 +180,8 @@ LIMIT ?`;
159
180
 
160
181
  async indexSessionFile(filePath: string): Promise<void> {
161
182
  try {
162
- const content = await Bun.file(filePath).text();
163
- const entries = parseJsonlLenient<Record<string, unknown>>(content);
183
+ const raw = await Bun.file(filePath).text();
184
+ const entries = parseJsonlLenient<Record<string, unknown>>(raw);
164
185
  if (entries.length === 0) return;
165
186
 
166
187
  const header = entries.find(e => e.type === "session") as
@@ -168,7 +189,7 @@ LIMIT ?`;
168
189
  | undefined;
169
190
  if (!header?.id) return;
170
191
 
171
- let firstMessage = "";
192
+ const userMessages: string[] = [];
172
193
  let messageCount = 0;
173
194
  const fileSet = new Set<string>();
174
195
 
@@ -179,8 +200,9 @@ LIMIT ?`;
179
200
 
180
201
  messageCount++;
181
202
 
182
- if (msg.role === "user" && !firstMessage) {
183
- firstMessage = extractTextContent(msg.content);
203
+ if (msg.role === "user") {
204
+ const text = extractTextContent(msg.content);
205
+ if (text) userMessages.push(text);
184
206
  }
185
207
 
186
208
  if (msg.role === "assistant") {
@@ -188,14 +210,15 @@ LIMIT ?`;
188
210
  }
189
211
  }
190
212
 
213
+ const firstMessage = userMessages[0] ?? "";
191
214
  const title = header.title || firstMessage.slice(0, 100) || "Untitled";
215
+ const content = [userMessages.join("\n"), [...fileSet].join(" ")].join("\n").slice(0, CONTENT_MAX_LENGTH);
192
216
  const createdAt = header.timestamp ? Math.floor(new Date(header.timestamp).getTime() / 1000) : 0;
193
217
 
194
218
  this.upsert({
195
219
  sessionId: header.id,
196
220
  title,
197
- firstMessage: firstMessage.slice(0, 500),
198
- files: [...fileSet].join(" "),
221
+ content,
199
222
  cwd: header.cwd || "",
200
223
  createdAt,
201
224
  messageCount,
@@ -283,21 +306,25 @@ LIMIT ?`;
283
306
  return null;
284
307
  }
285
308
 
286
- #buildFtsQuery(query: string): string | null {
287
- const tokens = query
309
+ #buildFtsQuery(raw: string): string | null {
310
+ const phrases: string[] = [];
311
+ const withoutPhrases = raw.replace(/"([^"]+)"/g, (_, phrase: string) => {
312
+ const trimmed = phrase.trim();
313
+ if (trimmed) phrases.push(`"${trimmed}"`);
314
+ return "";
315
+ });
316
+
317
+ const words = withoutPhrases
288
318
  .trim()
289
319
  .split(/\s+/)
290
- .map(t => t.trim())
291
- .filter(Boolean);
292
-
293
- if (tokens.length === 0) return null;
294
-
295
- return tokens
296
- .map(token => {
297
- const escaped = token.replace(/"/g, '""');
320
+ .filter(Boolean)
321
+ .map(w => {
322
+ const escaped = w.replace(/"/g, '""');
298
323
  return `"${escaped}"*`;
299
- })
300
- .join(" ");
324
+ });
325
+
326
+ const parts = [...phrases, ...words];
327
+ return parts.length > 0 ? parts.join(" ") : null;
301
328
  }
302
329
  }
303
330
 
@@ -303,15 +303,29 @@ export async function runInteractiveBashPty(
303
303
  component.setComplete({ exitCode: run.exitCode, cancelled: run.cancelled, timedOut: run.timedOut });
304
304
  tui.requestRender();
305
305
  void (async () => {
306
- await component.flushOutput();
307
- await pendingChunks;
308
- const summary = await sink.dump();
309
- done({
310
- exitCode: run.exitCode,
311
- cancelled: run.cancelled,
312
- timedOut: run.timedOut,
313
- ...summary,
314
- });
306
+ try {
307
+ await component.flushOutput();
308
+ await pendingChunks;
309
+ const summary = await sink.dump();
310
+ done({
311
+ exitCode: run.exitCode,
312
+ cancelled: run.cancelled,
313
+ timedOut: run.timedOut,
314
+ ...summary,
315
+ });
316
+ } catch {
317
+ done({
318
+ exitCode: run.exitCode,
319
+ cancelled: run.cancelled,
320
+ timedOut: run.timedOut,
321
+ output: "",
322
+ truncated: false,
323
+ totalLines: 0,
324
+ totalBytes: 0,
325
+ outputLines: 0,
326
+ outputBytes: 0,
327
+ });
328
+ }
315
329
  })();
316
330
  };
317
331
  const cols = Math.max(20, tui.terminal.columns - 2);
@@ -82,7 +82,6 @@ export function checkBashInterception(
82
82
  availableTools: string[],
83
83
  rules: BashInterceptorRule[] = DEFAULT_BASH_INTERCEPTOR_RULES,
84
84
  ): InterceptionResult {
85
- // Normalize command for pattern matching
86
85
  const normalizedCommand = command.trim();
87
86
  const compiled = compileRules(rules);
88
87
 
@@ -1,72 +1,3 @@
1
- /**
2
- * Bash command normalizer - extracts patterns that are better handled natively.
3
- *
4
- * Detects and extracts:
5
- * - `| head -n N` / `| head -N` - extracted to headLines
6
- * - `| tail -n N` / `| tail -N` - extracted to tailLines
7
- */
8
-
9
- interface NormalizedCommand {
10
- /** Cleaned command with patterns stripped */
11
- command: string;
12
- /** Extracted head line count, if any */
13
- headLines?: number;
14
- /** Extracted tail line count, if any */
15
- tailLines?: number;
16
- }
17
-
18
- /**
19
- * Pattern to match trailing pipe to head/tail.
20
- * Captures: full match, command (head/tail), line count
21
- *
22
- * Matches:
23
- * - `| head -n 50`
24
- * - `| head -50`
25
- * - `| tail -n 100`
26
- * - `| tail -100`
27
- *
28
- * Does NOT match head/tail with other flags or without line count.
29
- */
30
- const TRAILING_HEAD_TAIL_PATTERN = /\|\s*(head|tail)\s+(?:-n\s*(\d+)|(-\d+))\s*$/;
31
-
32
- /**
33
- * Normalize a bash command by stripping patterns better handled natively.
34
- *
35
- * Extracts `| head -n N` and `| tail -n N` suffixes into separate fields
36
- * so they can be applied post-execution without breaking streaming.
37
- *
38
- * Strips `2>&1` since we already merge stdout/stderr.
39
- */
40
- export function normalizeBashCommand(command: string): NormalizedCommand {
41
- let normalized = command;
42
- let headLines: number | undefined;
43
- let tailLines: number | undefined;
44
-
45
- // Extract trailing head/tail
46
- const match = normalized.match(TRAILING_HEAD_TAIL_PATTERN);
47
- if (match) {
48
- const [fullMatch, cmd, nValue, dashValue] = match;
49
- const lineCount = nValue ? Number.parseInt(nValue, 10) : Number.parseInt(dashValue.slice(1), 10);
50
-
51
- if (cmd === "head") {
52
- headLines = lineCount;
53
- } else {
54
- tailLines = lineCount;
55
- }
56
-
57
- normalized = normalized.slice(0, -fullMatch.length);
58
- }
59
-
60
- // Preserve internal whitespace (important for heredocs / indentation-sensitive scripts)
61
- normalized = normalized.trim();
62
-
63
- return {
64
- command: normalized,
65
- headLines,
66
- tailLines,
67
- };
68
- }
69
-
70
1
  /**
71
2
  * Apply head/tail limits to output text.
72
3
  *
@@ -86,13 +17,11 @@ export function applyHeadTail(
86
17
  let headApplied: number | undefined;
87
18
  let tailApplied: number | undefined;
88
19
 
89
- // Apply head first (keep first N lines)
90
20
  if (headLines !== undefined && headLines > 0 && lines.length > headLines) {
91
21
  lines = lines.slice(0, headLines);
92
22
  headApplied = headLines;
93
23
  }
94
24
 
95
- // Then apply tail (keep last N lines)
96
25
  if (tailLines !== undefined && tailLines > 0 && lines.length > tailLines) {
97
26
  lines = lines.slice(-tailLines);
98
27
  tailApplied = tailLines;
@@ -4,9 +4,6 @@ import { validateRelativePath } from "../internal-urls/skill-protocol";
4
4
  import type { InternalResource } from "../internal-urls/types";
5
5
  import { ToolError } from "./tool-errors";
6
6
 
7
- /** Regex to find skill:// tokens in command text. */
8
- const SKILL_URL_PATTERN = /'skill:\/\/[^'\s")`\\]+'|"skill:\/\/[^"\s')`\\]+"|skill:\/\/[^\s'")`\\]+/g;
9
-
10
7
  /** Regex to find supported internal URL tokens in command text. */
11
8
  const INTERNAL_URL_PATTERN =
12
9
  /'(?:skill|agent|artifact|plan|rule):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|rule):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|rule):\/\/[^\s'")`\\]+/g;
@@ -133,23 +130,6 @@ async function resolveInternalUrlToPath(
133
130
  return path.resolve(resource.sourcePath);
134
131
  }
135
132
 
136
- /**
137
- * Expand all skill:// URIs in a bash command string.
138
- * Returns the command with URIs replaced by shell-escaped absolute paths.
139
- * Throws ToolError if any URI cannot be resolved.
140
- */
141
- export function expandSkillUrls(command: string, skills: readonly Skill[]): string {
142
- if (skills.length === 0 || !command.includes("skill://")) {
143
- return command;
144
- }
145
-
146
- return command.replace(SKILL_URL_PATTERN, token => {
147
- const url = unquoteToken(token);
148
- const resolvedPath = resolveSkillUrlToPath(url, skills);
149
- return shellEscape(resolvedPath);
150
- });
151
- }
152
-
153
133
  /**
154
134
  * Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
155
135
  * Supported schemes: skill://, agent://, artifact://, plan://, rule://
package/src/tools/bash.ts CHANGED
@@ -38,20 +38,10 @@ export interface BashToolDetails {
38
38
  meta?: OutputMeta;
39
39
  }
40
40
 
41
- export interface BashToolOptions {}
42
-
43
41
  function normalizeResultOutput(result: BashResult | BashInteractiveResult): string {
44
42
  return result.output || "";
45
43
  }
46
44
 
47
- function isInteractiveResult(result: BashResult | BashInteractiveResult): result is BashInteractiveResult {
48
- return "timedOut" in result;
49
- }
50
- /**
51
- * Bash tool implementation.
52
- *
53
- * Executes bash commands with optional timeout and working directory.
54
- */
55
45
  export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, Theme> {
56
46
  readonly name = "bash";
57
47
  readonly label = "Bash";
@@ -71,11 +61,9 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, T
71
61
  ): Promise<AgentToolResult<BashToolDetails>> {
72
62
  let command = rawCommand;
73
63
 
74
- // Only apply explicit head/tail params from tool input.
75
64
  const headLines = head;
76
65
  const tailLines = tail;
77
66
 
78
- // Check interception if enabled and available tools are known
79
67
  if (this.session.settings.get("bashInterceptor.enabled")) {
80
68
  const rules = this.session.settings.getBashInterceptorRules();
81
69
  const interception = checkBashInterception(command, ctx?.toolNames ?? [], rules);
@@ -103,14 +91,11 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, T
103
91
  throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
104
92
  }
105
93
 
106
- // Clamp to reasonable range: 1s - 3600s (1 hour)
107
94
  const timeoutSec = Math.max(1, Math.min(3600, rawTimeout));
108
95
  const timeoutMs = timeoutSec * 1000;
109
96
 
110
- // Track output for streaming updates (tail only)
111
97
  const tailBuffer = createTailBuffer(DEFAULT_MAX_BYTES);
112
98
 
113
- // Set up artifacts environment and allocation
114
99
  const artifactsDir = this.session.getArtifactsDir?.();
115
100
  const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
116
101
  const { artifactPath, artifactId } = await allocateOutputArtifact(this.session, "bash");
@@ -148,16 +133,15 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, T
148
133
  }
149
134
  },
150
135
  });
136
+ if (result.timedOut) {
137
+ throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
138
+ }
151
139
  if (result.cancelled) {
152
140
  if (signal?.aborted) {
153
141
  throw new ToolAbortError(normalizeResultOutput(result) || "Command aborted");
154
142
  }
155
143
  throw new ToolError(normalizeResultOutput(result) || "Command aborted");
156
144
  }
157
- if (isInteractiveResult(result) && result.timedOut) {
158
- throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
159
- }
160
- // Apply head/tail filtering if specified
161
145
  let outputText = normalizeResultOutput(result);
162
146
  const headTailResult = applyHeadTail(outputText, headLines, tailLines);
163
147
  if (headTailResult.applied) {
@@ -171,7 +155,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, T
171
155
  if (result.exitCode === undefined) {
172
156
  throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
173
157
  }
174
- if (result.exitCode !== 0 && result.exitCode !== undefined) {
158
+ if (result.exitCode !== 0) {
175
159
  throw new ToolError(`${outputText}\n\nCommand exited with code ${result.exitCode}`);
176
160
  }
177
161