@phantomcodes/cli 1.2.1 → 1.2.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 CHANGED
@@ -19,7 +19,6 @@ phantom setup --agent claude
19
19
  ```
20
20
 
21
21
  The skill covers workspace setup, funding, spending controls and model routing.
22
- [Skill docs](docs/guides/agent-skill.md)
23
22
 
24
23
  ## Let your agent set it up
25
24
 
@@ -65,7 +64,7 @@ key. Restart your agent, or open a new terminal, to use the connection.
65
64
 
66
65
  Codex, Claude Code, opencode, Continue, Goose, Aider, GitHub Copilot CLI and Cline
67
66
  support saved setup. Cursor gets instructions for its settings screen; pi gets
68
- the agent skill. [Agent-specific setup](docs/guides/agent-setup.md).
67
+ the agent skill.
69
68
 
70
69
  Preview without changing files with `phantom setup --dry-run`. The Phantom CLI keeps backups;
71
70
  undo the changes with `phantom setup --restore`.
@@ -83,7 +82,6 @@ phantom codex
83
82
  Both setup options require **Node 24+** and your coding agent installed. Connect, fund your
84
83
  workspace, then launch **Codex** (`phantom codex`), **Claude Code** (`phantom claude`),
85
84
  **pi** (`phantom pi`), or **any other compatible agent**.
86
- [Set up other agents](docs/guides/agent-setup.md).
87
85
 
88
86
  Login opens a device approval page and saves a key privately. Funding opens
89
87
  checkout; check your workspace balance after payment. Agent runs use temporary
@@ -103,8 +101,6 @@ phantom fund --amount 10 --coin usdc
103
101
 
104
102
  Credit is available after the payment is verified. Check balance anytime with `phantom balance`.
105
103
 
106
- [Funding and payment details](docs/guides/buying-credit.md).
107
-
108
104
  ## Give each agent a budget
109
105
 
110
106
  Create a key with its own cap, sharing your workspace balance:
@@ -132,7 +128,6 @@ PHANTOM_KEY_NAME=coding phantom route test --table
132
128
 
133
129
  Prefer a specific model? Select it in your agent. Policies that apply only to
134
130
  `auto` leave explicit model choices alone.
135
- [Routing](docs/guides/routing.md) covers conditions, fallback and conversation affinity.
136
131
 
137
132
  `phantom-1` picks a tier for each request: economy, standard or frontier. Your key's
138
133
  lineup picks the model in each tier, and a fallback model to try when it fails:
@@ -199,13 +194,7 @@ Exit codes: 0 success, 1 error, 2 rejected key; agent runs return the agent's ex
199
194
  <details>
200
195
  <summary><strong>Advanced tools, integrations and legacy commands</strong></summary>
201
196
 
202
- `phantom --help` shows the main workflows. `phantom --help --all` and the
203
- [interface reference](docs/reference.md) cover every command.
204
-
205
- - [Signed receipts](docs/guides/receipts.md): check which model answered with `phantom verify`.
206
- - [Funding credit](docs/guides/buying-credit.md): `phantom fund` and checking payments.
207
- - [Saved secrets](docs/guides/saved-secrets.md): macOS Keychain or private files; guest recovery uses `phantom workspace export` and `phantom workspace import`.
208
- - [Key selection](docs/guides/which-key.md) and [API address](docs/guides/base-url.md): environment overrides and shell integration.
197
+ `phantom --help` shows the main workflows. `phantom --help --all` covers every command.
209
198
 
210
199
  The older `phantom key` commands still work.
211
200
  `phantom keys list` reads the workspace; `phantom keys saved` lists local copies.
@@ -221,11 +210,9 @@ npm ci
221
210
  npm run typecheck
222
211
  npm run build
223
212
  npm run coverage
224
- npm run docs
225
- node scripts/gen-docs.mjs --check
226
213
  ```
227
214
 
228
- See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
215
+ Report a security problem privately to support@phantom.codes, not in a public issue.
229
216
 
230
217
  </details>
231
218
 
