amness 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -7
- package/dist/cli/amness.js +5 -4
- package/dist/cli/config.d.ts +22 -1
- package/dist/cli/config.js +130 -11
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- 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 +93 -23
- package/dist/cli/tui/complete.d.ts +23 -3
- package/dist/cli/tui/complete.js +38 -3
- package/dist/cli/tui/full.js +18 -8
- package/dist/src/anthropic.d.ts +1 -1
- package/docs/architecture.md +1 -1
- package/docs/bench.md +4 -5
- package/docs/development.md +1 -1
- package/docs/getting-started.md +18 -9
- package/docs/headless.md +6 -1
- package/docs/library.md +9 -18
- package/docs/providers.md +14 -2
- package/docs/safety.md +14 -1
- package/docs/usage.md +27 -3
- package/package.json +4 -10
|
@@ -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;
|
package/dist/cli/tui/cards.js
CHANGED
|
@@ -123,7 +123,8 @@ export function renderBlock(b, c) {
|
|
|
123
123
|
// ---------------------------------------------------------------- tool card
|
|
124
124
|
const sub = !!b.agent && b.agent !== "lead", failed = b.status === "fail", edit = b.edit;
|
|
125
125
|
const hue = failed ? t.err : ({ read_file: t.tool.read, list_files: t.tool.read, grep: t.tool.read, write_file: t.tool.edit, edit_file: t.tool.edit, apply_patch: t.tool.edit, sh: t.tool.shell, web_search: t.tool.web, fetch_url: t.tool.web, memory_save: t.tool.memory, memory_forget: t.tool.memory, memory_search: t.tool.read, todo_write: t.tool.plan, exit_plan_mode: t.tool.plan, task: t.tool.explore })[b.name] ?? (b.name.includes("__") ? t.tool.web : t.tool.generic);
|
|
126
|
-
const expanded = c.expandAll ? true : b.expanded ?? (!!edit || failed);
|
|
126
|
+
const expanded = c.expandAll ? true : b.expanded ?? (!!edit || failed || (!!c.last && !sub && b.name !== "read_file" && b.name !== "task"));
|
|
127
|
+
const preview = !c.expandAll && b.expanded === undefined && !edit && !failed && !!c.last && b.name !== "read_file" && b.name !== "task"; // opened automatically, not by the user: show a short preview only
|
|
127
128
|
const bg = failed ? mix(t.surface, t.err, 0.14) : t.surface, spinner = SPIN[c.frame % SPIN.length];
|
|
128
129
|
const label = BOLD(F(failed ? t.err : t.text, toolLabel(b.name))), args = F(t.dim, argSummary(b.name, b.args));
|
|
129
130
|
const status = b.status === "running"
|
|
@@ -163,7 +164,7 @@ export function renderBlock(b, c) {
|
|
|
163
164
|
}
|
|
164
165
|
else {
|
|
165
166
|
const res = b.result ?? "", hl = (k, s) => F(k === "plain" ? t.syn.plain : t.syn[k], s);
|
|
166
|
-
let lines = res.split("\n"), cap = c.expandAll ? 200 : 60;
|
|
167
|
+
let lines = res.split("\n"), cap = c.expandAll ? 200 : preview ? 20 : 60;
|
|
167
168
|
if (b.name === "sh") {
|
|
168
169
|
let cmd = "";
|
|
169
170
|
try {
|
|
@@ -191,7 +192,7 @@ export function renderBlock(b, c) {
|
|
|
191
192
|
body(w); // only the lines that look like errors are red
|
|
192
193
|
}
|
|
193
194
|
if (lines.length > cap)
|
|
194
|
-
body(F(t.faint, `… ${lines.length - cap} more lines
|
|
195
|
+
body(F(t.faint, `… ${lines.length - cap} more line${lines.length - cap === 1 ? "" : "s"} · /output shows all of it${preview ? "" : " · ctrl+o expands everything"}`));
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
return done();
|
package/dist/cli/tui/commands.js
CHANGED
|
@@ -3,7 +3,9 @@ import { relatives } from "./session.js";
|
|
|
3
3
|
import { SHELL_CHOICES, STRATEGIES, chooseShell, currentShell, loadPrompts, setShell, shellFallbackNote } from "../../src/index.js";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { collectFiles, effortsFor, loadMcp, readBundle, writeBundle } from "../../src/index.js";
|
|
6
|
-
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS } from "../config.js";
|
|
6
|
+
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS, connections, keyFor, storeKey } from "../config.js";
|
|
7
|
+
import { noStore, where } from "../keystore.js";
|
|
8
|
+
import { fetchModels } from "../models.js";
|
|
7
9
|
import { bold, cyan, dim, green, red, yellow } from "../ui.js";
|
|
8
10
|
import { estimateCost, fmtCost, fmtDuration, fmtTokens } from "./pricing.js";
|
|
9
11
|
import { codeBlocks } from "./clipboard.js";
|
|
@@ -15,6 +17,29 @@ import { THEMES, getTheme, themeNames } from "./themes.js";
|
|
|
15
17
|
import { helpLines, renderDiff, toolCall, todoList } from "./view.js";
|
|
16
18
|
const INIT_PROMPT = "Explore this repository and write an AGENTS.md at the repo root for future coding sessions: what the project is, how to build/test/run it (exact commands), the code layout, conventions you can verify from the code, and gotchas. Keep it under 80 lines and do not invent anything you cannot confirm from the files.";
|
|
17
19
|
const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
20
|
+
const keyStatus = (c) => !c.needsKey ? "local, no key needed" : c.source === "env" ? `connected · from ${c.env}` : c.source === "saved" ? `connected · ${noStore() ? "this session only" : where()}` : "not connected";
|
|
21
|
+
/** Make `id` the active provider, using its saved key (or asking for one). Returns false if the user gave up. */
|
|
22
|
+
async function switchProvider(a, id) {
|
|
23
|
+
let apiKey = keyFor(id).key;
|
|
24
|
+
if (PROVIDERS[id].needsKey && !apiKey) {
|
|
25
|
+
apiKey = await a.askText(`${PROVIDERS[id].label} API key (hidden; ${where()})`, { secret: true });
|
|
26
|
+
if (!apiKey)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
Object.assign(a.cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model, apiKey });
|
|
30
|
+
if (id === "custom" || id === "ollama") {
|
|
31
|
+
const m = await a.askText("Model id", { initial: a.cfg.model });
|
|
32
|
+
if (m)
|
|
33
|
+
a.cfg.model = m;
|
|
34
|
+
const u = await a.askText("Base URL", { initial: a.cfg.baseUrl });
|
|
35
|
+
if (u)
|
|
36
|
+
a.cfg.baseUrl = u;
|
|
37
|
+
}
|
|
38
|
+
a.saveConfig();
|
|
39
|
+
a.buildAgent();
|
|
40
|
+
a.say(`provider → ${cyan(PROVIDERS[id].label)}, model ${cyan(a.cfg.model)}`, dim);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
18
43
|
export function buildCommands() {
|
|
19
44
|
return [
|
|
20
45
|
{ name: "help", alias: "?", help: "keys and commands", live: true, run: (a) => a.openPager(helpLines(a.cmds, (a.io.output.columns || 80) - 1), "help") },
|
|
@@ -22,7 +47,10 @@ export function buildCommands() {
|
|
|
22
47
|
let id = arg;
|
|
23
48
|
if (!id) {
|
|
24
49
|
const list = a.cfg.provider === "anthropic" ? CLAUDE_MODELS : a.cfg.provider === "openai" ? OPENAI_MODELS : [{ id: a.cfg.model, hint: "current" }];
|
|
25
|
-
const
|
|
50
|
+
const have = new Set(list.map((m) => m.id));
|
|
51
|
+
a.note("asking your provider for its models…", 7000);
|
|
52
|
+
const extra = (await fetchModels({ provider: a.cfg.provider, baseUrl: a.cfg.baseUrl, apiKey: a.cfg.apiKey })).filter((m) => !have.has(m)).map((m) => ({ id: m, hint: "from your provider" }));
|
|
53
|
+
const known = [...list, ...extra].map((m) => ({ label: m.id, value: m.id, hint: m.hint }));
|
|
26
54
|
id = await a.pick("Model", [...known, { label: "Other…", value: "" }], Math.max(0, known.findIndex((m) => m.value === a.cfg.model)));
|
|
27
55
|
if (id === undefined)
|
|
28
56
|
return;
|
|
@@ -41,29 +69,54 @@ export function buildCommands() {
|
|
|
41
69
|
a.buildAgent();
|
|
42
70
|
a.say(`model → ${cyan(id)}`, dim);
|
|
43
71
|
} },
|
|
44
|
-
{ name: "provider", help: "switch provider (OpenAI, Claude, Ollama…)", run: async (a) => {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
{ name: "provider", help: "switch provider (OpenAI, Claude, Ollama…); each provider keeps its own saved key", run: async (a) => {
|
|
73
|
+
const list = connections();
|
|
74
|
+
const id = await a.pick("Provider", list.map((c) => ({ label: `${!c.needsKey || c.source !== "none" ? "●" : "○"} ${c.label}${c.id === a.cfg.provider ? " (in use)" : ""}`, value: c.id, hint: keyStatus(c) })));
|
|
75
|
+
if (id)
|
|
76
|
+
await switchProvider(a, id);
|
|
77
|
+
} },
|
|
78
|
+
{ name: "connect", alias: "keys", help: "add, replace or remove API keys for several providers and see which are connected", run: async (a) => {
|
|
79
|
+
for (;;) {
|
|
80
|
+
const list = connections();
|
|
81
|
+
const id = await a.pick("Providers ● connected ○ not connected", list.map((c) => ({ label: `${!c.needsKey || c.source !== "none" ? "●" : "○"} ${c.label}${c.id === a.cfg.provider ? " (in use)" : ""}`, value: c.id, hint: keyStatus(c) })));
|
|
82
|
+
if (!id)
|
|
53
83
|
return;
|
|
84
|
+
const c = list.find((x) => x.id === id);
|
|
85
|
+
if (!c.needsKey) {
|
|
86
|
+
a.say(`${c.label} needs no key. Use /provider to switch to it.`, dim);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const act = c.source === "none" ? "add" : await a.pick("What do you want to do?", [
|
|
90
|
+
...(id === a.cfg.provider ? [] : [{ label: "Use this provider now", value: "use" }]),
|
|
91
|
+
{ label: c.source === "env" ? `Also save a key (${c.env} still wins while it is set)` : "Replace the key", value: "add" },
|
|
92
|
+
...(c.source === "saved" ? [{ label: "Remove the saved key", value: "remove" }] : []),
|
|
93
|
+
]);
|
|
94
|
+
if (!act)
|
|
95
|
+
continue;
|
|
96
|
+
if (act === "use") {
|
|
97
|
+
await switchProvider(a, id);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (act === "add") {
|
|
101
|
+
const key = await a.askText(`${c.label} API key (hidden; ${where()})`, { secret: true });
|
|
102
|
+
if (!key)
|
|
103
|
+
continue;
|
|
104
|
+
storeKey(id, key);
|
|
105
|
+
if (id === a.cfg.provider) {
|
|
106
|
+
a.cfg.apiKey = keyFor(id).key;
|
|
107
|
+
a.buildAgent();
|
|
108
|
+
}
|
|
109
|
+
a.say(`${green("✓")} ${c.label} connected (${noStore() ? "this session only" : where()})`, dim);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
storeKey(id, undefined);
|
|
113
|
+
if (id === a.cfg.provider) {
|
|
114
|
+
a.cfg.apiKey = keyFor(id).key;
|
|
115
|
+
a.buildAgent();
|
|
116
|
+
}
|
|
117
|
+
a.say(`${c.label}: saved key removed`, dim);
|
|
118
|
+
}
|
|
54
119
|
}
|
|
55
|
-
Object.assign(a.cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model, apiKey });
|
|
56
|
-
if (id === "custom" || id === "ollama") {
|
|
57
|
-
const m = await a.askText("Model id", { initial: a.cfg.model });
|
|
58
|
-
if (m)
|
|
59
|
-
a.cfg.model = m;
|
|
60
|
-
const u = await a.askText("Base URL", { initial: a.cfg.baseUrl });
|
|
61
|
-
if (u)
|
|
62
|
-
a.cfg.baseUrl = u;
|
|
63
|
-
}
|
|
64
|
-
a.saveConfig();
|
|
65
|
-
a.buildAgent();
|
|
66
|
-
a.say(`provider → ${cyan(PROVIDERS[id].label)}, model ${cyan(a.cfg.model)}`, dim);
|
|
67
120
|
} },
|
|
68
121
|
{ name: "effort", help: "reasoning effort for the current model", args: "[level]", run: async (a, arg) => {
|
|
69
122
|
const levels = a.cfg.provider === "anthropic" ? [...EFFORTS] : effortsFor(a.cfg.model);
|
|
@@ -105,6 +158,23 @@ export function buildCommands() {
|
|
|
105
158
|
return void a.say(`unknown mode "${m}"`, red);
|
|
106
159
|
a.setMode(m);
|
|
107
160
|
} },
|
|
161
|
+
{ name: "output", alias: "last", help: "the full output of the last tool call in a pager (/output 2 for the one before)", args: "[n]", live: true, run: (a, arg) => {
|
|
162
|
+
const calls = a.blocks.blocks.filter((b) => b.kind === "tool" && (!b.agent || b.agent === "lead") && !!b.result && b.name !== "todo_write");
|
|
163
|
+
if (!calls.length)
|
|
164
|
+
return void a.say("no tool output yet", dim);
|
|
165
|
+
const n = Math.max(1, Math.floor(Number(arg) || 1)), b = calls[calls.length - n];
|
|
166
|
+
if (!b)
|
|
167
|
+
return void a.say(`only ${calls.length} tool call${calls.length === 1 ? "" : "s"} so far`, dim);
|
|
168
|
+
const body = b.result.split("\n");
|
|
169
|
+
let head = toolCall(b.name, b.args);
|
|
170
|
+
if (b.name === "sh") {
|
|
171
|
+
try {
|
|
172
|
+
head = `$ ${JSON.parse(b.args).cmd}`;
|
|
173
|
+
}
|
|
174
|
+
catch { /* keep the generic header */ }
|
|
175
|
+
}
|
|
176
|
+
a.openPager([head, "", ...body], "output");
|
|
177
|
+
} },
|
|
108
178
|
{ name: "plan", help: "enter plan mode (read-only)", run: (a) => a.setMode("plan") },
|
|
109
179
|
{ name: "clear", help: "start a fresh conversation", alias: "new", run: (a) => {
|
|
110
180
|
a.newConversation();
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
export type Suggestion = {
|
|
2
2
|
label: string;
|
|
3
3
|
insert: string;
|
|
4
|
-
detail?: string;
|
|
4
|
+
detail?: string; /** arg suggestions: more words are expected, so do not submit on Enter */
|
|
5
|
+
more?: boolean; /** arg suggestions: any value is allowed, so Enter runs what was typed unless the user moved into the list */
|
|
6
|
+
open?: boolean; /** category header shown above the first item of each group (only when the list is not filtered) */
|
|
7
|
+
group?: string;
|
|
5
8
|
};
|
|
6
9
|
export type Token = {
|
|
7
|
-
kind: "slash" | "file";
|
|
10
|
+
kind: "slash" | "file" | "arg";
|
|
8
11
|
query: string;
|
|
9
12
|
start: number;
|
|
10
|
-
end: number;
|
|
13
|
+
end: number; /** arg tokens: the command, and the words already typed after it */
|
|
14
|
+
cmd?: string;
|
|
15
|
+
words?: string[];
|
|
11
16
|
};
|
|
12
17
|
/** What is the user completing right now? A slash command (first word of the input) or an @file mention. */
|
|
13
18
|
export declare function tokenAtCursor(text: string, cursor: number): Token | null;
|
|
19
|
+
/** Categories of the built-in commands, in the order the menu shows them. A command that is not listed (yours, or a plugin's) goes under "Custom". */
|
|
20
|
+
export declare const GROUP_ORDER: readonly ["Conversation", "Model & keys", "Safety", "Quality", "Project", "Extend", "Interface", "Custom"];
|
|
21
|
+
export declare const groupOf: (name: string) => string;
|
|
22
|
+
/** All matching commands. With no query they come grouped by category (in GROUP_ORDER, A to Z inside a group); with a query, best match first. */
|
|
14
23
|
export declare function rankCommands(query: string, cmds: {
|
|
15
24
|
name: string;
|
|
16
25
|
help: string;
|
|
@@ -37,3 +46,14 @@ export declare class FileIndex {
|
|
|
37
46
|
private load;
|
|
38
47
|
private walk;
|
|
39
48
|
}
|
|
49
|
+
export type MenuRow = {
|
|
50
|
+
head: string;
|
|
51
|
+
} | {
|
|
52
|
+
item: number;
|
|
53
|
+
};
|
|
54
|
+
/** The rows to draw for a suggestion list: a window of `win` rows that keeps the selected item in view, with a header row above each new group (headers cannot be selected). */
|
|
55
|
+
export declare function menuRows(items: Suggestion[], sel: number, win: number): {
|
|
56
|
+
rows: MenuRow[];
|
|
57
|
+
above: number;
|
|
58
|
+
below: number;
|
|
59
|
+
};
|
package/dist/cli/tui/complete.js
CHANGED
|
@@ -8,17 +8,37 @@ export function tokenAtCursor(text, cursor) {
|
|
|
8
8
|
const slash = before.match(/^\/(\S*)$/);
|
|
9
9
|
if (slash)
|
|
10
10
|
return { kind: "slash", query: slash[1], start: 0, end: cursor };
|
|
11
|
+
const arg = before.match(/^\/(\S+)((?:\s+\S+)*)\s+(\S*)$/); // "/model gp": the command, the words before the last one, the word being typed
|
|
12
|
+
if (arg)
|
|
13
|
+
return { kind: "arg", cmd: arg[1], words: arg[2].trim() ? arg[2].trim().split(/\s+/) : [], query: arg[3], start: cursor - arg[3].length, end: cursor };
|
|
11
14
|
const file = before.match(/(?:^|\s)@([^\s@]*)$/);
|
|
12
15
|
if (file)
|
|
13
16
|
return { kind: "file", query: file[1], start: cursor - file[1].length - 1, end: cursor };
|
|
14
17
|
return null;
|
|
15
18
|
}
|
|
16
|
-
|
|
19
|
+
/** Categories of the built-in commands, in the order the menu shows them. A command that is not listed (yours, or a plugin's) goes under "Custom". */
|
|
20
|
+
export const GROUP_ORDER = ["Conversation", "Model & keys", "Safety", "Quality", "Project", "Extend", "Interface", "Custom"];
|
|
21
|
+
const GROUP_MEMBERS = {};
|
|
22
|
+
for (const [g, names] of Object.entries({
|
|
23
|
+
Conversation: "output clear compact resume branch branches export import undo rewind copy todos exit",
|
|
24
|
+
"Model & keys": "model provider connect effort api models router keepwarm budget tokens",
|
|
25
|
+
Safety: "mode plan permissions policy sandbox approver hooks",
|
|
26
|
+
Quality: "verify check review reviewer lsp report loop diff",
|
|
27
|
+
Project: "init pr tools procs shell status live",
|
|
28
|
+
Extend: "memory mcp skills agents plugins reload-plugins prompt strategy profile",
|
|
29
|
+
Interface: "help theme sidebar details diffmode mouse ui",
|
|
30
|
+
}))
|
|
31
|
+
for (const n of names.split(" "))
|
|
32
|
+
GROUP_MEMBERS[n] = g;
|
|
33
|
+
export const groupOf = (name) => GROUP_MEMBERS[name] ?? "Custom";
|
|
34
|
+
/** All matching commands. With no query they come grouped by category (in GROUP_ORDER, A to Z inside a group); with a query, best match first. */
|
|
35
|
+
export function rankCommands(query, cmds, limit = 500) {
|
|
17
36
|
const q = query.toLowerCase();
|
|
18
37
|
const score = (c) => (c.name.startsWith(q) ? 3 : c.alias?.startsWith(q) ? 2 : c.name.includes(q) || c.alias?.includes(q) ? 1 : 0);
|
|
38
|
+
const gi = (n) => GROUP_ORDER.indexOf(groupOf(n));
|
|
19
39
|
return cmds.map((c) => ({ c, score: score(c) }))
|
|
20
|
-
.filter((x) => x.score > 0 || !q).sort((a, b) => b.score - a.score || a.c.name.localeCompare(b.c.name)).slice(0, limit)
|
|
21
|
-
.map(({ c }) => ({ label: "/" + c.name, insert: "/" + c.name + " ", detail: c.help + (c.alias && !c.name.includes(query.toLowerCase()) && c.alias.includes(query.toLowerCase()) ? ` (also /${c.alias})` : "") }));
|
|
40
|
+
.filter((x) => x.score > 0 || !q).sort((a, b) => (q ? b.score - a.score : gi(a.c.name) - gi(b.c.name)) || a.c.name.localeCompare(b.c.name)).slice(0, limit)
|
|
41
|
+
.map(({ c }) => ({ group: q ? undefined : groupOf(c.name), label: "/" + c.name, insert: "/" + c.name + " ", detail: c.help + (c.alias && !c.name.includes(query.toLowerCase()) && c.alias.includes(query.toLowerCase()) ? ` (also /${c.alias})` : "") }));
|
|
22
42
|
}
|
|
23
43
|
/** Subsequence fuzzy match. Tries the greedy alignment and the exact-substring alignments and keeps the best. null = no match. */
|
|
24
44
|
export function fuzzyScore(query, path) {
|
|
@@ -111,3 +131,18 @@ export class FileIndex {
|
|
|
111
131
|
return out;
|
|
112
132
|
}
|
|
113
133
|
}
|
|
134
|
+
/** The rows to draw for a suggestion list: a window of `win` rows that keeps the selected item in view, with a header row above each new group (headers cannot be selected). */
|
|
135
|
+
export function menuRows(items, sel, win) {
|
|
136
|
+
const all = [];
|
|
137
|
+
let selRow = 0, last;
|
|
138
|
+
items.forEach((it, i) => { if (it.group && it.group !== last)
|
|
139
|
+
all.push({ head: it.group }); last = it.group; if (i === sel)
|
|
140
|
+
selRow = all.length; all.push({ item: i }); });
|
|
141
|
+
win = Math.max(1, win);
|
|
142
|
+
let top = Math.max(0, Math.min(selRow - Math.floor(win / 2), all.length - win));
|
|
143
|
+
if (selRow - 1 >= 0 && "head" in all[selRow - 1] && top > selRow - 1)
|
|
144
|
+
top = selRow - 1; // keep the group title with its first command
|
|
145
|
+
const rows = all.slice(top, top + win);
|
|
146
|
+
const count = (r) => r.filter((x) => "item" in x).length;
|
|
147
|
+
return { rows, above: count(all.slice(0, top)), below: count(all.slice(top + win)) };
|
|
148
|
+
}
|
package/dist/cli/tui/full.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The full-screen UI: header, scrollable transcript, sidebar, input panel, modals, toasts and mouse handling, drawn on a cell canvas.
|
|
2
2
|
import { copyToClipboard } from "./clipboard.js";
|
|
3
|
+
import { menuRows } from "./complete.js";
|
|
3
4
|
import { basename } from "node:path";
|
|
4
5
|
import { A, mix } from "./ansi.js";
|
|
5
6
|
import { brandOfModel } from "./brand.js";
|
|
@@ -162,6 +163,7 @@ export class FullView {
|
|
|
162
163
|
const live = new Set();
|
|
163
164
|
const blocks = app.blocks.blocks.filter((b) => !(b.kind === "tool" && (b.name === "todo_write" || (b.parent && b.agent && b.agent !== "lead")))); // todos live in the sidebar; a subagent's steps are drawn inside its card
|
|
164
165
|
let prevTool = false;
|
|
166
|
+
const lastTool = [...blocks].reverse().find((b) => b.kind === "tool" && (!b.agent || b.agent === "lead"))?.id; // its output stays open
|
|
165
167
|
for (let i = 0; i < blocks.length; i++) {
|
|
166
168
|
const b = blocks[i];
|
|
167
169
|
live.add(b.id);
|
|
@@ -169,7 +171,7 @@ export class FullView {
|
|
|
169
171
|
if (isQuiet(b))
|
|
170
172
|
while (i + run < blocks.length && isQuiet(blocks[i + run]))
|
|
171
173
|
run++;
|
|
172
|
-
const tctx = { ...ctx, tight: prevTool };
|
|
174
|
+
const tctx = { ...ctx, tight: prevTool, last: b.id === lastTool };
|
|
173
175
|
if (run >= 2) {
|
|
174
176
|
const members = blocks.slice(i, i + run), open = this.expandAll || this.groupOpen.has(b.id);
|
|
175
177
|
const key = `g|${tctx.tight}|${open}|${ctx.theme.name}|${ctx.width}|` + members.map((m) => `${m.id}:${m.status}:${m.ms ?? 0}`).join(",") + (members.some((m) => m.status === "running") ? `|${ctx.frame}` : ""), hit = this.cache.get(b.id);
|
|
@@ -182,7 +184,7 @@ export class FullView {
|
|
|
182
184
|
prevTool = r.rows.length <= 2;
|
|
183
185
|
continue;
|
|
184
186
|
}
|
|
185
|
-
const key = this.sig(b, tctx) + (tctx.tight ? "|t" : ""), hit = this.cache.get(b.id);
|
|
187
|
+
const key = this.sig(b, tctx) + (tctx.tight ? "|t" : "") + (tctx.last ? "|last" : ""), hit = this.cache.get(b.id);
|
|
186
188
|
const r = hit && hit.key === key ? hit.r : (() => { const rr = renderBlock(b, tctx); this.cache.set(b.id, { key, r: rr }); return rr; })();
|
|
187
189
|
this.items.push({ b, r, start: total });
|
|
188
190
|
total += r.rows.length;
|
|
@@ -406,13 +408,19 @@ export class FullView {
|
|
|
406
408
|
const s = this.app.sugg;
|
|
407
409
|
if (!s || !s.items.length || this.app.overlay)
|
|
408
410
|
return;
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
411
|
+
const w = Math.min(86, mainW), room = inputY - 4; // rows between the top of the screen and the box
|
|
412
|
+
if (room < 3)
|
|
411
413
|
return;
|
|
412
|
-
|
|
413
|
-
s.items.
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const { rows, above, below } = menuRows(s.items, s.sel, Math.min(14, room)), y = inputY - rows.length - 2;
|
|
415
|
+
const pos = s.items.length > rows.filter((r) => "item" in r).length ? ` ${s.sel + 1}/${s.items.length}${above ? " ↑" : ""}${below ? " ↓" : ""}` : "";
|
|
416
|
+
c.box(x + 1, y, w, rows.length + 2, { border: t.border, bg: t.surface, style: "round", title: (s.kind === "slash" ? "commands" : s.kind === "arg" ? `/${s.token.cmd} options` : "files") + pos, titleFg: t.dim });
|
|
417
|
+
rows.forEach((r, i) => {
|
|
418
|
+
if ("head" in r) {
|
|
419
|
+
c.ansi(x + 2, y + 1 + i, fit(" " + F(t.faint, "── ") + BOLD(F(t.dim, r.head)) + " " + F(t.faint, "─".repeat(Math.max(2, 24 - r.head.length))), w - 2, t.surface), {}, w - 2);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const idx = r.item, it = s.items[idx], on = idx === s.sel, bg = on ? t.selection : t.surface;
|
|
423
|
+
const label = s.kind === "file" ? (() => { const n = it.label.split("/").pop(), d = it.label.slice(0, it.label.length - n.length); return F(t.faint, d) + BOLD(F(t.text, n)); })() : BOLD(F(t.accent2, it.label.padEnd(16)));
|
|
416
424
|
c.ansi(x + 2, y + 1 + i, fit((on ? F(t.accent, "▌") : " ") + " " + label + (it.detail ? " " + F(t.faint, it.detail) : ""), w - 2, bg), {}, w - 2);
|
|
417
425
|
this.regions.push({ x: x + 2, y: y + 1 + i, w: w - 2, h: 1, kind: "sugg", click: () => { s.sel = idx; this.app.acceptSuggPublic(true); } });
|
|
418
426
|
});
|
|
@@ -588,6 +596,8 @@ export class FullView {
|
|
|
588
596
|
} return false; }
|
|
589
597
|
/** Scroll keys; returns whether the key was used. */
|
|
590
598
|
key(name, ctrl) {
|
|
599
|
+
if (this.app.sugg && (name === "pageup" || name === "pagedown"))
|
|
600
|
+
return false; // the open menu pages through its list instead
|
|
591
601
|
const page = Math.max(3, Math.floor(this.viewH / 2));
|
|
592
602
|
if (name === "pageup") {
|
|
593
603
|
this.scrollBy(page);
|
package/dist/src/anthropic.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Message, Model } from "./harness.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Claude backend via the official Anthropic SDK (an optional
|
|
3
|
+
* Claude backend via the official Anthropic SDK (an optional dependency, installed with the package: `npm i @anthropic-ai/sdk` if it is missing).
|
|
4
4
|
* Defaults follow Anthropic's guidance for agent loops: adaptive thinking, auto prompt caching, streaming,
|
|
5
5
|
* server-side refusal fallbacks. Credentials: `apiKey`, ANTHROPIC_API_KEY, or an `ant auth login` profile.
|
|
6
6
|
*/
|
package/docs/architecture.md
CHANGED
|
@@ -87,7 +87,7 @@ flowchart TB
|
|
|
87
87
|
|
|
88
88
|
**Limits.** The diagrams show the main paths only. Many small files in `src/` (for example `permissions.ts`, `hooks.ts`, `sandbox.ts`) are called by the agent layer and are not drawn.
|
|
89
89
|
|
|
90
|
-
The runtime has no required packages. The Claude adapter
|
|
90
|
+
The runtime has no required packages. The Claude adapter uses the optional dependency `@anthropic-ai/sdk`, which npm installs with Amness.
|
|
91
91
|
|
|
92
92
|
## Find which file does what
|
|
93
93
|
|
package/docs/bench.md
CHANGED
|
@@ -44,16 +44,15 @@ Do these steps in PowerShell.
|
|
|
44
44
|
|
|
45
45
|
2. Install [Git for Windows](https://git-scm.com/download/win). The bench needs `git` on your PATH. The hidden checks of the built-in tasks use bash tools such as `test`. Amness looks for the `bash.exe` that comes with Git (in `Program Files\Git\bin`) and for any `bash.exe` on PATH. It skips `C:\Windows\System32\bash.exe`, because that is the WSL launcher and it sees other files (see `src/shell.ts`).
|
|
46
46
|
|
|
47
|
-
3.
|
|
47
|
+
3. Install Amness from npm:
|
|
48
48
|
|
|
49
49
|
```powershell
|
|
50
|
-
|
|
51
|
-
cd Amness
|
|
52
|
-
npm install
|
|
53
|
-
npm link
|
|
50
|
+
npm install -g amness
|
|
54
51
|
amness --version
|
|
55
52
|
```
|
|
56
53
|
|
|
54
|
+
To bench the latest `main` instead of the release, install from a clone: `git clone https://github.com/amirsdream/Amness.git`, then `cd Amness`, `npm install` and `npm link`.
|
|
55
|
+
|
|
57
56
|
4. Install and log in to the other tools yourself. Follow their own documentation. This page does not repeat install commands, because they change.
|
|
58
57
|
- Claude Code: <https://docs.claude.com/en/docs/claude-code>
|
|
59
58
|
- Codex: <https://github.com/openai/codex>
|
package/docs/development.md
CHANGED
|
@@ -70,7 +70,7 @@ The file `.github/workflows/ci.yml` has these jobs:
|
|
|
70
70
|
| `windows` | `npm run typecheck` and `npm test` on Windows. It is informational (`continue-on-error`) |
|
|
71
71
|
| `action` | runs the GitHub Action (`action.yml`) with no API key and checks that it reports instead of crashing |
|
|
72
72
|
|
|
73
|
-
A second workflow, `.github/workflows/publish.yml`, runs type check and tests, then `npm publish`, when you push a tag that starts with `v`. It needs an `NPM_TOKEN` secret.
|
|
73
|
+
A second workflow, `.github/workflows/publish.yml`, runs type check and tests, then `npm stage publish --provenance`, when you push a tag that starts with `v`. It needs an `NPM_TOKEN` secret (a stage-only token is enough). It only stages the version; you release it with `npm stage approve <id>` and your 2FA code.
|
|
74
74
|
|
|
75
75
|
## Find your way around the project
|
|
76
76
|
|
package/docs/getting-started.md
CHANGED
|
@@ -13,21 +13,19 @@ You need Node.js 22.18 or newer, git, and a terminal. A *diff* is a view of the
|
|
|
13
13
|
|
|
14
14
|
## 1. Install Amness
|
|
15
15
|
|
|
16
|
-
Amness is
|
|
16
|
+
Amness is a package on npm. You install it with one command.
|
|
17
17
|
|
|
18
|
-
1.
|
|
18
|
+
1. Install it. The `-g` flag makes the `amness` command available in every folder.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
21
|
+
npm install -g amness
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
This also installs the Anthropic SDK, which the Claude models use. It is an optional dependency, so Amness still installs if the SDK cannot. If you see "Claude support needs the SDK", run `npm install -g @anthropic-ai/sdk`.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
npm link
|
|
28
|
-
```
|
|
26
|
+
You can also run Amness once without installing it: `npx amness`.
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
2. Check that the command works.
|
|
31
29
|
|
|
32
30
|
```bash
|
|
33
31
|
amness --version
|
|
@@ -35,6 +33,17 @@ Amness is **not on npm yet**. You install it from the source code.
|
|
|
35
33
|
|
|
36
34
|
You see a version number, for example `0.1.0`.
|
|
37
35
|
|
|
36
|
+
To update later, run `npm install -g amness@latest`.
|
|
37
|
+
|
|
38
|
+
**Run the latest `main` from source.** Use this if you want a change that is not released yet, or if you want to work on Amness itself:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/amirsdream/Amness.git && cd Amness && npm install
|
|
42
|
+
npm link
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`npm link` makes the `amness` command point at your clone, so `git pull` updates it.
|
|
46
|
+
|
|
38
47
|
Amness is developed on macOS and Linux. The tests also run on Windows, but Windows has **no OS sandbox** for shell commands.
|
|
39
48
|
|
|
40
49
|
## 2. Set an API key or choose a local model
|
|
@@ -57,7 +66,7 @@ ollama pull qwen2.5-coder:7b
|
|
|
57
66
|
|
|
58
67
|
Start Amness, run `/provider`, and choose "Ollama (local)". You need no key. Small local models can be weak at tool calling. See [Providers and models](providers.md).
|
|
59
68
|
|
|
60
|
-
You can also do nothing now. On the first start, Amness asks you to pick a provider and paste a key. It saves the key in
|
|
69
|
+
You can also do nothing now. On the first start, Amness asks you to pick a provider and paste a key. It saves the key in your system's secure store (the macOS Keychain, the Linux keyring, or Windows DPAPI) and tells you which one. See [Safety](safety.md) for the details and for servers that have no keyring.
|
|
61
70
|
|
|
62
71
|
## 3. Start in a project
|
|
63
72
|
|
package/docs/headless.md
CHANGED
|
@@ -166,7 +166,12 @@ You see one line on stderr for each attempt, for example `attempt 1: failed veri
|
|
|
166
166
|
|
|
167
167
|
## Docker and GitHub Actions
|
|
168
168
|
|
|
169
|
-
- A `Dockerfile` is included: `docker build -t amness .`, then `docker run -it --rm -v "$PWD":/work -e ANTHROPIC_API_KEY amness`.
|
|
169
|
+
- A `Dockerfile` is included: `docker build -t amness .`, then `docker run -it --rm -v "$PWD":/work -e ANTHROPIC_API_KEY amness`. CI builds the image and runs `--version` and a self-test in it (see [Development](development.md)).
|
|
170
|
+
- **Keys in Docker.** Inside a container Amness never saves a key to disk. Pass it in one of two ways:
|
|
171
|
+
- an environment variable: `-e ANTHROPIC_API_KEY` (with no value, Docker copies it from your shell, so it is not typed on the command line);
|
|
172
|
+
- a Docker secret, which is mounted as a file: `-e ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic`. `OPENAI_API_KEY_FILE` and `TAVILY_API_KEY_FILE` work the same way.
|
|
173
|
+
|
|
174
|
+
The image sets `AMNESS_KEYSTORE=none`. Amness also detects `/.dockerenv` and Kubernetes on its own. If you type a key into the chat in a container, it is used for that session only.
|
|
170
175
|
- The repository has a GitHub Action in `action.yml`. Its inputs are `task`, `verify`, `provider`, `model`, `max-tokens` and `comment`. See [Features: Scheduled runs and the GitHub Action](features.md#scheduled-runs-and-the-github-action).
|
|
171
176
|
|
|
172
177
|
## Related pages
|
package/docs/library.md
CHANGED
|
@@ -17,34 +17,25 @@ Everything is exported from `src/index.ts`. The code samples on this page were t
|
|
|
17
17
|
|
|
18
18
|
**Why you would use it.** You get the same guards as the chat (argument checks, verify gate, loop guard, budgets) inside a script, a server or a test.
|
|
19
19
|
|
|
20
|
-
**How to use it.**
|
|
21
|
-
|
|
22
|
-
1. Clone and build:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
git clone https://github.com/amirsdream/Amness.git
|
|
26
|
-
cd Amness
|
|
27
|
-
npm install
|
|
28
|
-
npm run build
|
|
29
|
-
npm pack
|
|
30
|
-
```
|
|
20
|
+
**How to use it.**
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
1. Install the package in your own project:
|
|
33
23
|
|
|
34
24
|
```bash
|
|
35
|
-
|
|
36
|
-
npm install ../Amness/amness-0.1.0.tgz
|
|
25
|
+
npm install amness
|
|
37
26
|
```
|
|
38
27
|
|
|
39
|
-
|
|
28
|
+
2. Import what you need:
|
|
40
29
|
|
|
41
30
|
```ts
|
|
42
|
-
import { createCodeAgent, openai } from "
|
|
31
|
+
import { createCodeAgent, openai } from "amness";
|
|
43
32
|
```
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
To use the latest `main` before it is released, clone the repository, run `npm install && npm run build && npm pack`, and install the tarball it makes with `npm install ../Amness/amness-<version>.tgz`. You can also import the source directly, because Node.js can run `.ts` files: `import { createCodeAgent, openai } from "../Amness/src/index.ts";`.
|
|
35
|
+
|
|
36
|
+
The CI job `package` packs the tarball, installs it in a new folder, and checks that `import("amness")` works.
|
|
46
37
|
|
|
47
|
-
**Limits.** The Claude adapter
|
|
38
|
+
**Limits.** The Claude adapter uses the optional dependency `@anthropic-ai/sdk`, which `npm install amness` adds for you (if it is missing, run `npm install @anthropic-ai/sdk`). The OpenAI adapter uses plain `fetch` and has no dependency.
|
|
48
39
|
|
|
49
40
|
## Run a coding agent
|
|
50
41
|
|