@phantomcodes/cli 1.2.3 → 1.2.5

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
@@ -62,8 +62,8 @@ The Phantom CLI detects supported agents, previews the configuration changes, th
62
62
  applying them. It connects their model calls to Phantom AI and wires up the saved
63
63
  key. Restart your agent, or open a new terminal, to use the connection.
64
64
 
65
- Codex, Claude Code, opencode, Continue, Goose, Aider, GitHub Copilot CLI and Cline
66
- support saved setup. Cursor gets instructions for its settings screen; pi gets
65
+ Codex, Claude Code, opencode, Continue, Goose, Aider, GitHub Copilot CLI, Cline
66
+ and Gemini CLI support saved setup. Cursor gets instructions for its settings screen; pi gets
67
67
  the agent skill.
68
68
 
69
69
  Preview without changing files with `phantom setup --dry-run`. The Phantom CLI keeps backups;
@@ -74,14 +74,16 @@ undo the changes with `phantom setup --restore`.
74
74
  ```bash
75
75
  npm i -g @phantomcodes/cli
76
76
  phantom login
77
- phantom workspace fund --amount 10 --card
77
+ phantom fund --amount 10 --card
78
78
  phantom workspace status
79
79
  phantom codex
80
80
  ```
81
81
 
82
- Both setup options require **Node 24+** and your coding agent installed. Connect, fund your
83
- workspace, then launch **Codex** (`phantom codex`), **Claude Code** (`phantom claude`),
84
- **pi** (`phantom pi`), or **any other compatible agent**.
82
+ Both setup options require **Node 24+** and your coding agent installed. Connect, then
83
+ launch **Codex** (`phantom codex`), **Claude Code** (`phantom claude`), **pi**
84
+ (`phantom pi`), or **any other compatible agent**. Each runs phantom-1 unless you
85
+ pick a model (`--model` after `--`), and phantom-1 runs on free models until you add
86
+ credit. Free calls leave out web search, which costs money on any model.
85
87
 
86
88
  Login opens a device approval page and saves a key privately. Funding opens
87
89
  checkout; check your workspace balance after payment. Agent runs use temporary
@@ -175,7 +177,7 @@ it cancels. A named model, not `phantom-1`, still spends credit.
175
177
  | Connect | `phantom login`, `phantom logout` |
176
178
  | Connect existing agents | `phantom setup`, `phantom setup --dry-run`, `phantom setup --restore` |
177
179
  | Run inference | `phantom claude`, `phantom codex`, `phantom pi` |
178
- | Fund and check spending | `phantom workspace fund`, `phantom workspace status`, `phantom balance`, `phantom spend` |
180
+ | Fund and check spending | `phantom fund`, `phantom balance`, `phantom spend`, `phantom workspace status` |
179
181
  | Manage workspace keys | `phantom keys create`, `phantom keys set`, `phantom keys rotate`, `phantom keys revoke` |
180
182
  | Manage local copies | `phantom keys saved`, `phantom keys save`, `phantom keys rm` |
181
183
  | Route models | `phantom route` |
@@ -187,9 +189,14 @@ Put phantom flags before agent arguments; use `--` to separate them:
187
189
  phantom codex --key coding -- exec "explain this repository"
