@yeaft/webchat-agent 0.1.511 → 0.1.512

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.511",
3
+ "version": "0.1.512",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -43,6 +43,47 @@ import { normalizeEffort } from './models.js';
43
43
  /** Maximum auto-continue turns when stopReason is 'max_tokens'. */
44
44
  const MAX_CONTINUE_TURNS = 3;
45
45
 
46
+ /**
47
+ * task-331 — Map a conversationMessages entry into the snapshot shape used
48
+ * by `debug_turn.messages`. Preserves the function-calling metadata that
49
+ * the Debug panel needs to render:
50
+ * - `toolCalls` on assistant turns (the LLM's function_call requests)
51
+ * - `toolCallId` + `isError` on tool turns (the paired tool_result)
52
+ *
53
+ * Content is truncated at 50000 chars; each tool_call input is JSON-stringified
54
+ * + sliced at 10000 chars before being re-parsed, so a runaway `input` blob
55
+ * can't blow past the WebSocket frame budget. Unknown roles pass through
56
+ * unchanged.
57
+ *
58
+ * Pure function — no side effects on the input message.
59
+ *
60
+ * @param {{ role: string, content?: any, toolCalls?: Array, toolCallId?: string, isError?: boolean }} m
61
+ * @returns {{ role: string, content: any, toolCalls?: Array, toolCallId?: string, isError?: boolean }}
62
+ */
63
+ export function mapDebugMessage(m) {
64
+ const out = { role: m.role };
65
+ out.content = typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content;
66
+ if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
67
+ out.toolCalls = m.toolCalls.map(tc => {
68
+ let input = tc.input;
69
+ try {
70
+ const s = JSON.stringify(input);
71
+ if (typeof s === 'string' && s.length > 10000) {
72
+ input = { __truncated: true, preview: s.slice(0, 10000) };
73
+ }
74
+ } catch {
75
+ // Non-serializable input — fall through with raw reference; the
76
+ // frontend's JSON.stringify will hit the same failure and replace
77
+ // it with a placeholder string.
78
+ }
79
+ return { id: tc.id, name: tc.name, input };
80
+ });
81
+ }
82
+ if (m.toolCallId) out.toolCallId = m.toolCallId;
83
+ if (m.isError != null) out.isError = m.isError;
84
+ return out;
85
+ }
86
+
46
87
  // ─── Engine Events (superset of adapter events) ──────────────────
47
88
 
48
89
  /**
@@ -289,6 +330,13 @@ export class Engine {
289
330
  conversationStore: this.#conversationStore,
290
331
  adapter: this.#adapter,
291
332
  config: this.#config,
333
+ // ViewImage (task-333b PR-B rev-3 P1-A): expose size cap + allowlist
334
+ // via tool ctx so hosts can override via ~/.yeaft/config.json without
335
+ // touching the tool impl.
336
+ maxImageBytes: this.#config?.unify?.maxImageBytes,
337
+ imageAllowlist: Array.isArray(this.#config?.unify?.imageAllowlist)
338
+ ? this.#config.unify.imageAllowlist
339
+ : [],
292
340
  };
293
341
  }
294
342
 
@@ -508,8 +556,8 @@ export class Engine {
508
556
 
509
557
  // ─── Pre-query: Memory Injection (task-287) + Compact Summary ──
510
558
  // New layout: always inject Memory Index + user-preferences + project
511
- // header excerpt. No per-turn fuzzy recall — LLM calls memory_search /
512
- // memory_query on demand.
559
+ // header excerpt. No per-turn fuzzy recall — LLM calls memory_load /
560
+ // memory_query on demand (memory_search still works as a deprecated alias).
513
561
  let memoryInjection = '';
514
562
  if (this.#yeaftDir) {
515
563
  try {
@@ -634,7 +682,7 @@ export class Engine {
634
682
  turnNumber,
635
683
  model: currentModel,
636
684
  systemPrompt,
637
- messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
685
+ messages: conversationMessages.map(mapDebugMessage),
638
686
  response: responseText || `Error: ${err.message}`,
639
687
  toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
640
688
  usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
@@ -702,12 +750,15 @@ export class Engine {
702
750
 
703
751
  // Emit debug_turn event for web UI debug panel
704
752
  // (conversationMessages does NOT yet include the assistant response at this point)
753
+ // task-331: preserve toolCalls / toolCallId / isError on each message so
754
+ // the Debug panel can render function_call requests and their paired
755
+ // tool_result responses across turns.
705
756
  yield {
706
757
  type: 'debug_turn',
707
758
  turnNumber,
708
759
  model: currentModel,
709
760
  systemPrompt,
710
- messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
761
+ messages: conversationMessages.map(mapDebugMessage),
711
762
  response: responseText,
712
763
  toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
713
764
  usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
@@ -265,7 +265,7 @@ export function renderIndex(yeaftDir, entryCount) {
265
265
  lines.push('## entries', '', `- ${entryCount} atomic entries (use memory_query to search)`, '');
266
266
 
267
267
  lines.push(
268
- '_Note: use the `memory_search` tool with one or more paths to load a classification',
268
+ '_Note: use the `memory_load` tool with one or more paths to load a classification',
269
269
  'file in full, or `memory_query` to search atomic entries by keywords/tags._',
270
270
  );
271
271
 
package/unify/prompts.js CHANGED
@@ -32,18 +32,53 @@ const TEMPLATES_DIR = join(__dirname, 'templates');
32
32
 
33
33
  /**
34
34
  * Read a template file from the templates/ directory.
35
- * Returns empty string if file doesn't exist or can't be read.
35
+ *
36
+ * task-332c F3 — missing-template guard:
37
+ * Required templates MUST be present. If a required template is missing or
38
+ * unreadable, throw a clear error instead of silently degrading to the
39
+ * hardcoded fallback. Silent skip previously hid misconfigured deployments
40
+ * (empty prompts shipped to production), so we now fail fast at load time.
41
+ *
42
+ * Non-required templates (passed with { required: false }) retain the old
43
+ * "return empty string on absence" behavior for optional inclusions.
44
+ *
36
45
  * @param {string} name — filename (e.g. 'base.md')
46
+ * @param {{ required?: boolean }} [opts]
37
47
  * @returns {string}
48
+ * @throws {Error} when required=true and the file is missing / unreadable / empty
38
49
  */
