amness 0.1.0 → 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/README.md +32 -7
- package/dist/cli/amness.js +3 -2
- package/dist/cli/config.js +67 -10
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/tui/commands.js +2 -1
- 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 +1 -1
- package/docs/safety.md +14 -1
- package/package.json +4 -10
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/amirsdream/Amness/actions/workflows/ci.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
<p align="center"><img src="docs/images/chat.png" alt="Amness full-screen interface: cards, diff, sidebar, input panel" width="900"></p>
|
|
11
11
|
|
|
@@ -16,12 +16,26 @@
|
|
|
16
16
|
You need **Node 22.18+**, git, and an API key (OpenAI or Anthropic) or a local model server.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
npm link # gives you the `amness` command
|
|
22
|
-
cd ~/your-project && amness # start a conversation in your project
|
|
19
|
+
npm install -g amness # gives you the `amness` command
|
|
20
|
+
cd ~/your-project && amness # first start: pick a provider and paste your key (see below)
|
|
23
21
|
```
|
|
24
22
|
|
|
23
|
+
Or try it without installing: `npx amness`. To run the latest `main` from source (or to work on Amness), see [Run from source](#run-from-source).
|
|
24
|
+
|
|
25
|
+
### Your API key
|
|
26
|
+
|
|
27
|
+
Give Amness the key once, in whichever way suits you:
|
|
28
|
+
|
|
29
|
+
| How | Command or step | Saved? |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| **Paste it on first start** | run `amness`, choose a provider, paste the key (hidden). Change it later in the settings or with `/provider`. | Yes, in your system's secure store: the macOS Keychain, the Linux keyring, or Windows DPAPI. The prompt tells you which. |
|
|
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
|
+
| **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
|
+
| **Docker** | `docker run -it --rm -v "$PWD":/work -e ANTHROPIC_API_KEY amness` | No. Inside a container nothing is saved. |
|
|
35
|
+
| **Library or CI** | pass `apiKey` in code, or set the variable as a CI secret | No. |
|
|
36
|
+
|
|
37
|
+
`OPENAI_API_KEY` is used for every provider except Anthropic, including OpenRouter. 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
|
+
|
|
25
39
|
More ways to start:
|
|
26
40
|
|
|
27
41
|
```bash
|
|
@@ -33,6 +47,17 @@ amness demo # a scripted tour
|
|
|
33
47
|
|
|
34
48
|
New to it? Follow the [step-by-step guide](docs/getting-started.md). Inside the chat: `/help` lists everything · `Shift+Tab` changes how much it may do without asking · `Ctrl+P` opens the command palette · `@file` attaches a file or image · `!cmd` runs a shell command · `#note` saves a memory.
|
|
35
49
|
|
|
50
|
+
## Run from source
|
|
51
|
+
|
|
52
|
+
Use this for the latest `main` (changes not yet released to npm) or to work on Amness itself.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/amirsdream/Amness.git && cd Amness && npm install
|
|
56
|
+
npm link # `amness` now runs your clone; `git pull` updates it
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Update the npm version with `npm install -g amness@latest`. Contributors: see [Development](docs/development.md).
|
|
60
|
+
|
|
36
61
|
## What it looks like
|
|
37
62
|
|
|
38
63
|
<p align="center"><img src="docs/images/approval.png" alt="An edit waiting for your approval, with the diff" width="780"></p>
|
|
@@ -172,7 +197,7 @@ More plugin ideas (finish gates, policies, loop strategies, custom metrics) are
|
|
|
172
197
|
| **Evaluate and improve** | `amness eval` (ablations, SWE-bench-format suites, injection suite, leak audit), trace replay, failure mining, `amness tune` (changes kept only if they hold on unseen tasks). |
|
|
173
198
|
| **Works where you work** | Full-screen and inline terminal UI, headless mode, git worktree sessions with `/review` and `/pr`, scheduled runs, a GitHub Action, an editor server (`amness acp`), image input, session branching. |
|
|
174
199
|
|
|
175
|
-
Models: OpenAI (Responses and Chat), Claude, and any OpenAI-compatible local server. No
|
|
200
|
+
Models: OpenAI (Responses and Chat), Claude, and any OpenAI-compatible local server. No required dependencies (the Claude SDK is an optional one that npm installs for you).
|
|
176
201
|
|
|
177
202
|
## Choose Amness if, choose something else if
|
|
178
203
|
|
|
@@ -184,7 +209,7 @@ Models: OpenAI (Responses and Chat), Claude, and any OpenAI-compatible local ser
|
|
|
184
209
|
|
|
185
210
|
- **Tested:** about 375 automated tests (scripted models, mock servers, real git repositories, a pseudo-terminal), CI on Linux and Windows, and a Docker and npm-package smoke test.
|
|
186
211
|
- **Measured on real models (small):** the guards and the reviewer showed no detectable pass-rate effect on [these tasks and models](docs/evaluation.md#measured-results-so-far); the guards and reviewer cost extra tokens, most visibly on strong models. Every newer feature (routing, `tune`, best-of-n, parallel parts, language-server diagnostics, the approval reviewer) is implemented and unit-tested but **not yet shown to help on real work**. The [roadmap](ROADMAP.md) lists the measurements still to run.
|
|
187
|
-
- **Platforms:** developed on macOS and Linux. Windows runs the test suite but has no OS sandbox.
|
|
212
|
+
- **Platforms:** developed on macOS and Linux. Windows runs the test suite but has no OS sandbox. Installed from npm; the Claude SDK comes with it.
|
|
188
213
|
- Cost figures are estimates. Small local models vary widely in tool-calling quality.
|
|
189
214
|
|
|
190
215
|
## Documentation
|
package/dist/cli/amness.js
CHANGED
|
@@ -16,6 +16,7 @@ import { runSchedule } from "./schedulecli.js";
|
|
|
16
16
|
import { ACP_HELP, runAcp } from "./acpcli.js";
|
|
17
17
|
import { runEval, runTune } from "./evalcli.js";
|
|
18
18
|
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS, home, loadConfig, mask, ready, saveConfig } from "./config.js";
|
|
19
|
+
import { where } from "./keystore.js";
|
|
19
20
|
import { DEMO_TASK, explain, runDemo } from "./demo.js";
|
|
20
21
|
import { formatEvent } from "./format.js";
|
|
21
22
|
import { Cancel, ask, bold, box, clearScreen, confirm, cyan, dim, gray, green, log, pause, red, select, spin, yellow } from "./ui.js";
|
|
@@ -90,7 +91,7 @@ async function ensureReady() {
|
|
|
90
91
|
cfg.model = (await ask("Model", cfg.model)) || cfg.model;
|
|
91
92
|
}
|
|
92
93
|
if (PROVIDERS[id].needsKey)
|
|
93
|
-
cfg.apiKey = (await ask(
|
|
94
|
+
cfg.apiKey = (await ask(`API key (hidden; ${where()})`, "", { secret: true })) || undefined;
|
|
94
95
|
saveConfig(cfg);
|
|
95
96
|
if (!ready(cfg))
|
|
96
97
|
return false;
|
|
@@ -452,7 +453,7 @@ async function settings() {
|
|
|
452
453
|
else if (what === "url")
|
|
453
454
|
cfg.baseUrl = (await ask("Base URL", cfg.baseUrl)) || cfg.baseUrl;
|
|
454
455
|
else if (what === "key")
|
|
455
|
-
cfg.apiKey = (await ask(
|
|
456
|
+
cfg.apiKey = (await ask(`API key (hidden; ${where()})`, cfg.apiKey ?? "", { secret: true })) || undefined;
|
|
456
457
|
else if (what === "tavily")
|
|
457
458
|
cfg.tavilyKey = (await ask("Tavily API key (blank to clear)", "", { secret: true })) || undefined;
|
|
458
459
|
else if (what === "budget")
|
package/dist/cli/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { deleteSecret, getSecret, noStore, setSecret } from "./keystore.js";
|
|
4
5
|
export const PROVIDERS = {
|
|
5
6
|
anthropic: { label: "Anthropic (Claude)", baseUrl: "https://api.anthropic.com", model: "claude-opus-5-5", needsKey: true },
|
|
6
7
|
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", model: "gpt-6.1-sol", needsKey: true },
|
|
@@ -11,30 +12,86 @@ export const PROVIDERS = {
|
|
|
11
12
|
export const home = () => process.env.AMNESS_HOME ?? join(homedir(), ".amness");
|
|
12
13
|
const file = () => join(home(), "config.json");
|
|
13
14
|
export const defaults = () => ({ provider: "openai", baseUrl: PROVIDERS.openai.baseUrl, model: PROVIDERS.openai.model, maxTokens: 300_000 });
|
|
15
|
+
const SECRETS = ["apiKey", "tavilyKey"];
|
|
16
|
+
/** An environment variable, or the file NAME_FILE points to (Docker and Kubernetes secrets are mounted as files, for example /run/secrets/openai). */
|
|
17
|
+
const envSecret = (name) => {
|
|
18
|
+
const v = process.env[name];
|
|
19
|
+
if (v)
|
|
20
|
+
return v;
|
|
21
|
+
const f = process.env[`${name}_FILE`];
|
|
22
|
+
if (!f)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(f, "utf8").trim() || undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
let fromEnv = {};
|
|
14
32
|
export function loadConfig() {
|
|
15
|
-
let c = defaults();
|
|
33
|
+
let c = defaults(), onDisk = {};
|
|
16
34
|
try {
|
|
17
35
|
if (existsSync(file()))
|
|
18
|
-
|
|
36
|
+
onDisk = JSON.parse(readFileSync(file(), "utf8"));
|
|
37
|
+
c = { ...c, ...onDisk };
|
|
19
38
|
}
|
|
20
39
|
catch { /* corrupt config: start fresh */ }
|
|
21
|
-
//
|
|
40
|
+
// keys live in the OS credential store when there is one; a key still in the file (an older version wrote it there) is moved over, and removed from the file only after it reads back
|
|
41
|
+
let migrated = false;
|
|
42
|
+
for (const k of SECRETS) {
|
|
43
|
+
const stored = getSecret(k);
|
|
44
|
+
if (stored) {
|
|
45
|
+
c[k] = stored;
|
|
46
|
+
if (onDisk[k]) {
|
|
47
|
+
delete onDisk[k];
|
|
48
|
+
migrated = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (onDisk[k] && setSecret(k, onDisk[k])) {
|
|
52
|
+
delete onDisk[k];
|
|
53
|
+
migrated = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (migrated)
|
|
57
|
+
writeConfigFile({ ...defaults(), ...onDisk, ...Object.fromEntries(SECRETS.map((k) => [k, undefined])) });
|
|
58
|
+
// environment wins, so CI and one-off runs work without touching the saved key. It is never written back (see saveConfig).
|
|
22
59
|
const claude = c.provider === "anthropic";
|
|
23
|
-
|
|
60
|
+
fromEnv = {};
|
|
61
|
+
const key = envSecret(claude ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
|
|
24
62
|
if (key)
|
|
25
|
-
c.apiKey = key;
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
c.apiKey = fromEnv.apiKey = key;
|
|
64
|
+
const tavily = envSecret("TAVILY_API_KEY");
|
|
65
|
+
if (tavily)
|
|
66
|
+
c.tavilyKey = fromEnv.tavilyKey = tavily;
|
|
28
67
|
if (process.env.OPENAI_BASE_URL && !claude)
|
|
29
68
|
c.baseUrl = process.env.OPENAI_BASE_URL;
|
|
30
69
|
if (process.env.AMNESS_MODEL)
|
|
31
70
|
c.model = process.env.AMNESS_MODEL;
|
|
32
71
|
return c;
|
|
33
72
|
}
|
|
34
|
-
|
|
73
|
+
function writeConfigFile(c) {
|
|
35
74
|
mkdirSync(home(), { recursive: true });
|
|
36
|
-
writeFileSync(file(), JSON.stringify(c, null, 2));
|
|
37
|
-
chmodSync(file(), 0o600); //
|
|
75
|
+
writeFileSync(file(), JSON.stringify(c, null, 2), { mode: 0o600 }); // owner-only from the first byte
|
|
76
|
+
chmodSync(file(), 0o600); // and for a file that already existed
|
|
77
|
+
}
|
|
78
|
+
export function saveConfig(c) {
|
|
79
|
+
const out = { ...c };
|
|
80
|
+
for (const k of SECRETS) {
|
|
81
|
+
const v = c[k];
|
|
82
|
+
if (v && v === fromEnv[k]) {
|
|
83
|
+
delete out[k];
|
|
84
|
+
continue;
|
|
85
|
+
} // this value came from an environment variable: do not store it
|
|
86
|
+
if (!v) {
|
|
87
|
+
deleteSecret(k);
|
|
88
|
+
delete out[k];
|
|
89
|
+
continue;
|
|
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
|
|
93
|
+
}
|
|
94
|
+
writeConfigFile(out);
|
|
38
95
|
}
|
|
39
96
|
export const ready = (c) => !PROVIDERS[c.provider].needsKey || !!c.apiKey;
|
|
40
97
|
export const mask = (k) => (k ? k.slice(0, 3) + "…" + k.slice(-4) : "not set");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Backend = "keychain" | "secret-service" | "dpapi" | "file" | "none";
|
|
2
|
+
export declare function backend(): Backend;
|
|
3
|
+
export declare const where: () => string;
|
|
4
|
+
/** True when keys must never be written to disk (containers). */
|
|
5
|
+
export declare const noStore: () => boolean;
|
|
6
|
+
/** Save a secret. Returns false when the OS store is not available or refused, so the caller can fall back to the file. */
|
|
7
|
+
export declare function setSecret(name: string, value: string): boolean;
|
|
8
|
+
export declare function getSecret(name: string): string | undefined;
|
|
9
|
+
export declare function deleteSecret(name: string): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Where API keys live. macOS: the login Keychain. Linux: the desktop secret service (`secret-tool`). Windows: DPAPI, which encrypts the key so only your Windows user on this machine can decrypt it.
|
|
2
|
+
// If the OS store is missing or refuses (a Linux server or Docker image with no keyring), the key stays in the config file with mode 600, and `where()` says so.
|
|
3
|
+
// The key goes to the OS tool on stdin, never on the command line (a command line is visible to other users in `ps`), and is hex-encoded so no quoting can go wrong.
|
|
4
|
+
// Tests and any run with a custom AMNESS_HOME use the file, so they never touch your real store. AMNESS_KEYSTORE=file|keychain|secret-service|dpapi|none forces one.
|
|
5
|
+
// In a container (Docker, Kubernetes) nothing is saved at all ("none"): pass the key as an environment variable or a secret file (see config.ts), so it is never written into an image or a volume.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
const SERVICE = process.env.AMNESS_KEYSTORE_SERVICE || "amness"; // the override is for tests
|
|
11
|
+
const run = (cmd, args, input, timeout = 10_000, env) => {
|
|
12
|
+
try {
|
|
13
|
+
const r = spawnSync(cmd, args, { input, encoding: "utf8", timeout, env });
|
|
14
|
+
return r.error ? undefined : r;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export function backend() {
|
|
21
|
+
const want = process.env.AMNESS_KEYSTORE;
|
|
22
|
+
if (want === "file" || (!want && process.env.AMNESS_HOME))
|
|
23
|
+
return "file";
|
|
24
|
+
if (want === "keychain" || want === "secret-service" || want === "dpapi" || want === "none")
|
|
25
|
+
return want;
|
|
26
|
+
if (process.platform === "linux" && (existsSync("/.dockerenv") || process.env.KUBERNETES_SERVICE_HOST))
|
|
27
|
+
return "none";
|
|
28
|
+
if (process.platform === "darwin")
|
|
29
|
+
return "keychain";
|
|
30
|
+
if (process.platform === "win32")
|
|
31
|
+
return "dpapi";
|
|
32
|
+
if (process.platform === "linux" && run("secret-tool", ["--version"])?.status !== undefined)
|
|
33
|
+
return "secret-service";
|
|
34
|
+
return "file";
|
|
35
|
+
}
|
|
36
|
+
export const where = () => ({ keychain: "saved in the macOS Keychain", "secret-service": "saved in the system keyring (secret-tool)", dpapi: "saved with Windows encryption (DPAPI, this user only)", file: "saved in ~/.amness/config.json, mode 600, not encrypted", none: "not saved: this is a container, so pass the key as an environment variable or a secret file (OPENAI_API_KEY_FILE / ANTHROPIC_API_KEY_FILE)" })[backend()];
|
|
37
|
+
/** True when keys must never be written to disk (containers). */
|
|
38
|
+
export const noStore = () => backend() === "none";
|
|
39
|
+
// Windows: PowerShell turns the key into an encrypted string that only this Windows user on this machine can decrypt (DPAPI). The string is kept in ~/.amness/secrets/<name>.dpapi.
|
|
40
|
+
const dir = () => join(process.env.AMNESS_HOME ?? join(homedir(), ".amness"), "secrets");
|
|
41
|
+
const blob = (name) => join(dir(), `${name}.dpapi`);
|
|
42
|
+
// A parent PowerShell 7 (pwsh) leaves a PSModulePath that Windows PowerShell 5.1 cannot load its Security module from; dropping it lets 5.1 use its own default.
|
|
43
|
+
const psEnv = () => { const e = { ...process.env }; for (const k of Object.keys(e))
|
|
44
|
+
if (k.toLowerCase() === "psmodulepath")
|
|
45
|
+
delete e[k]; return e; };
|
|
46
|
+
const ps = (script, input) => run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], input, 60_000, psEnv()); // Windows PowerShell can take several seconds to start
|
|
47
|
+
const PS_ENCRYPT = "$p = [Console]::In.ReadToEnd().Trim(); ConvertTo-SecureString $p -AsPlainText -Force | ConvertFrom-SecureString";
|
|
48
|
+
const PS_DECRYPT = "$e = [Console]::In.ReadToEnd().Trim(); $s = ConvertTo-SecureString $e; [Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))";
|
|
49
|
+
const hex = (s) => Buffer.from(s, "utf8").toString("hex");
|
|
50
|
+
const unhex = (s) => Buffer.from(s.trim(), "hex").toString("utf8");
|
|
51
|
+
/** Save a secret. Returns false when the OS store is not available or refused, so the caller can fall back to the file. */
|
|
52
|
+
export function setSecret(name, value) {
|
|
53
|
+
const b = backend();
|
|
54
|
+
if (b === "file" || b === "none")
|
|
55
|
+
return false;
|
|
56
|
+
if (b === "dpapi") {
|
|
57
|
+
const r = ps(PS_ENCRYPT, hex(value));
|
|
58
|
+
if (r?.status !== 0 || !r.stdout.trim())
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
mkdirSync(dir(), { recursive: true });
|
|
62
|
+
writeFileSync(blob(name), r.stdout.trim(), { mode: 0o600 });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return getSecret(name) === value;
|
|
68
|
+
}
|
|
69
|
+
if (b === "keychain") {
|
|
70
|
+
const r = run("security", ["-i"], `add-generic-password -U -s ${SERVICE} -a ${name} -w ${hex(value)}\n`);
|
|
71
|
+
return r?.status === 0 && !/error|fail/i.test(r.stderr ?? "") && getSecret(name) === value;
|
|
72
|
+
}
|
|
73
|
+
const r = run("secret-tool", ["store", `--label=amness ${name}`, "service", SERVICE, "account", name], hex(value));
|
|
74
|
+
return r?.status === 0 && getSecret(name) === value;
|
|
75
|
+
}
|
|
76
|
+
export function getSecret(name) {
|
|
77
|
+
const b = backend();
|
|
78
|
+
if (b === "file" || b === "none")
|
|
79
|
+
return undefined;
|
|
80
|
+
if (b === "dpapi") {
|
|
81
|
+
if (!existsSync(blob(name)))
|
|
82
|
+
return undefined;
|
|
83
|
+
const r = ps(PS_DECRYPT, readFileSync(blob(name), "utf8"));
|
|
84
|
+
const v = r?.status === 0 ? unhex(r.stdout) : "";
|
|
85
|
+
return v || undefined;
|
|
86
|
+
}
|
|
87
|
+
const r = b === "keychain" ? run("security", ["find-generic-password", "-s", SERVICE, "-a", name, "-w"]) : run("secret-tool", ["lookup", "service", SERVICE, "account", name]);
|
|
88
|
+
if (r?.status !== 0 || !r.stdout.trim())
|
|
89
|
+
return undefined;
|
|
90
|
+
const v = unhex(r.stdout);
|
|
91
|
+
return v || undefined;
|
|
92
|
+
}
|
|
93
|
+
export function deleteSecret(name) {
|
|
94
|
+
const b = backend();
|
|
95
|
+
if (b === "file" || b === "none")
|
|
96
|
+
return;
|
|
97
|
+
if (b === "dpapi") {
|
|
98
|
+
try {
|
|
99
|
+
rmSync(blob(name), { force: true });
|
|
100
|
+
}
|
|
101
|
+
catch { /* already gone */ }
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (b === "keychain")
|
|
105
|
+
run("security", ["delete-generic-password", "-s", SERVICE, "-a", name]);
|
|
106
|
+
else
|
|
107
|
+
run("secret-tool", ["clear", "service", SERVICE, "account", name]);
|
|
108
|
+
}
|
package/dist/cli/tui/commands.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SHELL_CHOICES, STRATEGIES, chooseShell, currentShell, loadPrompts, setS
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { collectFiles, effortsFor, loadMcp, readBundle, writeBundle } from "../../src/index.js";
|
|
6
6
|
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS } from "../config.js";
|
|
7
|
+
import { where } from "../keystore.js";
|
|
7
8
|
import { bold, cyan, dim, green, red, yellow } from "../ui.js";
|
|
8
9
|
import { estimateCost, fmtCost, fmtDuration, fmtTokens } from "./pricing.js";
|
|
9
10
|
import { codeBlocks } from "./clipboard.js";
|
|
@@ -48,7 +49,7 @@ export function buildCommands() {
|
|
|
48
49
|
const key = id === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
|
|
49
50
|
let apiKey = key;
|
|
50
51
|
if (PROVIDERS[id].needsKey && !apiKey) {
|
|
51
|
-
apiKey = await a.askText(`${PROVIDERS[id].label} API key (hidden;
|
|
52
|
+
apiKey = await a.askText(`${PROVIDERS[id].label} API key (hidden; ${where()})`, { secret: true });
|
|
52
53
|
if (!apiKey)
|
|
53
54
|
return;
|
|
54
55
|
}
|
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
|
|
package/docs/providers.md
CHANGED
|
@@ -64,7 +64,7 @@ The model list in `/model` (with prices per million tokens as shown in the picke
|
|
|
64
64
|
|
|
65
65
|
**Limits and common problems.**
|
|
66
66
|
|
|
67
|
-
- Amness saves a key you type in `~/.amness/config.json` with file mode 600
|
|
67
|
+
- Amness saves a key you type in your system's secure store: the macOS Keychain, the Linux keyring (`secret-tool`) or Windows DPAPI. In Docker or Kubernetes it saves nothing: pass the key as an environment variable or a `*_FILE` secret. On a Linux server with no keyring it uses `~/.amness/config.json` with file mode 600, which is not encrypted. See [Safety](safety.md).
|
|
68
68
|
- The environment variable wins over the saved file. `ANTHROPIC_API_KEY` is used when the provider is `anthropic`. `OPENAI_API_KEY` is used for **every other provider**, including `openrouter`. `OPENROUTER_API_KEY` is not read as a key.
|
|
69
69
|
- If the chat says "check your key with /provider", the provider answered with an authentication error (401 or 403).
|
|
70
70
|
- The test call checks that the model replies. It does not check that the model can call tools.
|
package/docs/safety.md
CHANGED
|
@@ -35,7 +35,20 @@ Notes about the provider:
|
|
|
35
35
|
|
|
36
36
|
## Know where your keys are stored
|
|
37
37
|
|
|
38
|
-
- Keys come from the environment variables `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`, or from
|
|
38
|
+
- Keys come from the environment variables `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`, or from the key store. A key from an environment variable is used for that run and is never saved.
|
|
39
|
+
- The key store depends on your system. The prompt tells you which one is in use.
|
|
40
|
+
|
|
41
|
+
| System | Where the key is saved | Protection |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| macOS | the login Keychain (service `amness`) | encrypted by macOS |
|
|
44
|
+
| Linux with a desktop keyring | the secret service, through `secret-tool` | encrypted by the keyring |
|
|
45
|
+
| Windows | `~/.amness/secrets/apiKey.dpapi` | encrypted with DPAPI: only your Windows user on this machine can decrypt it |
|
|
46
|
+
| Docker or Kubernetes | nowhere: kept in memory for that run | nothing is written to the image or a volume. Pass the key as an environment variable or a secret file (`ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic`). |
|
|
47
|
+
| Linux server with no keyring | `~/.amness/config.json` with mode 600 | **not encrypted**, only your user can read it. Prefer an environment variable or your platform's secret manager. |
|
|
48
|
+
|
|
49
|
+
- Amness passes the key to the system tool on standard input, never on the command line, where other users could see it in the process list.
|
|
50
|
+
- A key that an older version saved in `config.json` is moved to the key store the next time you start Amness, and removed from the file once the move is checked.
|
|
51
|
+
- To remove a saved key, open the settings and clear "API key", or on macOS run `security delete-generic-password -s amness -a apiKey`.
|
|
39
52
|
- The environment variable wins over the saved key.
|
|
40
53
|
- Amness does not write keys into a project folder.
|
|
41
54
|
- Commands the agent runs do not get your Amness keys. The variables `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `TAVILY_API_KEY`, `OPENROUTER_API_KEY` and `AMNESS_OTLP` are removed from their environment (`childEnv` in `src/secrets.ts`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A terminal coding agent for any model: streaming chat TUI, permission modes, plan mode, undo, memory, MCP, with a small reliability harness underneath. OpenAI (Responses API), Claude, and local models.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,19 +52,13 @@
|
|
|
52
52
|
"test:pty": "python3 test/pty/tui.py",
|
|
53
53
|
"prepack": "npm run build"
|
|
54
54
|
},
|
|
55
|
+
"optionalDependencies": {
|
|
56
|
+
"@anthropic-ai/sdk": "^1.3.0"
|
|
57
|
+
},
|
|
55
58
|
"devDependencies": {
|
|
56
|
-
"@anthropic-ai/sdk": "^1.3.0",
|
|
57
59
|
"@types/node": "^22.0.0",
|
|
58
60
|
"typescript": "^5.8.0"
|
|
59
61
|
},
|
|
60
|
-
"peerDependencies": {
|
|
61
|
-
"@anthropic-ai/sdk": "^1.3.0"
|
|
62
|
-
},
|
|
63
|
-
"peerDependenciesMeta": {
|
|
64
|
-
"@anthropic-ai/sdk": {
|
|
65
|
-
"optional": true
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
62
|
"files": [
|
|
69
63
|
"dist",
|
|
70
64
|
"docs",
|