@phantomcodes/cli 1.2.3 → 1.2.4
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 +10 -5
- package/dist/agents/command.mjs +15 -5
- package/dist/cli/app.mjs +162 -0
- package/dist/cli/commands.mjs +37 -14
- package/dist/cli/help.mjs +26 -5
- package/dist/cli/run.mjs +54 -27
- package/dist/cli/screens.mjs +330 -0
- package/dist/cli/start.mjs +142 -0
- package/dist/core/brand.mjs +31 -0
- package/dist/core/errors.mjs +2 -2
- package/dist/core/helpers.mjs +47 -14
- package/dist/core/motion.mjs +156 -0
- package/dist/core/pick.mjs +293 -0
- package/dist/core/style.mjs +36 -0
- package/dist/core/version.mjs +1 -1
- package/dist/keys/command.mjs +37 -13
- package/dist/phantom.mjs +1 -1
- package/dist/plan/command.mjs +26 -8
- package/dist/setup/agents.mjs +8 -8
- package/dist/setup/apply.mjs +26 -23
- package/dist/setup/command.mjs +150 -59
- package/dist/setup/editors.mjs +1 -1
- package/dist/setup/plan.mjs +4 -2
- package/dist/setup/screen.mjs +237 -0
- package/dist/spend/command.mjs +75 -38
- package/dist/spend/report.mjs +12 -3
- package/dist/workspace/command.mjs +355 -42
- package/package.json +1 -1
- package/skills/phantom-ai/SKILL.md +3 -2
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ undo the changes with `phantom setup --restore`.
|
|
|
74
74
|
```bash
|
|
75
75
|
npm i -g @phantomcodes/cli
|
|
76
76
|
phantom login
|
|
77
|
-
phantom
|
|
77
|
+
phantom fund --amount 10 --card
|
|
78
78
|
phantom workspace status
|
|
79
79
|
phantom codex
|
|
80
80
|
```
|
|
@@ -175,7 +175,7 @@ it cancels. A named model, not `phantom-1`, still spends credit.
|
|
|
175
175
|
| Connect | `phantom login`, `phantom logout` |
|
|
176
176
|
| Connect existing agents | `phantom setup`, `phantom setup --dry-run`, `phantom setup --restore` |
|
|
177
177
|
| Run inference | `phantom claude`, `phantom codex`, `phantom pi` |
|
|
178
|
-
| Fund and check spending | `phantom
|
|
178
|
+
| Fund and check spending | `phantom fund`, `phantom balance`, `phantom spend`, `phantom workspace status` |
|
|
179
179
|
| Manage workspace keys | `phantom keys create`, `phantom keys set`, `phantom keys rotate`, `phantom keys revoke` |
|
|
180
180
|
| Manage local copies | `phantom keys saved`, `phantom keys save`, `phantom keys rm` |
|
|
181
181
|
| Route models | `phantom route` |
|
|
@@ -187,9 +187,14 @@ Put phantom flags before agent arguments; use `--` to separate them:
|
|
|
187
187
|
phantom codex --key coding -- exec "explain this repository"
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
In a terminal, commands draw a screen for people, and choices are pickers
|
|
191
|
+
(arrow keys, Enter, Esc to go back). Piped, as scripts and agents run them,
|
|
192
|
+
they print JSON on stdout and errors on stderr; `--json` forces JSON in a
|
|
193
|
+
terminal, and `--table` the screen in a pipe. Unknown flags are refused.
|
|
194
|
+
Agent commands inherit the terminal and print a spending summary on stderr.
|
|
195
|
+
Exit codes: 0 success (and Esc), 1 error, 2 rejected key, 130 Ctrl+C;
|
|
196
|
+
`phantom setup` returns 1 when it could only install the skills (no key yet,
|
|
197
|
+
`"complete": false`). Agent runs return the agent's exit code.
|
|
193
198
|
|
|
194
199
|
<details>
|
|
195
200
|
<summary><strong>Advanced tools, integrations and legacy commands</strong></summary>
|
package/dist/agents/command.mjs
CHANGED
|
@@ -2,7 +2,7 @@ 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";
|
|
@@ -78,7 +78,7 @@ function waitFor(bin, args, env) {
|
|
|
78
78
|
const done = () => process.off('SIGINT', ignore);
|
|
79
79
|
child.on('error', (err) => {
|
|
80
80
|
done();
|
|
81
|
-
reject(err.code === 'ENOENT' ? new Error(
|
|
81
|
+
reject(err.code === 'ENOENT' ? new Error(notInstalled(bin)) : err);
|
|
82
82
|
});
|
|
83
83
|
child.on('exit', (code, signal) => {
|
|
84
84
|
done();
|
|
@@ -115,7 +115,7 @@ export async function agentCommand(input) {
|
|
|
115
115
|
if (a === '--key')
|
|
116
116
|
keyName = value(a);
|
|
117
117
|
else if (a === '--mpp')
|
|
118
|
-
die('--mpp has been removed. Phantom stays prepaid only. Run phantom login or use a saved key.');
|
|
118
|
+
die('--mpp has been removed. Phantom stays prepaid only. Run phantom login to sign in, or use a saved key.');
|
|
119
119
|
else if (a === '--no-browser')
|
|
120
120
|
noBrowser = true;
|
|
121
121
|
else
|
|
@@ -125,12 +125,12 @@ export async function agentCommand(input) {
|
|
|
125
125
|
let apiKey = keyName ? readNamedKey(env, keyName) : resolveApiKey(env);
|
|
126
126
|
if (!apiKey) {
|
|
127
127
|
if (!io.isTTY)
|
|
128
|
-
die(
|
|
128
|
+
die(NO_KEY);
|
|
129
129
|
await workspaceLogin({ ...input, flags: { ...(noBrowser ? { 'no-browser': true } : {}) } });
|
|
130
130
|
apiKey = resolveApiKey(env);
|
|
131
131
|
}
|
|
132
132
|
if (!apiKey)
|
|
133
|
-
die(
|
|
133
|
+
die(NO_KEY);
|
|
134
134
|
const auth = { kind: 'key', apiKey };
|
|
135
135
|
const before = await keyStatus(apiKey, baseUrl).catch(() => null);
|
|
136
136
|
const report = async () => {
|
|
@@ -155,3 +155,13 @@ export async function agentCommand(input) {
|
|
|
155
155
|
rmSync(tmp, { recursive: true, force: true });
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
/** Where to get each agent phantom starts, for when it isn't installed. */
|
|
159
|
+
const INSTALL = {
|
|
160
|
+
claude: 'Claude Code: https://code.claude.com/docs/en/setup',
|
|
161
|
+
codex: 'Codex: npm install -g @openai/codex',
|
|
162
|
+
};
|
|
163
|
+
/** The message when an agent's program isn't installed, in plain words, with where to get it. */
|
|
164
|
+
export function notInstalled(bin) {
|
|
165
|
+
const how = INSTALL[bin];
|
|
166
|
+
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.'}`;
|
|
167
|
+
}
|
package/dist/cli/app.mjs
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/commands.mjs
CHANGED
|
@@ -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
|
-
|
|
32
|
-
login --
|
|
33
|
-
login
|
|
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>
|
|
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
|
-
|
|
90
|
-
plan
|
|
91
|
-
and
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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 —
|
|
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
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
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');
|
package/dist/cli/run.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { VERSION } from "../core/version.mjs";
|
|
2
2
|
import { nodeTrustWarning } from "../core/node-trust.mjs";
|
|
3
|
-
import { die } from "../core/errors.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { die, NO_KEY } from "../core/errors.mjs";
|
|
4
|
+
import { colorOn } from "../core/style.mjs";
|
|
5
|
+
import { ALIASES, closestCommand, commandFromWords, startScreen } from "./start.mjs";
|
|
6
|
+
import { BackToMenu } from "../core/pick.mjs";
|
|
7
|
+
import { runApp } from "./app.mjs";
|
|
8
|
+
import { handleError, parseFlags, pickIo } from "../core/helpers.mjs";
|
|
5
9
|
import { resolveApiKey } from "../keys/saved.mjs";
|
|
6
10
|
import { COMMANDS } from "./commands.mjs";
|
|
7
11
|
import { HELP, PRIMARY_HELP } from "./help.mjs";
|
|
@@ -17,6 +21,16 @@ import { LEGACY_SETUP_HINT, legacySetupFound } from "../setup/legacy.mjs";
|
|
|
17
21
|
* @param node The Node version it runs on; one too old to reject forged
|
|
18
22
|
* eMudhra certificates gets a warning on stderr first.
|
|
19
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* Every flag phantom reads: each one a command's help names, plus the flags
|
|
26
|
+
* every command takes and two retired ones that explain themselves. A typo
|
|
27
|
+
* such as --yse is refused instead of quietly ignored, which in CI would
|
|
28
|
+
* otherwise drop the flag without a word.
|
|
29
|
+
*/
|
|
30
|
+
const KNOWN_FLAGS = new Set([
|
|
31
|
+
...COMMANDS.flatMap((command) => [...command.help.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1])),
|
|
32
|
+
'table', 'json', 'help', 'all', 'version', 'mcp', 'pay', 'mpp',
|
|
33
|
+
]);
|
|
20
34
|
export async function run(argv = process.argv.slice(2), env = process.env, io = {
|
|
21
35
|
stdout: (s) => process.stdout.write(s),
|
|
22
36
|
stderr: (s) => process.stderr.write(s),
|
|
@@ -30,23 +44,12 @@ export async function run(argv = process.argv.slice(2), env = process.env, io =
|
|
|
30
44
|
io.stdout(VERSION + '\n');
|
|
31
45
|
return 0;
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
phantom
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
2. Fund your balance:
|
|
41
|
-
phantom fund --amount 10 --card
|
|
42
|
-
(or phantom fund --amount 10 --coin usdc)
|
|
43
|
-
|
|
44
|
-
3. Run your coding agent:
|
|
45
|
-
phantom claude
|
|
46
|
-
(or phantom codex, phantom pi)
|
|
47
|
-
|
|
48
|
-
Run phantom --help for all commands.
|
|
49
|
-
`.trim();
|
|
47
|
+
// Words people type for a command run as that command: `phantom undo` is setup --restore,
|
|
48
|
+
// `phantom help setup` is setup --help, and `phantom start` opens the menu.
|
|
49
|
+
if (argv[0] === 'help' && COMMANDS.some((c) => c.name === argv[1]))
|
|
50
|
+
argv = [argv[1], '--help'];
|
|
51
|
+
else if (argv[0] && Object.hasOwn(ALIASES, argv[0]))
|
|
52
|
+
argv = [...ALIASES[argv[0]], ...(argv[0] === 'help' ? [] : argv.slice(1))];
|
|
50
53
|
// `--help` anywhere before a `--` asks for help, so `phantom setup --help`
|
|
51
54
|
// prints it rather than running setup.
|
|
52
55
|
const own = argv.includes('--') ? argv.slice(0, argv.indexOf('--')) : argv;
|
|
@@ -56,32 +59,56 @@ Run phantom --help for all commands.
|
|
|
56
59
|
io.stderr(LEGACY_SETUP_HINT + '\n');
|
|
57
60
|
}
|
|
58
61
|
if (argv.length === 0) {
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const signedIn = Boolean(resolveApiKey(env));
|
|
63
|
+
const pick = pickIo({ io, env });
|
|
64
|
+
if (!pick) {
|
|
65
|
+
io.stdout(startScreen(env, colorOn(env, io.stdoutIsTTY), signedIn) + '\n');
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
// At a terminal, phantom alone is an app you stay in: a menu, each command's result, and what to do next.
|
|
69
|
+
return runApp(env, pick, io.stderr, (next) => run(next, env, { ...io, fromMenu: true }, node));
|
|
61
70
|
}
|
|
62
71
|
if (own.includes('--help') || own.includes('-h')) {
|
|
63
72
|
const commandHelp = COMMANDS.find((entry) => entry.name === own[0])?.help;
|
|
64
|
-
io.stdout((own.includes('--all') ? HELP : commandHelp?.
|
|
73
|
+
io.stdout((own.includes('--all') ? HELP : commandHelp?.replace(/\s+$/, '') ?? PRIMARY_HELP) + '\n');
|
|
65
74
|
return 0;
|
|
66
75
|
}
|
|
67
76
|
try {
|
|
68
77
|
const command = COMMANDS.find((entry) => entry.name === argv[0]);
|
|
69
|
-
if (!command)
|
|
70
|
-
|
|
78
|
+
if (!command) {
|
|
79
|
+
// A near miss ("stup"), then a word in a sentence ("how do i add money"), then the way in.
|
|
80
|
+
const guess = closestCommand(argv[0], COMMANDS.map((entry) => entry.name)) ?? commandFromWords(argv);
|
|
81
|
+
die(guess ? `Unknown command "${argv[0]}". Did you mean phantom ${guess}?` : `Unknown command "${argv[0]}". Try phantom (a menu), phantom setup, or phantom --help for every command.`);
|
|
82
|
+
}
|
|
71
83
|
const keyless = command.keyless === true ||
|
|
72
84
|
(command.keyless?.some((flag) => own.slice(1).some((arg) => arg === flag || arg.startsWith(flag + '='))) ?? false);
|
|
73
85
|
const apiKey = keyless ? '' : resolveApiKey(env);
|
|
74
86
|
if (!apiKey && !keyless)
|
|
75
|
-
die(
|
|
87
|
+
die(NO_KEY);
|
|
76
88
|
const subcommand = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
|
|
77
89
|
const flagArgs = subcommand ? argv.slice(2) : argv.slice(1);
|
|
90
|
+
// Flags after -- belong to the agent phantom starts, so only the ones before it are checked.
|
|
78
91
|
const flags = parseFlags(flagArgs);
|
|
79
|
-
const
|
|
92
|
+
const ownFlags = parseFlags(flagArgs.includes('--') ? flagArgs.slice(0, flagArgs.indexOf('--')) : flagArgs);
|
|
93
|
+
// claude, codex and pi hand their arguments to the agent, so they check their own.
|
|
94
|
+
const passesArgs = command.handler === COMMANDS.find((c) => c.name === 'claude')?.handler;
|
|
95
|
+
for (const name of passesArgs ? [] : Object.keys(ownFlags)) {
|
|
96
|
+
if (KNOWN_FLAGS.has(name))
|
|
97
|
+
continue;
|
|
98
|
+
const guess = closestCommand(name, [...KNOWN_FLAGS]);
|
|
99
|
+
die(`Unknown flag --${name} for phantom ${argv[0]}.${guess ? ` Did you mean --${guess}?` : ''} See phantom ${argv[0]} --help.`, 'unknown_flag');
|
|
100
|
+
}
|
|
101
|
+
// A person at a terminal reads each command's screen; a pipe (scripts, agents) or --json gets JSON.
|
|
102
|
+
const table = Boolean(flags['table']) || (Boolean(io.stdoutIsTTY) && !flags['json']);
|
|
80
103
|
const baseUrl = env.PHANTOM_BASE_URL;
|
|
81
104
|
const input = { argv, env, io, apiKey, subcommand, flags, table, baseUrl };
|
|
82
105
|
return await command.handler(input);
|
|
83
106
|
}
|
|
84
107
|
catch (err) {
|
|
85
|
-
|
|
108
|
+
// Esc on the first step of a command the menu started: let the menu show again.
|
|
109
|
+
if (err instanceof BackToMenu && io.fromMenu)
|
|
110
|
+
throw err;
|
|
111
|
+
// A person at a terminal gets a sentence; scripts and agents reading stderr get JSON.
|
|
112
|
+
return handleError(err, io.stderr, { human: io.isTTY, env });
|
|
86
113
|
}
|
|
87
114
|
}
|