@phantomcodes/cli 0.0.0-stage → 1.1.0
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/LICENSE +81 -0
- package/README.md +231 -2
- package/bin/phantom.mjs +14 -0
- package/dist/agents/command.mjs +157 -0
- package/dist/agents/proxy.mjs +140 -0
- package/dist/cli/commands.mjs +154 -0
- package/dist/cli/help.mjs +38 -0
- package/dist/cli/run.mjs +79 -0
- package/dist/core/errors.mjs +39 -0
- package/dist/core/helpers.mjs +85 -0
- package/dist/core/request.mjs +108 -0
- package/dist/core/secrets.mjs +94 -0
- package/dist/core/shapes.mjs +1 -0
- package/dist/core/state.mjs +7 -0
- package/dist/core/version.mjs +2 -0
- package/dist/keys/command.mjs +116 -0
- package/dist/keys/saved.mjs +172 -0
- package/dist/phantom.mjs +60 -0
- package/dist/plan/command.mjs +68 -0
- package/dist/receipts/command.mjs +41 -0
- package/dist/receipts/receipts.mjs +82 -0
- package/dist/setup/agents.mjs +426 -0
- package/dist/setup/apply.mjs +210 -0
- package/dist/setup/command.mjs +112 -0
- package/dist/setup/editors.mjs +486 -0
- package/dist/setup/legacy.mjs +36 -0
- package/dist/setup/plan.mjs +121 -0
- package/dist/setup/shared.mjs +81 -0
- package/dist/spend/command.mjs +172 -0
- package/dist/spend/lineup.mjs +20 -0
- package/dist/spend/report.mjs +61 -0
- package/dist/spend/routing.mjs +32 -0
- package/dist/workspace/command.mjs +209 -0
- package/package.json +83 -4
- package/skills/phantom-ai/SKILL.md +155 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { DEFAULT_BASE_URL, httpsOnly, apiError } from "../core/request.mjs";
|
|
6
|
+
import { stateDir } from "../core/state.mjs";
|
|
7
|
+
import { writeSecret, readSecret, removeSecret, keychainOn } from "../core/secrets.mjs";
|
|
8
|
+
import { keyCommand } from "../keys/command.mjs";
|
|
9
|
+
import { die } from "../core/errors.mjs";
|
|
10
|
+
import { openUrl } from "../core/helpers.mjs";
|
|
11
|
+
import { keyPath, namedKeyPath, saveApiKey, saveNamedKey } from "../keys/saved.mjs";
|
|
12
|
+
const credentialPath = (env) => path.join(stateDir(env), 'workspace');
|
|
13
|
+
const base = (input) => httpsOnly(input.baseUrl || input.env.PHANTOM_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
14
|
+
async function wire(url, method, endpoint, body, token) {
|
|
15
|
+
const response = await fetch(url + endpoint, { method, redirect: 'error', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
if (!response.ok)
|
|
18
|
+
throw apiError(response.status, data);
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
function credentials(input) {
|
|
22
|
+
const file = credentialPath(input.env);
|
|
23
|
+
if (!existsSync(file))
|
|
24
|
+
die('No workspace connection. Run phantom login.', 'workspace_missing');
|
|
25
|
+
const saved = JSON.parse(readSecret(input.env, file));
|
|
26
|
+
if (saved.base_url !== base(input))
|
|
27
|
+
die('This workspace belongs to a different API address. Run phantom login for this address.', 'workspace_origin');
|
|
28
|
+
return saved;
|
|
29
|
+
}
|
|
30
|
+
/** A read from the saved workspace, such as `/spend`, with its management credential. */
|
|
31
|
+
export async function workspaceGet(input, path) {
|
|
32
|
+
const saved = credentials(input);
|
|
33
|
+
return wire(saved.base_url, 'GET', '/workspaces/' + encodeURIComponent(saved.workspace_id) + path, undefined, saved.management_credential);
|
|
34
|
+
}
|
|
35
|
+
/** Device authorization for accounts or guests, saving keys locally without copying a secret through the browser. */
|
|
36
|
+
export async function workspaceLogin(input) {
|
|
37
|
+
const { env, io, flags } = input;
|
|
38
|
+
if (flags.save === true)
|
|
39
|
+
die('--save requires a key name.');
|
|
40
|
+
const name = typeof flags.save === 'string' ? flags.save : 'default';
|
|
41
|
+
const target = name === 'default' ? keyPath(env) : namedKeyPath(env, name);
|
|
42
|
+
if (existsSync(target))
|
|
43
|
+
die(`Saved key ${name} already exists. Use --save <new-name>; no key was overwritten.`, 'key_exists');
|
|
44
|
+
const url = base(input);
|
|
45
|
+
const start = await wire(url, 'POST', '/auth/device', { client_name: `phantom on ${hostname()}` });
|
|
46
|
+
if (new URL(start.verification_uri_complete).origin !== new URL(url).origin)
|
|
47
|
+
die('The approval URL belongs to another site.', 'device_origin');
|
|
48
|
+
io.stderr(`Open ${start.verification_uri_complete}\nConfirm code ${start.user_code}\n`);
|
|
49
|
+
if (!flags['no-browser'])
|
|
50
|
+
openUrl(start.verification_uri_complete);
|
|
51
|
+
let interval = Math.max(1, start.interval) * 1000;
|
|
52
|
+
const deadline = Date.now() + Math.min(600, start.expires_in) * 1000;
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
55
|
+
let token;
|
|
56
|
+
try {
|
|
57
|
+
token = await wire(url, 'POST', '/auth/device/token', { device_code: start.device_code });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const code = error.code;
|
|
61
|
+
if (code === 'authorization_pending')
|
|
62
|
+
continue;
|
|
63
|
+
if (code === 'slow_down') {
|
|
64
|
+
interval += 5000;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
const saved = { version: 1, workspace_id: token.workspace_id, management_credential: token.management_token, base_url: url, recovery: token.recovery };
|
|
70
|
+
try {
|
|
71
|
+
writeSecret(env, credentialPath(env), JSON.stringify(saved));
|
|
72
|
+
if (name === 'default')
|
|
73
|
+
saveApiKey(env, token.api_key);
|
|
74
|
+
else
|
|
75
|
+
saveNamedKey(env, name, token.api_key);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
io.stderr('phantom could not save the connection. Sign in or restore your workspace to revoke the device key and reconnect.\n');
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
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');
|
|
82
|
+
io.stderr('Ready. Run phantom claude, phantom codex or phantom pi. Guests can export recovery with phantom workspace export --file <path>.\n');
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
die('Device code expired. Run phantom login again.', 'login_expired');
|
|
86
|
+
}
|
|
87
|
+
/** Manage a workspace from the terminal: balance, keys, funding, recovery and revocation. */
|
|
88
|
+
export async function workspaceCommand(input) {
|
|
89
|
+
const { subcommand, flags, io, env, argv } = input;
|
|
90
|
+
if (argv[0] === 'keys') {
|
|
91
|
+
if (subcommand === 'saved' || subcommand === 'save' || subcommand === 'show' || subcommand === 'rm') {
|
|
92
|
+
return keyCommand({ ...input, flags: { ...(flags.balance ? { balance: flags.balance } : {}) }, subcommand: subcommand === 'saved' ? 'list' : subcommand });
|
|
93
|
+
}
|
|
94
|
+
const remoteCommands = new Set(['list', 'create', 'rotate', 'revoke', 'set', undefined]);
|
|
95
|
+
if (!remoteCommands.has(subcommand)) {
|
|
96
|
+
return keyCommand({ ...input, subcommand });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const command = subcommand ?? 'status';
|
|
100
|
+
if (command === 'import' && argv[0] === 'workspace') {
|
|
101
|
+
if (typeof flags.file !== 'string')
|
|
102
|
+
die('Use phantom workspace import --file <recovery-file>.');
|
|
103
|
+
const recovery = JSON.parse(readFileSync(flags.file, 'utf8'));
|
|
104
|
+
if (!recovery || recovery.version !== 1)
|
|
105
|
+
die('Invalid recovery file.');
|
|
106
|
+
await wire(base(input), 'GET', '/workspaces/' + encodeURIComponent(recovery.workspace_id), undefined, recovery.management_credential);
|
|
107
|
+
writeSecret(env, credentialPath(env), JSON.stringify({ ...recovery, base_url: base(input), recovery }));
|
|
108
|
+
io.stdout('Workspace restored. Use phantom keys create to save a new API key.\n');
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
const saved = credentials(input);
|
|
112
|
+
const endpoint = '/workspaces/' + encodeURIComponent(saved.workspace_id);
|
|
113
|
+
if (command === 'export') {
|
|
114
|
+
if (!saved.recovery)
|
|
115
|
+
die('This is an account workspace. Sign in to recover it on another computer.');
|
|
116
|
+
if (typeof flags.file !== 'string')
|
|
117
|
+
die('Use phantom workspace export --file <path>.');
|
|
118
|
+
writeFileSync(flags.file, JSON.stringify(saved.recovery, null, 2), { mode: 0o600, flag: 'wx' });
|
|
119
|
+
io.stdout('Recovery exported. Keep that file private; it grants workspace management.\n');
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
if (command === 'create' || command === 'rotate') {
|
|
123
|
+
if (command === 'rotate' && typeof flags.id !== 'string')
|
|
124
|
+
die('Use --id <key-id> from phantom keys list.');
|
|
125
|
+
const name = typeof flags.save === 'string' ? flags.save : 'default';
|
|
126
|
+
if (flags.save === true)
|
|
127
|
+
die('--save requires a key name.');
|
|
128
|
+
const target = name === 'default' ? keyPath(env) : namedKeyPath(env, name);
|
|
129
|
+
const replacing = existsSync(target);
|
|
130
|
+
if (replacing && (command !== 'rotate' || createHash('sha256').update(readSecret(env, target).trim()).digest('hex') !== flags.id))
|
|
131
|
+
die('That saved key already exists. Use --save <new-name>.', 'key_exists');
|
|
132
|
+
const result = await wire(saved.base_url, 'POST', command === 'create' ? endpoint + '/keys' : endpoint + '/keys/' + String(flags.id), command === 'create' ? { name, limit_usd: flags.limit === undefined ? null : Number(flags.limit) } : { action: 'rotate' }, saved.management_credential);
|
|
133
|
+
if (name === 'default')
|
|
134
|
+
saveApiKey(env, result.api_key);
|
|
135
|
+
else
|
|
136
|
+
saveNamedKey(env, name, result.api_key, { replace: replacing });
|
|
137
|
+
io.stdout(JSON.stringify({ saved_as: name }) + '\n');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (command === 'revoke') {
|
|
141
|
+
if (typeof flags.id !== 'string')
|
|
142
|
+
die('Use phantom keys revoke --id <key-id>.');
|
|
143
|
+
await wire(saved.base_url, 'DELETE', endpoint + '/keys/' + flags.id, undefined, saved.management_credential);
|
|
144
|
+
io.stdout('Key revoked.\n');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (command === 'set') {
|
|
148
|
+
if (typeof flags.id !== 'string')
|
|
149
|
+
die('Use phantom keys set --id <key-id>.');
|
|
150
|
+
const body = {};
|
|
151
|
+
const controls = { limit_usd: flags.limit, budget_usd: flags.budget, period_days: flags.period, rate_usd_per_min: flags.rate };
|
|
152
|
+
for (const [field, value] of Object.entries(controls))
|
|
153
|
+
if (value !== undefined)
|
|
154
|
+
body[field] = value === 'none' ? null : Number(value);
|
|
155
|
+
if (typeof flags.name === 'string')
|
|
156
|
+
body.name = flags.name;
|
|
157
|
+
if (typeof flags['policy-file'] === 'string')
|
|
158
|
+
body.route_policy = JSON.parse(readFileSync(flags['policy-file'], 'utf8'));
|
|
159
|
+
if (flags['clear-policy'])
|
|
160
|
+
body.route_policy = null;
|
|
161
|
+
const result = await wire(saved.base_url, 'PATCH', endpoint + '/keys/' + flags.id, body, saved.management_credential);
|
|
162
|
+
io.stdout(JSON.stringify(result) + '\n');
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
if (command === 'revoke-access') {
|
|
166
|
+
if (typeof flags.id !== 'string')
|
|
167
|
+
die('Use --id <management-id> from phantom workspace status.');
|
|
168
|
+
await wire(saved.base_url, 'DELETE', endpoint + '/access/' + flags.id, undefined, saved.management_credential);
|
|
169
|
+
io.stdout('Management access revoked.\n');
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
if (command === 'payment') {
|
|
173
|
+
if (typeof flags.id !== 'string')
|
|
174
|
+
die('Use --id <payment-id> from phantom workspace fund.');
|
|
175
|
+
io.stdout(JSON.stringify(await wire(saved.base_url, 'GET', endpoint + '/payments/' + flags.id, undefined, saved.management_credential)) + '\n');
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
if (command === 'fund') {
|
|
179
|
+
if (flags.pay)
|
|
180
|
+
die('--pay has been removed. Use --card or --coin.');
|
|
181
|
+
if (!flags.card && !flags.coin)
|
|
182
|
+
die('fund requires --card or --coin.');
|
|
183
|
+
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);
|
|
184
|
+
io.stdout(JSON.stringify(result) + '\n');
|
|
185
|
+
if (result.checkout_url && !flags['no-browser'])
|
|
186
|
+
openUrl(result.checkout_url);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
const view = await wire(saved.base_url, 'GET', endpoint, undefined, saved.management_credential);
|
|
190
|
+
io.stdout(JSON.stringify(argv[0] === 'keys' ? view.keys : view, null, 2) + '\n');
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
/** Forget local management credentials and revoke server management access, leaving server-side keys and recovery files valid. */
|
|
194
|
+
export async function forgetWorkspace(env, baseUrl) {
|
|
195
|
+
const file = credentialPath(env);
|
|
196
|
+
if (existsSync(file)) {
|
|
197
|
+
try {
|
|
198
|
+
const saved = JSON.parse(readSecret(env, file));
|
|
199
|
+
if (saved?.workspace_id && saved?.management_credential) {
|
|
200
|
+
const url = httpsOnly(baseUrl || env.PHANTOM_BASE_URL || saved.base_url || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
201
|
+
await wire(url, 'POST', '/workspaces/' + encodeURIComponent(saved.workspace_id) + '/logout', {}, saved.management_credential).catch(() => { });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Ignore read or parse errors during forget
|
|
206
|
+
}
|
|
207
|
+
removeSecret(file);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantomcodes/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI inference for your coding agents. Route models, control spending, and fund your workspace.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"phantom",
|
|
7
|
+
"api-key",
|
|
8
|
+
"agent",
|
|
9
|
+
"llm",
|
|
10
|
+
"inference",
|
|
11
|
+
"cli",
|
|
12
|
+
"mcp",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"agent-skill",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"cursor",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"solana",
|
|
21
|
+
"wallet",
|
|
22
|
+
"subagents"
|
|
23
|
+
],
|
|
24
|
+
"homepage": "https://phantom.codes/",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/connortessaro/phantom.git",
|
|
28
|
+
"directory": "packages/cli"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"bin": {
|
|
32
|
+
"phantom": "bin/phantom.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"dist",
|
|
37
|
+
"skills",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"jsonc-parser": "^3.3.1",
|
|
46
|
+
"smol-toml": "^1.9.0",
|
|
47
|
+
"zod": "^4.5.4"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p .",
|
|
51
|
+
"coverage": "vitest run --coverage",
|
|
52
|
+
"dev": "node src/phantom.mts",
|
|
53
|
+
"docs": "node scripts/gen-docs.mjs",
|
|
54
|
+
"prepare": "npm run build",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"typecheck": "tsc -p . --noEmit",
|
|
57
|
+
"version": "node scripts/sync-version.mjs && git add src/core/version.mts"
|
|
58
|
+
},
|
|
59
|
+
"pi": {
|
|
60
|
+
"skills": [
|
|
61
|
+
"./skills"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/connortessaro/phantom/issues"
|
|
66
|
+
},
|
|
67
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
68
|
+
"author": "Harbor Financial LLC",
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^24.19.1",
|
|
71
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
72
|
+
"typedoc": "^0.28.20",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"vitest": "^5.0.2"
|
|
75
|
+
},
|
|
76
|
+
"publishConfig": {
|
|
77
|
+
"access": "public"
|
|
78
|
+
},
|
|
79
|
+
"exports": {
|
|
80
|
+
".": {
|
|
81
|
+
"types": "./dist/phantom.d.mts",
|
|
82
|
+
"import": "./dist/phantom.mjs"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: phantom-ai
|
|
3
|
+
description: Use the phantom CLI to connect and run coding agents through Phantom AI, manage workspace funding and spending controls, and route models. Applies when the user requests Phantom inference or payment workflows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Phantom AI
|
|
7
|
+
|
|
8
|
+
Phantom AI serves model inference at `https://phantom.codes/v1`. The Phantom CLI connects
|
|
9
|
+
existing coding agents to that API, manages model routing to control costs, and
|
|
10
|
+
handles workspace funding and controls. Management commands
|
|
11
|
+
print JSON; add `--table` for supported human-readable output.
|
|
12
|
+
|
|
13
|
+
## Connect and run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
phantom login
|
|
17
|
+
phantom workspace status
|
|
18
|
+
phantom codex
|
|
19
|
+
phantom claude --key coding
|
|
20
|
+
phantom pi
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Login uses browser device approval and saves a key privately; ask the user to
|
|
24
|
+
complete approval themselves. For an existing key, have them run
|
|
25
|
+
`phantom login --paste` locally, never paste it into chat.
|
|
26
|
+
Agent configuration is temporary. The Phantom CLI flags go before agent arguments, or `--`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
phantom codex --key coding -- exec "review this repository"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Named-key selection uses `PHANTOM_KEY_NAME`, then `PHANTOM_API_KEY`, then the
|
|
33
|
+
saved login key. An agent's explicit `--key` selects that local named key.
|
|
34
|
+
|
|
35
|
+
## Connect existing agents for future runs
|
|
36
|
+
|
|
37
|
+
Offer saved setup when the user wants their installed agent to use Phantom AI
|
|
38
|
+
without a phantom launcher. Ask whether they want saved setup or a temporary run;
|
|
39
|
+
when their request already specifies one, follow it.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
phantom setup --dry-run --table
|
|
43
|
+
phantom setup
|
|
44
|
+
phantom setup --restore
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Preview and show the configuration changes before applying the setup the user
|
|
48
|
+
approves. Plain setup detects supported agents and asks before writing. Restart
|
|
49
|
+
the agent or open a new terminal afterward. It keeps backups for restore.
|
|
50
|
+
To connect only Codex, preview the targeted setup first:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
phantom setup --agent codex --provider --dry-run --table
|
|
54
|
+
phantom setup --agent codex --provider
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Targeted provider setup applies without its own confirmation; run it only after
|
|
58
|
+
the user approves the preview. Cursor needs manual settings steps; pi receives
|
|
59
|
+
the skill only. Setup wires up a saved key through helpers or environment
|
|
60
|
+
variables where supported; Cline stores it in a private config file.
|
|
61
|
+
If the user requested a budget, verify that the key the configured agent uses
|
|
62
|
+
has that cap. Creating a separate capped key does not change the default key.
|
|
63
|
+
|
|
64
|
+
## Fund a workspace
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
phantom fund --amount 10 --card
|
|
68
|
+
phantom fund --amount 10 --coin usdc
|
|
69
|
+
phantom workspace payment --id <payment-id>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Card checkout opens the payment URL in your browser. Crypto funding (`--coin usdc|usdt|sol`)
|
|
73
|
+
provides a Solana pay URL. Credit is available once confirmed. Check pending payments with
|
|
74
|
+
`phantom workspace payment --id <id>`. Check balance anytime with `phantom balance`.
|
|
75
|
+
|
|
76
|
+
## Control spending and keys
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
phantom keys create --save coding --limit 5
|
|
80
|
+
phantom keys list
|
|
81
|
+
phantom keys set --id <key-id> --budget 20 --period 30 --rate 1
|
|
82
|
+
phantom keys revoke --id <key-id>
|
|
83
|
+
phantom keys saved
|
|
84
|
+
phantom keys rm <name>
|
|
85
|
+
phantom balance
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use the full ID from workspace key metadata. Key controls are enforced by the
|
|
89
|
+
server; the rate is dollars per minute. Revocation preserves shared funds.
|
|
90
|
+
`keys saved` lists local metadata; `keys rm` only forgets the local copy.
|
|
91
|
+
|
|
92
|
+
## Route models
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
phantom route
|
|
96
|
+
phantom route set --models <model-a>,<model-b>
|
|
97
|
+
phantom route rule add --if has_tools=true --use first
|
|
98
|
+
phantom route test --table
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Routing affects `auto` requests. Use `PHANTOM_KEY_NAME=coding` to manage the same
|
|
102
|
+
key used by an agent with `--key coding`. Route tests are free.
|
|
103
|
+
|
|
104
|
+
## Pick phantom-1's models
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
phantom lineup --table
|
|
108
|
+
phantom lineup set --frontier <model> --economy-fallback <model>
|
|
109
|
+
phantom lineup reset
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each tier (economy, standard, frontier) takes a model from its own choices, which
|
|
113
|
+
`phantom lineup` lists. A fallback is the model to try when the tier's model fails.
|
|
114
|
+
`default` in place of a model puts that tier back to its default. `lineup set`
|
|
115
|
+
keeps the tiers you don't name.
|
|
116
|
+
|
|
117
|
+
## See what was spent
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
phantom spend --table
|
|
121
|
+
phantom spend --workspace --from 2026-09-01 --to 2026-09-30
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`spend` shows what the key spent by model and day, and an estimate of what `phantom-1`
|
|
125
|
+
saved against its frontier model. `--workspace` shows every key in the workspace by
|
|
126
|
+
name. Dates are UTC days (`YYYY-MM-DD`); the default is this month.
|
|
127
|
+
|
|
128
|
+
## Plans
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
phantom plan --table
|
|
132
|
+
phantom plan pro
|
|
133
|
+
phantom plan manage
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A Pro or Max plan pays for `phantom-1` calls up to an amount of model cost in any 7 days,
|
|
137
|
+
then the key's credit pays. `plan` shows the plan and the week's use. `plan pro` and
|
|
138
|
+
`plan max` print a Stripe Checkout link (and open it unless `--no-browser`); the user
|
|
139
|
+
pays there. `plan manage` opens Stripe's billing page. Naming a model, not `phantom-1`,
|
|
140
|
+
still spends credit.
|
|
141
|
+
|
|
142
|
+
## Advanced tasks
|
|
143
|
+
|
|
144
|
+
Use `phantom --help --all` for receipts, MCP and direct-key purchases.
|
|
145
|
+
Export guest recovery with `phantom workspace export --file
|
|
146
|
+
<path>` only to a private destination. Recovery grants workspace management access.
|
|
147
|
+
|
|
148
|
+
## Rules
|
|
149
|
+
|
|
150
|
+
- Never print, log or repeat API keys, wallet secrets, management credentials or recovery files. `phantom keys show` is for shell integration, not model context.
|
|
151
|
+
- Use CLI commands to manage state; do not manually edit or delete its files.
|
|
152
|
+
- Ask before payments the user has not requested. Existing task approval covers only the requested payment and amount.
|
|
153
|
+
- Run permanent `phantom setup` changes only when requested; otherwise preview with `--dry-run`.
|
|
154
|
+
- Use a user's existing subscription agent when they request that workflow. Phantom inference requires their intent.
|
|
155
|
+
- Exit code 2 means a rejected key; report it rather than retrying unchanged credentials.
|