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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI](https://github.com/amirsdream/Amness/actions/workflows/ci.yml/badge.svg)](https://github.com/amirsdream/Amness/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  ![Node](https://img.shields.io/badge/node-%E2%89%A5%2022.18-339933)
8
- ![Runtime dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)
8
+ ![Runtime dependencies](https://img.shields.io/badge/required%20dependencies-0-brightgreen)
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
- git clone https://github.com/amirsdream/Amness.git && cd Amness && npm install
20
- export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY=sk-ant-...
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 runtime dependencies.
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. Not on npm yet.
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
@@ -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("API key (hidden; saved to ~/.amness/config.json, mode 600)", "", { secret: true })) || undefined;
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("API key (hidden; stored in ~/.amness/config.json, mode 600)", cfg.apiKey ?? "", { secret: true })) || undefined;
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")
@@ -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
- c = { ...c, ...JSON.parse(readFileSync(file(), "utf8")) };
36
+ onDisk = JSON.parse(readFileSync(file(), "utf8"));
37
+ c = { ...c, ...onDisk };
19
38
  }
20
39
  catch { /* corrupt config: start fresh */ }
21
- // environment wins, so CI and one-off runs work without touching the saved file
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
- const key = claude ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
60
+ fromEnv = {};
61
+ const key = envSecret(claude ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
24
62
  if (key)
25
- c.apiKey = key;
26
- if (process.env.TAVILY_API_KEY)
27
- c.tavilyKey = process.env.TAVILY_API_KEY;
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
- export function saveConfig(c) {
73
+ function writeConfigFile(c) {
35
74
  mkdirSync(home(), { recursive: true });
36
- writeFileSync(file(), JSON.stringify(c, null, 2));
37
- chmodSync(file(), 0o600); // may hold API keys
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
+ }
@@ -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; stored in ~/.amness/config.json, mode 600)`, { secret: true });
52
+ apiKey = await a.askText(`${PROVIDERS[id].label} API key (hidden; ${where()})`, { secret: true });
52
53
  if (!apiKey)
53
54
  return;
54
55
  }
@@ -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 peer dependency: `npm i @anthropic-ai/sdk`).
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
  */
@@ -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 needs the optional package `@anthropic-ai/sdk`.
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. Download Amness and install its packages:
47
+ 3. Install Amness from npm:
48
48
 
49
49
  ```powershell
50
- git clone https://github.com/amirsdream/Amness.git
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>
@@ -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. Whether the package is published on npm is not verified here.
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
 
@@ -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 **not on npm yet**. You install it from the source code.
16
+ Amness is a package on npm. You install it with one command.
17
17
 
18
- 1. Download the code and install its packages.
18
+ 1. Install it. The `-g` flag makes the `amness` command available in every folder.
19
19
 
20
20
  ```bash
21
- git clone https://github.com/amirsdream/Amness.git && cd Amness && npm install
21
+ npm install -g amness
22
22
  ```
23
23
 
24
- 2. Create the `amness` command.
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
- ```bash
27
- npm link
28
- ```
26
+ You can also run Amness once without installing it: `npx amness`.
29
27
 
30
- 3. Check that the command works.
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 `~/.amness/config.json`, where only you can read it.
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`. The old version of this page said it has not been built or tested. The README lists a Docker smoke test. **Not verified:** which of the two is right.
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.** The README says Amness is not on npm yet, and this page could not confirm a release. Use a clone.
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
- 2. Install the tarball in your own project:
22
+ 1. Install the package in your own project:
33
23
 
34
24
  ```bash
35
- cd ../my-project
36
- npm install ../Amness/amness-0.1.0.tgz
25
+ npm install amness
37
26
  ```
38
27
 
39
- 3. Or skip the build and import the source from the clone. Node.js can run `.ts` files:
28
+ 2. Import what you need:
40
29
 
41
30
  ```ts
42
- import { createCodeAgent, openai } from "../Amness/src/index.ts";
31
+ import { createCodeAgent, openai } from "amness";
43
32
  ```
44
33
 
45
- The CI job `package` does steps 1 and 2 and checks that `import("amness")` works.
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 needs the optional package `@anthropic-ai/sdk` (`npm install @anthropic-ai/sdk`). The OpenAI adapter uses plain `fetch` and has no dependency.
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 (only you can read it).
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 `~/.amness/config.json`. Amness writes that file with mode 600. Only your user can read it.
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.0",
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",