@bahulam/code 0.1.2 → 0.1.3
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 +4 -7
- package/pulse/lib/tool-categories.ts +13 -0
- package/src/commands/device.mjs +121 -0
- package/src/commands/pair.mjs +190 -0
- package/src/commands/remote.mjs +110 -0
- package/src/core/event-log.mjs +393 -0
- package/src/core/headless.mjs +198 -0
- package/src/core/loop.mjs +276 -0
- package/src/core/memory-disk.mjs +210 -0
- package/src/core/paths.mjs +36 -0
- package/src/core/stream-client.mjs +28 -9
- package/src/core/tool-executor.mjs +56 -16
- package/src/daemon/approval-store.mjs +253 -0
- package/src/daemon/attach-client.mjs +361 -0
- package/src/daemon/daemonize.mjs +151 -0
- package/src/daemon/event-tap.mjs +197 -0
- package/src/daemon/input-lock.mjs +191 -0
- package/src/daemon/relay-client.mjs +258 -0
- package/src/daemon/session-core.mjs +179 -0
- package/src/daemon/session-list.mjs +26 -0
- package/src/daemon/session-publisher.mjs +78 -0
- package/src/daemon/socket-server.mjs +329 -0
- package/src/daemon/stop-daemon.mjs +18 -0
- package/src/permissions/checker.mjs +6 -6
- package/src/permissions/prompt.mjs +8 -7
- package/src/terminal/ansi.mjs +20 -3
- package/src/terminal/main.mjs +97 -3
- package/src/terminal/repl.mjs +201 -2
- package/src/tools/analyze-code.mjs +39 -0
- package/src/tools/bash.mjs +1 -1
- package/src/tools/edit.mjs +18 -18
- package/src/tools/git-diff.mjs +34 -0
- package/src/tools/git-status.mjs +30 -0
- package/src/tools/glob.mjs +5 -2
- package/src/tools/grep.mjs +1 -1
- package/src/tools/meta-tools.mjs +85 -0
- package/src/tools/read-files.mjs +37 -0
- package/src/tools/read.mjs +20 -10
- package/src/tools/registry.mjs +20 -0
- package/src/tools/remember.mjs +147 -0
- package/src/tools/search-files.mjs +41 -0
- package/src/tools/write-project.mjs +62 -0
- package/src/tools/write.mjs +1 -1
- package/src/ui/sub-agent.mjs +8 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent meta-tools: explore, plan, verify, debug, refactor.
|
|
3
|
+
*
|
|
4
|
+
* These are the LLM-visible peer-agent tools that map to gateway
|
|
5
|
+
* /v1/agent/subagent calls. For branch-01 they are registered with
|
|
6
|
+
* full schema parity to the Python originals (tool_bridge.py);
|
|
7
|
+
* the actual dispatch becomes a gateway call in a later phase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HANDOFF_SCHEMA = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
description: 'Optional compact handoff envelope. Pass known facts so the peer starts with context.',
|
|
13
|
+
properties: {
|
|
14
|
+
objective: { type: 'string' },
|
|
15
|
+
context: { type: 'string' },
|
|
16
|
+
files: { type: 'array', items: { type: 'object' } },
|
|
17
|
+
findings: { type: 'array', items: { type: 'object' } },
|
|
18
|
+
constraints: { type: 'array', items: { type: 'string' } },
|
|
19
|
+
acceptance_criteria: { type: 'array', items: { type: 'string' } },
|
|
20
|
+
open_questions: { type: 'array', items: { type: 'string' } },
|
|
21
|
+
output_contract: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Stub call — signals this is a gateway-dispatched sub-agent. */
|
|
26
|
+
async function stubCall(_input) {
|
|
27
|
+
// Actual dispatch goes through gateway /v1/agent/subagent in a later phase.
|
|
28
|
+
// The LLM sees this tool registered so the gateway can serve its schema.
|
|
29
|
+
return 'Sub-agent dispatch via gateway — not yet implemented client-side.';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function metaTool(name, description, params) {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
description,
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
task: { type: 'string', description: 'The task to delegate to this sub-agent.' },
|
|
40
|
+
handoff: HANDOFF_SCHEMA,
|
|
41
|
+
...params,
|
|
42
|
+
},
|
|
43
|
+
required: ['task'],
|
|
44
|
+
},
|
|
45
|
+
validateInput(input) { return input.task ? [] : ['task required']; },
|
|
46
|
+
async call(input) { return stubCall(input); },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ExploreTool = metaTool(
|
|
51
|
+
'explore',
|
|
52
|
+
'Read-only codebase investigation and orientation.',
|
|
53
|
+
{
|
|
54
|
+
thoroughness: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
enum: ['quick', 'medium', 'thorough'],
|
|
57
|
+
description: 'How deep to investigate.',
|
|
58
|
+
default: 'medium',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export const PlanTool = metaTool(
|
|
64
|
+
'plan',
|
|
65
|
+
'Sequencing, design, and risk analysis for multi-file changes.',
|
|
66
|
+
{},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const VerifyTool = metaTool(
|
|
70
|
+
'verify',
|
|
71
|
+
'Tests, lint, build checks, and evidence gathering.',
|
|
72
|
+
{},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
export const DebugTool = metaTool(
|
|
76
|
+
'debug',
|
|
77
|
+
'Failure reproduction and root-cause investigation.',
|
|
78
|
+
{},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const RefactorTool = metaTool(
|
|
82
|
+
'refactor',
|
|
83
|
+
'Mechanical broad cleanup, renames, and structure-only changes.',
|
|
84
|
+
{},
|
|
85
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read Files Tool — read multiple files at once (matches Python schema).
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const ReadFilesTool = {
|
|
7
|
+
name: 'read_files',
|
|
8
|
+
description: 'Read multiple files at once',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
file_paths: {
|
|
13
|
+
type: 'array',
|
|
14
|
+
items: { type: 'string' },
|
|
15
|
+
description: 'File paths',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['file_paths'],
|
|
19
|
+
},
|
|
20
|
+
validateInput(input) {
|
|
21
|
+
return Array.isArray(input.file_paths) && input.file_paths.length > 0 ? [] : ['file_paths array required'];
|
|
22
|
+
},
|
|
23
|
+
async call(input) {
|
|
24
|
+
const { ReadTool } = await import('./read.mjs');
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const fp of input.file_paths) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await ReadTool.call({ file_path: path.resolve(fp) });
|
|
29
|
+
const text = typeof result === 'string' ? result : (result?.output || result?.content || String(result));
|
|
30
|
+
results.push(`## ${fp}\n${text}`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
results.push(`## ${fp}\nError: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return results.join('\n\n');
|
|
36
|
+
},
|
|
37
|
+
};
|
package/src/tools/read.mjs
CHANGED
|
@@ -35,14 +35,14 @@ function isBinary(buffer) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export const ReadTool = {
|
|
38
|
-
name: '
|
|
38
|
+
name: 'read_file',
|
|
39
39
|
description: 'Read a file from the local filesystem.',
|
|
40
40
|
inputSchema: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
43
43
|
file_path: { type: 'string', description: 'Absolute path to the file' },
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
start_line: { type: 'integer', description: 'Line number to start reading from (1-indexed)' },
|
|
45
|
+
end_line: { type: 'integer', description: 'Line number to stop reading at (1-indexed, inclusive)' },
|
|
46
46
|
pages: { type: 'string', description: 'Page range for PDF files (e.g. "1-5")' },
|
|
47
47
|
},
|
|
48
48
|
required: ['file_path'],
|
|
@@ -87,9 +87,6 @@ export const ReadTool = {
|
|
|
87
87
|
try {
|
|
88
88
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
89
|
const lines = content.split('\n');
|
|
90
|
-
const start = input.offset || 0;
|
|
91
|
-
const limit = input.limit || DEFAULT_LIMIT;
|
|
92
|
-
const end = Math.min(start + limit, lines.length);
|
|
93
90
|
|
|
94
91
|
// Track as read
|
|
95
92
|
readFiles.add(filePath);
|
|
@@ -99,13 +96,26 @@ export const ReadTool = {
|
|
|
99
96
|
return '[File exists but is empty]';
|
|
100
97
|
}
|
|
101
98
|
|
|
99
|
+
// start_line/end_line: 1-indexed, inclusive
|
|
100
|
+
const hasStart = input.start_line != null;
|
|
101
|
+
const hasEnd = input.end_line != null;
|
|
102
|
+
const startIdx = hasStart ? Math.max(0, Number(input.start_line) - 1) : 0;
|
|
103
|
+
let endIdx;
|
|
104
|
+
if (hasEnd) {
|
|
105
|
+
endIdx = Math.min(Number(input.end_line), lines.length); // inclusive → exclusive bound
|
|
106
|
+
} else if (hasStart) {
|
|
107
|
+
endIdx = lines.length;
|
|
108
|
+
} else {
|
|
109
|
+
endIdx = Math.min(DEFAULT_LIMIT, lines.length);
|
|
110
|
+
}
|
|
111
|
+
|
|
102
112
|
const output = lines
|
|
103
|
-
.slice(
|
|
104
|
-
.map((l, i) => `${
|
|
113
|
+
.slice(startIdx, endIdx)
|
|
114
|
+
.map((l, i) => `${startIdx + i + 1}\t${l}`)
|
|
105
115
|
.join('\n');
|
|
106
116
|
|
|
107
|
-
if (
|
|
108
|
-
return output + `\n\n[File has ${lines.length} lines total. Showing lines ${
|
|
117
|
+
if (endIdx < lines.length) {
|
|
118
|
+
return output + `\n\n[File has ${lines.length} lines total. Showing lines ${startIdx + 1}-${endIdx}. Use read_file with start_line/end_line for more.]`;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
return output;
|
package/src/tools/registry.mjs
CHANGED
|
@@ -29,6 +29,14 @@ import { CronDeleteTool } from './cron-delete.mjs';
|
|
|
29
29
|
import { CronListTool } from './cron-list.mjs';
|
|
30
30
|
import { LspTool } from './lsp.mjs';
|
|
31
31
|
import { ReadMcpResourceTool } from './read-mcp-resource.mjs';
|
|
32
|
+
import { GitDiffTool } from './git-diff.mjs';
|
|
33
|
+
import { GitStatusTool } from './git-status.mjs';
|
|
34
|
+
import { WriteProjectTool } from './write-project.mjs';
|
|
35
|
+
import { ReadFilesTool } from './read-files.mjs';
|
|
36
|
+
import { SearchFilesTool } from './search-files.mjs';
|
|
37
|
+
import { AnalyzeCodeTool } from './analyze-code.mjs';
|
|
38
|
+
import { ExploreTool, PlanTool, VerifyTool, DebugTool, RefactorTool } from './meta-tools.mjs';
|
|
39
|
+
import { RememberTool } from './remember.mjs';
|
|
32
40
|
|
|
33
41
|
const BUILTIN_TOOLS = [
|
|
34
42
|
BashTool,
|
|
@@ -41,6 +49,7 @@ const BUILTIN_TOOLS = [
|
|
|
41
49
|
WebFetchTool,
|
|
42
50
|
WebSearchTool,
|
|
43
51
|
TodoWriteTool,
|
|
52
|
+
RememberTool,
|
|
44
53
|
NotebookEditTool,
|
|
45
54
|
MultiEditTool,
|
|
46
55
|
LsTool,
|
|
@@ -56,6 +65,17 @@ const BUILTIN_TOOLS = [
|
|
|
56
65
|
CronListTool,
|
|
57
66
|
LspTool,
|
|
58
67
|
ReadMcpResourceTool,
|
|
68
|
+
GitDiffTool,
|
|
69
|
+
GitStatusTool,
|
|
70
|
+
WriteProjectTool,
|
|
71
|
+
ReadFilesTool,
|
|
72
|
+
SearchFilesTool,
|
|
73
|
+
AnalyzeCodeTool,
|
|
74
|
+
ExploreTool,
|
|
75
|
+
PlanTool,
|
|
76
|
+
VerifyTool,
|
|
77
|
+
DebugTool,
|
|
78
|
+
RefactorTool,
|
|
59
79
|
];
|
|
60
80
|
|
|
61
81
|
export function createToolRegistry() {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remember Tool — persist a cross-session memory fact to the user's disk.
|
|
3
|
+
*
|
|
4
|
+
* CLI-local tool. The agent calls this when it wants to remember something
|
|
5
|
+
* about the user, the project, or a preference. The fact lands in
|
|
6
|
+
* ~/.bahulam/memory.md (global) or <cwd>/.bahulam/memory.md (project),
|
|
7
|
+
* matching the disk-first CLI memory model (PRD-217).
|
|
8
|
+
*
|
|
9
|
+
* Contrast: chat / cloud-IDE / workspace surfaces continue writing to the
|
|
10
|
+
* Supabase agent_memory table via the framework's SupabaseMemoryBackend.
|
|
11
|
+
* Only CLI-originated requests use this local tool.
|
|
12
|
+
*
|
|
13
|
+
* The persisted shape round-trips 1:1 with the Supabase agent_memory
|
|
14
|
+
* schema — see src/core/memory-disk.mjs for the file format and helpers.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { upsertFacts, loadDiskMemory, ensureBahulamDir } from '../core/memory-disk.mjs';
|
|
18
|
+
|
|
19
|
+
// Slug-safe generator when the caller didn't supply a fact_id. Prefer the
|
|
20
|
+
// caller's own id when they give one — that's how they update an existing
|
|
21
|
+
// fact instead of piling on near-duplicates.
|
|
22
|
+
function _makeFactId(hint) {
|
|
23
|
+
const base = String(hint || 'fact').toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
25
|
+
const stamp = Math.floor(Date.now() / 1000).toString(36);
|
|
26
|
+
return `${base || 'fact'}-${stamp}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const RememberTool = {
|
|
30
|
+
name: 'remember',
|
|
31
|
+
description:
|
|
32
|
+
'Save a cross-session memory fact to the user\'s disk (~/.bahulam/memory.md). ' +
|
|
33
|
+
'Use when the user tells you a preference, when you learn a durable detail ' +
|
|
34
|
+
'about their project, or when the current turn establishes a decision worth ' +
|
|
35
|
+
'carrying forward. Do NOT use for ephemeral session state (that belongs in ' +
|
|
36
|
+
'the current conversation) or for temporary task tracking (use TodoWrite).',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
content: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description:
|
|
43
|
+
'The fact to remember, in prose. One or a few sentences. ' +
|
|
44
|
+
'Include enough context to be useful without the surrounding conversation.',
|
|
45
|
+
},
|
|
46
|
+
fact_type: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
enum: ['preference', 'entity', 'decision', 'context', 'other'],
|
|
49
|
+
description:
|
|
50
|
+
'Category. `preference` = user always/never wants X. `entity` = ' +
|
|
51
|
+
'thing/person/service (their company, their stack, their env). ' +
|
|
52
|
+
'`decision` = choice made this session worth remembering. ' +
|
|
53
|
+
'`context` = ongoing situation the agent should know next turn. ' +
|
|
54
|
+
'`other` = anything else.',
|
|
55
|
+
},
|
|
56
|
+
fact_id: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description:
|
|
59
|
+
'Stable ID. Reuse the same id to UPDATE a prior fact (in-place ' +
|
|
60
|
+
'overwrite). Auto-generated from `content` if omitted, so ' +
|
|
61
|
+
'auto-generated ids create new rows every time — pass an ' +
|
|
62
|
+
'explicit id for anything you might want to update later.',
|
|
63
|
+
},
|
|
64
|
+
memory_scope: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
enum: ['global', 'project'],
|
|
67
|
+
description:
|
|
68
|
+
'`global` (default) writes to ~/.bahulam/memory.md — visible ' +
|
|
69
|
+
'across every session. `project` writes to ' +
|
|
70
|
+
'<cwd>/.bahulam/memory.md and only surfaces when running in ' +
|
|
71
|
+
'this project. Use `project` for facts tied to this specific ' +
|
|
72
|
+
'codebase (its architecture, its conventions, its history).',
|
|
73
|
+
},
|
|
74
|
+
confidence: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
minimum: 0,
|
|
77
|
+
maximum: 1,
|
|
78
|
+
description:
|
|
79
|
+
'0..1. How sure are you this fact is durably true? Use 0.9+ ' +
|
|
80
|
+
'when the user stated it explicitly. Use 0.5-0.7 for inferred ' +
|
|
81
|
+
'facts. Below 0.5 usually isn\'t worth saving.',
|
|
82
|
+
},
|
|
83
|
+
tags: {
|
|
84
|
+
type: 'array',
|
|
85
|
+
items: { type: 'string' },
|
|
86
|
+
description:
|
|
87
|
+
'Short lowercase labels (e.g. ["performance", "cache"]). ' +
|
|
88
|
+
'Help future turns retrieve related facts.',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ['content'],
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
validateInput(input) {
|
|
95
|
+
const errs = [];
|
|
96
|
+
if (!input || typeof input.content !== 'string' || !input.content.trim()) {
|
|
97
|
+
errs.push('content is required and must be a non-empty string');
|
|
98
|
+
}
|
|
99
|
+
if (input.confidence != null) {
|
|
100
|
+
const n = Number(input.confidence);
|
|
101
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) errs.push('confidence must be a number in [0, 1]');
|
|
102
|
+
}
|
|
103
|
+
return errs;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async call(input, _options = {}) {
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
const scope = input.memory_scope === 'project' ? 'project' : 'global';
|
|
109
|
+
const fact = {
|
|
110
|
+
fact_id: (input.fact_id || _makeFactId(input.content)).trim(),
|
|
111
|
+
content: String(input.content).trim(),
|
|
112
|
+
fact_type: input.fact_type || 'other',
|
|
113
|
+
confidence: typeof input.confidence === 'number' ? input.confidence : 0.8,
|
|
114
|
+
source: 'agent',
|
|
115
|
+
tags: Array.isArray(input.tags) ? input.tags.map(String) : [],
|
|
116
|
+
metadata: {},
|
|
117
|
+
project_id: null,
|
|
118
|
+
memory_scope: scope,
|
|
119
|
+
created_at: now,
|
|
120
|
+
updated_at: now,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Preserve created_at if we're updating an existing fact.
|
|
124
|
+
const existing = loadDiskMemory().find(f => f.fact_id === fact.fact_id);
|
|
125
|
+
if (existing?.created_at) fact.created_at = existing.created_at;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Belt-and-braces: upsertFacts also mkdir's, but a stale cwd on
|
|
129
|
+
// a project write could still miss the parent — do it explicitly.
|
|
130
|
+
ensureBahulamDir(scope);
|
|
131
|
+
upsertFacts([fact]);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
output: `remember failed: ${err?.message || err}`,
|
|
136
|
+
_tool: 'remember',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
output: `Saved ${scope} fact '${fact.fact_id}' (${fact.content.length} chars).`,
|
|
142
|
+
_tool: 'remember',
|
|
143
|
+
_fact_id: fact.fact_id,
|
|
144
|
+
_scope: scope,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Files Tool — regex-based file content search (matches Python schema).
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const SearchFilesTool = {
|
|
8
|
+
name: 'search_files',
|
|
9
|
+
description: 'Search file contents with regex',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
pattern: { type: 'string', description: 'Regex pattern' },
|
|
14
|
+
path: { type: 'string', description: 'Directory' },
|
|
15
|
+
},
|
|
16
|
+
required: ['pattern'],
|
|
17
|
+
},
|
|
18
|
+
validateInput(input) {
|
|
19
|
+
return input.pattern ? [] : ['pattern required'];
|
|
20
|
+
},
|
|
21
|
+
async call(input) {
|
|
22
|
+
const dir = path.resolve(input.path || '.');
|
|
23
|
+
try {
|
|
24
|
+
const args = ['-rn', '--max-count', '10', '--max-filesize', '500K'];
|
|
25
|
+
args.push('-e', input.pattern);
|
|
26
|
+
args.push(dir);
|
|
27
|
+
const result = spawnSync('rg', args, {
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
timeout: 15_000,
|
|
30
|
+
stdio: 'pipe',
|
|
31
|
+
});
|
|
32
|
+
if (result.status === 0 || result.status === 1) {
|
|
33
|
+
const output = (result.stdout || '').trim();
|
|
34
|
+
return output || `No matches for "${input.pattern}" in ${dir}`;
|
|
35
|
+
}
|
|
36
|
+
return `grep error (exit ${result.status}): ${result.stderr?.trim() || 'unknown'}`;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return `Error: ${err.message}`;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write Project Tool — batch write multiple files at once (matches Python schema).
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { hasBeenRead, markRead } from './read.mjs';
|
|
7
|
+
|
|
8
|
+
export const WriteProjectTool = {
|
|
9
|
+
name: 'write_project',
|
|
10
|
+
description: 'Write multiple files at once',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
files: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string', description: 'File path' },
|
|
20
|
+
content: { type: 'string', description: 'File content' },
|
|
21
|
+
},
|
|
22
|
+
required: ['path', 'content'],
|
|
23
|
+
},
|
|
24
|
+
description: 'Files to write',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['files'],
|
|
28
|
+
},
|
|
29
|
+
validateInput(input) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
if (!Array.isArray(input.files) || input.files.length === 0) errors.push('files array required');
|
|
32
|
+
return errors;
|
|
33
|
+
},
|
|
34
|
+
async call(input) {
|
|
35
|
+
const results = [];
|
|
36
|
+
const errors = [];
|
|
37
|
+
for (const file of input.files) {
|
|
38
|
+
const filePath = path.resolve(file.path || file.file_path);
|
|
39
|
+
try {
|
|
40
|
+
const dir = path.dirname(filePath);
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
if (fs.existsSync(filePath) && !hasBeenRead(filePath)) {
|
|
43
|
+
// Read first for overwrites
|
|
44
|
+
const { ReadTool } = await import('./read.mjs');
|
|
45
|
+
await ReadTool.call({ file_path: filePath, limit: 1 });
|
|
46
|
+
}
|
|
47
|
+
fs.writeFileSync(filePath, file.content || '', 'utf-8');
|
|
48
|
+
markRead(filePath);
|
|
49
|
+
results.push(filePath);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
errors.push(`${filePath}: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const output = results.length > 0
|
|
55
|
+
? `Created ${results.length} file(s):\n${results.map(f => ` ✓ ${f}`).join('\n')}`
|
|
56
|
+
: 'No files written';
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
return `${output}\n\nErrors:\n${errors.map(e => ` ✗ ${e}`).join('\n')}`;
|
|
59
|
+
}
|
|
60
|
+
return output;
|
|
61
|
+
},
|
|
62
|
+
};
|
package/src/tools/write.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import path from 'path';
|
|
|
11
11
|
import { hasBeenRead, markRead } from './read.mjs';
|
|
12
12
|
|
|
13
13
|
export const WriteTool = {
|
|
14
|
-
name: '
|
|
14
|
+
name: 'write_file',
|
|
15
15
|
description: 'Write content to a file. Creates parent dirs if needed.',
|
|
16
16
|
inputSchema: {
|
|
17
17
|
type: 'object',
|
package/src/ui/sub-agent.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { paint } from './palette.mjs';
|
|
21
21
|
import { icons } from './icons.mjs';
|
|
22
|
+
import { formatSeconds } from '../terminal/ansi.mjs';
|
|
22
23
|
|
|
23
24
|
const SUB_ICONS = {
|
|
24
25
|
explore: '🔭',
|
|
@@ -122,9 +123,14 @@ export function renderSubAgentClose({
|
|
|
122
123
|
const parts = [];
|
|
123
124
|
if (toolCalls > 0) parts.push(`${toolCalls} tools`);
|
|
124
125
|
if (iterations > 0) parts.push(`${iterations} iter`);
|
|
125
|
-
|
|
126
|
+
// Tokens: pass the OUTPUT (generation) count only. Summing input+output
|
|
127
|
+
// across a multi-iteration sub-agent double-counts the context that is
|
|
128
|
+
// re-shipped each iteration, and the resulting number reads huge and
|
|
129
|
+
// misleading (e.g. 632.8k for a 16-iter run whose actual generation was
|
|
130
|
+
// a fraction of that). Output tokens are the honest "work done" number.
|
|
131
|
+
if (tokens > 0) parts.push(`${formatTokens(tokens)} gen`);
|
|
126
132
|
if (typeof costUsd === 'number' && costUsd > 0) parts.push(formatCost(costUsd));
|
|
127
|
-
if (durationS != null) parts.push(
|
|
133
|
+
if (durationS != null) parts.push(formatSeconds(durationS));
|
|
128
134
|
const detail = parts.length ? paint.text.dim(' · ' + parts.join(' · ')) : '';
|
|
129
135
|
|
|
130
136
|
const body = summary
|