@@ -26,7 +26,7 @@ export const AGENTS = {
26
26
  const vars = { ANTHROPIC_BASE_URL: proxy, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1', ENABLE_TOOL_SEARCH: 'true' };
27
27
  const { ANTHROPIC_AUTH_TOKEN: _auth, ...rest } = env;
28
28
  return {
29
- args: ['--settings', JSON.stringify({ apiKeyHelper: 'printenv PHANTOM_AGENT_TOKEN', env: vars }), ...args],
29
+ args: ['--settings', JSON.stringify({ apiKeyHelper: 'node -p "process.env.PHANTOM_AGENT_TOKEN || \'\'"', env: vars }), ...args],
30
30
  env: { ...rest, ANTHROPIC_BASE_URL: proxy, ANTHROPIC_API_KEY: token, PHANTOM_AGENT_TOKEN: token },
31
31
  };
32
32
  },
@@ -138,6 +138,7 @@ export const COMMANDS = [
138
138
  [--yes] don't ask
139
139
  [--no-browser] print the sign-in link instead of opening it
140
140
  [--dry-run] show the changes, write nothing
141
+ [--json] print JSON in a terminal too (a pipe always gets JSON)
141
142
  [--model <id>] the model each agent starts on (default ${SETUP_MODEL})
142
143
  setup --agent <name> install the skill for one agent, without asking:
143
144
  ${orList(SETUP_NAMES)}
package/dist/cli/help.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  import { DEFAULT_BASE_URL } from "../core/request.mjs";
2
2
  import { COMMANDS } from "./commands.mjs";
3
3
  /**
4
- * Every environment variable phantom reads. The Environment sections of HELP and
5
- * docs/reference.md are built from this list, and `npm run docs` fails if the
6
- * code reads a variable that is not here.
4
+ * Every environment variable phantom reads. The Environment section of HELP is
5
+ * built from this list.
7
6
  */
8
7
  export const ENV_VARS = [
9
8
  { name: 'PHANTOM_API_KEY', about: 'your Phantom AI API key (or save one with login)', secret: true },
package/dist/cli/run.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { VERSION } from "../core/version.mjs";
2
+ import { nodeTrustWarning } from "../core/node-trust.mjs";
2
3
  import { die } from "../core/errors.mjs";
3
4
  import { handleError, parseFlags } from "../core/helpers.mjs";
4
5
  import { resolveApiKey } from "../keys/saved.mjs";
@@ -11,13 +12,20 @@ import { LEGACY_SETUP_HINT, legacySetupFound } from "../setup/legacy.mjs";
11
12
  * fails. Errors go to `io.stderr` as JSON. It never calls `process.exit`.
12
13
  * @param argv The arguments after `phantom`.
13
14
  * @param env The environment to read settings from.
14
- * @param io Where output goes, and whether stderr is a terminal (for progress lines).
15
+ * @param io Where output goes, whether stderr is a terminal (for progress lines),
16
+ * and whether stdout is one (setup prints a summary there instead of JSON).
17
+ * @param node The Node version it runs on; one too old to reject forged
18
+ * eMudhra certificates gets a warning on stderr first.
15
19
  */
16
20
  export async function run(argv = process.argv.slice(2), env = process.env, io = {
17
21
  stdout: (s) => process.stdout.write(s),
18
22
  stderr: (s) => process.stderr.write(s),
19
23
  isTTY: Boolean(process.stderr.isTTY),
20
- }) {
24
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
25
+ }, node = process.versions.node) {
26
+ const unsafe = nodeTrustWarning(node);
27
+ if (unsafe)
28
+ io.stderr(unsafe);
21
29
  if (argv[0] === '--version' || argv[0] === '-v') {
22
30
  io.stdout(VERSION + '\n');
23
31
  return 0;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Node before 24.22.1 still trusts certificates chained to the eMudhra CA
3
+ * keys found guessable in October 2026, so on a network someone controls, a
4
+ * forged phantom.codes could read the key this CLI sends. 24.22.1 rejects
5
+ * those chains ("tls: reject chains containing blocked public keys").
6
+ */
7
+ /** The first Node 24 that rejects the compromised chains. */
8
+ const FIXED = [24, 22, 1];
9
+ /**
10
+ * A warning for stderr when `version` (`process.versions.node`) is a Node 24
11
+ * older than 24.22.1, or null. Other majors get none: the CLI needs 24 or newer,
12
+ * and the newer lines carry the fix.
13
+ */
14
+ export function nodeTrustWarning(version) {
15
+ const [major, minor, patch] = version.split('.').map(Number);
16
+ if (major !== FIXED[0])
17
+ return null;
18
+ if (minor > FIXED[1] || (minor === FIXED[1] && patch >= FIXED[2]))
19
+ return null;
20
+ return (`phantom: Node ${version} still trusts certificates from eMudhra's compromised CA keys, ` +
21
+ `so on an untrusted network a forged phantom.codes could read your key. ` +
22
+ `Update to Node ${FIXED.join('.')} or newer (nvm install 24), or reinstall with ` +
23
+ `curl -fsSL https://phantom.codes/install | sh, which brings its own.\n`);
24
+ }
@@ -1,2 +1,2 @@
1
1
  /** This release of phantom. `npm version` keeps it equal to the version in package.json. */
2
- export const VERSION = '1.2.1';
2
+ export const VERSION = '1.2.3';
@@ -114,6 +114,13 @@ export function applySetup(env, plan) {
114
114
  from = `${f.file}.phantom.bak`;
115
115
  writeFileSync(from, f.before, { mode: 0o600 });
116
116
  }
117
+ if (f.before !== null && (f.agent === 'claude' || f.file.endsWith('settings.json'))) {
118
+ const standardBak = `${f.file}.bak`;
119
+ if (!existsSync(standardBak)) {
120
+ const content = from && existsSync(from) ? readFileSync(from, 'utf-8') : f.before;
121
+ writeFileSync(standardBak, content, { mode: 0o600 });
122
+ }
123
+ }
117
124
  if (from)
118
125
  backup = from;
119
126
  if (f.after === null && from === null)
@@ -43,6 +43,10 @@ export function tableSetup(d) {
43
43
  lines.push(n);
44
44
  if (r.applied === false)
45
45
  lines.push('Nothing was changed.');
46
+ // Agents that read the key from the shell only see it in a terminal opened after setup.
47
+ else if (!r.dry_run && (r.files ?? []).some((f) => f.agent === 'shell')) {
48
+ lines.push('', 'Open a new terminal before you start your agents, so they can read your Phantom AI key.');
49
+ }
46
50
  return lines.join('\n');
47
51
  }
48
52
  /** `--table` output for `setup --restore`. */
@@ -56,7 +60,9 @@ export function tableRestore(d) {
56
60
  }
57
61
  /** `phantom setup`: plans, previews, asks, applies or restores agent setup. Plain setup with no key yet signs in first when it runs in a terminal. Run by `run()`. */
58
62
  export async function setupCommand(input) {
59
- const { env, io, flags, table } = input;
63
+ const { env, io, flags } = input;
64
+ // A person at a terminal gets the short summary; a pipe, or --json, gets JSON.
65
+ const table = input.table || (Boolean(io.stdoutIsTTY) && !flags['json']);
60
66
  const agent = flags['agent'];
61
67
  if (agent === true)
62
68
  die(`--agent requires ${SETUP_NAMES.join(', ')} or all`);
@@ -80,7 +86,7 @@ export async function setupCommand(input) {
80
86
  // With no key yet, sign in first, as phantom claude does, so the agents
81
87
  // get one in this run. Its words go to stderr; stdout stays setup's result.
82
88
  if (guided && !dryRun && io.isTTY && !resolveApiKey(env)) {
83
- await workspaceLogin({ ...input, io: { ...io, stdout: io.stderr }, flags: flags['no-browser'] ? { 'no-browser': true } : {} });
89
+ await workspaceLogin({ ...input, io: { ...io, stdout: io.stderr }, flags: flags['no-browser'] ? { 'no-browser': true } : {} }, { inSetup: true });
84
90
  }
85
91
  const plan = planSetup(env, {
86
92
  agent,
@@ -7,7 +7,7 @@ import { stateDir } from "../core/state.mjs";
7
7
  import { writeSecret, readSecret, removeSecret, keychainOn } from "../core/secrets.mjs";
8
8
  import { keyCommand } from "../keys/command.mjs";
9
9
  import { die } from "../core/errors.mjs";
10
- import { openUrl } from "../core/helpers.mjs";
10
+ import { openUrl, flagNum } from "../core/helpers.mjs";
11
11
  import { keyPath, namedKeyPath, saveApiKey, saveNamedKey } from "../keys/saved.mjs";
12
12
  const credentialPath = (env) => path.join(stateDir(env), 'workspace');
13
13
  const base = (input) => httpsOnly(input.baseUrl || input.env.PHANTOM_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, '');
@@ -36,8 +36,12 @@ export async function workspaceGet(input, path) {
36
36
  const saved = credentials(input);
37
37
  return wire(saved.base_url, 'GET', '/workspaces/' + encodeURIComponent(saved.workspace_id) + path, undefined, saved.management_credential);
38
38
  }
39
- /** Device authorization for accounts or guests, saving keys locally without copying a secret through the browser. */
40
- export async function workspaceLogin(input) {
39
+ /**
40
+ * Device authorization for accounts or guests, saving keys locally without copying a secret through the browser.
41
+ * With `inSetup`, `phantom setup` carries on once it is signed in, so it says that instead of printing the
42
+ * saved key's details and the next commands.
43
+ */
44
+ export async function workspaceLogin(input, opts = {}) {
41
45
  const { env, io, flags } = input;
42
46
  if (flags.save === true)
43
47
  die('--save requires a key name.');
@@ -49,7 +53,9 @@ export async function workspaceLogin(input) {
49
53
  const start = await wire(url, 'POST', '/auth/device', { client_name: `phantom on ${hostname()}` });
50
54
  if (new URL(start.verification_uri_complete).origin !== new URL(url).origin)
51
55
  die('The approval URL belongs to another site.', 'device_origin');
52
- io.stderr(`Open ${start.verification_uri_complete}\nConfirm code ${start.user_code}\n`);
56
+ io.stderr(`Sign in at ${start.verification_uri_complete}\n` +
57
+ `Check that the page shows code ${start.user_code}, then approve.\n` +
58
+ 'Waiting for you to approve...\n');
53
59
  if (!flags['no-browser'])
54
60
  openUrl(start.verification_uri_complete);
55
61
  let interval = Math.max(1, start.interval) * 1000;
@@ -82,6 +88,10 @@ export async function workspaceLogin(input) {
82
88
  io.stderr('phantom could not save the connection. Sign in or restore your workspace to revoke the device key and reconnect.\n');
83
89
  throw error;
84
90
  }
91
+ if (opts.inSetup) {
92
+ io.stderr('Signed in. Setting up your agents next.\n\n');
93
+ return 0;
94
+ }
85
95
  io.stdout(JSON.stringify({ saved_as: name, key_name: token.key_name, workspace_id: token.workspace_id, storage: keychainOn(env) ? 'macOS Keychain' : 'private files' }) + '\n');
86
96
  io.stderr('Ready. Run phantom claude, phantom codex or phantom pi. Guests can export recovery with phantom workspace export --file <path>.\n');
87
97
  return 0;
@@ -184,7 +194,12 @@ export async function workspaceCommand(input) {
184
194
  die('--pay has been removed. Use --card or --coin.');
185
195
  if (!flags.card && !flags.coin)
186
196
  die('fund requires --card or --coin.');
187
- const result = await wire(saved.base_url, 'POST', endpoint + '/fund', { method: flags.card ? 'card' : 'crypto', amount_usd: Number(flags.amount), coin: flags.coin ?? 'usdc' }, saved.management_credential);
197
+ const amount = flagNum(flags, 'amount');
198
+ if (amount === undefined)
199
+ die('fund requires --amount <USD>.');
200
+ if (amount <= 0)
201
+ die('--amount must be greater than 0.');
202
+ const result = await wire(saved.base_url, 'POST', endpoint + '/fund', { method: flags.card ? 'card' : 'crypto', amount_usd: amount, coin: flags.coin ?? 'usdc' }, saved.management_credential);
188
203
  io.stdout(JSON.stringify(result) + '\n');
189
204
  if (result.checkout_url && !flags['no-browser'])
190
205
  openUrl(result.checkout_url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantomcodes/cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "AI inference for your coding agents. Route models, control spending, and fund your workspace.",
5
5
  "keywords": [
6
6
  "phantom",
@@ -50,7 +50,6 @@
50
50
  "build": "tsc -p .",
51
51
  "coverage": "vitest run --coverage",
52
52
  "dev": "node src/phantom.mts",
53
- "docs": "node scripts/gen-docs.mjs",
54
53
  "prepare": "npm run build",
55
54
  "test": "vitest run",
56
55
  "typecheck": "tsc -p . --noEmit",
@@ -69,7 +68,6 @@
69
68
  "devDependencies": {
70
69
  "@types/node": "^24.19.1",
71
70
  "@vitest/coverage-v8": "^5.0.3",
72
- "typedoc": "^0.28.20",
73
71
  "typescript": "^6.0.3",
74
72
  "vitest": "^5.0.2"
75
73
  },