amness 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
// The chat TUI: one persistent conversation with a live input box, streaming output, permission prompts and slash commands.
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { onRateLimitWait } from "../model.js";
|
|
6
|
+
import { STRATEGIES, setShell, shellFallbackNote, describeProjectPlugins, promptPlugin, loadPlugins, pluginFingerprint, runShell } from "../../src/index.js";
|
|
7
|
+
import { visionLikely, Budget, Snapshots, Tracer, irreversibleReason, loadProfile, routedModel, sandboxKind, withFallback, createCodeAgent, Hooks, Permissions, projectConfigFingerprint, openaiEmbedder, riskOf, startLive } from "../../src/index.js";
|
|
8
|
+
import { PROVIDERS, home, saveConfig } from "../config.js";
|
|
9
|
+
import { detectVerify } from "../detect.js";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { createModel } from "../model.js";
|
|
12
|
+
import { loadState, saveState } from "../state.js";
|
|
13
|
+
import { bold, cyan, dim, gray, green, magenta, red, yellow } from "../ui.js";
|
|
14
|
+
import { BlockStore } from "./blocks.js";
|
|
15
|
+
import { buildCommands } from "./commands.js";
|
|
16
|
+
import { FullView } from "./full.js";
|
|
17
|
+
import { loadCustomCommands } from "./custom.js";
|
|
18
|
+
import { FileIndex, applySuggestion, rankCommands, rankFiles, tokenAtCursor } from "./complete.js";
|
|
19
|
+
import { Editor } from "./editor.js";
|
|
20
|
+
import { KeyParser, PASTE_OFF, PASTE_ON } from "./keys.js";
|
|
21
|
+
import { renderMarkdown, takeCompleteBlocks } from "./markdown.js";
|
|
22
|
+
import { expandMentions, attachedNote } from "./mentions.js";
|
|
23
|
+
import { copyToClipboard } from "./clipboard.js";
|
|
24
|
+
import { ChoiceOverlay, InputOverlay, PickerOverlay, TextOverlay, editKey } from "./overlays.js";
|
|
25
|
+
import { Pager, transcriptLines } from "./pager.js";
|
|
26
|
+
import { TIPS, at, fg, palette, shimmer, sparkline } from "./theme.js";
|
|
27
|
+
import { contextWindow, estimateCost, fmtCost, fmtDuration, fmtTokens, priceOf } from "./pricing.js";
|
|
28
|
+
import { Screen } from "./screen.js";
|
|
29
|
+
import { SessionStore, forkSession, newSession, rewindFull, snapshot } from "./session.js";
|
|
30
|
+
import { basename } from "node:path";
|
|
31
|
+
import { cellWidth, clipWidth, wrapAnsi } from "./text.js";
|
|
32
|
+
import { assistantBlock, editSummary, footer, helpLines, isFailure, renderDiff, todoList, toolCall, toolHeader, toolResult, userMessage, welcome } from "./view.js";
|
|
33
|
+
const SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
34
|
+
const GLYPHS = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"]; // the status line "breathes"
|
|
35
|
+
const MODES = ["default", "acceptEdits", "plan"];
|
|
36
|
+
const slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "note";
|
|
37
|
+
const one = (s, n) => { const l = s.replace(/\s+/g, " ").trim(); return cellWidth(l) > n ? clipWidth(l, n - 1) + "…" : l; };
|
|
38
|
+
export class App {
|
|
39
|
+
cfg;
|
|
40
|
+
cwd;
|
|
41
|
+
version;
|
|
42
|
+
persist;
|
|
43
|
+
io;
|
|
44
|
+
screen;
|
|
45
|
+
keys = new KeyParser();
|
|
46
|
+
editor = new Editor();
|
|
47
|
+
files;
|
|
48
|
+
mode = "default";
|
|
49
|
+
shRules = new Set();
|
|
50
|
+
toolRules = new Set();
|
|
51
|
+
model;
|
|
52
|
+
agent;
|
|
53
|
+
budget = new Budget();
|
|
54
|
+
store;
|
|
55
|
+
session;
|
|
56
|
+
edits = [];
|
|
57
|
+
verify;
|
|
58
|
+
check;
|
|
59
|
+
trimmed = 0;
|
|
60
|
+
cacheBreaks = 0;
|
|
61
|
+
runSeries = [];
|
|
62
|
+
thinkBuf = "";
|
|
63
|
+
phase;
|
|
64
|
+
activity = [];
|
|
65
|
+
/** One entry per loop iteration of the conversation so far, for /loop. */
|
|
66
|
+
trace = [];
|
|
67
|
+
running = false;
|
|
68
|
+
ac;
|
|
69
|
+
runStart = 0;
|
|
70
|
+
runTokens = 0;
|
|
71
|
+
lastPrompt = 0;
|
|
72
|
+
inflight = new Map();
|
|
73
|
+
pendingEdits = new Map();
|
|
74
|
+
streamBuf = "";
|
|
75
|
+
msgOpen = false;
|
|
76
|
+
streamedThisMsg = false;
|
|
77
|
+
todos = [];
|
|
78
|
+
queued = [];
|
|
79
|
+
overlay = null;
|
|
80
|
+
pager = null;
|
|
81
|
+
sugg = null;
|
|
82
|
+
suggOff = "";
|
|
83
|
+
builtin;
|
|
84
|
+
customAt = 0;
|
|
85
|
+
customCmds = [];
|
|
86
|
+
mcp = [];
|
|
87
|
+
notice = "";
|
|
88
|
+
noticeUntil = 0;
|
|
89
|
+
pendingContext = [];
|
|
90
|
+
modeNotice = "";
|
|
91
|
+
branch;
|
|
92
|
+
live;
|
|
93
|
+
out = [];
|
|
94
|
+
timer;
|
|
95
|
+
flushT;
|
|
96
|
+
renderT;
|
|
97
|
+
escAt = 0;
|
|
98
|
+
ctrlcAt = 0;
|
|
99
|
+
frame = 0;
|
|
100
|
+
stopped = false;
|
|
101
|
+
/** The structured transcript the full-screen UI draws (kept in both modes so switching never loses history). */
|
|
102
|
+
blocks = new BlockStore();
|
|
103
|
+
ui;
|
|
104
|
+
full;
|
|
105
|
+
opts;
|
|
106
|
+
done;
|
|
107
|
+
resolveDone;
|
|
108
|
+
constructor(o) {
|
|
109
|
+
this.opts = o;
|
|
110
|
+
this.cfg = o.cfg;
|
|
111
|
+
this.cwd = o.cwd;
|
|
112
|
+
this.snaps = new Snapshots(o.cwd);
|
|
113
|
+
this.version = o.version;
|
|
114
|
+
this.persist = o.persist ?? true;
|
|
115
|
+
this.io = { input: o.input ?? process.stdin, output: o.output ?? process.stdout };
|
|
116
|
+
this.ui = o.ui ?? "inline";
|
|
117
|
+
this.screen = new Screen(this.io.output);
|
|
118
|
+
this.files = new FileIndex(o.cwd);
|
|
119
|
+
this.store = new SessionStore(o.cwd);
|
|
120
|
+
if (this.ui === "full")
|
|
121
|
+
this.full = new FullView(this);
|
|
122
|
+
this.session = newSession(o.cwd, o.cfg.model);
|
|
123
|
+
this.builtin = buildCommands();
|
|
124
|
+
this.done = new Promise((r) => (this.resolveDone = r));
|
|
125
|
+
const st = loadState();
|
|
126
|
+
this.verify = st.verifyByDir[o.cwd] ?? (detectVerify(o.cwd) || undefined);
|
|
127
|
+
if (this.persist)
|
|
128
|
+
try {
|
|
129
|
+
this.editor.history = JSON.parse(readFileSync(join(home(), "history.json"), "utf8")).slice(0, 200);
|
|
130
|
+
}
|
|
131
|
+
catch { /* first run */ }
|
|
132
|
+
this.buildAgent();
|
|
133
|
+
}
|
|
134
|
+
warnedShadow = new Set();
|
|
135
|
+
/** Built-in commands plus the user's own (.amness/commands/*.md). Built-ins win name clashes. */
|
|
136
|
+
get cmds() {
|
|
137
|
+
if (Date.now() - this.customAt > 2000) {
|
|
138
|
+
this.customAt = Date.now();
|
|
139
|
+
this.customCmds = loadCustomCommands([{ dir: join(this.cwd, ".amness", "commands"), scope: "project" }, { dir: join(home(), "commands"), scope: "user" }]);
|
|
140
|
+
}
|
|
141
|
+
const names = new Set(this.builtin.flatMap((c) => [c.name, c.alias ?? ""]));
|
|
142
|
+
for (const c of this.customCmds)
|
|
143
|
+
if (names.has(c.name) && !this.warnedShadow.has(c.name)) {
|
|
144
|
+
this.warnedShadow.add(c.name);
|
|
145
|
+
this.note(`your custom /${c.name} is hidden by the built-in /${c.name}; rename its file to use it`);
|
|
146
|
+
}
|
|
147
|
+
const custom = this.customCmds.filter((c) => !names.has(c.name)), taken = new Set([...names, ...custom.map((c) => c.name)]);
|
|
148
|
+
const ctx = { cwd: this.cwd, say: (t) => this.say(t, dim), ask: (t) => this.dispatch(t), run: async (cmd, ms) => { const out = await runShell(cmd, this.cwd, ms ?? 60_000); const m = /^exit=(\w+)\n?/.exec(out); return { code: m && /^\d+$/.test(m[1]) ? Number(m[1]) : 1, output: out.replace(/^exit=\w+\n?/, "") }; } };
|
|
149
|
+
const fromPlugins = this.plugins.flatMap((p) => (p.commands ?? []).filter((c) => !taken.has(c.name)).map((c) => ({ name: c.name, help: `${c.help} (plugin ${p.name})`, args: c.args, run: async (_a, arg) => { try {
|
|
150
|
+
await c.run(ctx, arg);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
this.note(`plugin ${p.name}: /${c.name} failed: ${e.message}`);
|
|
154
|
+
} } })));
|
|
155
|
+
return [...this.builtin, ...custom, ...fromPlugins];
|
|
156
|
+
}
|
|
157
|
+
// ------------------------------------------------------------------ agent plumbing
|
|
158
|
+
buildAgent() {
|
|
159
|
+
const cacheKey = "amness-" + createHash("sha1").update(this.cwd).digest("hex").slice(0, 12); // stable per project: better prompt-cache hits
|
|
160
|
+
const make = (c) => (this.opts.makeModel ?? ((x) => createModel(x, undefined, { cacheKey })))(c);
|
|
161
|
+
const main = make(this.cfg), fb = this.cfg.fallback && this.cfg.fallback !== this.cfg.model ? make({ ...this.cfg, model: this.cfg.fallback }) : undefined;
|
|
162
|
+
this.model = fb ? withFallback(main, fb, () => this.note(`${this.cfg.model} is unavailable; answering from ${this.cfg.fallback}`)) : main;
|
|
163
|
+
const role = (id) => (id && id !== this.cfg.model ? make({ ...this.cfg, model: id }) : undefined), m = this.cfg.models ?? {};
|
|
164
|
+
this.roles = { explore: role(m.explore), compact: role(m.compact), review: role(m.review), cheap: role(m.cheap) };
|
|
165
|
+
if (this.cfg.router && this.roles.cheap) {
|
|
166
|
+
const pm = priceOf(this.cfg.model), pc = priceOf(m.cheap ?? "");
|
|
167
|
+
this.model = routedModel({ strong: this.model, cheap: this.roles.cheap, prices: pm && pc ? { strong: { input: pm[1], output: pm[2] }, cheap: { input: pc[1], output: pc[2] } } : undefined,
|
|
168
|
+
onRoute: (d) => { this.lastRoute = `${d.tier === "cheap" ? m.cheap : this.cfg.model}`; if (d.switched)
|
|
169
|
+
this.stepNow().notes.push(`route → ${d.tier === "cheap" ? m.cheap : this.cfg.model}: ${d.reason}`); } });
|
|
170
|
+
}
|
|
171
|
+
this.agent = createCodeAgent({
|
|
172
|
+
model: this.model, modelById: (id) => make({ ...this.cfg, model: id }), cwd: this.cwd, chat: true, permissions: { mode: () => this.mode }, approve: this.approve, inbox: () => this.drainQueue(),
|
|
173
|
+
signal: () => this.ac?.signal, budget: this.budget, verify: this.verify, check: this.check, review: this.cfg.review || undefined, lsp: this.cfg.lsp || undefined, approvalReviewer: this.cfg.approvalReviewer || undefined, beforeShell: (cmd) => this.beforeShell(cmd), record: this.persist || undefined, models: this.roles, sandbox: () => this.cfg.sandbox ?? "off", hooks: this.hooks?.count() ? this.hooks : undefined, plugins: this.activePlugins().length ? this.activePlugins() : undefined, rules: this.rules, ...this.profileOptions(), embedder: this.cfg.embeddings && this.cfg.provider !== "anthropic" ? openaiEmbedder({ apiKey: this.cfg.apiKey, baseUrl: this.cfg.baseUrl }) : undefined, onEvent: (e) => this.onEvent(e), meta: { provider: this.cfg.provider, model: this.cfg.model },
|
|
174
|
+
});
|
|
175
|
+
for (const m of this.mcp)
|
|
176
|
+
this.agent.registry.add(...m.tools); // attached MCP servers survive rebuilds
|
|
177
|
+
}
|
|
178
|
+
get width() { return Math.max(24, this.screen.cols - 1); }
|
|
179
|
+
get ctxPct() { return this.lastPrompt ? Math.min(99, Math.round((this.lastPrompt / contextWindow(this.cfg.model)) * 100)) : null; }
|
|
180
|
+
get cost() { const u = this.session.usage; return fmtCost(estimateCost(this.cfg.model, u.prompt, u.completion, u.cached ?? 0)); }
|
|
181
|
+
// ------------------------------------------------------------------ lifecycle
|
|
182
|
+
async start() {
|
|
183
|
+
setShell(this.cfg.shell);
|
|
184
|
+
{
|
|
185
|
+
const fb = shellFallbackNote();
|
|
186
|
+
if (fb)
|
|
187
|
+
this.note(fb);
|
|
188
|
+
}
|
|
189
|
+
onRateLimitWait((ms, attempt, conc) => this.note(`rate limited by the provider: waiting ${Math.ceil(ms / 1000)}s, then retrying (attempt ${attempt}); ${conc} request${conc === 1 ? "" : "s"} at a time for now`));
|
|
190
|
+
const { input, output } = this.io;
|
|
191
|
+
input.setRawMode?.(true);
|
|
192
|
+
input.setEncoding?.("utf8");
|
|
193
|
+
input.resume?.();
|
|
194
|
+
output.write(PASTE_ON);
|
|
195
|
+
this.full?.enter();
|
|
196
|
+
input.on("data", this.onData);
|
|
197
|
+
void this.askHookTrust();
|
|
198
|
+
output.on?.("resize", () => { this.full?.invalidate(); this.render(); });
|
|
199
|
+
process.once("exit", this.restore);
|
|
200
|
+
this.files.ensure().catch(() => { });
|
|
201
|
+
this.refreshBranch();
|
|
202
|
+
let resumed;
|
|
203
|
+
if (this.opts.resume) {
|
|
204
|
+
const s = this.opts.resume === "last" ? this.store.latest() : this.store.load(this.opts.resume);
|
|
205
|
+
if (s) {
|
|
206
|
+
this.session = s;
|
|
207
|
+
resumed = `"${s.title}" · ${s.turns.length} turn${s.turns.length === 1 ? "" : "s"}`;
|
|
208
|
+
this.recount();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (this.full) {
|
|
212
|
+
if (resumed)
|
|
213
|
+
this.blocks.loadMessages(this.session.messages);
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
this.commit(welcome({ version: this.version, model: this.cfg.model, provider: this.cfg.provider, cwd: this.cwd, branch: this.branch, notes: this.agent.memory?.list().length ?? 0, verify: this.verify, mode: this.mode, resumed, tip: TIPS[Math.floor(Math.random() * TIPS.length)] }, this.width));
|
|
217
|
+
this.setTitle(`Amness · ${basename(this.cwd)}`);
|
|
218
|
+
if (resumed && !this.full)
|
|
219
|
+
this.replay();
|
|
220
|
+
this.render();
|
|
221
|
+
if (this.opts.prompt)
|
|
222
|
+
void this.dispatch(this.opts.prompt);
|
|
223
|
+
return this.done;
|
|
224
|
+
}
|
|
225
|
+
recount() { const l = [...this.session.messages].reverse().find((m) => m.role === "assistant" && m.usage?.prompt_tokens); this.lastPrompt = l?.usage?.prompt_tokens ?? 0; }
|
|
226
|
+
replay() {
|
|
227
|
+
const tail = this.session.messages.filter((m) => (m.role === "user" || (m.role === "assistant" && m.content?.trim()))).slice(-6);
|
|
228
|
+
this.commit([dim(" ── earlier in this conversation ──")]);
|
|
229
|
+
for (const m of tail)
|
|
230
|
+
this.commit(m.role === "user" ? userMessage(one((m.content ?? "").split("\n\n<attached>")[0].split("\n\n## ")[0], this.width - 4), this.width) : assistantBlock(one(m.content ?? "", this.width * 3), this.width, true));
|
|
231
|
+
this.commit([dim(" ── continue below ──"), ""]);
|
|
232
|
+
}
|
|
233
|
+
restore = () => {
|
|
234
|
+
if (this.stopped)
|
|
235
|
+
return;
|
|
236
|
+
this.stopped = true;
|
|
237
|
+
clearInterval(this.timer);
|
|
238
|
+
try {
|
|
239
|
+
if (this.full)
|
|
240
|
+
this.full.leave();
|
|
241
|
+
else {
|
|
242
|
+
this.screen.clear();
|
|
243
|
+
if (this.screen.alt)
|
|
244
|
+
this.screen.leaveAlt();
|
|
245
|
+
}
|
|
246
|
+
this.io.output.write(PASTE_OFF + "\x1b[?25h");
|
|
247
|
+
this.io.input.setRawMode?.(false);
|
|
248
|
+
this.io.input.pause?.();
|
|
249
|
+
this.io.input.off?.("data", this.onData);
|
|
250
|
+
}
|
|
251
|
+
catch { /* terminal already gone */ }
|
|
252
|
+
};
|
|
253
|
+
exit(code = 0) {
|
|
254
|
+
this.stopWarm();
|
|
255
|
+
this.agent.procs?.killAllNow();
|
|
256
|
+
void this.tracer?.close();
|
|
257
|
+
if (this.running)
|
|
258
|
+
this.ac?.abort();
|
|
259
|
+
this.saveSession();
|
|
260
|
+
this.restore();
|
|
261
|
+
const hint = this.session.turns.length ? dim(`Resume this conversation with: amness -c (session ${this.session.id})`) : "";
|
|
262
|
+
this.io.output.write(`\r\n${dim("bye 👋")}${hint ? "\r\n" + hint : ""}\r\n`);
|
|
263
|
+
this.mcp.splice(0).forEach((m) => m.close());
|
|
264
|
+
void this.live?.close();
|
|
265
|
+
this.resolveDone(code);
|
|
266
|
+
}
|
|
267
|
+
// ------------------------------------------------------------------ output
|
|
268
|
+
/** Raw pre-styled lines: scrollback in the inline UI, a "raw" block in the full-screen UI. */
|
|
269
|
+
commit(lines) { this.blocks.raw(lines); if (!this.full)
|
|
270
|
+
this.out.push(...lines); }
|
|
271
|
+
/** Scrollback-only output for things the full-screen UI draws from structured blocks instead. */
|
|
272
|
+
inlineOnly(lines) { if (!this.full)
|
|
273
|
+
this.out.push(...lines); }
|
|
274
|
+
notify(level, text, inline) { if (this.full)
|
|
275
|
+
this.blocks.notice(level, text);
|
|
276
|
+
else
|
|
277
|
+
this.out.push(...inline); }
|
|
278
|
+
addUser(text, note, queued) { this.blocks.addUser(text, note, queued); this.inlineOnly(userMessage(text, this.width, note ?? "")); }
|
|
279
|
+
say(text, paint = (s) => s) {
|
|
280
|
+
for (const t of [].concat(text))
|
|
281
|
+
this.commit(["", ...t.split("\n").flatMap((p) => wrapAnsi(paint(p), this.width - 2)).map((l) => " " + l)]);
|
|
282
|
+
}
|
|
283
|
+
note(text, ms = 3000) {
|
|
284
|
+
if (this.full && ms <= 5000) {
|
|
285
|
+
this.full.toast(text, "info", ms);
|
|
286
|
+
this.render();
|
|
287
|
+
setTimeout(() => this.render(), ms + 40);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this.notice = text;
|
|
291
|
+
this.noticeUntil = Date.now() + ms;
|
|
292
|
+
this.render();
|
|
293
|
+
setTimeout(() => { if (Date.now() >= this.noticeUntil)
|
|
294
|
+
this.render(); }, ms + 30); // redraw when it expires so the footer returns to normal
|
|
295
|
+
}
|
|
296
|
+
render = () => {
|
|
297
|
+
if (this.stopped)
|
|
298
|
+
return;
|
|
299
|
+
if (this.full) {
|
|
300
|
+
clearTimeout(this.renderT);
|
|
301
|
+
this.renderT = undefined;
|
|
302
|
+
return this.full.draw();
|
|
303
|
+
}
|
|
304
|
+
if (this.pager)
|
|
305
|
+
return;
|
|
306
|
+
clearTimeout(this.renderT);
|
|
307
|
+
this.renderT = undefined;
|
|
308
|
+
const w = this.width, live = [];
|
|
309
|
+
let cursor;
|
|
310
|
+
if (this.streamBuf.trim()) {
|
|
311
|
+
const r = renderMarkdown(this.streamBuf, w - 2).slice(-6);
|
|
312
|
+
live.push(...r.map((l, i) => (this.msgOpen || i ? " " : bold("● ")) + l));
|
|
313
|
+
}
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
const latest = new Map();
|
|
316
|
+
for (const [id, t] of this.inflight)
|
|
317
|
+
if (t.agent && t.agent !== "lead")
|
|
318
|
+
latest.set(t.parent ?? t.agent, id); // one line per subagent: its newest step
|
|
319
|
+
for (const [id, t] of this.inflight) {
|
|
320
|
+
const subagent = t.agent && t.agent !== "lead";
|
|
321
|
+
if (subagent && latest.get(t.parent ?? t.agent) !== id)
|
|
322
|
+
continue;
|
|
323
|
+
live.push(`${subagent ? dim(" │ ") : ""}${cyan(SPIN[this.frame % SPIN.length])} ${toolCall(t.name, t.args)} ${dim(fmtDuration(now - t.t0))}${subagent ? dim(` · ${t.agent}, step ${this.blocks.kids(t.parent ?? "").length || 1}`) : ""}`);
|
|
324
|
+
}
|
|
325
|
+
if (this.running && this.todos.length)
|
|
326
|
+
live.push(...todoList(this.todos, w));
|
|
327
|
+
if (this.running && this.thinkBuf.trim() && !this.streamBuf.trim()) { // the model's reasoning summary, as it forms (OpenAI Responses)
|
|
328
|
+
const last = this.thinkBuf.replace(/\s+/g, " ").trim().slice(-(w - 8));
|
|
329
|
+
live.push(dim(` ✻ ${last.replace(/^\S*\s/, (m) => (cellWidth(last) >= w - 8 ? "" : m))}`));
|
|
330
|
+
}
|
|
331
|
+
if (this.running) {
|
|
332
|
+
const glyph = fg(at(palette.brand, (this.frame % 30) / 30), GLYPHS[this.frame % GLYPHS.length]), spark = this.runSeries.length > 1 ? " " + sparkline(this.runSeries, 10) : "";
|
|
333
|
+
live.push(`${glyph} ${shimmer((this.mode === "plan" ? "Planning" : "Working") + "…", this.frame)}${dim(` (${fmtDuration(now - this.runStart)} · ↓ ${fmtTokens(this.runTokens)} tokens`)}${spark}${dim(" · esc to interrupt)")}`);
|
|
334
|
+
}
|
|
335
|
+
for (const q of this.queued)
|
|
336
|
+
live.push(dim(` ▸ queued: ${one(q.text, w - 14)}`));
|
|
337
|
+
if (live.length)
|
|
338
|
+
live.unshift("");
|
|
339
|
+
if (this.overlay) {
|
|
340
|
+
const d = this.overlay.render(w), at = live.length;
|
|
341
|
+
live.push(...d.lines);
|
|
342
|
+
if (d.cursor)
|
|
343
|
+
cursor = { row: at + d.cursor.row, col: d.cursor.col };
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
live.push(""); // breathing room above the input box
|
|
347
|
+
if (this.sugg)
|
|
348
|
+
live.push(...this.suggLines(w));
|
|
349
|
+
live.push(gray("─".repeat(w)));
|
|
350
|
+
const r = this.editor.render(w), prompt = this.editor.text.startsWith("!") ? yellow("! ") : this.editor.text.startsWith("#") ? green("# ") : bold(cyan("❯ "));
|
|
351
|
+
const at = live.length;
|
|
352
|
+
live.push(...r.lines.map((l, i) => (i === 0 ? prompt + this.styleInput(l.slice(2)) : " " + this.styleInput(l.slice(2)))));
|
|
353
|
+
cursor = { row: at + r.cursor.row, col: r.cursor.col };
|
|
354
|
+
live.push(gray("─".repeat(w)));
|
|
355
|
+
}
|
|
356
|
+
live.push(footer({ mode: this.mode, model: this.cfg.model, provider: this.cfg.provider, ctxPct: this.ctxPct, cost: this.cost, branch: this.branch, queued: this.queued.length, hint: this.overlay ? undefined : this.hint(), quiet: !!this.overlay, notice: this.notice && now < this.noticeUntil ? this.notice : undefined }, w));
|
|
357
|
+
this.screen.frame(this.out.splice(0), live, cursor);
|
|
358
|
+
};
|
|
359
|
+
schedule() { if (!this.renderT)
|
|
360
|
+
this.renderT = setTimeout(this.render, 12); }
|
|
361
|
+
styleInput(l) { return l.replace(/(@[^\s]+)/g, (m) => cyan(m)).replace(/\[Pasted text #\d+ \+\d+ lines?\]/g, (m) => magenta(m)); }
|
|
362
|
+
hint() {
|
|
363
|
+
if (!this.editor.empty)
|
|
364
|
+
return this.running ? "enter queues your message" : undefined;
|
|
365
|
+
if (this.running)
|
|
366
|
+
return "type to steer · esc to interrupt";
|
|
367
|
+
const p = this.ctxPct;
|
|
368
|
+
return p !== null && p >= 75 ? (p >= 90 ? "context almost full · /compact" : "context filling up · /compact") : "? for shortcuts · ctrl+p commands";
|
|
369
|
+
}
|
|
370
|
+
setTitle(t) { if (this.io.output.isTTY !== false)
|
|
371
|
+
this.io.output.write(`\x1b]0;${t}\x07`); }
|
|
372
|
+
suggLines(w) {
|
|
373
|
+
const s = this.sugg, win = 8, top = Math.max(0, Math.min(s.sel - 3, s.items.length - win));
|
|
374
|
+
return s.items.slice(top, top + win).map((it, i) => {
|
|
375
|
+
const on = top + i === s.sel, label = s.kind === "file" ? it.label : it.label.padEnd(12);
|
|
376
|
+
return (on ? cyan("❯ ") + bold(cyan(label)) : " " + label) + (it.detail ? " " + (on ? gray(it.detail) : dim(it.detail)) : "");
|
|
377
|
+
}).map((l) => (cellWidth(l) > w ? l.slice(0, w + 12) : l));
|
|
378
|
+
}
|
|
379
|
+
tick = () => { this.frame++; this.render(); };
|
|
380
|
+
ensureTimer() { if (!this.timer)
|
|
381
|
+
this.timer = setInterval(this.tick, 100); }
|
|
382
|
+
idleTimer() { if (!this.running && !this.inflight.size && this.timer) {
|
|
383
|
+
clearInterval(this.timer);
|
|
384
|
+
this.timer = undefined;
|
|
385
|
+
} }
|
|
386
|
+
// ------------------------------------------------------------------ input
|
|
387
|
+
onData = (d) => {
|
|
388
|
+
for (const k of this.keys.feed(typeof d === "string" ? d : d.toString("utf8")))
|
|
389
|
+
this.handleKey(k);
|
|
390
|
+
clearTimeout(this.flushT);
|
|
391
|
+
if (this.keys.pending)
|
|
392
|
+
this.flushT = setTimeout(() => { for (const k of this.keys.flush())
|
|
393
|
+
this.handleKey(k); }, 35);
|
|
394
|
+
};
|
|
395
|
+
handleKey(k) {
|
|
396
|
+
if (k.name === "mouse" && k.mouse) {
|
|
397
|
+
this.full?.mouse(k.mouse);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this.pager)
|
|
401
|
+
return this.pager.key(k);
|
|
402
|
+
if (this.overlay) {
|
|
403
|
+
this.overlay.key(k);
|
|
404
|
+
this.schedule();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const ed = this.editor;
|
|
408
|
+
if (this.full) { // full-screen only keys (scrolling, sidebar, details, theme)
|
|
409
|
+
if (this.full.key(k.name, k.ctrl))
|
|
410
|
+
return;
|
|
411
|
+
if (k.ctrl && k.name === "b") {
|
|
412
|
+
this.full.showSidebar = !this.full.showSidebar;
|
|
413
|
+
this.full.invalidate();
|
|
414
|
+
return this.render();
|
|
415
|
+
}
|
|
416
|
+
if (k.ctrl && k.name === "o") {
|
|
417
|
+
this.full.expandAll = !this.full.expandAll;
|
|
418
|
+
this.full.toast(this.full.expandAll ? "all tool output expanded" : "tool output collapsed");
|
|
419
|
+
return this.render();
|
|
420
|
+
}
|
|
421
|
+
if (k.ctrl && k.name === "t")
|
|
422
|
+
return void this.runCommand("/theme");
|
|
423
|
+
if (k.ctrl && k.name === "l") {
|
|
424
|
+
this.full.invalidate();
|
|
425
|
+
return this.render();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (k.ctrl && k.name === "c")
|
|
429
|
+
return this.ctrlC();
|
|
430
|
+
if (k.ctrl && k.name === "d") {
|
|
431
|
+
if (ed.empty)
|
|
432
|
+
return this.exit(0);
|
|
433
|
+
}
|
|
434
|
+
if (k.ctrl && k.name === "o")
|
|
435
|
+
return this.openTranscript();
|
|
436
|
+
if (k.ctrl && k.name === "p")
|
|
437
|
+
return void this.openPalette();
|
|
438
|
+
if (k.alt && k.name === "char" && k.text === "m")
|
|
439
|
+
return void this.runCommand("/model");
|
|
440
|
+
if (k.ctrl && k.name === "l") {
|
|
441
|
+
this.io.output.write("\x1b[2J\x1b[3J\x1b[H");
|
|
442
|
+
this.screen.height = 0;
|
|
443
|
+
return this.render();
|
|
444
|
+
}
|
|
445
|
+
if (k.name === "backtab")
|
|
446
|
+
return this.cycleMode();
|
|
447
|
+
if (k.name === "escape")
|
|
448
|
+
return this.escape();
|
|
449
|
+
if (k.name === "tab" && this.sugg)
|
|
450
|
+
return this.acceptSugg(false);
|
|
451
|
+
if (k.name === "enter" && !k.alt && !k.shift) {
|
|
452
|
+
if (this.sugg && this.sugg.items.length) {
|
|
453
|
+
const it = this.sugg.items[this.sugg.sel];
|
|
454
|
+
if (!(this.sugg.kind === "slash" && ed.text.trim() === it.label))
|
|
455
|
+
return this.acceptSugg(true);
|
|
456
|
+
}
|
|
457
|
+
if (ed.text.slice(0, ed.cursorIndex).endsWith("\\")) {
|
|
458
|
+
ed.backspace();
|
|
459
|
+
ed.newline();
|
|
460
|
+
return this.after();
|
|
461
|
+
}
|
|
462
|
+
return void this.submit();
|
|
463
|
+
}
|
|
464
|
+
if (k.name === "up" || k.name === "down") {
|
|
465
|
+
if (this.sugg) {
|
|
466
|
+
const n = this.sugg.items.length;
|
|
467
|
+
this.sugg.sel = (this.sugg.sel + (k.name === "up" ? n - 1 : 1)) % n;
|
|
468
|
+
return this.render();
|
|
469
|
+
}
|
|
470
|
+
if (!ed.vertical(k.name === "up" ? -1 : 1))
|
|
471
|
+
k.name === "up" ? ed.historyPrev() : ed.historyNext();
|
|
472
|
+
return this.after(false);
|
|
473
|
+
}
|
|
474
|
+
if (k.name === "char" && k.text === "?" && ed.empty)
|
|
475
|
+
return this.openPager(helpLines(this.cmds, this.screen.cols - 1), "help");
|
|
476
|
+
if (this.full && (k.name === "char" || k.name === "paste" || k.name === "backspace"))
|
|
477
|
+
this.full.clearSelection();
|
|
478
|
+
if (editKey(ed, k)) {
|
|
479
|
+
this.escAt = 0;
|
|
480
|
+
return this.after();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
after(suggest = true) { if (suggest)
|
|
484
|
+
this.updateSugg(); this.render(); }
|
|
485
|
+
updateSugg() {
|
|
486
|
+
const text = this.editor.text, tok = tokenAtCursor(text, this.editor.cursorIndex), key = tok ? `${tok.kind}:${tok.query}` : "";
|
|
487
|
+
if (!tok || key === this.suggOff) {
|
|
488
|
+
this.sugg = null;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.suggOff = "";
|
|
492
|
+
let items = [];
|
|
493
|
+
if (tok.kind === "slash")
|
|
494
|
+
items = rankCommands(tok.query, this.cmds);
|
|
495
|
+
else {
|
|
496
|
+
if (!this.files.fresh())
|
|
497
|
+
void this.files.ensure().then(() => this.updateSugg(), () => { }); // list still loading: fill the popup in once it arrives
|
|
498
|
+
items = rankFiles(tok.query, this.files.files);
|
|
499
|
+
}
|
|
500
|
+
const keep = this.sugg && this.sugg.kind === tok.kind && this.sugg.token.query === tok.query ? this.sugg.sel : 0;
|
|
501
|
+
this.sugg = items.length ? { items, sel: Math.min(keep, items.length - 1), token: tok, kind: tok.kind } : null;
|
|
502
|
+
if (!this.sugg)
|
|
503
|
+
return;
|
|
504
|
+
this.schedule();
|
|
505
|
+
}
|
|
506
|
+
acceptSuggPublic(fromEnter) { if (this.sugg)
|
|
507
|
+
this.acceptSugg(fromEnter); }
|
|
508
|
+
acceptSugg(fromEnter) {
|
|
509
|
+
const s = this.sugg, r = applySuggestion(this.editor.text, s.token, s.items[s.sel]);
|
|
510
|
+
this.editor.setText(r.text, r.cursor);
|
|
511
|
+
this.sugg = null;
|
|
512
|
+
this.suggOff = "";
|
|
513
|
+
if (fromEnter && s.kind === "slash" && !this.cmds.find((c) => "/" + c.name === s.items[s.sel].label)?.args)
|
|
514
|
+
return void this.submit();
|
|
515
|
+
this.updateSugg();
|
|
516
|
+
this.render();
|
|
517
|
+
}
|
|
518
|
+
escape() {
|
|
519
|
+
if (this.full?.clearSelection())
|
|
520
|
+
return this.render();
|
|
521
|
+
if (this.sugg) {
|
|
522
|
+
this.suggOff = `${this.sugg.token.kind}:${this.sugg.token.query}`;
|
|
523
|
+
this.sugg = null;
|
|
524
|
+
return this.render();
|
|
525
|
+
}
|
|
526
|
+
if (this.running) {
|
|
527
|
+
this.ac?.abort();
|
|
528
|
+
return this.note("interrupting after the current step…");
|
|
529
|
+
}
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
if (now - this.escAt < 700) {
|
|
532
|
+
this.escAt = 0;
|
|
533
|
+
if (!this.editor.empty) {
|
|
534
|
+
this.editor.clear();
|
|
535
|
+
return this.render();
|
|
536
|
+
}
|
|
537
|
+
return void this.openRewind();
|
|
538
|
+
}
|
|
539
|
+
this.escAt = now;
|
|
540
|
+
if (!this.editor.empty)
|
|
541
|
+
this.note("press esc again to clear");
|
|
542
|
+
else if (this.session.turns.length)
|
|
543
|
+
this.note("press esc again to rewind");
|
|
544
|
+
}
|
|
545
|
+
ctrlC() {
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
if (this.running) {
|
|
548
|
+
this.ac?.abort();
|
|
549
|
+
this.ctrlcAt = 0;
|
|
550
|
+
return this.note("interrupting after the current step…");
|
|
551
|
+
}
|
|
552
|
+
if (!this.editor.empty) {
|
|
553
|
+
this.editor.clear();
|
|
554
|
+
this.sugg = null;
|
|
555
|
+
return this.render();
|
|
556
|
+
}
|
|
557
|
+
if (now - this.ctrlcAt < 2000)
|
|
558
|
+
return this.exit(0);
|
|
559
|
+
this.ctrlcAt = now;
|
|
560
|
+
this.note("press ctrl-c again to exit");
|
|
561
|
+
}
|
|
562
|
+
cycleMode() { this.setMode(MODES[(MODES.indexOf(this.mode) + 1) % MODES.length]); }
|
|
563
|
+
setMode(m) {
|
|
564
|
+
if (m === this.mode)
|
|
565
|
+
return;
|
|
566
|
+
this.mode = m;
|
|
567
|
+
this.modeNotice = m === "plan" ? "[mode: plan mode is ON. You are read-only. Investigate, then present your plan with exit_plan_mode.]" : this.mode === "yolo" ? "" : `[mode: permission mode is now "${m}"; plan mode is off.]`;
|
|
568
|
+
if (m === "default")
|
|
569
|
+
this.note("default mode: asks before edits and commands");
|
|
570
|
+
else if (m === "yolo" && (this.cfg.sandbox ?? "off") === "off" && sandboxKind())
|
|
571
|
+
this.note("tip: /sandbox workspace makes yolo much safer (commands cannot write outside the project)");
|
|
572
|
+
else
|
|
573
|
+
this.render(); // other modes announce themselves in the footer label
|
|
574
|
+
}
|
|
575
|
+
// ------------------------------------------------------------------ submitting
|
|
576
|
+
async submit() {
|
|
577
|
+
const text = this.editor.submit();
|
|
578
|
+
this.sugg = null;
|
|
579
|
+
this.suggOff = "";
|
|
580
|
+
if (!text)
|
|
581
|
+
return this.render();
|
|
582
|
+
this.saveHistory();
|
|
583
|
+
if (this.running) {
|
|
584
|
+
if (text.startsWith("/")) {
|
|
585
|
+
const n = text.slice(1).split(/\s+/)[0];
|
|
586
|
+
const c = this.cmds.find((x) => x.name === n);
|
|
587
|
+
if (c?.live)
|
|
588
|
+
return void (await this.runCommand(text));
|
|
589
|
+
this.note("finish or interrupt (esc) before running commands");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const { text: full, attached } = expandMentions(text, this.cwd);
|
|
593
|
+
this.queued.push({ text, full });
|
|
594
|
+
void attached;
|
|
595
|
+
return this.render();
|
|
596
|
+
}
|
|
597
|
+
await this.dispatch(text);
|
|
598
|
+
}
|
|
599
|
+
async dispatch(text) {
|
|
600
|
+
this.stopWarm();
|
|
601
|
+
if (text.startsWith("/"))
|
|
602
|
+
return this.runCommand(text);
|
|
603
|
+
if (text.startsWith("!"))
|
|
604
|
+
return this.bash(text.slice(1).trim());
|
|
605
|
+
if (text.startsWith("#"))
|
|
606
|
+
return this.remember(text.slice(1).trim());
|
|
607
|
+
await this.turn(text);
|
|
608
|
+
}
|
|
609
|
+
async runCommand(text) {
|
|
610
|
+
const [name, ...rest] = text.slice(1).split(/\s+/), args = text.slice(1).slice(name.length).trim();
|
|
611
|
+
const c = this.cmds.find((x) => x.name === name || x.alias === name);
|
|
612
|
+
if (!c) {
|
|
613
|
+
this.commit(["", ` ${red("✗")} unknown command /${name}. Type /help.`]);
|
|
614
|
+
return this.render();
|
|
615
|
+
}
|
|
616
|
+
void rest;
|
|
617
|
+
try {
|
|
618
|
+
await c.run(this, args);
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
this.commit(["", ` ${red("✗")} /${name}: ${e.message}`]);
|
|
622
|
+
}
|
|
623
|
+
this.render();
|
|
624
|
+
}
|
|
625
|
+
drainQueue() {
|
|
626
|
+
const q = this.queued.splice(0);
|
|
627
|
+
for (const m of q)
|
|
628
|
+
this.addUser(m.text, "sent while working");
|
|
629
|
+
this.schedule();
|
|
630
|
+
return q.map((m) => m.full);
|
|
631
|
+
}
|
|
632
|
+
async bash(cmd) {
|
|
633
|
+
if (!cmd)
|
|
634
|
+
return void this.say("After ! type a command to run it now, for example !ls or !git status. Its output is added to your next message.", dim);
|
|
635
|
+
const { runShell, withNotFoundHint } = await import("../../src/index.js");
|
|
636
|
+
const callId = "!" + Date.now();
|
|
637
|
+
this.blocks.toolStart(callId, "sh", JSON.stringify({ cmd }));
|
|
638
|
+
this.inlineOnly(["", toolHeader("sh", JSON.stringify({ cmd }), null).replace("Bash", "Bash!")]);
|
|
639
|
+
this.render();
|
|
640
|
+
const out = withNotFoundHint(await runShell(cmd, this.cwd, 120_000), () => false, true);
|
|
641
|
+
this.blocks.toolEnd(callId, out);
|
|
642
|
+
this.inlineOnly(toolResult("sh", "{}", out, this.width));
|
|
643
|
+
this.pendingContext.push(`I ran \`${cmd}\` in my terminal:\n${out.slice(0, 6000)}`);
|
|
644
|
+
this.render();
|
|
645
|
+
}
|
|
646
|
+
remember(note) {
|
|
647
|
+
const mem = this.agent.memory;
|
|
648
|
+
if (!note || !mem)
|
|
649
|
+
return this.note("nothing to remember");
|
|
650
|
+
const e = mem.save({ name: slugify(note.split(/\s+/).slice(0, 6).join(" ")), type: "project", description: one(note, 120), content: note });
|
|
651
|
+
if (this.full)
|
|
652
|
+
this.blocks.notice("ok", `remembered: ${note} (saved to ${e.scope} memory as "${e.name}")`);
|
|
653
|
+
else
|
|
654
|
+
this.commit(["", ` ${green("✓")} ${dim("remembered:")} ${note}`, ` ${dim(`saved to ${e.scope} memory as "${e.name}"`)}`]);
|
|
655
|
+
this.render();
|
|
656
|
+
}
|
|
657
|
+
async turn(text) {
|
|
658
|
+
const { text: expanded, attached, images: found, problems } = expandMentions(text, this.cwd);
|
|
659
|
+
const vision = visionLikely(this.cfg.provider, this.cfg.model), images = vision ? found : [];
|
|
660
|
+
for (const p of problems)
|
|
661
|
+
this.note(p);
|
|
662
|
+
if (found.length && !vision)
|
|
663
|
+
this.note(`${this.cfg.model} does not look like a vision model: ${found.length} image(s) not sent. Use a model that accepts images (for example gpt-5 or Claude).`);
|
|
664
|
+
const prefix = [this.modeNotice, ...this.pendingContext].filter(Boolean).join("\n\n");
|
|
665
|
+
this.modeNotice = "";
|
|
666
|
+
this.pendingContext = [];
|
|
667
|
+
const full = prefix ? `${prefix}\n\n${expanded}` : expanded;
|
|
668
|
+
this.addUser(text, attachedNote(attached) || undefined);
|
|
669
|
+
if (!this.session.title)
|
|
670
|
+
this.session.title = one(text, 60);
|
|
671
|
+
const turn = { at: this.session.messages.length, text, t: Date.now(), snaps: {} };
|
|
672
|
+
this.session.turns.push(turn);
|
|
673
|
+
this.running = true;
|
|
674
|
+
this.ac = new AbortController();
|
|
675
|
+
this.runStart = Date.now();
|
|
676
|
+
this.runTokens = 0;
|
|
677
|
+
this.todos = [];
|
|
678
|
+
this.runSeries = [];
|
|
679
|
+
this.phase = undefined;
|
|
680
|
+
this.activity = [];
|
|
681
|
+
this.setTitle(`● working · ${basename(this.cwd)}`);
|
|
682
|
+
Object.assign(this.budget, { tokens: 0, cachedTokens: 0, calls: 0, t0: Date.now(), maxTokens: this.cfg.maxTokens, maxMs: 60 * 60_000 });
|
|
683
|
+
this.ensureTimer();
|
|
684
|
+
this.render();
|
|
685
|
+
let ok = false;
|
|
686
|
+
try {
|
|
687
|
+
const r = await this.agent.chat(this.session.messages, full, { images });
|
|
688
|
+
this.session.messages = r.messages;
|
|
689
|
+
ok = true;
|
|
690
|
+
if (r.stopped === "budget") {
|
|
691
|
+
const head = r.text.split("\n")[0];
|
|
692
|
+
this.notify("warn", head, ["", ` ${yellow("$")} ${head}`]);
|
|
693
|
+
}
|
|
694
|
+
else if (r.stopped === "stuck")
|
|
695
|
+
this.notify("warn", r.text + "; tell it what to try next", ["", ` ${yellow("!")} ${r.text}; tell it what to try next`]);
|
|
696
|
+
else if (r.stopped === "maxSteps")
|
|
697
|
+
this.notify("warn", "stopped after the maximum number of steps; send a message to continue", ["", ` ${yellow("!")} stopped after the maximum number of steps; send a message to continue`]);
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
this.session.turns.pop();
|
|
701
|
+
const msg = e.message;
|
|
702
|
+
this.notify("error", one(msg, 400) + (/40[13]|auth|api key|credential/i.test(msg) ? " (check your key with /provider)" : ""), ["", ` ${red("✗")} ${one(msg, this.width * 2)}`, ...(/40[13]|auth|api key|credential/i.test(msg) ? [` ${dim("check your key with /provider")}`] : [])]);
|
|
703
|
+
}
|
|
704
|
+
this.flushStream(true);
|
|
705
|
+
this.blocks.endAssistant();
|
|
706
|
+
this.blocks.endThinking();
|
|
707
|
+
this.blocks.closeRunning(ok ? "ended without a result" : "interrupted");
|
|
708
|
+
const secs = Date.now() - this.runStart;
|
|
709
|
+
if (ok && (secs > 4000 || this.runTokens > 2000))
|
|
710
|
+
this.inlineOnly([dim(` ✻ Worked for ${fmtDuration(secs)} · ${fmtTokens(this.runTokens)} tokens`)]);
|
|
711
|
+
if (this.todos.length && this.todos.some((t) => t.status !== "done"))
|
|
712
|
+
this.inlineOnly(todoList(this.todos, this.width));
|
|
713
|
+
this.todos = [];
|
|
714
|
+
this.inflight.clear();
|
|
715
|
+
this.running = false;
|
|
716
|
+
this.ac = undefined;
|
|
717
|
+
this.thinkBuf = "";
|
|
718
|
+
this.scheduleWarm();
|
|
719
|
+
this.setTitle(`${ok ? "✔" : "✗"} Amness · ${basename(this.cwd)}`);
|
|
720
|
+
if (secs > 20_000)
|
|
721
|
+
this.io.output.write("\x07"); // a long turn finished: nudge the user, even if they switched tabs
|
|
722
|
+
this.saveSession();
|
|
723
|
+
this.refreshBranch();
|
|
724
|
+
this.idleTimer();
|
|
725
|
+
this.render();
|
|
726
|
+
if (this.queued.length) {
|
|
727
|
+
const q = this.queued.splice(0);
|
|
728
|
+
await this.dispatch(q.map((m) => m.text).join("\n\n"));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// ------------------------------------------------------------------ events from the agent
|
|
732
|
+
stepNow() { if (!this.trace.length)
|
|
733
|
+
this.trace.push({ n: 1, t0: Date.now(), ms: 0, prompt: 0, completion: 0, tools: [], notes: [] }); return this.trace[this.trace.length - 1]; }
|
|
734
|
+
/** The session's trace (local JSONL file, plus an OpenTelemetry collector when AMNESS_OTLP is set). */
|
|
735
|
+
tracer;
|
|
736
|
+
tracerFor = "";
|
|
737
|
+
trace_(e) {
|
|
738
|
+
if (!this.persist)
|
|
739
|
+
return;
|
|
740
|
+
if (!this.tracer || this.tracerFor !== this.session.id) {
|
|
741
|
+
void this.tracer?.close();
|
|
742
|
+
this.tracerFor = this.session.id;
|
|
743
|
+
this.tracer = new Tracer({ file: join(this.cwd, ".amness", "traces", `${this.session.id}.jsonl`), otlp: process.env.AMNESS_OTLP, sessionId: this.session.id, model: this.cfg.model });
|
|
744
|
+
}
|
|
745
|
+
this.tracer.event(e);
|
|
746
|
+
}
|
|
747
|
+
onEvent(e) {
|
|
748
|
+
this.live?.onEvent(e);
|
|
749
|
+
this.trace_(e);
|
|
750
|
+
const sub = e.agent && e.agent !== "lead" ? e.agent : undefined, w = this.width;
|
|
751
|
+
switch (e.kind) {
|
|
752
|
+
case "thinking":
|
|
753
|
+
if (!sub) {
|
|
754
|
+
this.thinkBuf = (this.thinkBuf + e.delta).slice(-600);
|
|
755
|
+
this.blocks.streamThinking(e.delta);
|
|
756
|
+
}
|
|
757
|
+
break;
|
|
758
|
+
case "turn":
|
|
759
|
+
this.session.usage.prompt += e.prompt;
|
|
760
|
+
this.session.usage.completion += e.completion;
|
|
761
|
+
this.session.usage.calls++;
|
|
762
|
+
if (!sub) {
|
|
763
|
+
const s = this.stepNow();
|
|
764
|
+
s.ms = Date.now() - s.t0;
|
|
765
|
+
s.prompt = e.prompt;
|
|
766
|
+
s.completion = e.completion;
|
|
767
|
+
s.cached = e.cached;
|
|
768
|
+
}
|
|
769
|
+
this.session.usage.cached = (this.session.usage.cached ?? 0) + e.cached;
|
|
770
|
+
if (!sub)
|
|
771
|
+
this.lastCached = e.cached;
|
|
772
|
+
this.session.usage.reasoning = (this.session.usage.reasoning ?? 0) + e.reasoning;
|
|
773
|
+
this.session.usage.series = [...(this.session.usage.series ?? []), e.prompt].slice(-200);
|
|
774
|
+
this.runSeries = [...this.runSeries, e.completion].slice(-40);
|
|
775
|
+
if (!sub) {
|
|
776
|
+
this.runTokens = e.tokens;
|
|
777
|
+
this.lastPrompt = e.prompt;
|
|
778
|
+
}
|
|
779
|
+
else
|
|
780
|
+
this.runTokens += e.completion;
|
|
781
|
+
break;
|
|
782
|
+
case "text":
|
|
783
|
+
if (!sub) {
|
|
784
|
+
this.thinkBuf = "";
|
|
785
|
+
this.blocks.endThinking();
|
|
786
|
+
this.blocks.streamAssistant(e.delta);
|
|
787
|
+
this.streamBuf += e.delta;
|
|
788
|
+
this.streamedThisMsg = true;
|
|
789
|
+
this.flushStream(false);
|
|
790
|
+
}
|
|
791
|
+
break;
|
|
792
|
+
case "assistant":
|
|
793
|
+
if (!sub) {
|
|
794
|
+
if (!this.streamedThisMsg || e.streamed === false) {
|
|
795
|
+
this.blocks.setAssistant(e.text);
|
|
796
|
+
this.streamBuf = e.text;
|
|
797
|
+
this.streamedThisMsg = false;
|
|
798
|
+
}
|
|
799
|
+
this.blocks.endAssistant();
|
|
800
|
+
this.flushStream(true);
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
case "toolstart":
|
|
804
|
+
this.blocks.toolStart(e.id, e.name, e.args, sub, e.parent);
|
|
805
|
+
this.inflight.set(e.id, { name: e.name, args: e.args, t0: Date.now(), agent: sub, parent: e.parent });
|
|
806
|
+
this.ensureTimer();
|
|
807
|
+
break;
|
|
808
|
+
case "edit":
|
|
809
|
+
this.pendingEdits.set(e.path, e);
|
|
810
|
+
this.edits.push(e);
|
|
811
|
+
snapshot(this.session.turns.at(-1), e.path, e.before);
|
|
812
|
+
break;
|
|
813
|
+
case "tool": {
|
|
814
|
+
const t = this.inflight.get(e.id ?? "");
|
|
815
|
+
this.inflight.delete(e.id ?? "");
|
|
816
|
+
this.idleTimer();
|
|
817
|
+
const bad = isFailure(e.result);
|
|
818
|
+
if (!sub)
|
|
819
|
+
this.stepNow().tools.push({ name: e.name, args: e.args, ms: t ? Date.now() - t.t0 : 0, ok: !bad });
|
|
820
|
+
if (!sub && e.name !== "todo_write")
|
|
821
|
+
this.activity = [...this.activity, { name: e.name, args: e.args, ms: t ? Date.now() - t.t0 : 0, ok: !bad }].slice(-12);
|
|
822
|
+
let editPath = "";
|
|
823
|
+
if ((e.name === "write_file" || e.name === "edit_file") && !bad) {
|
|
824
|
+
try {
|
|
825
|
+
editPath = JSON.parse(e.args).path;
|
|
826
|
+
}
|
|
827
|
+
catch { /* keep empty */ }
|
|
828
|
+
}
|
|
829
|
+
let ed = editPath ? this.pendingEdits.get(editPath) : undefined;
|
|
830
|
+
if (editPath)
|
|
831
|
+
this.pendingEdits.delete(editPath);
|
|
832
|
+
let more = [];
|
|
833
|
+
if (e.name === "apply_patch" && !bad) {
|
|
834
|
+
[ed, ...more] = [...this.pendingEdits.values()];
|
|
835
|
+
this.pendingEdits.clear();
|
|
836
|
+
editPath = ed?.path ?? "";
|
|
837
|
+
}
|
|
838
|
+
this.blocks.toolEnd(e.id ?? "", e.result, ed ? { path: ed.path, created: ed.created, added: ed.added, removed: ed.removed, diff: ed.diff } : undefined);
|
|
839
|
+
for (const x of more) {
|
|
840
|
+
const id = `${e.id}#${x.path}`;
|
|
841
|
+
this.blocks.toolStart(id, "edit_file", JSON.stringify({ path: x.path }));
|
|
842
|
+
this.blocks.toolEnd(id, `patched ${x.path}`, { path: x.path, created: x.created, added: x.added, removed: x.removed, diff: x.diff });
|
|
843
|
+
}
|
|
844
|
+
if (sub)
|
|
845
|
+
break; // a subagent's steps live inside its own card (full-screen) or the live status line (inline), never scattered in the transcript
|
|
846
|
+
if (e.name === "todo_write")
|
|
847
|
+
break;
|
|
848
|
+
const lines = ["", toolHeader(e.name, e.args, !bad)];
|
|
849
|
+
if (ed) {
|
|
850
|
+
lines.push(` ${gray("⎿")} ${dim(editSummary(ed))}`, ...renderDiff(ed.diff, w));
|
|
851
|
+
for (const x of more)
|
|
852
|
+
lines.push(` ${gray("⎿")} ${dim(editSummary(x))}`, ...renderDiff(x.diff, w));
|
|
853
|
+
}
|
|
854
|
+
else if (!editPath)
|
|
855
|
+
lines.push(...toolResult(e.name, e.args, e.result, w));
|
|
856
|
+
void t;
|
|
857
|
+
this.inlineOnly(lines);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case "todos":
|
|
861
|
+
this.todos = e.todos;
|
|
862
|
+
break;
|
|
863
|
+
case "review":
|
|
864
|
+
if (!sub) {
|
|
865
|
+
this.stepNow().notes.push(e.pass ? "reviewer: no problems" : "reviewer: " + e.findings.join("; ").slice(0, 200));
|
|
866
|
+
if (!e.pass)
|
|
867
|
+
this.notify("warn", `reviewer found ${e.findings.length} problem${e.findings.length === 1 ? "" : "s"}: ${one(e.findings.join("; "), 300)}`, [dim(` ✖ reviewer: ${one(e.findings.join("; "), w - 16)}`)]);
|
|
868
|
+
else
|
|
869
|
+
this.note("reviewer: no problems found");
|
|
870
|
+
}
|
|
871
|
+
break;
|
|
872
|
+
case "audit":
|
|
873
|
+
if (!sub && (e.changed.length || e.notes.length)) {
|
|
874
|
+
const line = `${e.changed.length} file${e.changed.length === 1 ? "" : "s"} changed${e.testsAfterLastEdit === true ? " · tests passed after the last edit" : ""}`;
|
|
875
|
+
this.stepNow().notes.push("audit: " + line + (e.notes.length ? " · " + e.notes.join("; ") : ""));
|
|
876
|
+
this.notify(e.notes.length ? "warn" : "ok", `${line}${e.notes.length ? ". Note: " + e.notes.join("; ") : ""}`, [dim(` ◐ ${line}${e.notes.length ? " · " + e.notes.join("; ") : ""}`)]);
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
case "hook":
|
|
880
|
+
this.stepNow().notes.push(`hook ${e.event}${e.blocked ? " blocked" : ""}: ${one(e.detail, 100)}`);
|
|
881
|
+
if (e.blocked || e.detail !== "added context")
|
|
882
|
+
this.notify(e.blocked ? "warn" : "info", `hook ${e.event}: ${one(e.detail, 200)}`, [dim(` ⚙ hook ${e.event}: ${one(e.detail, w - 20)}`)]);
|
|
883
|
+
break;
|
|
884
|
+
case "policy":
|
|
885
|
+
if (!sub)
|
|
886
|
+
this.policyLog = [...this.policyLog, `${e.action.padEnd(5)} ${e.tool}: ${e.reason}`].slice(-20);
|
|
887
|
+
break;
|
|
888
|
+
case "cachebreak":
|
|
889
|
+
if (!sub) {
|
|
890
|
+
this.cacheBreaks++;
|
|
891
|
+
this.stepNow().notes.push("cache break: " + e.reason);
|
|
892
|
+
}
|
|
893
|
+
break;
|
|
894
|
+
case "trim":
|
|
895
|
+
if (!sub) {
|
|
896
|
+
this.trimmed += e.saved;
|
|
897
|
+
this.stepNow().notes.push(`trimmed ~${fmtTokens(Math.round(e.saved / 4))} tokens (${e.what})`);
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
case "phase":
|
|
901
|
+
if (!sub) {
|
|
902
|
+
this.phase = { phase: e.phase, step: e.step, max: e.max, n: e.n };
|
|
903
|
+
if (e.phase === "model")
|
|
904
|
+
this.trace = [...this.trace, { n: this.trace.length + 1, t0: Date.now(), ms: 0, prompt: 0, completion: 0, tools: [], notes: [] }].slice(-300);
|
|
905
|
+
else if (e.phase === "tools")
|
|
906
|
+
this.stepNow().parallel = e.n;
|
|
907
|
+
else if (e.phase === "verify")
|
|
908
|
+
this.stepNow().notes.push("verify gate running");
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
case "finish":
|
|
912
|
+
if (!sub) {
|
|
913
|
+
this.flushStream(true);
|
|
914
|
+
this.blocks.summary(e.summary);
|
|
915
|
+
this.inlineOnly(["", ...renderMarkdown(e.summary, w - 4).map((l, i) => (i ? " " : " " + green("✔ ")) + l)]);
|
|
916
|
+
}
|
|
917
|
+
break;
|
|
918
|
+
case "rejected":
|
|
919
|
+
this.stepNow().notes.push("finish rejected: " + one(e.detail, 120));
|
|
920
|
+
this.notify("warn", `finish rejected by verification: ${one(e.detail, 300)}`, ["", ` ${yellow("⛔")} ${dim("finish rejected by verification:")} ${one(e.detail, w - 10)}`]);
|
|
921
|
+
break;
|
|
922
|
+
case "denied":
|
|
923
|
+
this.stepNow().notes.push("denied: " + one(e.detail, 120));
|
|
924
|
+
this.notify("warn", `denied: ${one(e.detail, 300)}`, ["", ` ${red("⊘")} ${dim("denied: " + one(e.detail, w - 14))}`]);
|
|
925
|
+
break;
|
|
926
|
+
case "nudge":
|
|
927
|
+
if (sub)
|
|
928
|
+
break;
|
|
929
|
+
this.stepNow().notes.push("nudged: " + one(e.detail, 80));
|
|
930
|
+
{
|
|
931
|
+
const msg = /^budget \d+% used/.test(e.detail) ? `${e.detail}: asked the model to wrap up` : /^(step limit|stagnation|stuck|explored)/.test(e.detail) ? e.detail : "nudged the model to act instead of describing";
|
|
932
|
+
this.notify("info", msg, [dim(` ↻ ${msg}`)]);
|
|
933
|
+
}
|
|
934
|
+
break;
|
|
935
|
+
case "repair":
|
|
936
|
+
this.stepNow().notes.push("repaired a tool call: " + one(e.detail, 100));
|
|
937
|
+
this.notify("info", `repaired a malformed tool call: ${one(e.detail, 200)}`, [dim(` ⚒ repaired a malformed tool call: ${one(e.detail, w - 30)}`)]);
|
|
938
|
+
break;
|
|
939
|
+
case "loop":
|
|
940
|
+
this.stepNow().notes.push("loop guard: " + one(e.detail, 100));
|
|
941
|
+
this.notify("info", "blocked a repeated identical call", [dim(" ∞ blocked a repeated identical call")]);
|
|
942
|
+
break;
|
|
943
|
+
case "rollover":
|
|
944
|
+
this.notify("info", "context getting large: continued from a handoff note", [magenta(" ⟳ ") + dim("context getting large: continued from a handoff note")]);
|
|
945
|
+
break;
|
|
946
|
+
case "budget":
|
|
947
|
+
if (sub)
|
|
948
|
+
break;
|
|
949
|
+
if (/exhausted/.test(e.detail))
|
|
950
|
+
this.notify("error", e.detail, ["", ` ${red("$")} ${e.detail}`]);
|
|
951
|
+
else
|
|
952
|
+
this.notify("warn", `${e.detail}: writing a final report`, [dim(` $ ${e.detail}: writing a final report`)]);
|
|
953
|
+
break;
|
|
954
|
+
case "aborted":
|
|
955
|
+
this.notify("warn", "interrupted. Your progress is kept; send a message to continue.", ["", ` ${yellow("⏹")} ${dim("interrupted. Your progress is kept; send a message to continue.")}`]);
|
|
956
|
+
break;
|
|
957
|
+
case "steer":
|
|
958
|
+
case "tools": break;
|
|
959
|
+
}
|
|
960
|
+
this.schedule();
|
|
961
|
+
}
|
|
962
|
+
/** Stream assistant text: complete paragraphs are committed to scrollback immediately; the unfinished tail stays in the live region. */
|
|
963
|
+
flushStream(final) {
|
|
964
|
+
const w = this.width;
|
|
965
|
+
if (this.full) {
|
|
966
|
+
if (final) {
|
|
967
|
+
this.streamBuf = "";
|
|
968
|
+
this.msgOpen = false;
|
|
969
|
+
this.streamedThisMsg = false;
|
|
970
|
+
}
|
|
971
|
+
return;
|
|
972
|
+
} // the full-screen UI draws the block store
|
|
973
|
+
if (final) {
|
|
974
|
+
const rest = this.streamBuf;
|
|
975
|
+
this.streamBuf = "";
|
|
976
|
+
if (rest.trim()) {
|
|
977
|
+
this.commit(assistantBlock(rest, w, !this.msgOpen).map((l, i) => (i === 0 && !this.msgOpen ? ["", l] : [l])).flat());
|
|
978
|
+
this.msgOpen = true;
|
|
979
|
+
}
|
|
980
|
+
this.msgOpen = false;
|
|
981
|
+
this.streamedThisMsg = false;
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const { done, rest } = takeCompleteBlocks(this.streamBuf);
|
|
985
|
+
if (done.trim()) {
|
|
986
|
+
this.commit(assistantBlock(done, w, !this.msgOpen).map((l, i) => (i === 0 && !this.msgOpen ? ["", l] : [l])).flat());
|
|
987
|
+
this.msgOpen = true;
|
|
988
|
+
}
|
|
989
|
+
this.streamBuf = rest;
|
|
990
|
+
}
|
|
991
|
+
// ------------------------------------------------------------------ permission prompts
|
|
992
|
+
approve = async (req) => {
|
|
993
|
+
const { name, args, preview, reason } = req, w = this.width;
|
|
994
|
+
if (!req.floor && name === "sh" && this.shAllowed(String(args.cmd)))
|
|
995
|
+
return true;
|
|
996
|
+
if (!req.floor && this.toolRules.has(name))
|
|
997
|
+
return true;
|
|
998
|
+
let title = "Permission", body = [], question = "Allow this?", options;
|
|
999
|
+
let paint, detail;
|
|
1000
|
+
if (name === "exit_plan_mode") {
|
|
1001
|
+
title = "Ready to code?";
|
|
1002
|
+
body = renderMarkdown(String(args.plan ?? preview ?? ""), w - 6);
|
|
1003
|
+
question = "Would you like to proceed?";
|
|
1004
|
+
detail = { kind: "plan", md: String(args.plan ?? preview ?? "") };
|
|
1005
|
+
options = [{ label: "Yes, and accept edits automatically" }, { label: "Yes, and ask before each edit" }, { label: "No, keep planning", hint: "tell it what to change" }];
|
|
1006
|
+
}
|
|
1007
|
+
else if (name === "write_file" || name === "edit_file" || name === "apply_patch") {
|
|
1008
|
+
const target = name === "apply_patch" ? `${(String(args.patch).match(/^(?:\*\*\* (?:Update|Add|Delete) File:|\+\+\+ )/gm) ?? []).length || "?"} file(s)` : String(args.path);
|
|
1009
|
+
title = `${name === "write_file" ? "Write" : name === "apply_patch" ? "Patch" : "Edit"} ${target}`;
|
|
1010
|
+
body = preview ? renderDiff(preview, w - 4, 18).map((l) => l.slice(5)) : [String(args.path)];
|
|
1011
|
+
question = "Apply this change?";
|
|
1012
|
+
if (preview)
|
|
1013
|
+
detail = { kind: "diff", path: name === "apply_patch" ? target : String(args.path), diff: preview };
|
|
1014
|
+
options = [{ label: "Yes" }, { label: "Yes, and accept all edits this session", hint: "shift+tab" }, { label: "No, and tell Amness what to do instead", hint: "esc" }];
|
|
1015
|
+
}
|
|
1016
|
+
else if (name === "sh") {
|
|
1017
|
+
const danger = riskOf(String(args.cmd));
|
|
1018
|
+
paint = danger ? yellow : undefined;
|
|
1019
|
+
const irr = irreversibleReason(String(args.cmd));
|
|
1020
|
+
title = "Run command";
|
|
1021
|
+
body = [bold(String(args.cmd)), ...(danger ? ["", yellow(`⚠ ${danger}`)] : []), ...(irr ? [danger ? "" : "", yellow(`⚠ cannot be undone: ${irr}`)].filter((x, i, arr) => x || (i === 0 && arr.length > 1 && !danger)) : [])];
|
|
1022
|
+
question = "Run this command?";
|
|
1023
|
+
detail = { kind: "command", cmd: String(args.cmd), danger: danger || undefined };
|
|
1024
|
+
const prefix = shPrefix(String(args.cmd));
|
|
1025
|
+
const simple = !danger && !/[;&|<>`$()]/.test(String(args.cmd));
|
|
1026
|
+
options = [{ label: "Yes" }, ...(simple ? [{ label: `Yes, and don't ask again for ${bold(prefix)} commands`, hint: "this session" }, { label: `Yes, and always allow ${bold(prefix + " *")} in this project`, hint: "saves a rule" }] : []), { label: "No, and tell Amness what to do instead", hint: "esc" }];
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
title = name;
|
|
1030
|
+
detail = { kind: "text", text: `${reason ?? "requires approval"}\n${JSON.stringify(args)}` };
|
|
1031
|
+
body = [reason ?? "requires approval", ...(preview ? ["", preview] : []), "", dim(one(JSON.stringify(args), w - 8))];
|
|
1032
|
+
options = [{ label: "Yes" }, { label: `Yes, and don't ask again for ${bold(name)}`, hint: "this session" }, { label: "No, and tell Amness what to do instead", hint: "esc" }];
|
|
1033
|
+
}
|
|
1034
|
+
this.ensureTimer();
|
|
1035
|
+
const idx = await new Promise((res) => { this.overlay = new ChoiceOverlay(title, body, question, options, res, paint, detail); this.render(); });
|
|
1036
|
+
this.overlay = null;
|
|
1037
|
+
this.render();
|
|
1038
|
+
if (idx === undefined)
|
|
1039
|
+
return "the user declined this action; take a different approach or ask what they want";
|
|
1040
|
+
const last = idx === options.length - 1;
|
|
1041
|
+
if (last) {
|
|
1042
|
+
const note = await this.askText("What should Amness do instead?");
|
|
1043
|
+
return note || "the user declined this action";
|
|
1044
|
+
}
|
|
1045
|
+
if (name === "exit_plan_mode")
|
|
1046
|
+
this.setMode(idx === 0 ? "acceptEdits" : "default");
|
|
1047
|
+
else if (idx === 1 && (name === "write_file" || name === "edit_file" || name === "apply_patch"))
|
|
1048
|
+
this.setMode("acceptEdits");
|
|
1049
|
+
else if (idx === 1 && name === "sh")
|
|
1050
|
+
this.shRules.add(shPrefix(String(args.cmd)));
|
|
1051
|
+
else if (idx === 2 && name === "sh") {
|
|
1052
|
+
this.shRules.add(shPrefix(String(args.cmd)));
|
|
1053
|
+
try {
|
|
1054
|
+
Permissions.addProjectRule(this.cwd, "allow", `sh(${shPrefix(String(args.cmd))} *)`);
|
|
1055
|
+
const fp = projectConfigFingerprint(this.cwd), st = loadState();
|
|
1056
|
+
if (fp && this.persist) {
|
|
1057
|
+
st.hookTrust = { ...(st.hookTrust ?? {}), [this.cwd]: fp };
|
|
1058
|
+
saveState(st);
|
|
1059
|
+
}
|
|
1060
|
+
this.loadHooks();
|
|
1061
|
+
this.note(`saved: allow sh(${shPrefix(String(args.cmd))} *)`);
|
|
1062
|
+
}
|
|
1063
|
+
catch { /* the session rule still applies */ }
|
|
1064
|
+
}
|
|
1065
|
+
else if (idx === 1)
|
|
1066
|
+
this.toolRules.add(name);
|
|
1067
|
+
return true;
|
|
1068
|
+
};
|
|
1069
|
+
shAllowed(cmd) {
|
|
1070
|
+
if (/[;&|<>`$()]/.test(cmd) || riskOf(cmd))
|
|
1071
|
+
return false;
|
|
1072
|
+
return [...this.shRules].some((p) => cmd === p || cmd.startsWith(p + " "));
|
|
1073
|
+
}
|
|
1074
|
+
// ------------------------------------------------------------------ helpers used by commands
|
|
1075
|
+
pick(title, items, initial = 0, onMove) {
|
|
1076
|
+
return new Promise((res) => { const p = new PickerOverlay(title, items, (v) => { this.overlay = null; this.render(); res(v); }, initial); if (onMove)
|
|
1077
|
+
p.onMove = onMove; this.overlay = p; this.render(); });
|
|
1078
|
+
}
|
|
1079
|
+
askText(prompt, o = {}) {
|
|
1080
|
+
return new Promise((res) => { this.overlay = new InputOverlay(prompt, (v) => { this.overlay = null; this.render(); res(v); }, o); this.render(); });
|
|
1081
|
+
}
|
|
1082
|
+
showText(lines, title) { return new Promise((res) => { this.overlay = new TextOverlay(lines, () => { this.overlay = null; this.render(); res(); }, title); this.render(); }); }
|
|
1083
|
+
openPager(lines, title = "transcript", start = title === "transcript" ? "bottom" : "top") {
|
|
1084
|
+
if (this.full) {
|
|
1085
|
+
void this.showText(lines, title);
|
|
1086
|
+
return;
|
|
1087
|
+
} // the full-screen UI shows these as a scrollable modal
|
|
1088
|
+
this.screen.enterAlt();
|
|
1089
|
+
this.pager = new Pager(lines, this.io.output, () => { this.pager = null; this.screen.leaveAlt(); this.render(); }, title, start);
|
|
1090
|
+
}
|
|
1091
|
+
openTranscript() { this.openPager(transcriptLines(this.session.messages, this.screen.cols - 1)); }
|
|
1092
|
+
/** Ctrl+P: every command in a filterable list. Commands that need an argument are typed into the input instead of run. */
|
|
1093
|
+
async openPalette() {
|
|
1094
|
+
if (this.overlay)
|
|
1095
|
+
return;
|
|
1096
|
+
const pick = await this.pick("Commands", this.cmds.map((c) => ({ label: "/" + c.name, value: c, hint: c.help })));
|
|
1097
|
+
if (!pick)
|
|
1098
|
+
return;
|
|
1099
|
+
if (pick.args) {
|
|
1100
|
+
this.editor.setText(`/${pick.name} `);
|
|
1101
|
+
this.render();
|
|
1102
|
+
}
|
|
1103
|
+
else
|
|
1104
|
+
await this.runCommand("/" + pick.name);
|
|
1105
|
+
}
|
|
1106
|
+
async openRewind() {
|
|
1107
|
+
if (this.running || !this.session.turns.length)
|
|
1108
|
+
return;
|
|
1109
|
+
const idx = await this.pick("Rewind to before…", this.session.turns.map((t, i) => ({ label: `${i + 1}. ${one(t.text, 60)}`, value: i, hint: `${Object.keys(t.snaps).length} file(s) changed` })).reverse());
|
|
1110
|
+
if (idx !== undefined)
|
|
1111
|
+
await this.rewindTo(idx);
|
|
1112
|
+
}
|
|
1113
|
+
async rewindTo(idx) {
|
|
1114
|
+
const r = await rewindFull(this.session, this.cwd, idx, this.snaps);
|
|
1115
|
+
this.pendingEdits.clear();
|
|
1116
|
+
this.recount();
|
|
1117
|
+
this.saveSession();
|
|
1118
|
+
this.rebuildBlocks();
|
|
1119
|
+
this.commit(["", ` ${green("↩")} ${dim(`rewound the conversation to before turn ${idx + 1}`)}${r.restored.length || r.removed.length ? dim(` · restored ${r.restored.length} file(s), removed ${r.removed.length}`) : ""}${r.tree ? dim(` · undid shell changes: ${r.tree.restored.length} restored, ${r.tree.removed.length} removed`) : ""}`, ...(r.treeError ? [` ${yellow("!")} ${dim("could not undo shell changes: " + r.treeError)}`] : []), ...(r.irreversible.length ? [` ${yellow("!")} ${dim("these commands had effects undo cannot take back:")}`, ...r.irreversible.map((x) => ` ${dim(x)}`)] : [])]);
|
|
1120
|
+
this.render();
|
|
1121
|
+
}
|
|
1122
|
+
/** Fork the conversation after turn `idx`. The current conversation is saved as it is; the working tree is put back to how it was at that point. */
|
|
1123
|
+
async branchAfter(idx) {
|
|
1124
|
+
const before = this.session, fork = forkSession(before, idx);
|
|
1125
|
+
this.saveSession();
|
|
1126
|
+
let files = "";
|
|
1127
|
+
if (idx + 1 < before.turns.length) {
|
|
1128
|
+
const r = await rewindFull(structuredClone(before), this.cwd, idx + 1, this.snaps);
|
|
1129
|
+
files = ` · restored ${r.restored.length} file(s), removed ${r.removed.length}${r.tree ? ", undid shell changes" : ""}`;
|
|
1130
|
+
}
|
|
1131
|
+
this.session = fork;
|
|
1132
|
+
this.recount();
|
|
1133
|
+
this.edits = [];
|
|
1134
|
+
this.pendingEdits.clear();
|
|
1135
|
+
this.saveSession();
|
|
1136
|
+
this.rebuildBlocks();
|
|
1137
|
+
this.commit(["", ` ${green("⑂")} ${dim(`branched after turn ${idx + 1} of "${before.title || "(untitled)"}"${files}`)}`, dim(" the original is kept: /branches switches back (files are not swapped back automatically)")]);
|
|
1138
|
+
this.render();
|
|
1139
|
+
}
|
|
1140
|
+
/** Switch UI mode live. History is kept in the block store, so nothing is lost either way. */
|
|
1141
|
+
switchUi(to) {
|
|
1142
|
+
if (to === this.ui)
|
|
1143
|
+
return;
|
|
1144
|
+
if (this.full) {
|
|
1145
|
+
this.full.leave();
|
|
1146
|
+
this.full = undefined;
|
|
1147
|
+
}
|
|
1148
|
+
else
|
|
1149
|
+
this.screen.clear();
|
|
1150
|
+
this.ui = to;
|
|
1151
|
+
this.screen.height = 0;
|
|
1152
|
+
if (to === "full") {
|
|
1153
|
+
this.full = new FullView(this);
|
|
1154
|
+
this.full.enter();
|
|
1155
|
+
this.rebuildBlocksIfEmpty();
|
|
1156
|
+
}
|
|
1157
|
+
else
|
|
1158
|
+
this.out.push(...welcome({ version: this.version, model: this.cfg.model, provider: this.cfg.provider, cwd: this.cwd, branch: this.branch, notes: this.agent.memory?.list().length ?? 0, verify: this.verify, mode: this.mode, tip: "use /ui full to go back to the full-screen UI" }, this.width), ...(this.session.turns.length ? [dim(" (earlier messages are kept; /ui full shows them)")] : []));
|
|
1159
|
+
this.render();
|
|
1160
|
+
}
|
|
1161
|
+
rebuildBlocksIfEmpty() { if (!this.blocks.blocks.length && this.session.messages.length)
|
|
1162
|
+
this.blocks.loadMessages(this.session.messages); }
|
|
1163
|
+
rebuildBlocks() { this.blocks.loadMessages(this.session.messages); this.full?.invalidate(); }
|
|
1164
|
+
async resumeById(id) { const s = this.store.load(id); if (!s)
|
|
1165
|
+
return; this.loadSession(s); this.rebuildBlocks(); this.render(); }
|
|
1166
|
+
newConversation() { this.saveSession(); this.session = newSession(this.cwd, this.cfg.model); this.edits = []; this.trace = []; this.trimmed = 0; this.cacheBreaks = 0; this.lastPrompt = 0; this.todos = []; this.pendingContext = []; this.blocks.clear(); this.full?.invalidate(); this.buildAgent(); }
|
|
1167
|
+
loadSession(s) { this.saveSession(); this.session = s; this.recount(); this.edits = []; }
|
|
1168
|
+
saveSession() { if (this.persist && this.session.turns.length)
|
|
1169
|
+
try {
|
|
1170
|
+
this.session.model = this.cfg.model;
|
|
1171
|
+
this.store.save(this.session);
|
|
1172
|
+
}
|
|
1173
|
+
catch { /* read-only dir */ } }
|
|
1174
|
+
saveHistory() { if (!this.persist)
|
|
1175
|
+
return; try {
|
|
1176
|
+
mkdirSync(home(), { recursive: true });
|
|
1177
|
+
writeFileSync(join(home(), "history.json"), JSON.stringify(this.editor.history.slice(0, 200)));
|
|
1178
|
+
}
|
|
1179
|
+
catch { /* ignore */ } }
|
|
1180
|
+
saveConfig() { if (this.persist)
|
|
1181
|
+
saveConfig(this.cfg); }
|
|
1182
|
+
/** Put text on the clipboard (OSC 52 plus the platform tool). */
|
|
1183
|
+
copyText(text) { if (this.full)
|
|
1184
|
+
this.full.copy(text);
|
|
1185
|
+
else
|
|
1186
|
+
copyToClipboard(this.io.output, text, this.persist); }
|
|
1187
|
+
// ---- cache keep-alive: Claude drops a cached prefix after ~5 idle minutes; a tiny re-send every ~4.5 minutes costs a cache read instead of a cold re-read.
|
|
1188
|
+
warmTimer;
|
|
1189
|
+
warmCount = 0;
|
|
1190
|
+
lastCached = 0;
|
|
1191
|
+
stopWarm() { if (this.warmTimer)
|
|
1192
|
+
clearTimeout(this.warmTimer); this.warmTimer = undefined; this.warmCount = 0; }
|
|
1193
|
+
scheduleWarm() {
|
|
1194
|
+
if (this.warmTimer)
|
|
1195
|
+
clearTimeout(this.warmTimer);
|
|
1196
|
+
if (!this.cfg.keepWarm || this.cfg.provider !== "anthropic" || !this.session.messages.length || this.lastCached <= 0 || this.warmCount >= 3)
|
|
1197
|
+
return;
|
|
1198
|
+
this.warmTimer = setTimeout(async () => {
|
|
1199
|
+
this.warmTimer = undefined;
|
|
1200
|
+
if (this.running || !this.cfg.keepWarm)
|
|
1201
|
+
return;
|
|
1202
|
+
try {
|
|
1203
|
+
const r = await this.agent.warm(this.session.messages);
|
|
1204
|
+
this.warmCount++;
|
|
1205
|
+
this.lastCached = r.cached;
|
|
1206
|
+
this.note(`cache kept warm (${r.prompt ? Math.round((r.cached / r.prompt) * 100) : 0}% of the prompt was cached)`);
|
|
1207
|
+
this.scheduleWarm();
|
|
1208
|
+
}
|
|
1209
|
+
catch { /* a failed ping is harmless: the next real request just re-reads the prefix */ }
|
|
1210
|
+
}, this.opts.keepWarmMs ?? 270_000);
|
|
1211
|
+
this.warmTimer.unref?.();
|
|
1212
|
+
}
|
|
1213
|
+
/** Project hooks run commands on this machine, so a cloned repo's hooks only run after the user approves this exact file. */
|
|
1214
|
+
policyLog = [];
|
|
1215
|
+
hooks;
|
|
1216
|
+
rules;
|
|
1217
|
+
roles = {};
|
|
1218
|
+
lastRoute = "";
|
|
1219
|
+
loadHooks() {
|
|
1220
|
+
const fp = projectConfigFingerprint(this.cwd), trusted = !!fp && loadState().hookTrust?.[this.cwd] === fp;
|
|
1221
|
+
try {
|
|
1222
|
+
this.hooks = Hooks.load(this.cwd, { trustProject: trusted });
|
|
1223
|
+
}
|
|
1224
|
+
catch (e) {
|
|
1225
|
+
this.hooks = Hooks.load(this.cwd, { trustProject: false });
|
|
1226
|
+
this.note(`hooks ignored: ${e.message}`);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
this.rules = Permissions.load(this.cwd, { trustProject: trusted });
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
this.rules = new Permissions({}, this.cwd);
|
|
1233
|
+
this.note(`permission rules ignored: ${e.message}`);
|
|
1234
|
+
}
|
|
1235
|
+
return { fp, trusted };
|
|
1236
|
+
}
|
|
1237
|
+
plugins = [];
|
|
1238
|
+
statusAt = 0;
|
|
1239
|
+
statusLines = [];
|
|
1240
|
+
/** one line per plugin that has a `status` function, cached for a second; a failing plugin shows nothing */
|
|
1241
|
+
pluginStatus() {
|
|
1242
|
+
if (Date.now() - this.statusAt < 1000)
|
|
1243
|
+
return this.statusLines;
|
|
1244
|
+
this.statusAt = Date.now();
|
|
1245
|
+
this.statusLines = this.plugins.flatMap((p) => { try {
|
|
1246
|
+
const s = p.status?.({ cwd: this.cwd, running: this.running });
|
|
1247
|
+
return s ? [`${p.name}: ${String(s).slice(0, 60)}`] : [];
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
return [];
|
|
1251
|
+
} });
|
|
1252
|
+
return this.statusLines;
|
|
1253
|
+
}
|
|
1254
|
+
/** loaded plugins plus the loop strategy chosen with /strategy and the prompt chosen with /prompt */
|
|
1255
|
+
activePlugins() {
|
|
1256
|
+
const extra = [];
|
|
1257
|
+
const s = this.cfg.strategy && STRATEGIES[this.cfg.strategy];
|
|
1258
|
+
if (s)
|
|
1259
|
+
extra.push(s());
|
|
1260
|
+
if (this.cfg.prompt) {
|
|
1261
|
+
try {
|
|
1262
|
+
extra.push(promptPlugin(this.cwd, this.cfg.prompt));
|
|
1263
|
+
}
|
|
1264
|
+
catch (e) {
|
|
1265
|
+
this.note(`prompt ignored: ${e.message}`);
|
|
1266
|
+
this.cfg.prompt = undefined;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return extra.length ? [...this.plugins, ...extra] : this.plugins;
|
|
1270
|
+
}
|
|
1271
|
+
/** Load plugins: yours always, the project's only once you approved exactly these files. */
|
|
1272
|
+
async loadPluginsNow() {
|
|
1273
|
+
const fp = pluginFingerprint(this.cwd), trusted = !!fp && loadState().pluginTrust?.[this.cwd] === fp;
|
|
1274
|
+
const r = await loadPlugins(this.cwd, { trustProject: trusted });
|
|
1275
|
+
this.plugins = r.plugins;
|
|
1276
|
+
for (const w of r.warnings)
|
|
1277
|
+
this.note(`plugin: ${w}`);
|
|
1278
|
+
return { r, fp };
|
|
1279
|
+
}
|
|
1280
|
+
async askPluginTrust() {
|
|
1281
|
+
const { r, fp } = await this.loadPluginsNow();
|
|
1282
|
+
if (!r.untrusted || !fp || !this.persist)
|
|
1283
|
+
return;
|
|
1284
|
+
const ok = await this.pick(`This project has ${r.untrusted.count} plugin file${r.untrusted.count === 1 ? "" : "s"} (.amness/plugins). A plugin is code that runs with your permissions and can change what the agent does. Trust this version of the files?\n${describeProjectPlugins(this.cwd).map((l) => " " + l).join("\n")}\n(a text scan, not a guarantee: read them first)`, [{ label: "Yes, trust this version", value: "y" }, { label: "No, run without them", value: "n" }]);
|
|
1285
|
+
if (ok === "y") {
|
|
1286
|
+
const st = loadState();
|
|
1287
|
+
st.pluginTrust = { ...(st.pluginTrust ?? {}), [this.cwd]: fp };
|
|
1288
|
+
saveState(st);
|
|
1289
|
+
await this.loadPluginsNow();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async askHookTrust() {
|
|
1293
|
+
const { fp, trusted } = this.loadHooks();
|
|
1294
|
+
await this.askPluginTrust();
|
|
1295
|
+
if (!fp || trusted || !this.persist) {
|
|
1296
|
+
this.buildAgent();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const ok = await this.pick("This project has its own hooks and/or permission rules (.amness/hooks.json, permissions.json). Hooks run commands on your machine and allow-rules skip prompts. Trust them?", [{ label: "Yes, trust this version of the files", value: "y" }, { label: "No, ignore them (deny and ask rules still apply)", value: "n" }]);
|
|
1300
|
+
if (ok === "y") {
|
|
1301
|
+
const st = loadState();
|
|
1302
|
+
st.hookTrust = { ...(st.hookTrust ?? {}), [this.cwd]: fp };
|
|
1303
|
+
saveState(st);
|
|
1304
|
+
this.loadHooks();
|
|
1305
|
+
}
|
|
1306
|
+
this.buildAgent();
|
|
1307
|
+
}
|
|
1308
|
+
/** Shell snapshots: before the first command of a turn that may change files, remember the working tree so /undo can put it back. */
|
|
1309
|
+
snaps;
|
|
1310
|
+
async beforeShell(cmd) {
|
|
1311
|
+
const turn = this.session.turns.at(-1);
|
|
1312
|
+
if (!turn)
|
|
1313
|
+
return;
|
|
1314
|
+
const irr = irreversibleReason(cmd);
|
|
1315
|
+
turn.shell ??= { cmds: [], irreversible: [] };
|
|
1316
|
+
turn.shell.cmds.push(cmd.slice(0, 200));
|
|
1317
|
+
if (irr)
|
|
1318
|
+
turn.shell.irreversible.push(`${cmd.slice(0, 80)} (${irr})`);
|
|
1319
|
+
if (!turn.shell.tree)
|
|
1320
|
+
turn.shell.tree = await this.snaps.take();
|
|
1321
|
+
}
|
|
1322
|
+
/** The harness profile for the current model (guards switched off, limits retuned): .amness/profile.json and ~/.amness/profile.json. */
|
|
1323
|
+
profile = { profile: {}, sources: [] };
|
|
1324
|
+
profileOptions() {
|
|
1325
|
+
try {
|
|
1326
|
+
this.profile = loadProfile(this.cwd, this.cfg.model);
|
|
1327
|
+
}
|
|
1328
|
+
catch (e) {
|
|
1329
|
+
this.profile = { profile: {}, sources: [] };
|
|
1330
|
+
this.note(`profile ignored: ${e.message}`);
|
|
1331
|
+
}
|
|
1332
|
+
const p = this.profile.profile;
|
|
1333
|
+
return { ...(p.off?.length && { off: p.off }), ...(p.stagnationTurns !== undefined && { stagnationTurns: p.stagnationTurns }), ...(p.maxSteps !== undefined && { maxSteps: p.maxSteps }) };
|
|
1334
|
+
}
|
|
1335
|
+
setCheck(cmd) { this.check = cmd; this.buildAgent(); }
|
|
1336
|
+
setVerify(cmd) { this.verify = cmd; if (this.persist) {
|
|
1337
|
+
const st = loadState();
|
|
1338
|
+
if (cmd)
|
|
1339
|
+
st.verifyByDir[this.cwd] = cmd;
|
|
1340
|
+
else
|
|
1341
|
+
delete st.verifyByDir[this.cwd];
|
|
1342
|
+
saveState(st);
|
|
1343
|
+
} this.buildAgent(); }
|
|
1344
|
+
async ensureLive() { if (!this.live)
|
|
1345
|
+
this.live = await startLive({ title: "Amness" }); return this.live; }
|
|
1346
|
+
providerInfo() { return PROVIDERS[this.cfg.provider]; }
|
|
1347
|
+
refreshBranch() { execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: this.cwd }, (e, out) => { const b = e ? undefined : out.trim(); if (b !== this.branch) {
|
|
1348
|
+
this.branch = b;
|
|
1349
|
+
this.schedule();
|
|
1350
|
+
} }); }
|
|
1351
|
+
get memory() { return this.agent.memory; }
|
|
1352
|
+
get messagesForExport() { return this.session.messages; }
|
|
1353
|
+
get hasFile() { return (p) => existsSync(join(this.cwd, p)); }
|
|
1354
|
+
}
|
|
1355
|
+
export function shPrefix(cmd) {
|
|
1356
|
+
const t = cmd.trim().split(/\s+/);
|
|
1357
|
+
return /^(npm|pnpm|yarn|git|cargo|go|make|docker|kubectl|pip|pip3|python|python3|node|npx|bun|deno|pytest|uv|poetry)$/.test(t[0]) && t[1] && !t[1].startsWith("-") ? `${t[0]} ${t[1]}` : t[0];
|
|
1358
|
+
}
|