39
- function readTemplate(name) {
50
+ function readTemplate(name, { required = true } = {}) {
40
51
  const path = join(TEMPLATES_DIR, name);
41
- if (!existsSync(path)) return '';
52
+ if (!existsSync(path)) {
53
+ if (required) {
54
+ throw new Error(
55
+ `[prompts] Required template missing: ${name} ` +
56
+ `(expected at ${path}). Templates are part of the agent package — ` +
57
+ `check the install or build output.`
58
+ );
59
+ }
60
+ return '';
61
+ }
62
+ let content;
42
63
  try {
43
- return readFileSync(path, 'utf8').trim();
44
- } catch {
64
+ content = readFileSync(path, 'utf8');
65
+ } catch (e) {
66
+ if (required) {
67
+ throw new Error(
68
+ `[prompts] Required template unreadable: ${name} ` +
69
+ `(at ${path}): ${e.message}`
70
+ );
71
+ }
45
72
  return '';
46
73
  }
74
+ const trimmed = content.trim();
75
+ if (!trimmed && required) {
76
+ throw new Error(
77
+ `[prompts] Required template is empty: ${name} (at ${path}). ` +
78
+ `An empty system prompt template would ship a degenerate prompt to the LLM.`
79
+ );
80
+ }
81
+ return trimmed;
47
82
  }
48
83
 
49
84
  /**
@@ -20,7 +20,7 @@ import exitWorktree from './exit-worktree.js';
20
20
  import askUser from './ask-user.js';
21
21
  import memoryRead from './memory-read.js';
22
22
  import memoryWrite from './memory-write.js';
23
- import memorySearch from './memory-search.js';
23
+ import memorySearch, { memorySearchAlias } from './memory-search.js';
24
24
  import memoryQuery from './memory-query.js';
25
25
  import webSearch from './web-search.js';
26
26
  import webFetch from './web-fetch.js';
@@ -67,13 +67,14 @@ import {
67
67
  } from './thread-tools.js';
68
68
 
69
69
  // --- P2 Auxiliary tools ---
70
+ // task-333b L1 delete: ToolSearch and WriteStdin removed — the function-call
71
+ // schema already exposes all tools, so ToolSearch was redundant; WriteStdin
72
+ // was a stub returning a hint about Bash piping.
70
73
  import { jsRepl, jsReplReset } from './js-repl.js';
71
74
  import notebookEdit from './notebook-edit.js';
72
75
  import imageGeneration from './image-generation.js';
73
76
  import viewImage from './view-image.js';
74
- import toolSearch from './tool-search.js';
75
77
  import requestPermissions from './request-permissions.js';
76
- import writeStdin from './write-stdin.js';
77
78
 
78
79
  /**
79
80
  * All built-in tools, flattened into a single array.
@@ -92,6 +93,7 @@ export const allTools = [
92
93
  memoryRead,
93
94
  memoryWrite,
94
95
  memorySearch,
96
+ memorySearchAlias,
95
97
  memoryQuery,
96
98
  webSearch,
97
99
  webFetch,
@@ -139,9 +141,7 @@ export const allTools = [
139
141
  notebookEdit,
140
142
  imageGeneration,
141
143
  viewImage,
142
- toolSearch,
143
144
  requestPermissions,
144
- writeStdin,
145
145
  ];
146
146
 
147
147
  /**
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * Runs JavaScript code in a persistent VM context, allowing
5
5
  * state to be maintained across calls.
6
+ *
7
+ * task-333b: merged the former `JsReplReset` tool into this one as a
8
+ * `reset: true` parameter. A single tool with a reset flag eliminates a
9
+ * duplicate schema entry in the function-call catalogue. The old
10
+ * JsReplReset name is kept below as a deprecated alias for one release.
6
11
  */
7
12
 
8
13
  import { defineTool } from './types.js';
@@ -55,7 +60,9 @@ defined in one call are available in subsequent calls.
55
60
 
56
61
  Guidelines:
57
62
  - Use for calculations, data transformations, and quick experiments
58
- - State is preserved between calls (use JsReplReset to clear)
63
+ - State is preserved between calls
64
+ - Pass \`reset: true\` to wipe all state before evaluating (clean slate).
65
+ \`code\` becomes optional in this mode — pass reset alone to just clear.
59
66
  - console.log output is captured and returned
60
67
  - Returns the last expression's value plus any console output
61
68
  - No filesystem or network access from within the REPL`,
@@ -64,16 +71,27 @@ Guidelines:
64
71
  properties: {
65
72
  code: {
66
73
  type: 'string',
67
- description: 'JavaScript code to evaluate',
74
+ description: 'JavaScript code to evaluate. Optional when reset=true and you only want to clear state.',
75
+ },
76
+ reset: {
77
+ type: 'boolean',
78
+ description: 'When true, reset the REPL context BEFORE evaluating `code`. If `code` is omitted, just resets.',
68
79
  },
69
80
  },
70
- required: ['code'],
71
81
  },
72
82
  isConcurrencySafe: () => false,
73
83
  isReadOnly: () => true,
74
84
  async execute(input, ctx) {
75
- const { code } = input;
76
- if (!code) return JSON.stringify({ error: 'code is required' });
85
+ const { code, reset } = input || {};
86
+
87
+ if (reset) {
88
+ vmContext = null;
89
+ if (!code) {
90
+ return JSON.stringify({ success: true, message: 'REPL context reset' });
91
+ }
92
+ }
93
+
94
+ if (!code) return JSON.stringify({ error: 'code is required (or pass reset=true to clear state)' });
77
95
 
78
96
  const vmCtx = getContext();
79
97
 
@@ -87,6 +105,7 @@ Guidelines:
87
105
  const resultStr = result === undefined ? '' : String(result);
88
106
 
89
107
  const parts = [];
108
+ if (reset) parts.push('(REPL context reset)');
90
109
  if (output.length > 0) parts.push(output.join('\n'));
91
110
  if (resultStr) parts.push(`→ ${resultStr}`);
92
111
 
@@ -101,20 +120,28 @@ Guidelines:
101
120
  },
102
121
  });
103
122
 
