amness 0.1.2 → 0.1.4
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 +2 -2
- package/dist/cli/amness.js +4 -4
- package/dist/cli/config.d.ts +22 -1
- package/dist/cli/config.js +102 -40
- package/dist/cli/evalcli.d.ts +18 -1
- package/dist/cli/evalcli.js +53 -4
- package/dist/cli/models.d.ts +8 -0
- package/dist/cli/models.js +31 -0
- package/dist/cli/tui/app.d.ts +3 -1
- package/dist/cli/tui/app.js +34 -7
- package/dist/cli/tui/argcomplete.d.ts +12 -0
- package/dist/cli/tui/argcomplete.js +55 -0
- package/dist/cli/tui/cards.d.ts +2 -0
- package/dist/cli/tui/cards.js +4 -3
- package/dist/cli/tui/commands.js +98 -26
- package/dist/cli/tui/complete.d.ts +23 -3
- package/dist/cli/tui/complete.js +38 -3
- package/dist/cli/tui/full.d.ts +4 -0
- package/dist/cli/tui/full.js +42 -12
- package/dist/cli/tui/looppanel.d.ts +71 -0
- package/dist/cli/tui/looppanel.js +218 -0
- package/dist/cli/tui/sidebar.d.ts +4 -18
- package/dist/cli/tui/sidebar.js +8 -10
- package/dist/src/evals.d.ts +23 -2
- package/dist/src/evals.js +19 -5
- package/docs/agent-loop.md +15 -2
- package/docs/evaluation.md +32 -0
- package/docs/providers.md +13 -1
- package/docs/safety.md +3 -3
- package/docs/usage.md +51 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,13 +28,13 @@ Give Amness the key once, in whichever way suits you:
|
|
|
28
28
|
|
|
29
29
|
| How | Command or step | Saved? |
|
|
30
30
|
|---|---|---|
|
|
31
|
-
| **Paste it on first start** | run `amness`, choose a provider, paste the key (hidden).
|
|
31
|
+
| **Paste it on first start** | run `amness`, choose a provider, paste the key (hidden). Add more providers any time with `/connect`, and switch with `/provider`. | Yes, in your system's secure store: the macOS Keychain, the Linux keyring, or Windows DPAPI. The prompt tells you which. |
|
|
32
32
|
| **Environment variable** | `export ANTHROPIC_API_KEY=sk-ant-...` or `export OPENAI_API_KEY=sk-...` (PowerShell: `$env:OPENAI_API_KEY="sk-..."`) | No. Used for that shell only, and never written to disk. It wins over a saved key. |
|
|
33
33
|
| **Secret file** (Docker, Kubernetes) | `ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic` (also `OPENAI_API_KEY_FILE`, `TAVILY_API_KEY_FILE`) | No. Read from the file each run. |
|
|
34
34
|
| **Docker** | `docker run -it --rm -v "$PWD":/work -e ANTHROPIC_API_KEY amness` | No. Inside a container nothing is saved. |
|
|
35
35
|
| **Library or CI** | pass `apiKey` in code, or set the variable as a CI secret | No. |
|
|
36
36
|
|
|
37
|
-
`OPENAI_API_KEY` is used for every provider except Anthropic
|
|
37
|
+
Each provider keeps its own saved key, so you can connect several and switch with `/provider`; `/connect` shows which are connected. `OPENAI_API_KEY` is used for every provider except Anthropic (OpenRouter prefers `OPENROUTER_API_KEY`). To remove a saved key, clear "API key" in the settings. A Linux server with no keyring falls back to `~/.amness/config.json` (mode 600, **not encrypted**), so use an environment variable or your platform's secret manager there. Details: [Safety](docs/safety.md).
|
|
38
38
|
|
|
39
39
|
More ways to start:
|
|
40
40
|
|
package/dist/cli/amness.js
CHANGED
|
@@ -15,7 +15,7 @@ import { runPlugin } from "./plugincli.js";
|
|
|
15
15
|
import { runSchedule } from "./schedulecli.js";
|
|
16
16
|
import { ACP_HELP, runAcp } from "./acpcli.js";
|
|
17
17
|
import { runEval, runTune } from "./evalcli.js";
|
|
18
|
-
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS, home, loadConfig, mask, ready, saveConfig } from "./config.js";
|
|
18
|
+
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS, home, keyFor, loadConfig, mask, ready, saveConfig } from "./config.js";
|
|
19
19
|
import { where } from "./keystore.js";
|
|
20
20
|
import { DEMO_TASK, explain, runDemo } from "./demo.js";
|
|
21
21
|
import { formatEvent } from "./format.js";
|
|
@@ -91,7 +91,7 @@ async function ensureReady() {
|
|
|
91
91
|
cfg.model = (await ask("Model", cfg.model)) || cfg.model;
|
|
92
92
|
}
|
|
93
93
|
if (PROVIDERS[id].needsKey)
|
|
94
|
-
cfg.apiKey = (await ask(`API key (hidden; ${where()})`, "", { secret: true })) || undefined;
|
|
94
|
+
cfg.apiKey = keyFor(id).key ?? ((await ask(`API key (hidden; ${where()})`, "", { secret: true })) || undefined);
|
|
95
95
|
saveConfig(cfg);
|
|
96
96
|
if (!ready(cfg))
|
|
97
97
|
return false;
|
|
@@ -432,7 +432,7 @@ async function settings() {
|
|
|
432
432
|
if (what === "provider") {
|
|
433
433
|
const id = await select("Provider", Object.keys(PROVIDERS).map((k) => ({ label: PROVIDERS[k].label, value: k, hint: PROVIDERS[k].baseUrl })));
|
|
434
434
|
if (id)
|
|
435
|
-
Object.assign(cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model });
|
|
435
|
+
Object.assign(cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model, apiKey: keyFor(id).key });
|
|
436
436
|
}
|
|
437
437
|
else if (what === "model") {
|
|
438
438
|
if (cfg.provider === "anthropic" || cfg.provider === "openai") {
|
|
@@ -610,7 +610,7 @@ async function main() {
|
|
|
610
610
|
if (quick === "--help" || quick === "-h" || quick === "help")
|
|
611
611
|
return void console.log(TOP_HELP);
|
|
612
612
|
if (quick === "eval" && argv.length > 1) {
|
|
613
|
-
process.exitCode = await runEval(argv.slice(1), { model: makeModel, modelFor: (id) => createModel({ ...cfg, model: id }), maxTokens: cfg.maxTokens, confirm });
|
|
613
|
+
process.exitCode = await runEval(argv.slice(1), { model: makeModel, modelName: cfg.model, modelFor: (id) => createModel({ ...cfg, model: id }), maxTokens: cfg.maxTokens, confirm });
|
|
614
614
|
return;
|
|
615
615
|
}
|
|
616
616
|
if (quick === "bench") {
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export type Config = {
|
|
|
35
35
|
provider: ProviderId;
|
|
36
36
|
baseUrl: string;
|
|
37
37
|
model: string;
|
|
38
|
-
apiKey?: string;
|
|
38
|
+
apiKey?: string; /** saved keys for the other providers, only where there is no OS store */
|
|
39
|
+
apiKeys?: Partial<Record<ProviderId, string>>;
|
|
39
40
|
tavilyKey?: string;
|
|
40
41
|
maxTokens: number;
|
|
41
42
|
effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** OpenAI wire API: auto (default) picks Responses for reasoning models and self-corrects */
|
|
@@ -65,6 +66,26 @@ export type Config = {
|
|
|
65
66
|
};
|
|
66
67
|
export declare const home: () => string;
|
|
67
68
|
export declare const defaults: () => Config;
|
|
69
|
+
/** The variable that holds a provider's key. Anthropic has its own; OpenRouter may have its own; every other provider uses OPENAI_API_KEY. */
|
|
70
|
+
export declare const envNameFor: (p: ProviderId) => "ANTHROPIC_API_KEY" | "OPENROUTER_API_KEY" | "OPENAI_API_KEY";
|
|
71
|
+
export declare const envKeyFor: (p: ProviderId) => string | undefined;
|
|
72
|
+
export type KeyState = {
|
|
73
|
+
key?: string;
|
|
74
|
+
source: "env" | "saved" | "none";
|
|
75
|
+
};
|
|
76
|
+
/** The key to use for a provider: an environment variable wins over a saved key. */
|
|
77
|
+
export declare function keyFor(p: ProviderId): KeyState;
|
|
78
|
+
/** Save (or, with no key, remove) a provider's key. Uses the OS store; else config.json (mode 600); in a container, memory only. */
|
|
79
|
+
export declare function storeKey(p: ProviderId, key?: string): void;
|
|
80
|
+
export type Connection = {
|
|
81
|
+
id: ProviderId;
|
|
82
|
+
label: string;
|
|
83
|
+
needsKey: boolean;
|
|
84
|
+
source: KeyState["source"];
|
|
85
|
+
env: string;
|
|
86
|
+
};
|
|
87
|
+
/** Every provider and whether it is ready to use. */
|
|
88
|
+
export declare function connections(): Connection[];
|
|
68
89
|
export declare function loadConfig(): Config;
|
|
69
90
|
export declare function saveConfig(c: Config): void;
|
|
70
91
|
export declare const ready: (c: Config) => boolean;
|
package/dist/cli/config.js
CHANGED
|
@@ -12,7 +12,11 @@ export const PROVIDERS = {
|
|
|
12
12
|
export const home = () => process.env.AMNESS_HOME ?? join(homedir(), ".amness");
|
|
13
13
|
const file = () => join(home(), "config.json");
|
|
14
14
|
export const defaults = () => ({ provider: "openai", baseUrl: PROVIDERS.openai.baseUrl, model: PROVIDERS.openai.model, maxTokens: 300_000 });
|
|
15
|
-
|
|
15
|
+
// ---- keys. One saved key per provider (account "apiKey-<provider>" in the OS store), so you can be connected to several at once. `cfg.apiKey` is always the active provider's key.
|
|
16
|
+
const acct = (p) => `apiKey-${p}`;
|
|
17
|
+
let fileKeys = {}, fileKeysLoaded = false; // only where there is no OS store (a Linux server): kept in config.json, mode 600
|
|
18
|
+
const sessionKeys = {}; // in a container: this run only
|
|
19
|
+
const persisted = {}; // what is stored now, so a save does not rewrite an unchanged key
|
|
16
20
|
/** An environment variable, or the file NAME_FILE points to (Docker and Kubernetes secrets are mounted as files, for example /run/secrets/openai). */
|
|
17
21
|
const envSecret = (name) => {
|
|
18
22
|
const v = process.env[name];
|
|
@@ -28,7 +32,58 @@ const envSecret = (name) => {
|
|
|
28
32
|
return undefined;
|
|
29
33
|
}
|
|
30
34
|
};
|
|
31
|
-
|
|
35
|
+
/** The variable that holds a provider's key. Anthropic has its own; OpenRouter may have its own; every other provider uses OPENAI_API_KEY. */
|
|
36
|
+
export const envNameFor = (p) => (p === "anthropic" ? "ANTHROPIC_API_KEY" : p === "openrouter" && envSecret("OPENROUTER_API_KEY") ? "OPENROUTER_API_KEY" : "OPENAI_API_KEY");
|
|
37
|
+
export const envKeyFor = (p) => envSecret(envNameFor(p));
|
|
38
|
+
const loadFileKeys = () => { if (!fileKeysLoaded) {
|
|
39
|
+
fileKeysLoaded = true;
|
|
40
|
+
fileKeys = { ...readFileCfg().apiKeys };
|
|
41
|
+
} };
|
|
42
|
+
const savedKey = (p) => { loadFileKeys(); return getSecret(acct(p)) ?? fileKeys[p] ?? sessionKeys[p]; };
|
|
43
|
+
/** The key to use for a provider: an environment variable wins over a saved key. */
|
|
44
|
+
export function keyFor(p) {
|
|
45
|
+
const e = envKeyFor(p);
|
|
46
|
+
if (e)
|
|
47
|
+
return { key: e, source: "env" };
|
|
48
|
+
const s = savedKey(p);
|
|
49
|
+
if (s) {
|
|
50
|
+
persisted[p] = s;
|
|
51
|
+
return { key: s, source: "saved" };
|
|
52
|
+
}
|
|
53
|
+
return { source: "none" };
|
|
54
|
+
}
|
|
55
|
+
/** Save (or, with no key, remove) a provider's key. Uses the OS store; else config.json (mode 600); in a container, memory only. */
|
|
56
|
+
export function storeKey(p, key) {
|
|
57
|
+
loadFileKeys();
|
|
58
|
+
persisted[p] = key;
|
|
59
|
+
if (!key) {
|
|
60
|
+
deleteSecret(acct(p));
|
|
61
|
+
delete fileKeys[p];
|
|
62
|
+
delete sessionKeys[p];
|
|
63
|
+
writeConfigFile(readFileCfg());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (setSecret(acct(p), key)) {
|
|
67
|
+
delete fileKeys[p];
|
|
68
|
+
delete sessionKeys[p];
|
|
69
|
+
}
|
|
70
|
+
else if (noStore())
|
|
71
|
+
sessionKeys[p] = key;
|
|
72
|
+
else
|
|
73
|
+
fileKeys[p] = key;
|
|
74
|
+
writeConfigFile(readFileCfg());
|
|
75
|
+
}
|
|
76
|
+
/** Every provider and whether it is ready to use. */
|
|
77
|
+
export function connections() {
|
|
78
|
+
return Object.keys(PROVIDERS).map((id) => ({ id, label: PROVIDERS[id].label, needsKey: PROVIDERS[id].needsKey, source: PROVIDERS[id].needsKey ? keyFor(id).source : "saved", env: envNameFor(id) }));
|
|
79
|
+
}
|
|
80
|
+
const readFileCfg = () => { try {
|
|
81
|
+
return { ...defaults(), ...JSON.parse(readFileSync(file(), "utf8")) };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return defaults();
|
|
85
|
+
} };
|
|
86
|
+
let tavilyEnv;
|
|
32
87
|
export function loadConfig() {
|
|
33
88
|
let c = defaults(), onDisk = {};
|
|
34
89
|
try {
|
|
@@ -37,33 +92,40 @@ export function loadConfig() {
|
|
|
37
92
|
c = { ...c, ...onDisk };
|
|
38
93
|
}
|
|
39
94
|
catch { /* corrupt config: start fresh */ }
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
fileKeys = { ...onDisk.apiKeys };
|
|
96
|
+
fileKeysLoaded = true;
|
|
97
|
+
delete c.apiKeys;
|
|
98
|
+
let dirty = false;
|
|
99
|
+
// older versions kept ONE key (account "apiKey", or a field in the file) for whichever provider was active: give it to that provider
|
|
100
|
+
const legacy = [getSecret("apiKey"), onDisk.apiKey].find(Boolean);
|
|
101
|
+
if (legacy && !savedKey(c.provider)) {
|
|
102
|
+
fileKeys[c.provider] = legacy;
|
|
103
|
+
dirty = true;
|
|
104
|
+
}
|
|
105
|
+
if (getSecret("apiKey"))
|
|
106
|
+
deleteSecret("apiKey");
|
|
107
|
+
if (onDisk.apiKey) {
|
|
108
|
+
delete onDisk.apiKey;
|
|
109
|
+
dirty = true;
|
|
110
|
+
}
|
|
111
|
+
// keys still in the file move to the OS store, and leave the file only after they read back
|
|
112
|
+
for (const p of Object.keys(fileKeys))
|
|
113
|
+
if (fileKeys[p] && setSecret(acct(p), fileKeys[p])) {
|
|
114
|
+
delete fileKeys[p];
|
|
115
|
+
dirty = true;
|
|
54
116
|
}
|
|
117
|
+
const tv = getSecret("tavilyKey") ?? onDisk.tavilyKey;
|
|
118
|
+
if (onDisk.tavilyKey && setSecret("tavilyKey", onDisk.tavilyKey)) {
|
|
119
|
+
delete onDisk.tavilyKey;
|
|
120
|
+
dirty = true;
|
|
55
121
|
}
|
|
56
|
-
if (
|
|
57
|
-
writeConfigFile({ ...defaults(), ...onDisk,
|
|
58
|
-
// environment wins, so CI and one-off runs work without touching the saved key. It is never written back (see saveConfig).
|
|
122
|
+
if (dirty)
|
|
123
|
+
writeConfigFile({ ...defaults(), ...onDisk, apiKey: undefined, tavilyKey: onDisk.tavilyKey });
|
|
59
124
|
const claude = c.provider === "anthropic";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const tavily = envSecret("TAVILY_API_KEY");
|
|
65
|
-
if (tavily)
|
|
66
|
-
c.tavilyKey = fromEnv.tavilyKey = tavily;
|
|
125
|
+
c.apiKey = keyFor(c.provider).key;
|
|
126
|
+
// environment wins, so CI and one-off runs work without touching the saved key. It is never written back (see saveConfig).
|
|
127
|
+
tavilyEnv = envSecret("TAVILY_API_KEY");
|
|
128
|
+
c.tavilyKey = tavilyEnv ?? tv;
|
|
67
129
|
if (process.env.OPENAI_BASE_URL && !claude)
|
|
68
130
|
c.baseUrl = process.env.OPENAI_BASE_URL;
|
|
69
131
|
if (process.env.AMNESS_MODEL)
|
|
@@ -72,25 +134,25 @@ export function loadConfig() {
|
|
|
72
134
|
}
|
|
73
135
|
function writeConfigFile(c) {
|
|
74
136
|
mkdirSync(home(), { recursive: true });
|
|
75
|
-
|
|
137
|
+
const out = { ...c, apiKeys: Object.keys(fileKeys).length ? fileKeys : undefined };
|
|
138
|
+
writeFileSync(file(), JSON.stringify(out, null, 2), { mode: 0o600 }); // owner-only from the first byte
|
|
76
139
|
chmodSync(file(), 0o600); // and for a file that already existed
|
|
77
140
|
}
|
|
78
141
|
export function saveConfig(c) {
|
|
79
142
|
const out = { ...c };
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
if (setSecret(k, v) || noStore())
|
|
92
|
-
delete out[k]; // stored in the OS credential store; otherwise it stays in the file (mode 600), except in a container, where it is kept in memory only
|
|
143
|
+
delete out.apiKey;
|
|
144
|
+
delete out.apiKeys;
|
|
145
|
+
if (c.apiKey !== persisted[c.provider] && !(c.apiKey && c.apiKey === envKeyFor(c.provider)))
|
|
146
|
+
storeKey(c.provider, c.apiKey); // a key from an environment variable is never stored
|
|
147
|
+
const tv = c.tavilyKey;
|
|
148
|
+
if (tv && tv === tavilyEnv)
|
|
149
|
+
delete out.tavilyKey;
|
|
150
|
+
else if (!tv) {
|
|
151
|
+
deleteSecret("tavilyKey");
|
|
152
|
+
delete out.tavilyKey;
|
|
93
153
|
}
|
|
154
|
+
else if (setSecret("tavilyKey", tv) || noStore())
|
|
155
|
+
delete out.tavilyKey;
|
|
94
156
|
writeConfigFile(out);
|
|
95
157
|
}
|
|
96
158
|
export const ready = (c) => !PROVIDERS[c.provider].needsKey || !!c.apiKey;
|
package/dist/cli/evalcli.d.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import { type Model } from "../src/index.ts";
|
|
1
|
+
import { type Model, type Row } from "../src/index.ts";
|
|
2
2
|
export declare const EVAL_HELP: string;
|
|
3
|
+
/** Estimated dollars for some rows (prompt, cached and completion tokens priced separately; the rate card is in pricing.ts). Null when the model's price is unknown. */
|
|
4
|
+
export declare function rowsCost(model: string, rows: Row[]): number | null;
|
|
5
|
+
/** What makes a result repeatable: the Amness version and git commit (when run from a clone), and a hash of the tasks (their files, text and checks). */
|
|
6
|
+
export declare function provenance(cases: {
|
|
7
|
+
name: string;
|
|
8
|
+
files: Record<string, string>;
|
|
9
|
+
task: string;
|
|
10
|
+
verify?: string;
|
|
11
|
+
check: unknown;
|
|
12
|
+
solution?: Record<string, string>;
|
|
13
|
+
}[]): {
|
|
14
|
+
version: string | undefined;
|
|
15
|
+
commit: string | undefined;
|
|
16
|
+
suite: string;
|
|
17
|
+
tasks: number;
|
|
18
|
+
};
|
|
3
19
|
export declare function runEval(argv: string[], deps: {
|
|
4
20
|
model: () => Model;
|
|
21
|
+
modelName?: string;
|
|
5
22
|
modelFor?: (id: string) => Model;
|
|
6
23
|
maxTokens: number;
|
|
7
24
|
confirm: (q: string) => Promise<boolean>;
|
package/dist/cli/evalcli.js
CHANGED
|
@@ -7,10 +7,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
// `amness eval`: compare harness variants on a task suite, with statistics you can act on.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
10
12
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
13
|
import { join, resolve } from "node:path";
|
|
12
|
-
import { BUILTIN_METRICS, gateCompare, importPlugins, tune, writeProfile, adversarialCases, hardCases, analyze, builtinCases, evaluate, recommend, leakAudit, loadSuite, suiteToCases, formatReport, harnessVariant, loadCases, selfTest, variantNames, VARIANT_HELP } from "../src/index.js";
|
|
14
|
+
import { BUILTIN_METRICS, gateCompare, importPlugins, tune, writeProfile, adversarialCases, hardCases, analyze, builtinCases, evaluate, recommend, leakAudit, loadSuite, suiteToCases, formatReport, calibrate, harnessVariant, loadCases, selfTest, variantNames, VARIANT_HELP } from "../src/index.js";
|
|
13
15
|
import { bold, dim, green, red } from "./ui.js";
|
|
16
|
+
import { estimateCost, priceOf } from "./tui/pricing.js";
|
|
14
17
|
import { pathToFileURL } from "node:url";
|
|
15
18
|
async function casesFromModule(file) {
|
|
16
19
|
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href + `?v=${Date.now()}`)), list = mod.default ?? mod.cases;
|
|
@@ -46,7 +49,32 @@ export const EVAL_HELP = `usage: amness eval [flags]
|
|
|
46
49
|
--out <file> where to save raw results (default .amness/evals/<time>.json)
|
|
47
50
|
--report <file> re-analyse saved results; runs nothing
|
|
48
51
|
--selftest check that every task is solvable and its held-out check is sound; runs no model
|
|
52
|
+
--max-cost <dollars> stop starting new sessions once the estimated spend reaches this amount (sessions already running finish). Off by default. Needs a model with a known price.
|
|
53
|
+
--calibrate how hard is each task? runs only \`baseline\` (5 trials unless you set --trials) and lists each task's pass rate: keep 30% to 70%, drop tasks that are too easy or too hard
|
|
49
54
|
--yes skip the cost confirmation`;
|
|
55
|
+
/** Estimated dollars for some rows (prompt, cached and completion tokens priced separately; the rate card is in pricing.ts). Null when the model's price is unknown. */
|
|
56
|
+
export function rowsCost(model, rows) {
|
|
57
|
+
if (!priceOf(model))
|
|
58
|
+
return null;
|
|
59
|
+
return rows.reduce((n, r) => n + (estimateCost(model, r.prompt ?? r.tokens, Math.max(0, r.tokens - (r.prompt ?? r.tokens)), r.cached ?? 0) ?? 0), 0);
|
|
60
|
+
}
|
|
61
|
+
/** What makes a result repeatable: the Amness version and git commit (when run from a clone), and a hash of the tasks (their files, text and checks). */
|
|
62
|
+
export function provenance(cases) {
|
|
63
|
+
let commit;
|
|
64
|
+
try {
|
|
65
|
+
commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: new URL("..", import.meta.url), encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
66
|
+
}
|
|
67
|
+
catch { /* not a git checkout (an installed package) */ }
|
|
68
|
+
let version;
|
|
69
|
+
try {
|
|
70
|
+
version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
71
|
+
}
|
|
72
|
+
catch { /* no package.json next to us */ }
|
|
73
|
+
const h = createHash("sha256");
|
|
74
|
+
for (const x of [...cases].sort((a, b) => a.name.localeCompare(b.name)))
|
|
75
|
+
h.update(JSON.stringify([x.name, x.files, x.task, x.verify ?? "", typeof x.check === "function" ? x.check.toString() : x.check, x.solution ?? {}]));
|
|
76
|
+
return { version, commit, suite: h.digest("hex").slice(0, 16), tasks: cases.length };
|
|
77
|
+
}
|
|
50
78
|
export async function runEval(argv, deps) {
|
|
51
79
|
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; }, has = (f) => argv.includes(f);
|
|
52
80
|
if (has("--help") || has("-h")) {
|
|
@@ -108,20 +136,41 @@ export async function runEval(argv, deps) {
|
|
|
108
136
|
console.log(`${r.ok ? green("ok ") : red("bad")} ${r.case}${r.why ? dim(" " + r.why) : ""}`);
|
|
109
137
|
return res.every((r) => r.ok) ? 0 : 1;
|
|
110
138
|
}
|
|
111
|
-
const names = variantNames(val("--variants") ?? "ab"), trials = Math.max(1, Number(val("--trials") ?? 3)), jobs = Math.max(1, Number(val("--jobs") ?? 1));
|
|
139
|
+
const calib = has("--calibrate"), names = variantNames(val("--variants") ?? (calib ? "baseline" : "ab")), trials = Math.max(1, Number(val("--trials") ?? (calib ? 5 : 3))), jobs = Math.max(1, Number(val("--jobs") ?? 1));
|
|
112
140
|
const maxTokens = Number(val("--max-tokens") ?? deps.maxTokens), maxSteps = Number(val("--max-steps") ?? 30);
|
|
113
141
|
const model = deps.model(), cheap = val("--cheap-model") && deps.modelFor ? deps.modelFor(val("--cheap-model")) : undefined, variants = names.map((n) => harnessVariant(model, n, { maxTokens, maxSteps, cheap, plugins, modelFor: deps.modelFor }));
|
|
114
142
|
const runs = cases.length * variants.length * trials;
|
|
115
143
|
console.log(`${bold("plan")} ${cases.length} tasks × ${variants.length} variants (${names.join(", ")}) × ${trials} trials = ${bold(String(runs))} agent sessions, up to ${maxTokens.toLocaleString()} tokens each`);
|
|
144
|
+
const mname = deps.modelName ?? "", maxCost = val("--max-cost") === undefined ? undefined : Number(val("--max-cost"));
|
|
145
|
+
if (maxCost !== undefined && !(maxCost > 0))
|
|
146
|
+
throw new Error("--max-cost needs a positive number of dollars, for example --max-cost 0.50");
|
|
147
|
+
if (maxCost !== undefined && !priceOf(mname))
|
|
148
|
+
throw new Error(`--max-cost needs a known price for the model "${mname}" (see cli/tui/pricing.ts); without one Amness cannot estimate the spend`);
|
|
149
|
+
const p = priceOf(mname);
|
|
150
|
+
if (p)
|
|
151
|
+
console.log(dim(`worst case about $${(runs * maxTokens * p[2] / 1e6).toFixed(2)} (every token priced as output; real runs are far cheaper: most of a session is cached input)${maxCost !== undefined ? ` · stops at about $${maxCost.toFixed(2)}` : " · no spending cap (--max-cost sets one)"}`));
|
|
116
152
|
if (!has("--yes") && !(await deps.confirm(`This calls your model up to ${(runs * maxTokens).toLocaleString()} tokens in the worst case. Continue?`)))
|
|
117
153
|
return 1;
|
|
118
|
-
const { rows, analysis } = await evaluate(cases, variants, {
|
|
154
|
+
const { rows, analysis, stopped, skipped } = await evaluate(cases, variants, {
|
|
155
|
+
stop: maxCost === undefined ? undefined : (rs) => { const spent = rowsCost(mname, rs) ?? 0; return spent >= maxCost ? `reached the cost cap: about $${spent.toFixed(3)} spent of $${maxCost.toFixed(2)}` : undefined; },
|
|
119
156
|
metrics: Object.keys(useMetrics).length ? useMetrics : undefined, trials, jobs, baseline: val("--baseline"), k: Math.min(3, trials), seed: val("--seed") ? Number(val("--seed")) : undefined,
|
|
120
157
|
onRow: (r) => console.error(` ${r.pass ? green("PASS") : red("FAIL")} ${r.case.padEnd(22)} ${r.variant.padEnd(18)} ${dim(`${r.steps} steps · ${r.tokens} tok · ${(r.ms / 1000).toFixed(1)}s${r.note ? " · " + r.note : ""}`)}`),
|
|
121
158
|
});
|
|
122
159
|
const out = resolve(val("--out") ?? join(".amness", "evals", new Date().toISOString().replace(/[:.]/g, "-") + ".json"));
|
|
123
160
|
mkdirSync(join(out, ".."), { recursive: true });
|
|
124
|
-
|
|
161
|
+
const spent = rowsCost(mname, rows);
|
|
162
|
+
writeFileSync(out, JSON.stringify({ at: new Date().toISOString(), model: mname || undefined, ...provenance(cases), cliArgs: argv.filter((a, i) => !/key|token/i.test(a) && !/key|token/i.test(argv[i - 1] ?? "")), baseline: analysis.baseline, k: Math.min(3, trials), variants: names, trials, estimatedCost: spent ?? undefined, stopped, rows }, null, 1));
|
|
163
|
+
if (stopped)
|
|
164
|
+
console.log(`\n${red("stopped early")}: ${stopped}. ${skipped} planned session${skipped === 1 ? "" : "s"} did not run, so the results below cover only the runs that finished.`);
|
|
165
|
+
if (spent !== null)
|
|
166
|
+
console.log(dim(`estimated spend: about $${spent.toFixed(3)} for ${rows.length} session${rows.length === 1 ? "" : "s"}${rows.length ? ` ($${(spent / rows.length).toFixed(4)} each)` : ""} (an estimate from token counts, not an invoice)`));
|
|
167
|
+
if (calib) {
|
|
168
|
+
console.log("\n" + bold("how hard is each task?") + dim(` (baseline, ${trials} trials; keep 30% to 70%)`));
|
|
169
|
+
for (const x of calibrate(rows, names[0]))
|
|
170
|
+
console.log(` ${x.verdict === "keep" ? green("keep ") : red(x.verdict.padEnd(10))} ${x.case.padEnd(26)} ${x.passes}/${x.runs} passed ${dim((x.rate * 100).toFixed(0) + "%")}`);
|
|
171
|
+
console.log(dim(`raw results: ${out}`));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
125
174
|
if (val("--profile-out")) {
|
|
126
175
|
const rec = recommend(analysis, names.includes("full") ? "full" : analysis.baseline), pm = val("--profile-model") ?? "*";
|
|
127
176
|
console.log("\nguard verdicts:");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const cache = new Map();
|
|
2
|
+
const TTL = 10 * 60_000;
|
|
3
|
+
const NOT_CHAT = /embed|whisper|tts|dall-e|moderation|davinci|babbage|transcribe|realtime|audio|image|rerank/i;
|
|
4
|
+
export async function fetchModels(p, fetchImpl = fetch) {
|
|
5
|
+
if (!p.apiKey && p.provider !== "ollama" && p.provider !== "custom")
|
|
6
|
+
return []; // a hosted provider refuses a request with no key: do not even try
|
|
7
|
+
const key = `${p.provider}|${p.baseUrl}|${(p.apiKey ?? "").slice(-6)}`, hit = cache.get(key);
|
|
8
|
+
if (hit && Date.now() - hit.at < TTL)
|
|
9
|
+
return hit.ids;
|
|
10
|
+
const base = p.baseUrl.replace(/\/+$/, "");
|
|
11
|
+
const anthropic = p.provider === "anthropic";
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetchImpl(anthropic ? `${base}/v1/models?limit=100` : `${base}/models`, {
|
|
14
|
+
headers: anthropic ? { "x-api-key": p.apiKey ?? "", "anthropic-version": "2023-06-01" } : p.apiKey ? { authorization: `Bearer ${p.apiKey}` } : {},
|
|
15
|
+
signal: AbortSignal.timeout(6000),
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return [];
|
|
19
|
+
const body = (await res.json());
|
|
20
|
+
const ids = [...(body.data ?? []).map((m) => m.id), ...(body.models ?? []).map((m) => m.model ?? m.name)] // Ollama's native shape uses `models`
|
|
21
|
+
.filter((x) => typeof x === "string" && !!x && !NOT_CHAT.test(x));
|
|
22
|
+
const uniq = [...new Set(ids)].sort();
|
|
23
|
+
cache.set(key, { at: Date.now(), ids: uniq });
|
|
24
|
+
return uniq;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** For tests. */
|
|
31
|
+
export const clearModelCache = () => cache.clear();
|
package/dist/cli/tui/app.d.ts
CHANGED
|
@@ -131,9 +131,11 @@ export declare class App {
|
|
|
131
131
|
items: Suggestion[];
|
|
132
132
|
sel: number;
|
|
133
133
|
token: Token;
|
|
134
|
-
kind: Token["kind"];
|
|
134
|
+
kind: Token["kind"]; /** the user moved into the list with the arrow keys */
|
|
135
|
+
nav?: boolean;
|
|
135
136
|
} | null;
|
|
136
137
|
suggOff: string;
|
|
138
|
+
private argPending;
|
|
137
139
|
private builtin;
|
|
138
140
|
private customAt;
|
|
139
141
|
private customCmds;
|
package/dist/cli/tui/app.js
CHANGED
|
@@ -15,7 +15,8 @@ import { BlockStore } from "./blocks.js";
|
|
|
15
15
|
import { buildCommands } from "./commands.js";
|
|
16
16
|
import { FullView } from "./full.js";
|
|
17
17
|
import { loadCustomCommands } from "./custom.js";
|
|
18
|
-
import {
|
|
18
|
+
import { argOptions } from "./argcomplete.js";
|
|
19
|
+
import { FileIndex, fuzzyScore, applySuggestion, menuRows, rankCommands, rankFiles, tokenAtCursor } from "./complete.js";
|
|
19
20
|
import { Editor } from "./editor.js";
|
|
20
21
|
import { KeyParser, PASTE_OFF, PASTE_ON } from "./keys.js";
|
|
21
22
|
import { renderMarkdown, takeCompleteBlocks } from "./markdown.js";
|
|
@@ -80,6 +81,7 @@ export class App {
|
|
|
80
81
|
pager = null;
|
|
81
82
|
sugg = null;
|
|
82
83
|
suggOff = "";
|
|
84
|
+
argPending = new Set();
|
|
83
85
|
builtin;
|
|
84
86
|
customAt = 0;
|
|
85
87
|
customCmds = [];
|
|
@@ -370,11 +372,16 @@ export class App {
|
|
|
370
372
|
setTitle(t) { if (this.io.output.isTTY !== false)
|
|
371
373
|
this.io.output.write(`\x1b]0;${t}\x07`); }
|
|
372
374
|
suggLines(w) {
|
|
373
|
-
const s = this.sugg,
|
|
374
|
-
|
|
375
|
-
|
|
375
|
+
const s = this.sugg, { rows, above, below } = menuRows(s.items, s.sel, Math.min(12, Math.max(4, this.screen.rows - 8)));
|
|
376
|
+
const lines = rows.map((r) => {
|
|
377
|
+
if ("head" in r)
|
|
378
|
+
return dim(` ── ${r.head} ${"─".repeat(Math.max(2, 20 - r.head.length))}`);
|
|
379
|
+
const it = s.items[r.item], on = r.item === s.sel, label = s.kind === "file" ? it.label : it.label.padEnd(16);
|
|
376
380
|
return (on ? cyan("❯ ") + bold(cyan(label)) : " " + label) + (it.detail ? " " + (on ? gray(it.detail) : dim(it.detail)) : "");
|
|
377
381
|
}).map((l) => (cellWidth(l) > w ? l.slice(0, w + 12) : l));
|
|
382
|
+
if (above || below)
|
|
383
|
+
lines.push(dim(` ${s.sel + 1}/${s.items.length} ${above ? "↑" : " "}${below ? "↓" : " "} ↑↓ move · PgUp/PgDn page`));
|
|
384
|
+
return lines;
|
|
378
385
|
}
|
|
379
386
|
tick = () => { this.frame++; this.render(); };
|
|
380
387
|
ensureTimer() { if (!this.timer)
|
|
@@ -451,7 +458,7 @@ export class App {
|
|
|
451
458
|
if (k.name === "enter" && !k.alt && !k.shift) {
|
|
452
459
|
if (this.sugg && this.sugg.items.length) {
|
|
453
460
|
const it = this.sugg.items[this.sugg.sel];
|
|
454
|
-
if (!(this.sugg.kind === "slash" && ed.text.trim() === it.label))
|
|
461
|
+
if (!(this.sugg.kind === "slash" && ed.text.trim() === it.label) && !(this.sugg.kind === "arg" && (this.sugg.items.some((x) => x.label === this.sugg.token.query) || (it.open && !this.sugg.nav))))
|
|
455
462
|
return this.acceptSugg(true);
|
|
456
463
|
}
|
|
457
464
|
if (ed.text.slice(0, ed.cursorIndex).endsWith("\\")) {
|
|
@@ -461,10 +468,16 @@ export class App {
|
|
|
461
468
|
}
|
|
462
469
|
return void this.submit();
|
|
463
470
|
}
|
|
471
|
+
if (this.sugg && (k.name === "pageup" || k.name === "pagedown")) {
|
|
472
|
+
const n = this.sugg.items.length;
|
|
473
|
+
this.sugg.sel = Math.max(0, Math.min(n - 1, this.sugg.sel + (k.name === "pageup" ? -8 : 8)));
|
|
474
|
+
return this.render();
|
|
475
|
+
}
|
|
464
476
|
if (k.name === "up" || k.name === "down") {
|
|
465
477
|
if (this.sugg) {
|
|
466
478
|
const n = this.sugg.items.length;
|
|
467
479
|
this.sugg.sel = (this.sugg.sel + (k.name === "up" ? n - 1 : 1)) % n;
|
|
480
|
+
this.sugg.nav = true;
|
|
468
481
|
return this.render();
|
|
469
482
|
}
|
|
470
483
|
if (!ed.vertical(k.name === "up" ? -1 : 1))
|
|
@@ -492,13 +505,25 @@ export class App {
|
|
|
492
505
|
let items = [];
|
|
493
506
|
if (tok.kind === "slash")
|
|
494
507
|
items = rankCommands(tok.query, this.cmds);
|
|
508
|
+
else if (tok.kind === "arg") {
|
|
509
|
+
const cmd = this.cmds.find((c) => c.name === tok.cmd || c.alias === tok.cmd);
|
|
510
|
+
if (cmd) {
|
|
511
|
+
const r = argOptions(this, cmd.name, tok.words ?? []), pk = `${cmd.name}|${(tok.words ?? []).join(" ")}`;
|
|
512
|
+
if (r.pending && !this.argPending.has(pk)) {
|
|
513
|
+
this.argPending.add(pk);
|
|
514
|
+
void r.pending.catch(() => { }).then(() => { this.argPending.delete(pk); this.updateSugg(); this.render(); });
|
|
515
|
+
}
|
|
516
|
+
items = r.options.map((o) => ({ o, s: fuzzyScore(tok.query, o.value) })).filter((x) => x.s !== null)
|
|
517
|
+
.sort((x, y) => (tok.query ? y.s - x.s : 0)).map(({ o }) => ({ label: o.value, insert: o.value + (o.more ? " " : ""), detail: o.detail, more: o.more, open: r.open }));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
495
520
|
else {
|
|
496
521
|
if (!this.files.fresh())
|
|
497
522
|
void this.files.ensure().then(() => this.updateSugg(), () => { }); // list still loading: fill the popup in once it arrives
|
|
498
523
|
items = rankFiles(tok.query, this.files.files);
|
|
499
524
|
}
|
|
500
|
-
const
|
|
501
|
-
this.sugg = items.length ? { items, sel: Math.min(keep, items.length - 1), token: tok, kind: tok.kind } : null;
|
|
525
|
+
const same = this.sugg && this.sugg.kind === tok.kind && this.sugg.token.query === tok.query, keep = same ? this.sugg.sel : 0;
|
|
526
|
+
this.sugg = items.length ? { items, sel: Math.min(keep, items.length - 1), token: tok, kind: tok.kind, nav: same ? this.sugg.nav : false } : null;
|
|
502
527
|
if (!this.sugg)
|
|
503
528
|
return;
|
|
504
529
|
this.schedule();
|
|
@@ -512,6 +537,8 @@ export class App {
|
|
|
512
537
|
this.suggOff = "";
|
|
513
538
|
if (fromEnter && s.kind === "slash" && !this.cmds.find((c) => "/" + c.name === s.items[s.sel].label)?.args)
|
|
514
539
|
return void this.submit();
|
|
540
|
+
if (fromEnter && s.kind === "arg" && !s.items[s.sel].more)
|
|
541
|
+
return void this.submit(); // a finished value: run the command
|
|
515
542
|
this.updateSugg();
|
|
516
543
|
this.render();
|
|
517
544
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { App } from "./app.ts";
|
|
2
|
+
export type ArgOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
detail?: string; /** more words are expected after this one: insert a space and do not submit */
|
|
5
|
+
more?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type ArgResult = {
|
|
8
|
+
options: ArgOption[];
|
|
9
|
+
pending?: Promise<unknown>; /** any value is allowed (a number, a model id, a command): Enter runs what you typed unless you moved into the list; Tab still completes */
|
|
10
|
+
open?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function argOptions(a: App, cmd: string, words: string[]): ArgResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Completion for the words after a slash command: `/mode ` lists the modes, `/model ` the models your provider offers, `/theme ` the themes, and so on.
|
|
2
|
+
// A completer returns its options now and, when it needs the network, a promise that settles when the list has been filled in (the menu refreshes then).
|
|
3
|
+
import { effortsFor, loadPrompts, SANDBOX_MODES, SHELL_CHOICES, STRATEGIES } from "../../src/index.js";
|
|
4
|
+
import { CLAUDE_MODELS, OPENAI_MODELS } from "../config.js";
|
|
5
|
+
import { fetchModels } from "../models.js";
|
|
6
|
+
import { themeNames, THEMES } from "./themes.js";
|
|
7
|
+
const onOff = [{ value: "on" }, { value: "off" }];
|
|
8
|
+
const opts = (...xs) => xs.map(([value, detail]) => ({ value, detail }));
|
|
9
|
+
/** Known models with their hints first, then any other model the provider reports. The provider is asked once per ten minutes; until it answers, only the known models show. */
|
|
10
|
+
const fetched = new Map();
|
|
11
|
+
function modelOptions(a, extra = []) {
|
|
12
|
+
const known = a.cfg.provider === "anthropic" ? CLAUDE_MODELS : a.cfg.provider === "openai" ? OPENAI_MODELS : [];
|
|
13
|
+
const key = `${a.cfg.provider}|${a.cfg.baseUrl}`, have = fetched.get(key), fresh = have && Date.now() - have.at < 10 * 60_000;
|
|
14
|
+
const seen = new Set(known.map((m) => m.id));
|
|
15
|
+
const options = [...extra, ...known.map((m) => ({ value: m.id, detail: m.hint })), ...(have?.ids ?? []).filter((id) => !seen.has(id)).map((id) => ({ value: id, detail: "from your provider" }))];
|
|
16
|
+
if (fresh)
|
|
17
|
+
return { options, open: true };
|
|
18
|
+
return { options, open: true, pending: fetchModels({ provider: a.cfg.provider, baseUrl: a.cfg.baseUrl, apiKey: a.cfg.apiKey }).then((ids) => { fetched.set(key, { at: Date.now(), ids }); }) };
|
|
19
|
+
}
|
|
20
|
+
export function argOptions(a, cmd, words) {
|
|
21
|
+
const first = words.length === 0;
|
|
22
|
+
const ok = (options) => ({ options });
|
|
23
|
+
switch (cmd) {
|
|
24
|
+
case "mode": return first ? ok(opts(["default", "ask before edits and non-trivial commands"], ["acceptEdits", "edits are automatic, commands still ask"], ["plan", "read-only: investigate and propose a plan"], ["yolo", "never ask (dangerous)"])) : ok([]);
|
|
25
|
+
case "api": return first ? ok(opts(["auto", "pick Responses for reasoning models, self-correct"], ["responses", "OpenAI Responses API"], ["chat", "Chat Completions API"])) : ok([]);
|
|
26
|
+
case "effort": return first ? ok([{ value: "default", detail: "let the model decide" }, ...(a.cfg.provider === "anthropic" ? ["low", "medium", "high", "xhigh", "max"] : effortsFor(a.cfg.model)).map((e) => ({ value: e }))]) : ok([]);
|
|
27
|
+
case "model": return first ? modelOptions(a) : ok([]);
|
|
28
|
+
case "models": return first ? ok([...["explore", "compact", "review", "cheap"].map((r) => ({ value: r, detail: `model for ${r} steps`, more: true })), { value: "fallback", detail: "answer from this model when the main one is down", more: true }])
|
|
29
|
+
: words.length === 1 ? modelOptions(a, [{ value: "off", detail: "use the main model" }]) : ok([]);
|
|
30
|
+
case "theme": return first ? ok(themeNames().map((n) => ({ value: n, detail: THEMES[n].label }))) : ok([]);
|
|
31
|
+
case "shell": return first ? ok(SHELL_CHOICES.map((n) => ({ value: n, detail: n === "auto" ? "detect from your system" : undefined }))) : ok([]);
|
|
32
|
+
case "strategy": return first ? ok([...Object.keys(STRATEGIES).map((n) => ({ value: n })), { value: "off", detail: "the default loop" }]) : ok([]);
|
|
33
|
+
case "prompt": return first ? ok([...loadPrompts(a.cwd).map((p) => ({ value: p.id, detail: "from .amness/prompts" })), { value: "list" }, { value: "off", detail: "the built-in prompt" }]) : ok([]);
|
|
34
|
+
case "sandbox": return first ? ok(SANDBOX_MODES.map((m) => ({ value: m, detail: m === "off" ? "no isolation" : m === "workspace" ? "writes only in the project" : "no network, caches read-only" }))) : ok([]);
|
|
35
|
+
case "diffmode": return first ? ok(opts(["auto"], ["stacked"], ["split"])) : ok([]);
|
|
36
|
+
case "ui": return first ? ok(opts(["full", "full-screen"], ["inline", "scrolls with the terminal"])) : ok([]);
|
|
37
|
+
case "mouse":
|
|
38
|
+
case "keepwarm":
|
|
39
|
+
case "lsp":
|
|
40
|
+
case "approver":
|
|
41
|
+
case "reviewer":
|
|
42
|
+
case "router": return first ? ok(onOff) : ok([]);
|
|
43
|
+
case "copy": return first ? ok(opts(["code", "the last code block"], ["all", "the whole conversation"])) : ok([]);
|
|
44
|
+
case "memory": return first ? ok([...["forget", "add"].map((v) => ({ value: v, more: true })), ...["stale", "prune", "consolidate", "graph", "embeddings"].map((v) => ({ value: v }))]) : ok([]);
|
|
45
|
+
case "permissions": return first ? ok([{ value: "list" }, ...["allow", "ask", "deny"].map((v) => ({ value: v, detail: `e.g. /permissions ${v} sh(npm test*)`, more: true }))]) : ok([]);
|
|
46
|
+
case "branch": return first ? ok(a.session.turns.map((t, i) => ({ value: String(i + 1), detail: t.text.replace(/\s+/g, " ").slice(0, 60) }))) : ok([]);
|
|
47
|
+
case "pr": return first ? ok(opts(["draft", "create it as a draft"])) : ok([]);
|
|
48
|
+
case "report": return first ? ok([{ value: "case", detail: "turn the last request into an eval task", more: true }]) : ok([]);
|
|
49
|
+
case "mcp": return first ? ok([{ value: "list" }, { value: "off" }]) : ok([]);
|
|
50
|
+
case "budget": return first ? { options: opts(["100k"], ["300k", "the default"], ["500k"], ["1m"], ["2m"]), open: true } : ok([]);
|
|
51
|
+
case "check":
|
|
52
|
+
case "verify": return first ? { options: [{ value: "off", detail: "switch it off" }], open: true } : ok([]);
|
|
53
|
+
default: return ok([]);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/cli/tui/cards.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type RenderCtx = {
|
|
|
16
16
|
diffMode: "auto" | "stacked" | "split";
|
|
17
17
|
/** The block above is also a tool call: draw no blank line between them, so a run of tool calls reads as one unit. */
|
|
18
18
|
tight?: boolean;
|
|
19
|
+
/** this is the most recent tool call of the lead agent: its output stays open (a short preview) until the next one starts */
|
|
20
|
+
last?: boolean;
|
|
19
21
|
};
|
|
20
22
|
export type Hit = {
|
|
21
23
|
row: number;
|