agent-from-zero 0.34.2 → 0.35.0
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/README.md +147 -0
- package/dist/account-status.js +172 -0
- package/dist/agent-progress.js +39 -0
- package/dist/agent.js +159 -34
- package/dist/auto-budget.js +15 -0
- package/dist/auto-context.js +125 -0
- package/dist/auto-debug.js +87 -0
- package/dist/auto-providers.js +154 -0
- package/dist/auto-review.js +113 -0
- package/dist/auto.js +295 -0
- package/dist/background.js +2 -1
- package/dist/cli.js +83 -0
- package/dist/clipboard.js +88 -0
- package/dist/completion.js +65 -0
- package/dist/config.js +8 -0
- package/dist/context.js +9 -3
- package/dist/cost.js +4 -0
- package/dist/effort.js +81 -0
- package/dist/follow-up.js +22 -0
- package/dist/form.js +8 -6
- package/dist/images.js +43 -0
- package/dist/ink/activity.js +62 -0
- package/dist/ink/agents.js +24 -0
- package/dist/ink/app.js +384 -171
- package/dist/ink/chat.js +12 -1
- package/dist/ink/commands.js +16 -3
- package/dist/ink/completion.js +43 -0
- package/dist/ink/setup.js +9 -4
- package/dist/ink/sink.js +16 -6
- package/dist/ink/viewport.js +15 -0
- package/dist/loop.js +251 -106
- package/dist/mcp.js +1 -0
- package/dist/menu.js +50 -8
- package/dist/output.js +1 -1
- package/dist/pasted-text.js +32 -0
- package/dist/permissions.js +467 -12
- package/dist/print.js +16 -0
- package/dist/prompt.js +7 -4
- package/dist/status.js +80 -0
- package/dist/team.js +9 -3
- package/dist/tools.js +13 -1
- package/dist/tui.js +31 -15
- package/dist/ui.js +4 -6
- package/package.json +4 -3
package/dist/status.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CONFIG } from "./config.js";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
import { displayWidth } from "./editor.js";
|
|
5
|
+
import { cacheHitRate } from "./cost.js";
|
|
6
|
+
import { getAccountStatus } from "./account-status.js";
|
|
7
|
+
function show(metric, format) {
|
|
8
|
+
return metric.state === "ok" ? format(metric.value) : chalk.dim(`unavailable — ${metric.reason}`);
|
|
9
|
+
}
|
|
10
|
+
const money = (values) => values.map((m) => `${m.currency} ${m.amount.toFixed(4)}`).join(" · ");
|
|
11
|
+
function accountLines(status) {
|
|
12
|
+
return [
|
|
13
|
+
{ text: chalk.bold("Vendor account") },
|
|
14
|
+
{ label: "Provider", text: chalk.cyan.bold(status.provider) },
|
|
15
|
+
{ label: "Balance", text: show(status.balance, (values) => chalk.green.bold(money(values))) },
|
|
16
|
+
{ label: "Reported cost", text: show(status.cost, (values) => chalk.yellow.bold(money(values))) },
|
|
17
|
+
{ label: "Account usage", text: show(status.usage, (u) => chalk.cyan(`input ${u.input} (${u.cached} cached) · output ${u.output}`)) },
|
|
18
|
+
{ label: "Period (UTC)", text: status.period },
|
|
19
|
+
{ label: "Checked at", text: chalk.dim(status.checkedAt) },
|
|
20
|
+
...(status.note ? [{ text: chalk.dim(status.note) }] : []),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
// Both frontends use the same ANSI/CJK-aware layout. Narrow terminals stack
|
|
24
|
+
// labels above values; wider ones align values in a second column.
|
|
25
|
+
function renderLines(lines, width) {
|
|
26
|
+
return lines.flatMap(({ label, text }) => {
|
|
27
|
+
const indent = label && width >= 44 ? 18 : 0;
|
|
28
|
+
const value = wrapAnsi(text, Math.max(1, width - indent), { hard: true, trim: false }).split("\n");
|
|
29
|
+
if (!label)
|
|
30
|
+
return value;
|
|
31
|
+
if (!indent)
|
|
32
|
+
return [chalk.dim(`${label}:`), ...value];
|
|
33
|
+
return value.map((line, i) => (i === 0 ? chalk.dim(`${label}:`.padEnd(indent)) : " ".repeat(indent)) + line);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function formatAccountStatus(status) {
|
|
37
|
+
return renderLines(accountLines(status), 92).join("\n");
|
|
38
|
+
}
|
|
39
|
+
function statusCard(lines, columns) {
|
|
40
|
+
const width = Math.max(1, Math.min(96, Math.floor(columns)));
|
|
41
|
+
if (width < 6)
|
|
42
|
+
return renderLines(lines, width).join("\n");
|
|
43
|
+
const inner = width - 4; // two borders and one space of padding per side
|
|
44
|
+
const body = renderLines(lines, inner).map((line) => chalk.dim("│ ") + line + " ".repeat(Math.max(0, inner - displayWidth(line))) + chalk.dim(" │"));
|
|
45
|
+
return [chalk.dim(`╭${"─".repeat(width - 2)}╮`), ...body, chalk.dim(`╰${"─".repeat(width - 2)}╯`)].join("\n");
|
|
46
|
+
}
|
|
47
|
+
// Shared by Ink and readline. No model call, conversation insertion or logging.
|
|
48
|
+
export async function statusCommand(line, meter, options = {}) {
|
|
49
|
+
const arg = line.trim().split(/\s+/).slice(1).join(" ");
|
|
50
|
+
if (arg && !["local", "deepseek", "openai", "anthropic"].includes(arg))
|
|
51
|
+
return "Usage: /status [local|deepseek|openai|anthropic]";
|
|
52
|
+
// A URL may itself contain credentials/query tokens; show only its origin.
|
|
53
|
+
let endpoint = "custom endpoint";
|
|
54
|
+
try {
|
|
55
|
+
endpoint = new URL(CONFIG.baseURL).origin;
|
|
56
|
+
}
|
|
57
|
+
catch { /* leave generic */ }
|
|
58
|
+
const usage = meter.snapshot();
|
|
59
|
+
const local = [
|
|
60
|
+
{ text: chalk.bold("mini-agent · Status") },
|
|
61
|
+
{ text: "" },
|
|
62
|
+
{ label: "Model", text: chalk.cyan.bold(CONFIG.model) },
|
|
63
|
+
{ label: "Endpoint", text: endpoint },
|
|
64
|
+
{ text: "" },
|
|
65
|
+
{ text: chalk.bold("Current session") },
|
|
66
|
+
{ label: "Model calls", text: chalk.cyan(String(usage.calls)) },
|
|
67
|
+
{ label: "Input tokens", text: chalk.cyan(String(usage.inputUncached + usage.inputCached)) },
|
|
68
|
+
{ label: "Cached input", text: `${usage.inputCached} (${(cacheHitRate(usage) * 100).toFixed(0)}% hit rate)` },
|
|
69
|
+
{ label: "Output tokens", text: chalk.cyan(String(usage.output)) },
|
|
70
|
+
{ label: "Estimated cost", text: chalk.yellow.bold(`$${usage.cost.toFixed(4)}`) },
|
|
71
|
+
{ text: "" },
|
|
72
|
+
{ text: chalk.dim("Main/subagent streams only; excludes permission review/Jev and other auxiliary calls.") },
|
|
73
|
+
{ text: chalk.dim("Estimates use settings.pricing, not account billing.") },
|
|
74
|
+
];
|
|
75
|
+
const columns = options.columns ?? (process.stdout.columns || 80);
|
|
76
|
+
if (arg === "local")
|
|
77
|
+
return statusCard(local, columns);
|
|
78
|
+
const account = await getAccountStatus({ baseURL: CONFIG.baseURL, apiKey: CONFIG.apiKey, env: options.env ?? process.env, provider: arg ? arg : undefined, request: options.request, signal: options.signal });
|
|
79
|
+
return statusCard([...local, { text: "" }, ...accountLines(account)], columns);
|
|
80
|
+
}
|
package/dist/team.js
CHANGED
|
@@ -151,14 +151,20 @@ export function teammateCount() {
|
|
|
151
151
|
return teammates.size;
|
|
152
152
|
}
|
|
153
153
|
// Record a newly spawned teammate. The caller supplies the promise for its loop.
|
|
154
|
-
export function registerTeammate(name, role, done) {
|
|
155
|
-
teammates.set(name, { name, role, status: "running", state: "active", startedAt: Date.now(), done });
|
|
154
|
+
export function registerTeammate(name, role, done, progress) {
|
|
155
|
+
teammates.set(name, { name, role, status: "running", state: "active", startedAt: Date.now(), done, progress });
|
|
156
156
|
}
|
|
157
157
|
// Mark a teammate finished. `ok=false` means its loop ended on a non-Done reason.
|
|
158
158
|
export function finishTeammate(name, ok) {
|
|
159
159
|
const t = teammates.get(name);
|
|
160
|
-
if (t)
|
|
160
|
+
if (t) {
|
|
161
161
|
t.status = ok ? "done" : "failed";
|
|
162
|
+
t.progress?.finish();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Unlike /team, this snapshot does not read mailboxes on each UI repaint.
|
|
166
|
+
export function listTeammateViews() {
|
|
167
|
+
return [...teammates.values()].flatMap((t) => t.progress ? [t.progress.view(`team:${t.name}`, t.name, t.role, t.status === "running" ? t.state === "idle" ? "idle" : "running" : t.status)] : []);
|
|
162
168
|
}
|
|
163
169
|
// How many teammates are still running — at least one running teammate means the
|
|
164
170
|
// team isn't disbanded yet.
|
package/dist/tools.js
CHANGED
|
@@ -8,6 +8,7 @@ import { recordMutation } from "./undo.js"; // capture the before-state so /undo
|
|
|
8
8
|
import { parseTodos, setTodos, summarizeTodos } from "./todos.js"; // the agent's own checklist (Day 36)
|
|
9
9
|
import { startBackground, readBackground } from "./background.js"; // long-running commands that outlive the turn (Day 37)
|
|
10
10
|
import { scheduleJob, cancelJob, listJobs } from "./cron.js"; // cron scheduler (Day s14): scheduled, recurring work
|
|
11
|
+
import { childEnv } from "./auto-debug.js"; // shells never inherit review tracing
|
|
11
12
|
// ============ Session state ============
|
|
12
13
|
// The foundation of "read before edit": which files has this session actually read?
|
|
13
14
|
// Files that were never read must not be edited — otherwise the model edits from
|
|
@@ -200,7 +201,7 @@ On error: failures return the error output — analyze it and try a different ap
|
|
|
200
201
|
// returned. The AbortSignal lets the loop kill the child on interruption
|
|
201
202
|
// instead of waiting out the 30s timeout.
|
|
202
203
|
run: (args, signal) => new Promise((resolve) => {
|
|
203
|
-
const child = spawn(args.command, { shell: true, stdio: ["ignore", "pipe", "pipe"] }); // shell:true → bash semantics
|
|
204
|
+
const child = spawn(args.command, { shell: true, stdio: ["ignore", "pipe", "pipe"], env: childEnv() }); // shell:true → bash semantics
|
|
204
205
|
let stdout = ""; // captured stdout
|
|
205
206
|
let stderr = ""; // captured stderr (for the model, not the user's terminal)
|
|
206
207
|
let settled = false; // resolve exactly once
|
|
@@ -428,6 +429,17 @@ export function toolDefinitions() {
|
|
|
428
429
|
// order. (See loop.ts for the batching.)
|
|
429
430
|
const READ_ONLY_TOOLS = new Set(["read_file", "search"]);
|
|
430
431
|
export const isReadOnlyTool = (name) => READ_ONLY_TOOLS.has(name);
|
|
432
|
+
// A server's own claim that a tool loses nothing: it only reads (readOnlyHint),
|
|
433
|
+
// or only adds — a message, an issue — without changing or deleting what exists
|
|
434
|
+
// (destructiveHint: false), so a mistake can be deleted afterwards. Any server,
|
|
435
|
+
// no per-service list. Trusted for auto-mode review only, NOT for the concurrent
|
|
436
|
+
// batching above: that needs our own guarantee. The MCP default is destructive,
|
|
437
|
+
// so an unannotated tool still goes to review.
|
|
438
|
+
export function declaresRecoverable(name) {
|
|
439
|
+
const hints = tools[name]?.annotations;
|
|
440
|
+
return hints?.readOnlyHint === true ? "read-only tool (server annotation)"
|
|
441
|
+
: hints?.destructiveHint === false ? "additive tool (server annotation: not destructive)" : null;
|
|
442
|
+
}
|
|
431
443
|
// Single entry point: every failure becomes text fed back to the model.
|
|
432
444
|
// It never throws — the main loop must never die because of a tool. Async since
|
|
433
445
|
// Day 13 so run_bash can spawn without blocking; the signal lets Ctrl+C kill a
|
package/dist/tui.js
CHANGED
|
@@ -78,37 +78,52 @@ export function revealReasoning() {
|
|
|
78
78
|
process.stdout.write(chalk.dim(reasoningTrace.join("\n\n— — —\n\n").replace(/^/gm, " ")) + "\n");
|
|
79
79
|
return true;
|
|
80
80
|
}
|
|
81
|
-
// ---- collapsed tool calls (the activity trace) -------------------------------
|
|
82
|
-
// A long task fires dozens of read_file/search calls; printing one line each
|
|
83
|
-
// floods the screen. Instead the loop prints a compact per-round tally and we
|
|
84
|
-
// keep the full call lines here for Ctrl+T to reveal. Cleared per turn, so
|
|
85
|
-
// Ctrl+T shows the calls behind the LAST answer. (Diffs, prompts, and results
|
|
86
|
-
// still print live — only the one-line announcements are folded.)
|
|
87
81
|
const toolCallLog = [];
|
|
88
|
-
const MAX_TOOL_LOG =
|
|
82
|
+
const MAX_TOOL_LOG = 100;
|
|
83
|
+
const MAX_DETAIL_CHARS = 32_000;
|
|
84
|
+
let toolCallCount = 0;
|
|
85
|
+
function boundedDetail(text) {
|
|
86
|
+
return text.length > MAX_DETAIL_CHARS ? text.slice(0, MAX_DETAIL_CHARS) + "\n… (display truncated)" : text;
|
|
87
|
+
}
|
|
89
88
|
// Record a tool call's full line for Ctrl+T, and stash its args for Tab.
|
|
90
|
-
export function recordToolCall(line, full) {
|
|
91
|
-
|
|
89
|
+
export function recordToolCall(line, full, action) {
|
|
90
|
+
const record = { line, full: boundedDetail(full), ...action, args: action ? boundedDetail(action.args) : undefined, startedAt: Date.now() };
|
|
91
|
+
toolCallCount++;
|
|
92
|
+
toolCallLog.push(record);
|
|
92
93
|
if (toolCallLog.length > MAX_TOOL_LOG)
|
|
93
94
|
toolCallLog.shift();
|
|
94
|
-
collapsed.push({ full, expanded: false });
|
|
95
|
+
collapsed.push({ full: record.full, expanded: false });
|
|
95
96
|
if (collapsed.length > MAX_COLLAPSED)
|
|
96
97
|
collapsed.shift();
|
|
98
|
+
return record;
|
|
99
|
+
}
|
|
100
|
+
export function recordToolResult(record, result) {
|
|
101
|
+
record.endedAt = Date.now();
|
|
102
|
+
record.result = boundedDetail(result);
|
|
97
103
|
}
|
|
104
|
+
export function recordToolDetail(id, text) {
|
|
105
|
+
const record = [...toolCallLog].reverse().find((r) => r.id === id);
|
|
106
|
+
if (record)
|
|
107
|
+
record.full = boundedDetail(record.full + "\n" + text);
|
|
108
|
+
}
|
|
109
|
+
export function getToolActivity() { return toolCallLog; }
|
|
110
|
+
export function getToolCallCount() { return toolCallCount; }
|
|
98
111
|
export function clearToolCalls() {
|
|
99
112
|
toolCallLog.length = 0;
|
|
113
|
+
collapsed.length = 0;
|
|
114
|
+
toolCallCount = 0;
|
|
100
115
|
}
|
|
101
116
|
// Ctrl+T handler: print every tool call behind the last answer. Returns false
|
|
102
117
|
// (nothing to show) so the editor can ignore the key.
|
|
103
118
|
export function revealToolCalls() {
|
|
104
|
-
|
|
119
|
+
const text = getToolCalls();
|
|
120
|
+
if (!text)
|
|
105
121
|
return false;
|
|
106
|
-
process.stdout.write(
|
|
107
|
-
process.stdout.write(toolCallLog.join("\n") + "\n");
|
|
122
|
+
process.stdout.write(text + "\n");
|
|
108
123
|
return true;
|
|
109
124
|
}
|
|
110
125
|
// Ink variants of the two reveals: RETURN the formatted block (or null) instead
|
|
111
|
-
// of writing to stdout, so
|
|
126
|
+
// of writing to stdout, so Ink can show it in a transient, scrollable panel.
|
|
112
127
|
export function getReasoning() {
|
|
113
128
|
if (!reasoningTrace.length)
|
|
114
129
|
return null;
|
|
@@ -117,7 +132,8 @@ export function getReasoning() {
|
|
|
117
132
|
export function getToolCalls() {
|
|
118
133
|
if (!toolCallLog.length)
|
|
119
134
|
return null;
|
|
120
|
-
|
|
135
|
+
const omitted = toolCallCount > toolCallLog.length ? ` — showing latest ${toolCallLog.length}` : "";
|
|
136
|
+
return chalk.dim(`tool calls this turn (${toolCallCount})${omitted}:\n`) + toolCallLog.map((r) => `${r.line}\n${r.full}\nresult: ${r.result ?? "(in progress)"}`).join("\n\n");
|
|
121
137
|
}
|
|
122
138
|
// ---- cleanup -----------------------------------------------------------------
|
|
123
139
|
// Called on exit. Nothing fancy to restore now (we no longer hijack the bottom
|
package/dist/ui.js
CHANGED
|
@@ -100,17 +100,15 @@ export function formatElapsed(ms) {
|
|
|
100
100
|
const h = Math.floor(m / 60);
|
|
101
101
|
return `${h}h${String(m % 60).padStart(2, "0")}m`;
|
|
102
102
|
}
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
export function spinnerText(word, elapsedSec, subAgent, model, tokens) {
|
|
103
|
+
// Live activity, elapsed time and streamed tokens. The persistent status line
|
|
104
|
+
// owns the model name, so each animation need not repeat it.
|
|
105
|
+
export function spinnerText(word, elapsedSec, subAgent, tokens) {
|
|
107
106
|
const head = subAgent ? "sub-agent" : word;
|
|
108
|
-
const tag = model && !subAgent ? chalk.dim(`${model} · `) : ""; // show the model on top-level calls
|
|
109
107
|
const bits = [`${elapsedSec}s`];
|
|
110
108
|
if (tokens && tokens > 0)
|
|
111
109
|
bits.push(`↓ ${formatTokens(tokens)} tokens`);
|
|
112
110
|
bits.push("Ctrl+C to interrupt");
|
|
113
|
-
return `${
|
|
111
|
+
return `${head}… ${chalk.dim(`(${bits.join(" · ")})`)}`;
|
|
114
112
|
}
|
|
115
113
|
// The persistent status line — model, project, branch, context use, spend, time.
|
|
116
114
|
// Pure (dir + branch passed in) so it's testable without a filesystem or git.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Build a Claude Code-style CLI agent from scratch, with AI as your pair programmer
|
|
3
|
+
"version": "0.35.0",
|
|
4
|
+
"description": "Build a Claude Code-style CLI agent from scratch, with AI as your pair programmer \u2014 companion code for a 10-day tutorial (DeepSeek by default, any OpenAI-compatible model)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mini-agent": "dist/agent.js"
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
"ink": "tsx --env-file=.env src/ink/main.tsx",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"eval": "tsx --env-file=.env eval/run.ts",
|
|
18
|
+
"auto:report": "tsx eval/auto-report.ts",
|
|
18
19
|
"build": "tsc -p tsconfig.build.json",
|
|
19
20
|
"prepare": "npm run build",
|
|
20
|
-
"test": "tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
21
|
+
"test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=20.6"
|