123
+ /**
124
+ * Deprecated alias — `JsReplReset`. task-333b merged reset behaviour into
125
+ * `JsRepl` via `reset: true`. Kept registered for one release so older
126
+ * prompts / saved tool calls still resolve. Emits a one-time deprecation
127
+ * warning on first invocation, then delegates to jsRepl.execute with reset.
128
+ */
129
+ const _jsReplResetWarned = { v: false };
130
+ function warnJsReplResetDeprecated() {
131
+ if (_jsReplResetWarned.v) return;
132
+ _jsReplResetWarned.v = true;
133
+ // eslint-disable-next-line no-console
134
+ console.warn('[deprecated] JsReplReset → JsRepl. Call JsRepl with { reset: true } to clear REPL state.');
135
+ }
136
+
104
137
  export const jsReplReset = defineTool({
105
138
  name: 'JsReplReset',
106
- description: `Reset the JavaScript REPL environment.
107
-
108
- Clears all variables and state from previous evaluations.
109
- Use when you want a clean slate.`,
110
- parameters: {
111
- type: 'object',
112
- properties: {},
113
- },
139
+ description: 'DEPRECATED — use JsRepl with `reset: true` instead. Resets the persistent REPL context. Removal target: v0.2.0.',
140
+ parameters: { type: 'object', properties: {} },
114
141
  isConcurrencySafe: () => false,
115
142
  isReadOnly: () => false,
116
143
  async execute(input, ctx) {
117
- vmContext = null;
118
- return JSON.stringify({ success: true, message: 'REPL context reset' });
144
+ warnJsReplResetDeprecated();
145
+ return jsRepl.execute({ reset: true }, ctx);
119
146
  },
120
147
  });
@@ -10,7 +10,9 @@
10
10
  * 3. Returns a compact list suitable for LLM consumption
11
11
  *
12
12
  * Use this for fuzzy discovery over atomic entries. For loading a known
13
- * classification file in full, use `memory_search` instead.
13
+ * classification file in full, use `memory_load` instead (renamed from
14
+ * the old `memory_search` in task-333b; the old name still works as a
15
+ * deprecated alias for one release).
14
16
  */
15
17
 
16
18
  import { defineTool } from './types.js';
@@ -1,7 +1,12 @@
1
1
  /**
2
- * memory-search.js Load memory classification files by path.
2
+ * memory-search.js memory_load (task-333b rename).
3
3
  *
4
- * New-layout tool (task-287 memory refactor).
4
+ * This tool loads memory classification files by path (it is NOT a search).
5
+ * task-333b renamed it from `memory_search` → `memory_load` so the name
6
+ * actually reflects behaviour; `memory_query` remains the fuzzy search tool.
7
+ *
8
+ * Backwards compatibility: a thin alias tool named `memory_search` is also
9
+ * exported (see bottom of file) so older transcripts/prompts still resolve.
5
10
  *
6
11
  * The system prompt injects `index.md` every turn, which lists all available
7
12
  * classification files under `~/.yeaft/memory/` (single files + by-project /
@@ -9,9 +14,6 @@
9
14
  * it calls this tool with `paths: [...]` to load one or more of those files
10
15
  * in full.
11
16
  *
12
- * This is NOT a fuzzy search. It is a precise file loader. Use `memory_query`
13
- * for fuzzy search over atomic entries.
14
- *
15
17
  * Only paths under `memory/` are accepted. `..` segments are rejected.
16
18
  */
17
19
 
@@ -21,9 +23,7 @@ import { readMemoryFile, listClassificationFiles } from '../memory/layout.js';
21
23
  const MAX_FILES_PER_CALL = 5;
22
24
  const MAX_BYTES_PER_FILE = 32000;
23
25
 
