@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 +3 -16
- package/dist/agents/command.mjs +1 -1
- package/dist/cli/commands.mjs +1 -0
- package/dist/cli/help.mjs +2 -3
- package/dist/cli/run.mjs +10 -2
- package/dist/core/node-trust.mjs +24 -0
- package/dist/core/version.mjs +1 -1
- package/dist/setup/apply.mjs +7 -0
- package/dist/setup/command.mjs +8 -2
- package/dist/workspace/command.mjs +20 -5
- package/package.json +1 -3
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.
|
|
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`
|
|
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
|
-
|
|
215
|
+
Report a security problem privately to support@phantom.codes, not in a public issue.
|
|
229
216
|
|
|
230
217
|
</details>
|
|
231
218
|
|
package/dist/agents/command.mjs
CHANGED
|
@@ -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: '
|
|
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
|
},
|
package/dist/cli/commands.mjs
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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,
|
|
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
|
+
}
|
package/dist/core/version.mjs
CHANGED
|
@@ -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.
|
|
2
|
+
export const VERSION = '1.2.3';
|
package/dist/setup/apply.mjs
CHANGED
|
@@ -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)
|
package/dist/setup/command.mjs
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
40
|
-
|
|
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(`
|
|
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
|
|
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.
|
|
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
|
},
|