188
190
  ```
189
191
 
190
- Commands print JSON; `--table` renders supported results for people. Agent
191
- commands inherit the terminal and print a spending summary on stderr.
192
- Exit codes: 0 success, 1 error, 2 rejected key; agent runs return the agent's exit code.
192
+ In a terminal, commands draw a screen for people, and choices are pickers
193
+ (arrow keys, Enter, Esc to go back). Piped, as scripts and agents run them,
194
+ they print JSON on stdout and errors on stderr; `--json` forces JSON in a
195
+ terminal, and `--table` the screen in a pipe. Unknown flags are refused.
196
+ Agent commands inherit the terminal and print a spending summary on stderr.
197
+ Exit codes: 0 success (and Esc), 1 error, 2 rejected key, 130 Ctrl+C;
198
+ `phantom setup` returns 1 when it could only install the skills (no key yet,
199
+ `"complete": false`). Agent runs return the agent's exit code.
193
200
 
194
201
  <details>
195
202
  <summary><strong>Advanced tools, integrations and legacy commands</strong></summary>
@@ -2,11 +2,14 @@ import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
5
- import { die } from "../core/errors.mjs";
5
+ import { die, NO_KEY } from "../core/errors.mjs";
6
6
  import { DEFAULT_BASE_URL, httpsOnly, request } from "../core/request.mjs";
7
7
  import { keyId, readNamedKey, resolveApiKey } from "../keys/saved.mjs";
8
8
  import { workspaceLogin } from "../workspace/command.mjs";
9
- const DEFAULT_MODEL = 'auto';
9
+ /** The model an agent runs when the user names none: phantom-1, which runs free on a key with no credit. */
10
+ const DEFAULT_MODEL = 'phantom-1';
11
+ /** phantom-1 as Claude Code names it: its picker keeps only ids that say "claude", so Phantom lists them as `claude-code/<id>`. */
12
+ const CLAUDE_DEFAULT_MODEL = `claude-code/${DEFAULT_MODEL}`;
10
13
  /** The key's spend and balance, or null when the answer has no numbers, such as a sign-in page that came back with status 200. */
11
14
  async function keyStatus(apiKey, baseUrl) {
12
15
  const s = await request('GET', '/key/balance', apiKey, undefined, baseUrl);
@@ -23,11 +26,16 @@ export const AGENTS = {
23
26
  // --settings outranks settings.json, so an env block or apiKeyHelper
24
27
  // `phantom setup` wrote can't send Claude Code past the proxy. The helper
25
28
  // reads the token from the environment, so it stays off the command line.
29
+ // It runs with this phantom's own Node: the curl installer puts none on PATH.
30
+ const helper = `${JSON.stringify(process.execPath)} -p "process.env.PHANTOM_AGENT_TOKEN || ''"`;
31
+ // phantom-1 unless the user picks a model. It goes in the environment, which
32
+ // Claude Code ranks below --model and below a model in its settings file.
33
+ const model = hasModel(args) || env.ANTHROPIC_MODEL ? {} : { ANTHROPIC_MODEL: CLAUDE_DEFAULT_MODEL };
26
34
  const vars = { ANTHROPIC_BASE_URL: proxy, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1', ENABLE_TOOL_SEARCH: 'true' };
27
35
  const { ANTHROPIC_AUTH_TOKEN: _auth, ...rest } = env;
28
36
  return {
29
- args: ['--settings', JSON.stringify({ apiKeyHelper: 'node -p "process.env.PHANTOM_AGENT_TOKEN || \'\'"', env: vars }), ...args],
30
- env: { ...rest, ANTHROPIC_BASE_URL: proxy, ANTHROPIC_API_KEY: token, PHANTOM_AGENT_TOKEN: token },
37
+ args: ['--settings', JSON.stringify({ apiKeyHelper: helper, env: vars }), ...args],
38
+ env: { ...rest, ...model, ANTHROPIC_BASE_URL: proxy, ANTHROPIC_API_KEY: token, PHANTOM_AGENT_TOKEN: token },
31
39
  };
32
40
  },
33
41
  },
@@ -78,7 +86,7 @@ function waitFor(bin, args, env) {
78
86
  const done = () => process.off('SIGINT', ignore);
79
87
  child.on('error', (err) => {
80
88
  done();
81
- reject(err.code === 'ENOENT' ? new Error(`${bin} is not on your PATH. Install it, then run this again.`) : err);
89
+ reject(err.code === 'ENOENT' ? new Error(notInstalled(bin)) : err);
82
90
  });
83
91
  child.on('exit', (code, signal) => {
84
92
  done();
@@ -115,7 +123,7 @@ export async function agentCommand(input) {
115
123
  if (a === '--key')
116
124
  keyName = value(a);
117
125
  else if (a === '--mpp')
118
- die('--mpp has been removed. Phantom stays prepaid only. Run phantom login or use a saved key.');
126
+ die('--mpp has been removed. Phantom stays prepaid only. Run phantom login to sign in, or use a saved key.');
119
127
  else if (a === '--no-browser')
120
128
  noBrowser = true;
121
129
  else
@@ -125,12 +133,12 @@ export async function agentCommand(input) {
125
133
  let apiKey = keyName ? readNamedKey(env, keyName) : resolveApiKey(env);
126
134
  if (!apiKey) {
127
135
  if (!io.isTTY)
128
- die('Connect a key with phantom login.');
136
+ die(NO_KEY);
129
137
  await workspaceLogin({ ...input, flags: { ...(noBrowser ? { 'no-browser': true } : {}) } });
130
138
  apiKey = resolveApiKey(env);
131
139
  }
132
140
  if (!apiKey)
133
- die('No API key. Run: phantom login');
141
+ die(NO_KEY);
134
142
  const auth = { kind: 'key', apiKey };
135
143
  const before = await keyStatus(apiKey, baseUrl).catch(() => null);
136
144
  const report = async () => {
@@ -155,3 +163,13 @@ export async function agentCommand(input) {
155
163
  rmSync(tmp, { recursive: true, force: true });
156
164
  }
157
165
  }
166
+ /** Where to get each agent phantom starts, for when it isn't installed. */
167
+ const INSTALL = {
168
+ claude: 'Claude Code: https://code.claude.com/docs/en/setup',
169
+ codex: 'Codex: npm install -g @openai/codex',
170
+ };
171
+ /** The message when an agent's program isn't installed, in plain words, with where to get it. */
172
+ export function notInstalled(bin) {
173
+ const how = INSTALL[bin];
174
+ return `I can't find the ${bin} program on this computer, so it isn't installed yet (or isn't on your PATH).${how ? ` Install ${how}, then run this again.` : ' Install it, then run this again.'}`;
175
+ }
@@ -0,0 +1,162 @@
1
+ import { colorOn, paint } from "../core/style.mjs";
2
+ import { BackToMenu, Cancelled, choose } from "../core/pick.mjs";
3
+ import { onPath } from "../setup/shared.mjs";
4
+ import { isSetUp } from "../setup/apply.mjs";
5
+ import { resolveApiKey } from "../keys/saved.mjs";
6
+ import { hideHints } from "./screens.mjs";
7
+ import { startHeader } from "./start.mjs";
8
+ import { COMMANDS } from "./commands.mjs";
9
+ /** The agents the menu can start, and the program each needs on this computer. */
10
+ const AGENTS = [
11
+ { label: 'Start Claude Code', bin: 'claude', run: ['claude'] },
12
+ { label: 'Start Codex', bin: 'codex', run: ['codex'] },
13
+ { label: 'Start pi', bin: 'pi', run: ['pi'] },
14
+ ];
15
+ /** The main menu for the state this computer is in. */
16
+ export function mainMenu(s) {
17
+ const quit = { label: 'Quit', value: { run: [] } };
18
+ if (!s.signedIn) {
19
+ return [
20
+ { label: 'Set up my coding agents', hint: 'sign in and connect them; phantom-1 is free to start', value: { run: ['setup'] } },
21
+ { label: 'Sign in only', hint: 'or save a Phantom key you already have', value: { run: ['login'] } },
22
+ { label: 'Every command', hint: 'pick one to run', value: { open: 'commands' } },
23
+ quit,
24
+ ];
25
+ }
26
+ // Only agents that are installed here, so the menu never offers one that can't start.
27
+ const agents = s.setUp ? AGENTS.filter((a) => s.installed(a.bin)).map((a) => ({ label: a.label, hint: 'on Phantom AI', value: { run: a.run } })) : [];
28
+ return [
29
+ ...(s.setUp ? [] : [{ label: 'Finish setting up my agents', hint: "you're signed in, but no agent uses Phantom yet", value: { run: ['setup'] } }]),
30
+ ...agents,
31
+ { label: 'Balance', hint: 'what this key can spend', value: { run: ['balance'] } },
32
+ { label: 'Spend', hint: 'what you spent, and what phantom-1 saved', value: { run: ['spend'] } },
33
+ { label: 'Add credit', hint: 'by card or crypto', value: { run: ['fund'] } },
34
+ { label: 'Choose models', hint: 'the model phantom-1 runs in each tier', value: { run: ['lineup', 'set'] } },
35
+ { label: 'Keys', hint: 'list, new, limits, revoke', value: { open: 'keys' } },
36
+ { label: 'Plan', hint: 'Pro or Max, instead of credit', value: { run: ['plan'] } },
37
+ { label: 'Workspace', hint: 'balance, keys and latest calls together', value: { run: ['workspace'] } },
38
+ { label: 'More', hint: 'setup, undo, routing, sign out, every command', value: { open: 'more' } },
39
+ quit,
40
+ ];
41
+ }
42
+ /** The menus a main-menu row opens. */
43
+ function subMenu(which, s) {
44
+ if (which === 'keys') {
45
+ return [
46
+ { label: 'See my keys', value: { run: ['keys'] } },
47
+ { label: 'New key', hint: 'for a teammate, CI or another computer', value: { run: ['keys', 'create'] } },
48
+ { label: 'Set a spending limit', value: { run: ['keys', 'set'] } },
49
+ { label: 'Revoke a key', hint: 'it stops working at once; its credit stays', value: { run: ['keys', 'revoke'] } },
50
+ { label: 'Rotate a key', hint: 'a new key in its place; the old one stops working', value: { run: ['keys', 'rotate'] } },
51
+ ];
52
+ }
53
+ if (which === 'commands') {
54
+ return COMMANDS.filter((c) => !c.advanced).map((c) => ({ label: c.name, hint: c.summary, value: { run: [c.name] } }));
55
+ }
56
+ return [
57
+ { label: s.setUp ? 'Set up more agents' : 'Set up my agents', hint: 'or change how they run', value: { run: ['setup'] } },
58
+ ...(s.setUp ? [{ label: 'Undo setup', hint: 'put your agents back how they were', value: { run: ['setup', '--restore'] } }] : []),
59
+ { label: 'Routing rules', hint: 'how model "auto" picks', value: { run: ['route'] } },
60
+ { label: 'Sign out', hint: 'on this computer', value: { run: ['logout'] } },
61
+ { label: 'Every command', hint: 'pick one to run', value: { open: 'commands' } },
62
+ ];
63
+ }
64
+ /**
65
+ * Next steps that change or reveal something (copying a key, signing out):
66
+ * the marker starts on "Back to the menu" instead, so a stray Enter is safe.
67
+ */
68
+ const RISKY = new Set(['keys show --copy', 'logout --all']);
69
+ /** After a command's result: the next steps that fit it, as choices, so nobody types the "→" line. */
70
+ export function nextSteps(ran) {
71
+ const key = ran.join(' ');
72
+ const steps = {
73
+ balance: [['Spend', ['spend']], ['Add credit', ['fund']]],
74
+ spend: [['Every key in the workspace', ['spend', '--workspace']], ['Choose models', ['lineup', 'set']]],
75
+ 'spend --workspace': [['Keys', ['keys']]],
76
+ fund: [['Balance', ['balance']]],
77
+ workspace: [['See my keys', ['keys']], ['Spend by key', ['spend', '--workspace']]],
78
+ keys: [['New key', ['keys', 'create']], ['Set a spending limit', ['keys', 'set']], ['Revoke a key', ['keys', 'revoke']]],
79
+ plan: [['Start Pro', ['plan', 'pro']], ['Start Max', ['plan', 'max']], ['Manage billing', ['plan', 'manage']]],
80
+ route: [['See what a request would get', ['route', 'test']], ['Choose models', ['lineup', 'set']]],
81
+ 'setup --restore': [['Sign out, and forget saved keys', ['logout', '--all']]],
82
+ login: [['Set up my agents', ['setup']]],
83
+ 'keys create': [['See my keys', ['keys']]],
84
+ 'keys revoke': [['See my keys', ['keys']]],
85
+ 'keys set': [['See my keys', ['keys']]],
86
+ 'keys rotate': [['See my keys', ['keys']]],
87
+ // Cursor and other editors take the key in their own settings: copy it without showing it.
88
+ setup: [['Copy my key (for Cursor or another app)', ['keys', 'show', '--copy']]],
89
+ };
90
+ return (steps[key] ?? []).map(([label, run]) => ({ label, value: run }));
91
+ }
92
+ /**
93
+ * Runs the app until Quit, Esc on the main menu, or Ctrl+C. `runCommand` runs
94
+ * one command the way `phantom <args>` would and returns its exit code; the app
95
+ * passes it `fromMenu`, so a command's first Esc comes back here.
96
+ */
97
+ export async function runApp(env, pick, write, runCommand, installed = onPath) {
98
+ const color = colorOn(env, true);
99
+ const p = paint(env, color);
100
+ const state = () => ({ signedIn: Boolean(resolveApiKey(env)), setUp: isSetUp(env), installed });
101
+ // The "→ type this next" lines would repeat the choices offered after each result, so they're hidden here.
102
+ const hintsWere = hideHints(true);
103
+ try {
104
+ for (;;) {
105
+ // A clean screen for each visit to the menu: a title, then where you are.
106
+ write('\x1b[2J\x1b[H' + startHeader(env, color) + '\n');
107
+ const s = state();
108
+ write(` ${p.dim(s.signedIn ? (s.setUp ? 'Signed in · your agents use Phantom AI' : 'Signed in · no agent set up yet') : 'Not signed in yet')}\n\n`);
109
+ let action = await choose({ ...pick, esc: 'quit' }, 'What would you like to do?', mainMenu(s));
110
+ if (action === null)
111
+ return 0;
112
+ // A row that opens another menu; Esc there comes back to the main menu.
113
+ while (action && 'open' in action) {
114
+ const sub = subMenu(action.open, s);
115
+ action = await choose({ ...pick, esc: 'back' }, action.open === 'keys' ? 'Keys' : action.open === 'commands' ? 'Which command?' : 'More', sub);
116
+ }
117
+ if (!action)
118
+ continue;
119
+ if (!action.run.length)
120
+ return 0;
121
+ let ran = action.run;
122
+ // Run it, then offer what fits next; Back to the menu, or Quit, end the chain.
123
+ while (ran) {
124
+ try {
125
+ // Ctrl+C inside a command (exit 130) stops the app too, as it would anywhere else.
126
+ if ((await runCommand(ran)) === 130)
127
+ return 130;
128
+ }
129
+ catch (err) {
130
+ if (err instanceof Cancelled)
131
+ return 130;
132
+ if (!(err instanceof BackToMenu))
133
+ throw err;
134
+ break;
135
+ }
136
+ write('\n');
137
+ const steps = nextSteps(ran);
138
+ // A risky step first in the list would run on a stray Enter, so the marker starts on Back then.
139
+ const safe = steps.some((s) => RISKY.has(s.value.join(' '))) ? steps.length : 0;
140
+ const next = await choose({ ...pick, esc: 'back' }, 'What next?', [
141
+ ...steps,
142
+ { label: 'Back to the menu', value: ['menu'] },
143
+ { label: 'Quit', value: [] },
144
+ ], safe);
145
+ if (next === null || next[0] === 'menu')
146
+ ran = null;
147
+ else if (!next.length)
148
+ return 0;
149
+ else
150
+ ran = next;
151
+ }
152
+ }
153
+ }
154
+ catch (err) {
155
+ if (err instanceof Cancelled)
156
+ return 130;
157
+ throw err;
158
+ }
159
+ finally {
160
+ hideHints(hintsWere);
161
+ }
162
+ }
@@ -8,13 +8,18 @@ import { verifyCommand } from "../receipts/command.mjs";
8
8
  import { setupCommand } from "../setup/command.mjs";