24
- export default defineTool({
25
- name: 'memory_search',
26
- description: `Load one or more memory classification files in full.
26
+ const DESCRIPTION = `Load one or more memory classification files in full.
27
27
 
28
28
  Paths are relative to ~/.yeaft/memory/. Allowed targets:
29
29
  - user-preferences.md — merged user preferences
@@ -36,66 +36,105 @@ of available files. Use this tool when the index suggests a file is relevant
36
36
  to the user's current request. For fuzzy search over atomic memory entries
37
37
  (facts, lessons, preferences), use the memory_query tool instead.
38
38
 
39
- Up to ${MAX_FILES_PER_CALL} files per call. Each file is capped at ${MAX_BYTES_PER_FILE} bytes.`,
40
- parameters: {
41
- type: 'object',
42
- properties: {
43
- paths: {
44
- type: 'array',
45
- items: { type: 'string' },
46
- description: 'Relative paths under memory/. Example: ["by-project/claude-web-chat.md", "user-preferences.md"]',
47
- },
39
+ Up to ${MAX_FILES_PER_CALL} files per call. Each file is capped at ${MAX_BYTES_PER_FILE} bytes.`;
40
+
41
+ const PARAMETERS = {
42
+ type: 'object',
43
+ properties: {
44
+ paths: {
45
+ type: 'array',
46
+ items: { type: 'string' },
47
+ description: 'Relative paths under memory/. Example: ["by-project/claude-web-chat.md", "user-preferences.md"]',
48
48
  },
49
- required: ['paths'],
50
49
  },
51
- isConcurrencySafe: () => true,
52
- isReadOnly: () => true,
53
- async execute(input, ctx) {
54
- const yeaftDir = ctx?.yeaftDir;
55
- if (!yeaftDir) {
56
- return JSON.stringify({ error: 'Memory system not initialized (no yeaftDir in context)' });
57
- }
50
+ required: ['paths'],
51
+ };
58
52
 
59
- const paths = Array.isArray(input?.paths) ? input.paths : [];
60
- if (paths.length === 0) {
61
- return JSON.stringify({
62
- error: 'paths is required and must be a non-empty string array',
63
- availablePaths: listClassificationFiles(yeaftDir).map(f => f.path),
64
- });
53
+ async function executeLoad(input, ctx) {
54
+ const yeaftDir = ctx?.yeaftDir;
55
+ if (!yeaftDir) {
56
+ return JSON.stringify({ error: 'Memory system not initialized (no yeaftDir in context)' });
57
+ }
58
+
59
+ const paths = Array.isArray(input?.paths) ? input.paths : [];
60
+ if (paths.length === 0) {
61
+ return JSON.stringify({
62
+ error: 'paths is required and must be a non-empty string array',
63
+ availablePaths: listClassificationFiles(yeaftDir).map(f => f.path),
64
+ });
65
+ }
66
+
67
+ const results = [];
68
+ const errors = [];
69
+
70
+ for (const rel of paths.slice(0, MAX_FILES_PER_CALL)) {
71
+ if (typeof rel !== 'string' || !rel.trim()) {
72
+ errors.push({ path: rel, error: 'not a non-empty string' });
73
+ continue;
74
+ }
75
+ if (rel.includes('..') || rel.startsWith('/')) {
76
+ errors.push({ path: rel, error: 'path must be relative and must not contain ..' });
77
+ continue;
78
+ }
79
+ if (!rel.endsWith('.md')) {
80
+ errors.push({ path: rel, error: 'only .md files are supported' });
81
+ continue;
65
82
  }
66
83
 
67
- const results = [];
68
- const errors = [];
69
-
70
- for (const rel of paths.slice(0, MAX_FILES_PER_CALL)) {
71
- if (typeof rel !== 'string' || !rel.trim()) {
72
- errors.push({ path: rel, error: 'not a non-empty string' });
73
- continue;
74
- }
75
- if (rel.includes('..') || rel.startsWith('/')) {
76
- errors.push({ path: rel, error: 'path must be relative and must not contain ..' });
77
- continue;
78
- }
79
- if (!rel.endsWith('.md')) {
80
- errors.push({ path: rel, error: 'only .md files are supported' });
81
- continue;
82
- }
83
-
84
- const text = readMemoryFile(yeaftDir, rel);
85
- if (!text) {
86
- errors.push({ path: rel, error: 'file not found or empty' });
87
- continue;
88
- }
89
-
90
- const truncated = text.length > MAX_BYTES_PER_FILE;
91
- results.push({
92
- path: rel,
93
- content: truncated ? text.slice(0, MAX_BYTES_PER_FILE) : text,
94
- truncated,
95
- size: text.length,
96
- });
84
+ const text = readMemoryFile(yeaftDir, rel);
85
+ if (!text) {
86
+ errors.push({ path: rel, error: 'file not found or empty' });
87
+ continue;
97
88
  }
98
89
 
99
- return JSON.stringify({ results, errors }, null, 2);
100
- },
90
+ const truncated = text.length > MAX_BYTES_PER_FILE;
91
+ results.push({
92
+ path: rel,
93
+ content: truncated ? text.slice(0, MAX_BYTES_PER_FILE) : text,
94
+ truncated,
95
+ size: text.length,
96
+ });
97
+ }
98
+
99
+ return JSON.stringify({ results, errors }, null, 2);
100
+ }
101
+
102
+ /**
103
+ * Canonical tool — `memory_load`. This is the default export so existing
104
+ * import sites (`import memorySearch from './memory-search.js'`) keep
105
+ * working; the renamed identity is expressed via the tool's `name`.
106
+ */
107
+ const memoryLoad = defineTool({
108
+ name: 'memory_load',
109
+ description: DESCRIPTION,
110
+ parameters: PARAMETERS,
111
+ isConcurrencySafe: () => true,
112
+ isReadOnly: () => true,
113
+ execute: executeLoad,
114
+ });
115
+
116
+ /**
117
+ * Deprecated alias — `memory_search`. Kept so older prompts / saved tool
118
+ * calls still resolve. Delegates to the same executor. Do not use for new
119
+ * call sites. Emits a one-time console.warn on first invocation.
120
+ */
121
+ const _memSearchWarned = { v: false };
122
+ async function executeLoadWithWarn(input, ctx) {
123
+ if (!_memSearchWarned.v) {
124
+ _memSearchWarned.v = true;
125
+ // eslint-disable-next-line no-console
126
+ console.warn('[deprecated] memory_search → memory_load. Use memory_load for path-based file loading; use memory_query for fuzzy keyword search.');
127
+ }
128
+ return executeLoad(input, ctx);
129
+ }
130
+
131
+ export const memorySearchAlias = defineTool({
132
+ name: 'memory_search',
133
+ description: 'DEPRECATED — use memory_load. Same params. Removal target: v0.2.0.',
134
+ parameters: PARAMETERS,
135
+ isConcurrencySafe: () => true,
136
+ isReadOnly: () => true,
137
+ execute: executeLoadWithWarn,
101
138
  });
139
+
140
+ export default memoryLoad;
@@ -48,7 +48,9 @@ export const taskCreate = defineTool({
48
48
 
49
49
  Tasks have a title, description, priority, and status.
50
50
  Each task gets its own folder with task.md, progress.md, and memory.md.
51
- Use this to break down complex work into trackable items.`,
51
+ Use this to break down complex work into trackable items.
52
+
53
+ Pass \`parent_id\` to create a subtask under an existing task.`,
52
54
  parameters: {
53
55
  type: 'object',
54
56
  properties: {
@@ -69,6 +71,10 @@ Use this to break down complex work into trackable items.`,
69
71
  type: 'string',
70
72
  description: 'Parent task ID for subtasks',
71
73
  },
74
+ // Note (task-333b): `parent_task_id` is accepted by execute() as a
75
+ // soft-compat alias for `parent_id` (absorbed from the former
76
+ // SpawnTask tool) but intentionally NOT advertised in the schema to
77
+ // avoid giving the LLM two live params for one field.
72
78
  },
73
79
  required: ['title'],
74
80
  },
@@ -78,9 +84,17 @@ Use this to break down complex work into trackable items.`,
78
84
  const err = requireStore();
79
85
  if (err) return err;
80
86
 
81
- const { title, description, priority = 'medium', parent_id } = input;
87
+ const { title, description, priority = 'medium', parent_id, parent_task_id } = input;
82
88
  if (!title) return JSON.stringify({ error: 'title is required' });
83
89
 
90
+ // task-333b: accept either `parent_id` (original TaskCreate field) or
91
+ // `parent_task_id` (the former SpawnTask field). When both are present,
92
+ // parent_id wins.
93
+ const parentId = parent_id || parent_task_id || null;
94
+ if (parentId && !taskStore.get(parentId)) {
95
+ return JSON.stringify({ error: `Parent task not found: ${parentId}` });
96
+ }
97
+
84
98
  const id = `task-${randomUUID().slice(0, 8)}`;
85
99
  const task = {
86
100
  id,
@@ -88,7 +102,8 @@ Use this to break down complex work into trackable items.`,
88
102
  description: description || '',
89
103
  priority,
90
104
  status: 'pending',
91
- parentId: parent_id || null,
105
+ parentId,
106
+ parentTaskId: parentId, // design §5 canonical field
92
107
  createdAt: Date.now(),
93
108
  updatedAt: Date.now(),
94
109
  };
@@ -97,8 +112,10 @@ Use this to break down complex work into trackable items.`,
97
112
 
98
113
  return JSON.stringify({
99
114
  success: true,
100
- task: { id, title, priority, status: 'pending' },
101
- message: `Task created: ${title} (${id})`,
115
+ task: { id, title, priority, status: 'pending', parentTaskId: parentId },
116
+ message: parentId
117
+ ? `Subtask created: ${title} (${id}) under ${parentId}`
118
+ : `Task created: ${title} (${id})`,
102
119
  });
103
120
  },
104
121
  });
@@ -193,20 +193,24 @@ the same thread.`,
193
193
  },
194
194
  });
195
195
 
196
- // ─── SpawnTask (mergedsubtask via parent_task_id) ────
196
+ // ─── SpawnTask (deprecated alias task-333b) ──────────────
197
+ //
198
+ // task-333b folded SpawnTask into TaskCreate (see task-tools.js). Kept
199
+ // registered for one release as a deprecated alias per PM constraint:
200
+ // LLM calls still resolve, but a one-time console.warn nudges migration.
201
+ // Prefer TaskCreate with `parent_task_id` for all new call sites.
202
+
203
+ const _spawnTaskWarned = { v: false };
204
+ function warnSpawnTaskDeprecated() {
205
+ if (_spawnTaskWarned.v) return;
206
+ _spawnTaskWarned.v = true;
207
+ // eslint-disable-next-line no-console
208
+ console.warn('[deprecated] SpawnTask → TaskCreate. Pass parent_task_id to TaskCreate for subtasks.');
209
+ }
197
210
 
198
- /**
199
- * SpawnTask replaces both the old SpawnTask and the separate SpawnSubtask:
200
- * pass `parent_task_id` when you want a subtask, omit it for top-level.
201
- * Per prev-1 rework note: eliminating two tools with the same effect.
202
- */
203
211
  export const spawnTask = defineTool({
204
212
  name: 'SpawnTask',
205
- description: `Spawn a new task. Pass parent_task_id to create a subtask.
206
-
207
- When parent_task_id is omitted → top-level task.
208
- When parent_task_id is provided → subtask under that parent (parent must exist).
209
- This replaces the deprecated SpawnSubtask tool.`,
213
+ description: `DEPRECATED — use TaskCreate with parent_id instead. Retained as a thin alias for backwards compatibility; delegates to the same task store. When parent_task_id is omitted this behaves like TaskCreate; when provided it creates a subtask under that parent (parent must exist). Removal target: v0.2.0.`,
210
214
  parameters: {
211
215
  type: 'object',
212
216
  properties: {
@@ -226,6 +230,7 @@ This replaces the deprecated SpawnSubtask tool.`,
226
230
  isConcurrencySafe: () => false,
227
231
  isReadOnly: () => false,
228
232
  async execute(input) {
233
+ warnSpawnTaskDeprecated();
229
234
  const store = getTaskStore();
230
235
  if (!store) {
231
236
  return JSON.stringify({ error: 'Task store not initialized. Session may still be loading.' });
@@ -1,31 +1,68 @@
1
1
  /**
2
- * view-image.js — Read and describe an image file.
2
+ * view-image.js — Load a local image file into the LLM's context.
3
3
  *
4
- * Returns image metadata (dimensions, format, size).
5
- * In a full multimodal integration, would pass the image to the LLM.
4
+ * task-333b PR-B: upgraded from metadata-only stub to a real multimodal
5
+ * loader. Returns a tool result that includes:
6
+ * - `image`: a base64-encoded data URI suitable for embedding into an
7
+ * LLM image content block (OpenAI / Anthropic style)
8
+ * - `media_type`: the canonical MIME (image/png, image/jpeg, ...)
9
+ * - `format`, `width`, `height`, `size`, `sizeFormatted`, `path`
10
+ *
11
+ * Safety rules (per PM 乔布斯 PR-B spec + prev-3 product review):
12
+ * - Path safety: no `..`, no absolute paths escaping cwd unless the
13
+ * resolved path lives under ctx.imageAllowlist[] (absolute dirs
14
+ * provided by the host).
15
+ * - Size cap: configurable via ctx.maxImageBytes (default 20 MiB).
16
+ * Larger files are rejected with a self-correcting error message
17
+ * that nudges resize/crop or config.json tuning.
18
+ * - MIME whitelist: png / jpeg / gif / webp / jfif. SVG / BMP / ICO /
19
+ * TIFF are intentionally excluded — they either aren't multimodal-
20
+ * LLM-safe (SVG = embedded script surface) or aren't supported by
21
+ * the mainstream vision endpoints.
22
+ * - HEIC is special-cased: we cannot decode it server-side, but the
23
+ * error nudges the user to convert via `sips -s format jpeg` (mac)
24
+ * instead of a generic "Unsupported format".
6
25
  */
7
26
 
8
27
  import { defineTool } from './types.js';
9
28
  import { stat, readFile } from 'fs/promises';
10
29
  import { existsSync } from 'fs';
11
- import { resolve, extname } from 'path';
30
+ import { resolve, extname, isAbsolute, relative } from 'path';
12
31
 
13
- /** Supported image formats. */
14
- const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg', '.ico']);
32
+ /** Default max image size in bytes (20 MiB). Override via ctx.maxImageBytes. */
33
+ const DEFAULT_MAX_IMAGE_BYTES = 20 * 1024 * 1024;
15
34
 
16
35
  /**
17
- * Parse basic image dimensions from headers.
36
+ * Extension canonical MIME type.
37
+ * The keys are the whitelist; anything else is rejected.
38
+ * `.jfif` (common Windows paste extension) maps to image/jpeg.
39
+ */
40
+ const EXT_TO_MIME = Object.freeze({
41
+ '.png': 'image/png',
42
+ '.jpg': 'image/jpeg',
43
+ '.jpeg': 'image/jpeg',
44
+ '.jfif': 'image/jpeg',
45
+ '.gif': 'image/gif',
46
+ '.webp': 'image/webp',
47
+ });
48
+
49
+ const ALLOWED_EXTS = Object.keys(EXT_TO_MIME);
50
+
51
+ /**
52
+ * Parse basic image dimensions from header bytes. Best-effort — returns
53
+ * null if the file is too short or the format isn't one we parse.
54
+ *
55
+ * @param {Buffer} buffer
56
+ * @param {string} ext — lowercase extension including the dot
18
57
  */
19
58
  function parseImageDimensions(buffer, ext) {
20
59
  try {
21
- if (ext === '.png') {
60
+ if (ext === '.png' && buffer.length >= 24) {
22
61
  // PNG: width at offset 16, height at 20 (big-endian 32-bit)
23
- if (buffer.length >= 24) {
24
- return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
25
- }
62
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
26
63
  }
27
- if (ext === '.jpg' || ext === '.jpeg') {
28
- // JPEG: scan for SOF0 marker (0xFF 0xC0)
64
+ if ((ext === '.jpg' || ext === '.jpeg' || ext === '.jfif') && buffer.length > 10) {
65
+ // JPEG: scan for SOF0 (0xFFC0) / SOF2 (0xFFC2) marker
29
66
  for (let i = 0; i < buffer.length - 9; i++) {
30
67
  if (buffer[i] === 0xFF && (buffer[i + 1] === 0xC0 || buffer[i + 1] === 0xC2)) {
31
68
  return {
@@ -35,30 +72,117 @@ function parseImageDimensions(buffer, ext) {
35
72
  }
36
73
  }
37
74
  }
38
- if (ext === '.gif') {
75
+ if (ext === '.gif' && buffer.length >= 10) {
39
76
  // GIF: width at offset 6, height at 8 (little-endian 16-bit)
40
- if (buffer.length >= 10) {
41
- return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
77
+ return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
78
+ }
79
+ if (ext === '.webp' && buffer.length >= 30) {
80
+ // WEBP: RIFF...WEBP...VP8(L|X| ). Three common sub-chunks.
81
+ if (buffer.slice(0, 4).toString('ascii') === 'RIFF' &&
82
+ buffer.slice(8, 12).toString('ascii') === 'WEBP') {
83
+ const fourcc = buffer.slice(12, 16).toString('ascii');
84
+ if (fourcc === 'VP8 ' && buffer.length >= 30) {
85
+ // Lossy: width/height at 26/28 as 14-bit LE (mask 0x3FFF)
86
+ return {
87
+ width: buffer.readUInt16LE(26) & 0x3FFF,
88
+ height: buffer.readUInt16LE(28) & 0x3FFF,
89
+ };
90
+ }
91
+ if (fourcc === 'VP8L' && buffer.length >= 25) {
92
+ // Lossless: packed 14+14 bits at offset 21
93
+ const b0 = buffer[21], b1 = buffer[22], b2 = buffer[23], b3 = buffer[24];
94
+ const width = 1 + (((b1 & 0x3F) << 8) | b0);
95
+ const height = 1 + (((b3 & 0x0F) << 10) | (b2 << 2) | ((b1 & 0xC0) >> 6));
96
+ return { width, height };
97
+ }
98
+ if (fourcc === 'VP8X' && buffer.length >= 30) {
99
+ // Extended: 24-bit LE widths/heights at 24/27, stored as (n-1)
100
+ const width = 1 + (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16));
101
+ const height = 1 + (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16));
102
+ return { width, height };
103
+ }
42
104
  }
43
105
  }
44
106
  } catch {
45
- // Dimension parsing is best-effort
107
+ // Dimension parsing is best-effort only.
46
108
  }
47
109
  return null;
48
110
  }
49
111
 
112
+ /**
113
+ * Check whether `absPath` is allowed given a project `cwd` and an optional
114
+ * allowlist of absolute directories. Returns `null` on success, or an object
115
+ * `{ kind, message }` describing the failure. The `kind` field lets callers
116
+ * tailor the error text (see prev-3 P2: distinguish "absolute path outside
117
+ * project" from "relative path containing ..").
118
+ */
119
+ function checkPathAllowed(absPath, cwd, allowlist) {
120
+ // Reject if the resolved path lives inside the project (good).
121
+ const relToCwd = relative(cwd, absPath);
122
+ const insideCwd = relToCwd && !relToCwd.startsWith('..') && !isAbsolute(relToCwd);
123
+ if (insideCwd) return null;
124
+
125
+ // Otherwise must match an allowlist entry.
126
+ if (Array.isArray(allowlist) && allowlist.length > 0) {
127
+ for (const dir of allowlist) {
128
+ if (typeof dir !== 'string' || !isAbsolute(dir)) continue;
129
+ const rel = relative(dir, absPath);
130
+ if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return null;
131
+ }
132
+ }
133
+
134
+ return {
135
+ kind: 'path_outside',
136
+ message:
137
+ 'Path is outside the project directory and not on the image allowlist. ' +
138
+ 'Either move the file into the project, or ask the user to add its parent ' +
139
+ 'directory to ctx.imageAllowlist (set via ~/.yeaft/config.json imageAllowlist[]).',
140
+ };
141
+ }
142
+
143
+ function formatBytes(n) {
144
+ if (n < 1024) return `${n}B`;
145
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
146
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
147
+ }
148
+
50
149
  export default defineTool({
51
150
  name: 'ViewImage',
52
- description: `View an image file and get its metadata.
151
+ description: `Load a local image file and attach it to the conversation so the LLM can see it.
152
+
153
+ Returns a base64 data URI (\`image\` field) plus metadata (format, dimensions,
154
+ size). The caller/bridge is responsible for turning the data URI into the
155
+ provider-specific image content block.
156
+
157
+ When to call:
158
+ - User references a local image path (screenshot, design, log/chart) and
159
+ asks you to read, analyse, or describe it.
160
+ - User says "look at this file" / "check the screenshot at ..." / "what's
161
+ in docs/assets/arch.png?".
162
+
163
+ When NOT to call:
164
+ - The image is already attached to the current message (the host has
165
+ already uploaded it — you can see it without this tool).
166
+ - The image is a remote URL (http/https). ViewImage only reads local
167
+ files; use a fetch-style tool for URLs.
168
+ - You only need the file's existence / mtime / size — use Read or a
169
+ filesystem tool instead; ViewImage loads the full bytes into memory.
53
170
 
54
- Returns image format, dimensions, and file size.
55
- Supports PNG, JPEG, GIF, BMP, WebP, SVG, and ICO.`,
171
+ Path examples:
172
+ - Relative (resolved against project cwd): "./screenshots/bug.png",
173
+ "docs/assets/arch.png"
174
+ - Absolute inside an allowlisted dir: "/home/user/Downloads/error.png"
175
+ (only works when the host added that dir to ctx.imageAllowlist)
176
+
177
+ Supported formats: PNG, JPEG (.jpg/.jpeg/.jfif), GIF, WebP.
178
+ Max size: 20 MiB by default (configurable via ctx.maxImageBytes).
179
+ Path must live under the project directory or an explicit host allowlist.`,
56
180
  parameters: {
57
181
  type: 'object',
58
182
  properties: {
59
183
  file_path: {
60
184
  type: 'string',
61
- description: 'Path to the image file',
185
+ description: 'Path to the image file. Relative paths are resolved against the project cwd.',
62
186
  },
63
187
  },
64
188
  required: ['file_path'],
@@ -66,51 +190,126 @@ Supports PNG, JPEG, GIF, BMP, WebP, SVG, and ICO.`,
66
190
  isConcurrencySafe: () => true,
67
191
  isReadOnly: () => true,
68
192
  async execute(input, ctx) {
69
- const { file_path } = input;
70
- if (!file_path) return JSON.stringify({ error: 'file_path is required' });
193
+ const { file_path } = input || {};
194
+ if (!file_path || typeof file_path !== 'string') {
195
+ return JSON.stringify({ error: 'file_path is required and must be a string' });
196
+ }
197
+
198
+ // Reject `..` segments explicitly before path resolution — catches the
199
+ // cases where resolve() might still land inside cwd by accident. This
200
+ // also gives LLMs a self-correcting error ("don't use ../") distinct
201
+ // from the "path outside project" message for absolute paths.
202
+ if (file_path.split(/[/\\]/).some(seg => seg === '..')) {
203
+ return JSON.stringify({
204
+ error:
205
+ 'file_path must not contain `..` segments. Use a path relative to the ' +
206
+ 'project (e.g. "docs/assets/foo.png") or an absolute path under an ' +
207
+ 'allowlisted directory.',
208
+ });
209
+ }
71
210
 
72
211
  const cwd = ctx?.cwd || process.cwd();
212
+ const allowlist = Array.isArray(ctx?.imageAllowlist) ? ctx.imageAllowlist : [];
213
+ // Size cap: ctx.maxImageBytes (host-injected from config.json) wins,
214
+ // falling back to 20 MiB. A non-finite / non-positive override is ignored.
215
+ const maxBytes =
216
+ Number.isFinite(ctx?.maxImageBytes) && ctx.maxImageBytes > 0
217
+ ? Math.floor(ctx.maxImageBytes)
218
+ : DEFAULT_MAX_IMAGE_BYTES;
73
219
  const absPath = resolve(cwd, file_path);
74
220
 
75
- if (!existsSync(absPath)) {
76
- return JSON.stringify({ error: `Image not found: ${absPath}` });
221
+ const pathErr = checkPathAllowed(absPath, cwd, allowlist);
222
+ if (pathErr) {
223
+ // prev-3 P2: split "absolute outside project" from "relative ..".
224
+ // The `..` case is already handled above, so anything reaching here
225
+ // is either an absolute path outside cwd/allowlist or a relative
226
+ // path that resolve() pushed outside cwd (rare). Either way, the
227
+ // host-level fix is the same, so we keep one nudge message.
228
+ const isAbs = isAbsolute(file_path);
229
+ const hint = isAbs
230
+ ? 'Absolute path is outside the project directory. '
231
+ : 'Resolved path is outside the project directory. ';
232
+ return JSON.stringify({
233
+ error: hint + pathErr.message,
234
+ path: absPath,
235
+ });
77
236
  }
78
237
 
79
238
  const ext = extname(absPath).toLowerCase();
80
- if (!IMAGE_EXTS.has(ext)) {
81
- return JSON.stringify({ error: `Not a recognized image format: ${ext}` });
239
+ // HEIC special-case: iPhone screenshots default to HEIC and silently
240
+ // fail today. Give users a concrete one-liner to fix it instead of a
241
+ // generic "Unsupported format".
242
+ if (ext === '.heic' || ext === '.heif') {
243
+ return JSON.stringify({
244
+ error:
245
+ 'HEIC images need to be converted to JPEG first. ' +
246
+ 'Use `sips -s format jpeg <file> --out <file>.jpg` on macOS ' +
247
+ '(or an equivalent tool like ImageMagick on Linux/Windows), then retry.',
248
+ format: ext.slice(1).toUpperCase(),
249
+ supported: ALLOWED_EXTS,
250
+ });
251
+ }
252
+ if (!(ext in EXT_TO_MIME)) {
253
+ return JSON.stringify({
254
+ error: `Unsupported image format: ${ext || '(none)'}`,
255
+ supported: ALLOWED_EXTS,
256
+ });
82
257
  }
83
258
 
259
+ if (!existsSync(absPath)) {
260
+ return JSON.stringify({ error: `Image not found: ${absPath}` });
261
+ }
262
+
263
+ let fileStat;
84
264
  try {
85
- const fileStat = await stat(absPath);
86
- const buffer = await readFile(absPath);
265
+ fileStat = await stat(absPath);
266
+ } catch (err) {
267
+ return JSON.stringify({ error: `Failed to stat image: ${err.message}` });
268
+ }
87
269
 
88
- const dimensions = parseImageDimensions(buffer, ext);
270
+ if (!fileStat.isFile()) {
271
+ return JSON.stringify({ error: 'file_path does not point to a regular file' });
272
+ }
89
273
 
90
- const result = {
91
- path: absPath,
92
- format: ext.slice(1).toUpperCase(),
274
+ if (fileStat.size > maxBytes) {
275
+ return JSON.stringify({
276
+ error:
277
+ `Image exceeds ${formatBytes(maxBytes)} (${formatBytes(fileStat.size)} actual). ` +
278
+ `Reduce image size (resize/crop), or set \`maxImageBytes\` in ` +
279
+ `~/.yeaft/config.json if your LLM supports more.`,
93
280
  size: fileStat.size,
94
- sizeFormatted: fileStat.size < 1024 ? `${fileStat.size}B`
95
- : fileStat.size < 1024 * 1024 ? `${(fileStat.size / 1024).toFixed(1)}KB`
96
- : `${(fileStat.size / 1024 / 1024).toFixed(1)}MB`,
97
- modified: fileStat.mtime.toISOString(),
98
- };
99
-
100
- if (dimensions) {
101
- result.width = dimensions.width;
102
- result.height = dimensions.height;
103
- }
104
-
105
- // For SVG, include a text preview
106
- if (ext === '.svg') {
107
- const svgText = buffer.toString('utf-8');
108
- result.preview = svgText.slice(0, 500);
109
- }
281
+ maxSize: maxBytes,
282
+ });
283
+ }
110
284
 
111
- return JSON.stringify(result, null, 2);
285
+ let buffer;
286
+ try {
287
+ buffer = await readFile(absPath);
112
288
  } catch (err) {
113
289
  return JSON.stringify({ error: `Failed to read image: ${err.message}` });
114
290
  }
291
+
292
+ const mediaType = EXT_TO_MIME[ext];
293
+ const base64 = buffer.toString('base64');
294
+ const dataUri = `data:${mediaType};base64,${base64}`;
295
+ const dimensions = parseImageDimensions(buffer, ext);
296
+
297
+ const result = {
298
+ path: absPath,
299
+ format: ext.slice(1).toUpperCase() === 'JPG' ? 'JPEG' : ext.slice(1).toUpperCase(),
300
+ media_type: mediaType,
301
+ size: fileStat.size,
302
+ sizeFormatted: formatBytes(fileStat.size),
303
+ modified: fileStat.mtime.toISOString(),
304
+ image: dataUri,
305
+ };
306
+ // Normalise .jfif to JPEG in display format too, for consistency.
307
+ if (ext === '.jfif') result.format = 'JPEG';
308
+ if (dimensions) {
309
+ result.width = dimensions.width;
310
+ result.height = dimensions.height;
311
+ }
312
+
313
+ return JSON.stringify(result, null, 2);
115
314
  },
116
315
  });
@@ -1,88 +0,0 @@
1
- /**
2
- * tool-search.js — Search available tools by name or description.
3
- *
4
- * task-311: chat/work mode was removed in task-297; this tool no longer
5
- * accepts or reports a `modes` filter. Results come back as plain
6
- * { name, description } pairs.
7
- */
8
-
9
- import { defineTool } from './types.js';
10
-
11
- export default defineTool({
12
- name: 'ToolSearch',
13
- description: `Search available tools by name or description keyword.
14
-
15
- Use when you're unsure which tool to use for a task.
16
- Returns matching tools with their descriptions.`,
17
- parameters: {
18
- type: 'object',
19
- properties: {
20
- query: {
21
- type: 'string',
22
- description: 'Search keyword to match against tool names and descriptions',
23
- },
24
- },
25
- required: ['query'],
26
- },
27
- isConcurrencySafe: () => true,
28
- isReadOnly: () => true,
29
- async execute(input, _ctx) {
30
- const { query } = input;
31
- if (!query) return JSON.stringify({ error: 'query is required' });
32
-
33
- const lowerQuery = query.toLowerCase();
34
-
35
- // Self-referential catalogue of tools available to the engine.
36
- const toolList = [
37
- { name: 'AskUser', description: 'Ask the user a question' },
38
- { name: 'MemoryRead', description: 'Read from memory system' },
39
- { name: 'MemoryWrite', description: 'Write to memory system' },
40
- { name: 'MemorySearch', description: 'Search memory entries' },
41
- { name: 'WebSearch', description: 'Search the web' },
42
- { name: 'WebFetch', description: 'Fetch web page content' },
43
- { name: 'HistorySearch', description: 'Search conversation history' },
44
- { name: 'Bash', description: 'Execute shell commands' },
45
- { name: 'FileRead', description: 'Read file with line numbers' },
46
- { name: 'FileWrite', description: 'Write/create files' },
47
- { name: 'FileEdit', description: 'Surgical string replacement in files' },
48
- { name: 'Glob', description: 'Find files by pattern' },
49
- { name: 'Grep', description: 'Search file contents' },
50
- { name: 'ListDir', description: 'List directory contents' },
51
- { name: 'ApplyPatch', description: 'Apply unified diff patches' },
52
- { name: 'Agent', description: 'Create sub-agents' },
53
- { name: 'SendMessage', description: 'Send message to sub-agent' },
54
- { name: 'WaitAgent', description: 'Wait for sub-agent result' },
55
- { name: 'CloseAgent', description: 'Close a sub-agent' },
56
- { name: 'ListAgents', description: 'List all sub-agents' },
57
- { name: 'TaskCreate', description: 'Create a task' },
58
- { name: 'TaskUpdate', description: 'Update task status' },
59
- { name: 'TaskList', description: 'List all tasks' },
60
- { name: 'TaskGet', description: 'Get task details' },
61
- { name: 'FollowupTask', description: 'Create follow-up task' },
62
- { name: 'UpdatePlan', description: 'View/update execution plan' },
63
- { name: 'JsRepl', description: 'JavaScript REPL evaluation' },
64
- { name: 'JsReplReset', description: 'Reset REPL state' },
65
- { name: 'NotebookEdit', description: 'Edit Jupyter notebooks' },
66
- { name: 'ImageGeneration', description: 'Generate images from text' },
67
- { name: 'ViewImage', description: 'View image metadata' },
68
- { name: 'RequestPermissions', description: 'Request dangerous operation permissions' },
69
- { name: 'WriteStdin', description: 'Write to running process stdin' },
70
- { name: 'Skill', description: 'Load skills from library' },
71
- { name: 'EnterWorktree', description: 'Create git worktree' },
72
- { name: 'ExitWorktree', description: 'Exit git worktree' },
73
- { name: 'mcp_list_tools', description: 'List MCP server tools' },
74
- { name: 'mcp_call_tool', description: 'Call MCP server tool' },
75
- ];
76
-
77
- const results = toolList.filter(t =>
78
- t.name.toLowerCase().includes(lowerQuery) ||
79
- t.description.toLowerCase().includes(lowerQuery)
80
- );
81
-
82
- return JSON.stringify({
83
- results,
84
- totalResults: results.length,
85
- query,
86
- }, null, 2);
87
- },
88
- });
@@ -1,53 +0,0 @@
1
- /**
2
- * write-stdin.js — Write data to a running process's stdin.
3
- *
4
- * Used in conjunction with Bash for processes that need interactive input.
5
- * Currently returns a guidance message since Bash tool uses 'ignore' for stdin.
6
- */
7
-
8
- import { defineTool } from './types.js';
9
-
10
- export default defineTool({
11
- name: 'WriteStdin',
12
- description: `Write data to a running process's standard input.
13
-
14
- This tool is intended for sending input to interactive processes.
15
- Since the Bash tool runs commands non-interactively, this is primarily
16
- useful with the terminal system or long-running processes.
17
-
18
- Note: For most use cases, pipe input via Bash: echo "input" | command`,
19
- parameters: {
20
- type: 'object',
21
- properties: {
22
- process_id: {
23
- type: 'string',
24
- description: 'Process identifier or terminal ID',
25
- },
26
- data: {
27
- type: 'string',
28
- description: 'Data to write to stdin',
29
- },
30
- newline: {
31
- type: 'boolean',
32
- description: 'Append newline after data (default: true)',
33
- },
34
- },
35
- required: ['data'],
36
- },
37
- isConcurrencySafe: () => false,
38
- isReadOnly: () => false,
39
- async execute(input, ctx) {
40
- const { process_id, data, newline = true } = input;
41
- if (!data && data !== '') return JSON.stringify({ error: 'data is required' });
42
-
43
- // The Bash tool uses 'ignore' for stdin, so direct stdin writing
44
- // is only possible through the terminal system.
45
- // For most interactive needs, recommend using pipe syntax.
46
- return JSON.stringify({
47
- hint: 'The Bash tool does not support interactive stdin. Use pipe syntax instead:',
48
- example: `echo "${data}" | your_command`,
49
- alternativeBash: `printf '%s\\n' '${data.replace(/'/g, "'\\''")}' | your_command`,
50
- message: 'For interactive processes, use the terminal system (not the AI tool system).',
51
- });
52
- },
53
- });