9
9
  import { SETUP_MODEL } from "../setup/shared.mjs";
10
10
  import { SETUP_NAMES, orList } from "../setup/agents.mjs";
11
+ /** The short `--help`'s headings, in the order they print. */
12
+ export const HELP_GROUPS = ['Start', 'Run', 'Money', 'Keys', 'Routing', 'More'];
11
13
  async function loginDispatch(input) {
12
14
  const { subcommand, flags } = input;
15
+ // `phantom login google` or `login github`: a word that isn't a key means the usual browser sign-in, not "that is not a key".
16
+ if (subcommand && !subcommand.startsWith('sk-'))
17
+ return workspaceLogin({ ...input, subcommand: undefined });
13
18
  return subcommand || flags.paste ? loginCommand(input) : workspaceLogin(input);
14
19
  }
15
20
  async function logoutDispatch(input) {
16
- await forgetWorkspace(input.env, input.baseUrl);
17
- return logoutCommand(input);
21
+ const forgot = await forgetWorkspace(input.env, input.baseUrl);
22
+ return logoutCommand(input, forgot);
18
23
  }
19
24
  async function fundDispatch(input) {
20
25
  const { flags } = input;
@@ -28,15 +33,14 @@ async function fundDispatch(input) {
28
33
  export const COMMANDS = [
29
34
  {
30
35
  name: 'login', handler: loginDispatch, keyless: true,
31
- help: ` login [--save <name>] [--no-browser] connect an account or guest workspace by device code
32
- login --paste validate and save an existing key
33
- login [key] save your key so you don't need PHANTOM_API_KEY
34
- (use --paste to prompt for an existing key)`,
36
+ group: 'Start', summary: 'sign in only, or save a Phantom key you already have',
37
+ help: ` login [--save <name>] [--no-browser] sign in by device code: an account or a guest workspace
38
+ login --paste save a Phantom key you already have (it asks for it)`,
35
39
  },
36
40
  {
37
41
  name: 'workspace', handler: workspaceCommand, keyless: true,
42
+ group: 'More', summary: 'the whole workspace: balance, recovery and access',
38
43
  help: ` workspace [status] shared balance, keys, recent usage and management access
39
- workspace fund --amount <USD> [--card | --coin usdc|usdt|sol] [--no-browser]
40
44
  workspace payment --id <payment-id> check verified funding status
41
45
  workspace export --file <path> export private guest recovery; refuses overwrites
42
46
  workspace import --file <path> restore a guest, then issue replacement keys
@@ -44,56 +48,68 @@ export const COMMANDS = [
44
48
  },
45
49
  {
46
50
  name: 'fund', handler: fundDispatch, keyless: true,
51
+ group: 'Money', summary: 'add credit by card or crypto',
47
52
  help: ` fund --amount <USD> [--card | --coin usdc|usdt|sol] [--no-browser]
48
53
  fund your workspace balance with card (plus Stripe's fee) or crypto`,
49
54
  },
50
55
  {
51
56
  name: 'keys', handler: workspaceCommand, keyless: true,
57
+ group: 'Keys', summary: "your workspace's keys: list, create, set limits, revoke",
52
58
  help: ` keys [list] remote workspace key metadata, never secrets
53
59
  keys create [--save <name>] [--limit <USD>]
54
60
  keys set --id <id> [--limit <USD|none>] [--budget <USD|none>]
55
61
  [--period <days|none>] [--rate <USD|none>] [--name <name>]
56
62
  [--policy-file <path> | --clear-policy]
57
63
  keys rotate --id <id> [--save <name>] replace only the corresponding saved key
58
- keys revoke --id <id> revoke access, keeping workspace credit
64
+ keys revoke --id <id> [--yes] revoke access, keeping workspace credit (--yes: don't ask)
59
65
  keys saved [--balance] local saved key metadata
60
66
  keys save <name> [key] validate and save an existing key (prompts if omitted)
61
67
  keys show [name] print a secret for shell integration only
68
+ keys show [name] --copy copy it to the clipboard instead (Mac), for an app's settings
62
69
  keys rm <name> forget a local copy; remote key keeps working`,
63
70
  },
64
71
  {
65
72
  name: 'claude', handler: agentCommand, keyless: true,
73
+ group: 'Run', summary: 'start Claude Code on Phantom AI',
66
74
  help: ` claude [--key <name>] [--no-browser] -- <agent arguments>`,
67
75
  },
68
76
  {
69
77
  name: 'codex', handler: agentCommand, keyless: true,
78
+ group: 'Run', summary: 'start Codex on Phantom AI',
70
79
  help: ` codex [--key <name>] [--no-browser] -- <agent arguments>`,
71
80
  },
72
81
  {
73
82
  name: 'pi', handler: agentCommand, keyless: true,
83
+ group: 'Run', summary: 'start pi on Phantom AI',
74
84
  help: ` pi [--key <name>] [--no-browser] -- <agent arguments>
75
85
  run with a saved key`,
76
86
  },
77
87
  {
78
88
  name: 'balance', handler: balanceCommand,
89
+ group: 'Money', summary: 'your credit, and when it expires',
79
90
  help: ` balance show credit balance and expiry`,
80
91
  },
81
92
  {
82
93
  name: 'spend', handler: spendCommand, keyless: ['--workspace'],
94
+ group: 'Money', summary: 'what you spent, and what phantom-1 saved',
83
95
  help: ` spend [--from <date>] [--to <date>] what this key spent, by model and day, and what phantom-1
84
96
  saved; dates are UTC days as YYYY-MM-DD, default this month
85
97
  spend --workspace every key in the workspace by name (needs phantom login)`,
86
98
  },
87
99
  {
88
100
  name: 'plan', handler: planCommand,
89
- help: ` plan [status] this key's Pro or Max plan and how much of the week is used
90
- plan pro | max [--no-browser] print a Stripe Checkout link that puts the plan on this key
91
- and open it
101
+ group: 'Money', summary: 'your Pro or Max plan: status, buy, manage',
102
+ help: ` plan [status] this workspace's Pro or Max plan, how much of the week is used,
103
+ and what each plan costs
104
+ plan pro | max [--no-browser] open Stripe Checkout to put the plan on this workspace (every
105
+ key in it); at a terminal, it says the price and asks first
92
106
  plan manage [--no-browser] the same for Stripe's billing page: card, switch plan, cancel`,
93
107
  },
94
108
  {
95
109
  name: 'key', advanced: true, handler: keyCommand, keyless: true,
96
- help: ` key list [--balance] saved keys, by name and id
110
+ group: 'More', summary: 'older names for keys saved, save, show and rm',
111
+ help: ` key list | save | show | rm older names for keys saved, keys save, keys show, keys rm
112
+ key list [--balance] saved keys, by name and id
97
113
  key save <name> [key] save a key by name (prompts if no key is given)
98
114
  key show [name] print a saved key, for PHANTOM_API_KEY=$(...);
99
115
  no name prints the key phantom is using
@@ -101,6 +117,7 @@ export const COMMANDS = [
101
117
  },
102
118
  {
103
119
  name: 'route', handler: routeCommand,
120
+ group: 'Routing', summary: 'which models "auto" runs, and rules for picking',
104
121
  help: ` route [get] show which model "auto" runs, and why
105
122
  route set --models a,b,c models for "auto", first is the default
106
123
  [--applies-to auto|all] [--on-empty stop|cheapest] [--fallback]
@@ -117,6 +134,7 @@ export const COMMANDS = [
117
134
  },
118
135
  {
119
136
  name: 'lineup', handler: lineupCommand,
137
+ group: 'Routing', summary: 'the model phantom-1 runs in each tier',
120
138
  help: ` lineup [get] show the model phantom-1 runs in each tier, and the choices
121
139
  lineup set --economy <model> pick the model for a tier; "default" goes back to the default
122
140
  [--standard <model>] [--frontier <model>]
@@ -127,17 +145,21 @@ export const COMMANDS = [
127
145
  },
128
146
  {
129
147
  name: 'logout', handler: logoutDispatch, keyless: true,
130
- help: ` logout forget workspace access and the default saved key`,
148
+ group: 'More', summary: 'sign out on this computer',
149
+ help: ` logout sign out: forget workspace access and the default saved key
150
+ logout --all also forget every key saved here by name`,
131
151
  },
132
152
  {
133
153
  name: 'setup', handler: setupCommand, keyless: true,
154
+ group: 'Start', summary: 'sign in and connect your coding agents',
134
155
  help: ` setup set up every agent found: install the skill and
135
156
  point its model calls at Phantom AI; shows each
136
157
  file's changes and asks first. With no key yet,
137
158
  signs in first (in a terminal)
138
159
  [--yes] don't ask
139
160
  [--no-browser] print the sign-in link instead of opening it
140
- [--dry-run] show the changes, write nothing
161
+ [--dry-run] preview the changes without making them
162
+ [--diff] show every line each file gains or loses
141
163
  [--json] print JSON in a terminal too (a pipe always gets JSON)
142
164
  [--model <id>] the model each agent starts on (default ${SETUP_MODEL})
143
165
  setup --agent <name> install the skill for one agent, without asking:
@@ -150,6 +172,7 @@ export const COMMANDS = [
150
172
  },
151
173
  {
152
174
  name: 'verify', advanced: true, handler: verifyCommand, keyless: ['--receipt'],
175
+ group: 'More', summary: 'check a signed receipt',
153
176
  help: ` verify --model <id> make one tiny call and check its signed receipt
154
177
  names the model you asked for (costs a fraction of a cent)
155
178
  verify --receipt <receipt> check a receipt you already have`,
package/dist/cli/help.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_BASE_URL } from "../core/request.mjs";
2
- import { COMMANDS } from "./commands.mjs";
2
+ import { COMMANDS, HELP_GROUPS } from "./commands.mjs";
3
3
  /**
4
4
  * Every environment variable phantom reads. The Environment section of HELP is
5
5
  * built from this list.
@@ -19,7 +19,7 @@ function envHelp() {
19
19
  }
20
20
  /** Full command reference, printed by `phantom --help --all`. */
21
21
  export const HELP = `
22
- phantom — AI inference for your coding agents
22
+ phantom — points your coding agents at Phantom AI
23
23
 
24
24
  Commands:
25
25
  ${COMMANDS.map((command) => command.help).join('\n')}
@@ -32,6 +32,27 @@ Flags:
32
32
 
33
33
  ${envHelp()}
34
34
  `.trim();
35
- /** Workflow help printed by default; command definitions remain the source of usage. */
36
- export const PRIMARY_HELP = HELP.replace(COMMANDS.map((command) => command.help).join('\n'), COMMANDS.filter((command) => !command.advanced).map((command) => command.help).join('\n') +
37
- '\n\nAdvanced: phantom --help --all (receipts and verification)');
35
+ /** The order commands take inside their group in the short help: the one most people need first. Others follow in list order. */
36
+ const FIRST = ['setup', 'login', 'claude', 'codex', 'pi', 'balance', 'fund', 'spend', 'plan'];
37
+ const rank = (name) => (FIRST.includes(name) ? FIRST.indexOf(name) : FIRST.length);
38
+ /** One row of the short help: a command name in its column, then what it does. */
39
+ const helpRow = (name, summary) => ` ${name.padEnd(20)}${summary}`;
40
+ /**
41
+ * The help printed by default: each command in one line, under a heading for
42
+ * what it's for, so it fits on one screen. The three agents share one row.
43
+ * `phantom <command> --help` and `--help --all` keep the full usage.
44
+ */
45
+ export const PRIMARY_HELP = [
46
+ 'phantom — points your coding agents at Phantom AI',
47
+ '',
48
+ ...HELP_GROUPS.flatMap((group) => {
49
+ const shown = COMMANDS.filter((command) => command.group === group && !command.advanced).sort((a, b) => rank(a.name) - rank(b.name));
50
+ const agents = shown.filter((command) => command.group === 'Run');
51
+ const rows = group === 'Run'
52
+ ? [helpRow(agents.map((command) => command.name).join(', '), 'start that agent on Phantom AI')]
53
+ : shown.map((command) => helpRow(command.name, command.summary));
54
+ return rows.length ? [group.toUpperCase(), ...rows, ''] : [];
55
+ }),
56
+ 'phantom <command> --help shows its flags. phantom --help --all lists every flag,',
57
+ 'the advanced commands (receipts and verification) and the environment variables.',
58
+ ].join('\n');