@phantomcodes/cli 1.0.0 → 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 +9 -9
- package/README.md +41 -1
- package/dist/agents/command.mjs +3 -1
- package/dist/cli/commands.mjs +29 -4
- package/dist/core/request.mjs +45 -15
- package/dist/core/version.mjs +1 -1
- package/dist/phantom.mjs +4 -1
- package/dist/plan/command.mjs +68 -0
- package/dist/setup/agents.mjs +2 -13
- package/dist/setup/apply.mjs +6 -14
- package/dist/setup/command.mjs +1 -3
- package/dist/setup/plan.mjs +3 -24
- package/dist/setup/shared.mjs +0 -8
- package/dist/spend/command.mjs +67 -0
- package/dist/spend/lineup.mjs +20 -0
- package/dist/spend/report.mjs +61 -0
- package/dist/spend/routing.mjs +1 -1
- package/dist/workspace/command.mjs +5 -0
- package/package.json +2 -2
- package/skills/phantom-ai/SKILL.md +38 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Phantom Proprietary License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 Harbor Financial LLC. All rights reserved.
|
|
4
4
|
|
|
5
5
|
This license covers the Phantom source code, the Phantom website, and the
|
|
6
6
|
Phantom CLI command-line tool ("the CLI"), together "the Software". It
|
|
@@ -8,7 +8,7 @@ applies to every copy of the Software that carries this file.
|
|
|
8
8
|
|
|
9
9
|
1. Ownership
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Harbor Financial LLC owns all right, title and interest in the Software,
|
|
12
12
|
including all copyrights and other intellectual property rights. Nothing
|
|
13
13
|
in this license transfers ownership to you.
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ applies to every copy of the Software that carries this file.
|
|
|
16
16
|
|
|
17
17
|
This license grants no rights in the Phantom source code, including the
|
|
18
18
|
code behind the website. You may not use, copy, modify or distribute any
|
|
19
|
-
part of it unless
|
|
19
|
+
part of it unless Harbor Financial LLC gives you written permission. The only
|
|
20
20
|
permission this license grants is the one in section 3, and it covers the
|
|
21
21
|
CLI alone. Visiting phantom.codes and using the Phantom service are
|
|
22
22
|
governed by the terms of service at phantom.codes/terms, not by this
|
|
@@ -24,7 +24,7 @@ applies to every copy of the Software that carries this file.
|
|
|
24
24
|
|
|
25
25
|
3. Limited permission for the Phantom CLI
|
|
26
26
|
|
|
27
|
-
If you got the CLI from
|
|
27
|
+
If you got the CLI from Harbor Financial LLC's official release (the
|
|
28
28
|
@phantomcodes/cli package on npm), you may install and run unmodified
|
|
29
29
|
copies of it, only to access the Phantom service. This permission is
|
|
30
30
|
revocable, non-exclusive, non-transferable and non-sublicensable.
|
|
@@ -46,14 +46,14 @@ applies to every copy of the Software that carries this file.
|
|
|
46
46
|
5. Confidentiality
|
|
47
47
|
|
|
48
48
|
The non-public parts of the Software, including its source code, routing
|
|
49
|
-
logic and design, are
|
|
50
|
-
must keep them confidential and use them only as
|
|
49
|
+
logic and design, are Harbor Financial LLC's trade secrets. If you see them, you
|
|
50
|
+
must keep them confidential and use them only as Harbor Financial LLC allows in
|
|
51
51
|
writing.
|
|
52
52
|
|
|
53
53
|
6. Termination
|
|
54
54
|
|
|
55
55
|
This license, and any permission under section 3, ends automatically if
|
|
56
|
-
you break any of its terms.
|
|
56
|
+
you break any of its terms. Harbor Financial LLC may also end the section 3
|
|
57
57
|
permission at any time. When it ends, you must stop using the Software and
|
|
58
58
|
destroy all copies you have.
|
|
59
59
|
|
|
@@ -65,10 +65,10 @@ applies to every copy of the Software that carries this file.
|
|
|
65
65
|
|
|
66
66
|
8. Limitation of liability
|
|
67
67
|
|
|
68
|
-
TO THE FULLEST EXTENT THE LAW ALLOWS,
|
|
68
|
+
TO THE FULLEST EXTENT THE LAW ALLOWS, HARBOR FINANCIAL LLC IS NOT LIABLE FOR ANY
|
|
69
69
|
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR FOR
|
|
70
70
|
ANY LOSS OF DATA, PROFITS OR REVENUE, ARISING FROM THE SOFTWARE OR THIS
|
|
71
|
-
LICENSE.
|
|
71
|
+
LICENSE. ITS TOTAL LIABILITY UNDER THIS LICENSE IS LIMITED TO THE AMOUNT
|
|
72
72
|
YOU PAID FOR THE SOFTWARE, IF ANY.
|
|
73
73
|
|
|
74
74
|
9. Governing law
|
package/README.md
CHANGED
|
@@ -132,6 +132,45 @@ Prefer a specific model? Select it in your agent. Policies that apply only to
|
|
|
132
132
|
`auto` leave explicit model choices alone.
|
|
133
133
|
[Routing](docs/guides/routing.md) covers conditions, fallback and conversation affinity.
|
|
134
134
|
|
|
135
|
+
`phantom-1` picks a tier for each request: economy, standard or frontier. Your key's
|
|
136
|
+
lineup picks the model in each tier, and a fallback model to try when it fails:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
phantom lineup --table
|
|
140
|
+
phantom lineup set --frontier <model> --economy-fallback <model>
|
|
141
|
+
phantom lineup reset
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`lineup` shows the choices for each tier. `default` in place of a model puts that tier
|
|
145
|
+
back to its default.
|
|
146
|
+
|
|
147
|
+
See what a key spent, and what `phantom-1` saved against always running its frontier
|
|
148
|
+
model:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
phantom spend --table
|
|
152
|
+
phantom spend --from 2026-09-01 --to 2026-09-30
|
|
153
|
+
phantom spend --workspace --table
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The dates are UTC days and the default is this month. `--workspace` lists every key in
|
|
157
|
+
your workspace by name; without it you see the key you run as. The saving is an
|
|
158
|
+
estimate: the same tokens at the frontier model's price today.
|
|
159
|
+
|
|
160
|
+
A Pro ($20 a month) or Max ($100 a month) plan pays for `phantom-1` calls up to an amount
|
|
161
|
+
of model cost in any 7 days, then your credit pays:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
phantom plan --table
|
|
165
|
+
phantom plan pro
|
|
166
|
+
phantom plan manage
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`plan` shows the key's plan and how much of the last 7 days it has paid for. `plan pro`
|
|
170
|
+
and `plan max` print a Stripe Checkout link that puts the plan on this key and open it.
|
|
171
|
+
`plan manage` opens Stripe's billing page, where the card changes, the plan switches and
|
|
172
|
+
it cancels. A named model, not `phantom-1`, still spends credit.
|
|
173
|
+
|
|
135
174
|
## Commands
|
|
136
175
|
|
|
137
176
|
| Task | Commands |
|
|
@@ -139,10 +178,11 @@ Prefer a specific model? Select it in your agent. Policies that apply only to
|
|
|
139
178
|
| Connect | `phantom login`, `phantom logout` |
|
|
140
179
|
| Connect existing agents | `phantom setup`, `phantom setup --dry-run`, `phantom setup --restore` |
|
|
141
180
|
| Run inference | `phantom claude`, `phantom codex`, `phantom pi` |
|
|
142
|
-
| Fund and check spending | `phantom workspace fund`, `phantom workspace status`, `phantom balance` |
|
|
181
|
+
| Fund and check spending | `phantom workspace fund`, `phantom workspace status`, `phantom balance`, `phantom spend` |
|
|
143
182
|
| Manage workspace keys | `phantom keys create`, `phantom keys set`, `phantom keys rotate`, `phantom keys revoke` |
|
|
144
183
|
| Manage local copies | `phantom keys saved`, `phantom keys save`, `phantom keys rm` |
|
|
145
184
|
| Route models | `phantom route` |
|
|
185
|
+
| Pick phantom-1's models | `phantom lineup`, `phantom lineup set`, `phantom lineup reset` |
|
|
146
186
|
|
|
147
187
|
Put phantom flags before agent arguments; use `--` to separate them:
|
|
148
188
|
|
package/dist/agents/command.mjs
CHANGED
|
@@ -7,8 +7,10 @@ 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
9
|
const DEFAULT_MODEL = 'auto';
|
|
10
|
+
/** 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. */
|
|
10
11
|
async function keyStatus(apiKey, baseUrl) {
|
|
11
|
-
|
|
12
|
+
const s = await request('GET', '/key/balance', apiKey, undefined, baseUrl);
|
|
13
|
+
return Number.isFinite(s.credit_spent_usd) && Number.isFinite(s.credit_balance_usd) ? s : null;
|
|
12
14
|
}
|
|
13
15
|
import { proxyToken, startProxy } from "./proxy.mjs";
|
|
14
16
|
const hasModel = (args) => args.some((a) => a === '-m' || a === '--model' || a.startsWith('--model=') || /^model=/.test(a));
|
package/dist/cli/commands.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { workspaceCommand, workspaceLogin, forgetWorkspace } from "../workspace/command.mjs";
|
|
2
2
|
import { agentCommand } from "../agents/command.mjs";
|
|
3
3
|
import { balanceCommand, keyCommand, loginCommand, logoutCommand } from "../keys/command.mjs";
|
|
4
|
-
import { routeCommand } from "../spend/command.mjs";
|
|
4
|
+
import { lineupCommand, routeCommand } from "../spend/command.mjs";
|
|
5
|
+
import { spendCommand } from "../spend/report.mjs";
|
|
6
|
+
import { planCommand } from "../plan/command.mjs";
|
|
5
7
|
import { verifyCommand } from "../receipts/command.mjs";
|
|
6
8
|
import { setupCommand } from "../setup/command.mjs";
|
|
7
9
|
import { SETUP_MODEL } from "../setup/shared.mjs";
|
|
@@ -43,7 +45,7 @@ export const COMMANDS = [
|
|
|
43
45
|
{
|
|
44
46
|
name: 'fund', handler: fundDispatch, keyless: true,
|
|
45
47
|
help: ` fund --amount <USD> [--card | --coin usdc|usdt|sol] [--no-browser]
|
|
46
|
-
fund your workspace balance with card or crypto`,
|
|
48
|
+
fund your workspace balance with card (plus Stripe's fee) or crypto`,
|
|
47
49
|
},
|
|
48
50
|
{
|
|
49
51
|
name: 'keys', handler: workspaceCommand, keyless: true,
|
|
@@ -76,6 +78,19 @@ export const COMMANDS = [
|
|
|
76
78
|
name: 'balance', handler: balanceCommand,
|
|
77
79
|
help: ` balance show credit balance and expiry`,
|
|
78
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'spend', handler: spendCommand, keyless: ['--workspace'],
|
|
83
|
+
help: ` spend [--from <date>] [--to <date>] what this key spent, by model and day, and what phantom-1
|
|
84
|
+
saved; dates are UTC days as YYYY-MM-DD, default this month
|
|
85
|
+
spend --workspace every key in the workspace by name (needs phantom login)`,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
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
|
|
92
|
+
plan manage [--no-browser] the same for Stripe's billing page: card, switch plan, cancel`,
|
|
93
|
+
},
|
|
79
94
|
{
|
|
80
95
|
name: 'key', advanced: true, handler: keyCommand, keyless: true,
|
|
81
96
|
help: ` key list [--balance] saved keys, by name and id
|
|
@@ -99,6 +114,16 @@ export const COMMANDS = [
|
|
|
99
114
|
route rule rm <n> remove rule n
|
|
100
115
|
route test [--model auto] which model a request would get now (free)
|
|
101
116
|
route clear remove the policy`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'lineup', handler: lineupCommand,
|
|
120
|
+
help: ` lineup [get] show the model phantom-1 runs in each tier, and the choices
|
|
121
|
+
lineup set --economy <model> pick the model for a tier; "default" goes back to the default
|
|
122
|
+
[--standard <model>] [--frontier <model>]
|
|
123
|
+
[--economy-fallback <model>] [--standard-fallback <model>]
|
|
124
|
+
[--frontier-fallback <model>]
|
|
125
|
+
the model to try when a tier's model fails
|
|
126
|
+
lineup reset every tier back to its default`,
|
|
102
127
|
},
|
|
103
128
|
{
|
|
104
129
|
name: 'logout', handler: logoutDispatch, keyless: true,
|
|
@@ -112,9 +137,9 @@ export const COMMANDS = [
|
|
|
112
137
|
[--yes] don't ask
|
|
113
138
|
[--dry-run] show the changes, write nothing
|
|
114
139
|
[--model <id>] the model each agent starts on (default ${SETUP_MODEL})
|
|
115
|
-
setup --agent <name>
|
|
140
|
+
setup --agent <name> install the skill for one agent, without asking:
|
|
116
141
|
${orList(SETUP_NAMES)}
|
|
117
|
-
or all
|
|
142
|
+
or all
|
|
118
143
|
setup [--agent <name>] --provider point that agent's model calls at Phantom AI;
|
|
119
144
|
--provider off removes only what phantom wrote
|
|
120
145
|
setup --restore [--agent <name>] put back the files setup changed, from the
|
package/dist/core/request.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PhantomApiError, die } from "./errors.mjs";
|
|
2
2
|
/** The Phantom AI API address phantom calls when PHANTOM_BASE_URL is unset. */
|
|
3
3
|
export const DEFAULT_BASE_URL = 'https://phantom.codes/v1';
|
|
4
|
+
/** How long {@link request} waits for the whole answer, in milliseconds, before it gives up. */
|
|
5
|
+
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
4
6
|
/**
|
|
5
7
|
* Calls the Phantom AI API with the key as a Bearer token and returns the
|
|
6
8
|
* JSON answer. Every client function in phantom goes through it.
|
|
@@ -10,20 +12,38 @@ export const DEFAULT_BASE_URL = 'https://phantom.codes/v1';
|
|
|
10
12
|
* @param body Sent as JSON when given.
|
|
11
13
|
* @param baseUrl The API base. Defaults to PHANTOM_BASE_URL, then {@link DEFAULT_BASE_URL}. Must be https ({@link httpsOnly}).
|
|
12
14
|
* @param headers Extra headers to send.
|
|
15
|
+
* @param timeoutMs How long to wait for the answer. Defaults to {@link REQUEST_TIMEOUT_MS}.
|
|
13
16
|
* @throws {@link PhantomApiError} when the API answers with an error status.
|
|
17
|
+
* @throws {@link CliError} with code `request_timeout` when no answer arrives in time.
|
|
14
18
|
*/
|
|
15
|
-
export async function request(method, path, apiKey, body, baseUrl, headers = {}) {
|
|
19
|
+
export async function request(method, path, apiKey, body, baseUrl, headers = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
16
20
|
const url = `${apiBase(baseUrl)}${path}`;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
22
|
+
let res;
|
|
23
|
+
let json;
|
|
24
|
+
try {
|
|
25
|
+
res = await fetch(url, {
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${apiKey}`,
|
|
29
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
30
|
+
...headers,
|
|
31
|
+
},
|
|
32
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
33
|
+
signal,
|
|
34
|
+
});
|
|
35
|
+
// The deadline covers the body too: a server can send headers and then stall.
|
|
36
|
+
json = await res.json().catch((err) => {
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
throw err;
|
|
39
|
+
return {};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
die(`Phantom AI did not answer ${method} ${path} within ${Math.round(timeoutMs / 1000)} seconds. Check your connection and try again.`, 'request_timeout');
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
27
47
|
if (!res.ok)
|
|
28
48
|
throw apiError(res.status, json);
|
|
29
49
|
return json;
|
|
@@ -51,6 +71,7 @@ export function httpsOnly(baseUrl) {
|
|
|
51
71
|
return baseUrl;
|
|
52
72
|
}
|
|
53
73
|
export function apiError(status, json) {
|
|
74
|
+
json = json && typeof json === 'object' ? json : {};
|
|
54
75
|
const err = json.error;
|
|
55
76
|
let code = String(status);
|
|
56
77
|
let msg = `HTTP ${status}`;
|
|
@@ -60,7 +81,9 @@ export function apiError(status, json) {
|
|
|
60
81
|
msg = typeof e.message === 'string' ? e.message : msg;
|
|
61
82
|
}
|
|
62
83
|
else if (typeof err === 'string') {
|
|
63
|
-
|
|
84
|
+
// The plan routes answer { error: <code>, detail: <words> }.
|
|
85
|
+
const detail = json.detail;
|
|
86
|
+
msg = typeof detail === 'string' && detail ? detail : err;
|
|
64
87
|
code = err;
|
|
65
88
|
}
|
|
66
89
|
else if (typeof json.detail === 'string') {
|
|
@@ -72,7 +95,14 @@ export function apiError(status, json) {
|
|
|
72
95
|
}
|
|
73
96
|
return new PhantomApiError(status, code, msg);
|
|
74
97
|
}
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Credit left, credit spent and expiry of a key (GET /key/balance).
|
|
100
|
+
* @throws {@link CliError} with code `balance_unreadable` when the answer has no numbers, such as a sign-in page that came back with status 200.
|
|
101
|
+
*/
|
|
102
|
+
export async function getBalance(apiKey, baseUrl) {
|
|
103
|
+
const balance = await request('GET', '/key/balance', apiKey, undefined, baseUrl);
|
|
104
|
+
if (!balance || !Number.isFinite(balance.credit_balance_usd) || !Number.isFinite(balance.credit_spent_usd)) {
|
|
105
|
+
die('The balance answer had no credit numbers. Check PHANTOM_BASE_URL, and that the address answers with the Phantom AI API.', 'balance_unreadable');
|
|
106
|
+
}
|
|
107
|
+
return balance;
|
|
78
108
|
}
|
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
|
+
export const VERSION = '1.1.0';
|
package/dist/phantom.mjs
CHANGED
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { realpathSync } from 'node:fs';
|
|
16
16
|
import { run } from "./cli/run.mjs";
|
|
17
|
-
export { DEFAULT_BASE_URL, request, httpsOnly, getBalance } from "./core/request.mjs";
|
|
17
|
+
export { DEFAULT_BASE_URL, REQUEST_TIMEOUT_MS, request, httpsOnly, getBalance } from "./core/request.mjs";
|
|
18
18
|
export { VERSION } from "./core/version.mjs";
|
|
19
19
|
export { PhantomApiError, CliError, die } from "./core/errors.mjs";
|
|
20
|
+
export { getLineup, putLineup, resetLineup, LINEUP_TIERS } from "./spend/lineup.mjs";
|
|
20
21
|
export { getRoute, putRoute, patchRoute, clearRoute, testRoute, parseCondition } from "./spend/routing.mjs";
|
|
22
|
+
export { getSpend, tableSpend } from "./spend/report.mjs";
|
|
23
|
+
export { getPlan, planCheckout, planPortal, tablePlan } from "./plan/command.mjs";
|
|
21
24
|
export { checkReceipt, modelsMatch, verifyModel } from "./receipts/receipts.mjs";
|
|
22
25
|
export { keychain } from "./core/secrets.mjs";
|
|
23
26
|
export { keyId, keySource, resolveApiKey, readNamedKey, saveNamedKey, removeNamedKey, listNamedKeys, saveApiKey, removeApiKey } from "./keys/saved.mjs";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { die } from "../core/errors.mjs";
|
|
2
|
+
import { openUrl, out } from "../core/helpers.mjs";
|
|
3
|
+
import { request } from "../core/request.mjs";
|
|
4
|
+
/** The key's plan and its week (GET /plan). */
|
|
5
|
+
export function getPlan(apiKey, baseUrl) {
|
|
6
|
+
return request('GET', '/plan', apiKey, undefined, baseUrl);
|
|
7
|
+
}
|
|
8
|
+
/** A Stripe Checkout link that puts `plan` on the key (POST /plan/checkout). */
|
|
9
|
+
export function planCheckout(apiKey, plan, baseUrl) {
|
|
10
|
+
return request('POST', '/plan/checkout', apiKey, { plan }, baseUrl);
|
|
11
|
+
}
|
|
12
|
+
/** A link to Stripe's billing page for the key's plan (POST /plan/portal). */
|
|
13
|
+
export function planPortal(apiKey, baseUrl) {
|
|
14
|
+
return request('POST', '/plan/portal', apiKey, undefined, baseUrl);
|
|
15
|
+
}
|
|
16
|
+
const dollars = (n) => `$${n.toFixed(2)}`;
|
|
17
|
+
/** `--table` output for `plan`. */
|
|
18
|
+
export function tablePlan(d) {
|
|
19
|
+
const p = d;
|
|
20
|
+
if (!p.plan) {
|
|
21
|
+
return p.available
|
|
22
|
+
? 'plan none\nsubscribe phantom plan pro or phantom plan max'
|
|
23
|
+
: 'plan none (plans are not available right now)';
|
|
24
|
+
}
|
|
25
|
+
const lines = [
|
|
26
|
+
`plan ${p.plan === 'max' ? 'Max' : 'Pro'}, on this ${p.scope}`,
|
|
27
|
+
`status ${p.status}`,
|
|
28
|
+
`used ${dollars(p.week_used_usd)} of ${dollars(p.week_limit_usd)} in the last 7 days (model cost, before markup)`,
|
|
29
|
+
];
|
|
30
|
+
if (p.resets_at)
|
|
31
|
+
lines.push(`room starts to free up ${p.resets_at}`);
|
|
32
|
+
if (p.renews_at)
|
|
33
|
+
lines.push(`period ends ${p.renews_at}`);
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
/** What `phantom plan` accepts after `plan`. */
|
|
37
|
+
const USAGE = 'Use phantom plan, phantom plan pro, phantom plan max or phantom plan manage.';
|
|
38
|
+
/**
|
|
39
|
+
* `phantom plan`: the key's plan and how much of the week is used.
|
|
40
|
+
* `phantom plan pro|max` prints a Stripe Checkout link that puts the plan on
|
|
41
|
+
* this key, and opens it unless `--no-browser`. `phantom plan manage` does the
|
|
42
|
+
* same for Stripe's billing page, where the card changes, the plan switches
|
|
43
|
+
* and it cancels. Run by `run()`.
|
|
44
|
+
*/
|
|
45
|
+
export async function planCommand(input) {
|
|
46
|
+
const { io, apiKey, table, baseUrl, flags, subcommand } = input;
|
|
47
|
+
const open = (url) => {
|
|
48
|
+
if (!flags['no-browser'])
|
|
49
|
+
openUrl(url);
|
|
50
|
+
};
|
|
51
|
+
if (subcommand === undefined || subcommand === 'status') {
|
|
52
|
+
out(await getPlan(apiKey, baseUrl), table, tablePlan, io.stdout);
|
|
53
|
+
}
|
|
54
|
+
else if (subcommand === 'pro' || subcommand === 'max') {
|
|
55
|
+
const result = await planCheckout(apiKey, subcommand, baseUrl);
|
|
56
|
+
out(result, table, (d) => `Open this link to pay: ${d.checkout_url}`, io.stdout);
|
|
57
|
+
open(result.checkout_url);
|
|
58
|
+
}
|
|
59
|
+
else if (subcommand === 'manage') {
|
|
60
|
+
const result = await planPortal(apiKey, baseUrl);
|
|
61
|
+
out(result, table, (d) => `Open this link to manage billing: ${d.portal_url}`, io.stdout);
|
|
62
|
+
open(result.portal_url);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
die(`Unknown plan command "${subcommand}". ${USAGE}`, 'plan_usage');
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
package/dist/setup/agents.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
5
|
import { die } from "../core/errors.mjs";
|
|
6
6
|
import { resolveApiKey } from "../keys/saved.mjs";
|
|
7
|
-
import { SETUP_MODEL, agentBaseUrl,
|
|
7
|
+
import { SETUP_MODEL, agentBaseUrl, readText, configHome, onPath, phantomCommand, offResult, unreadable, SETUP_MARK, SETUP_CONTEXT_WINDOW, } from "./shared.mjs";
|
|
8
8
|
import { jsonObject, jsonEdit, tomlSet, tomlRemoveOurs, yamlSetTop, yamlAddModel, yamlRemoveOurs } from "./editors.mjs";
|
|
9
9
|
// ── Manual Provider Helper ───────────────────────────────────────────────────
|
|
10
10
|
/** An agent whose model settings live on its own settings screen: setup prints the steps. */
|
|
@@ -318,7 +318,7 @@ export function clineProvider(ctx, off) {
|
|
|
318
318
|
* @deprecated Compatibility export. Use {@link planSetup}, {@link applySetup}, and {@link restoreSetup} for agent setup.
|
|
319
319
|
*/
|
|
320
320
|
export const SETUP_AGENTS = {
|
|
321
|
-
/** pi.
|
|
321
|
+
/** pi. It uses the CLI through the skill. */
|
|
322
322
|
pi: {
|
|
323
323
|
label: 'pi',
|
|
324
324
|
/** ~/.pi */
|
|
@@ -334,7 +334,6 @@ export const SETUP_AGENTS = {
|
|
|
334
334
|
detect: (env, home) => existsSync(codexDir(env, home)),
|
|
335
335
|
/** ~/.agents/skills */
|
|
336
336
|
skills: '.agents/skills',
|
|
337
|
-
mcp: mcpByCli('codex'),
|
|
338
337
|
provider: codexProvider,
|
|
339
338
|
shellEnv: () => ({}),
|
|
340
339
|
},
|
|
@@ -346,7 +345,6 @@ export const SETUP_AGENTS = {
|
|
|
346
345
|
detect: (env, home) => existsSync(claudeDir(env, home)),
|
|
347
346
|
/** ~/.claude/skills */
|
|
348
347
|
skills: '.claude/skills',
|
|
349
|
-
mcp: mcpByCli('claude'),
|
|
350
348
|
provider: (ctx, off) => claudePlan(ctx.env, { off, model: ctx.model, onPath: ctx.onPath }),
|
|
351
349
|
},
|
|
352
350
|
/** Cursor. Its models are set on its settings screen; the shell block gives it PHANTOM_API_KEY to copy. */
|
|
@@ -356,15 +354,6 @@ export const SETUP_AGENTS = {
|
|
|
356
354
|
home: '.cursor',
|
|
357
355
|
/** ~/.cursor/skills */
|
|
358
356
|
skills: '.cursor/skills',
|
|
359
|
-
/** Merges the server into ~/.cursor/mcp.json, keeping the others. */
|
|
360
|
-
mcp: (ctx, add) => {
|
|
361
|
-
const server = { command: 'npx', args: MCP_ARGS };
|
|
362
|
-
const mcp = `add to ~/.cursor/mcp.json: ${JSON.stringify({ mcpServers: { phantom: server } })}`;
|
|
363
|
-
if (!add)
|
|
364
|
-
return { mcp };
|
|
365
|
-
const file = path.join(ctx.home, '.cursor', 'mcp.json');
|
|
366
|
-
return { mcp, edits: [{ file, after: jsonEdit(file, readText(file), [[['mcpServers', 'phantom'], server]]) }] };
|
|
367
|
-
},
|
|
368
357
|
provider: manualProvider('Cursor Settings, Models', (ctx) => [
|
|
369
358
|
'Open Cursor Settings (Cmd+Shift+J on macOS), then Models.',
|
|
370
359
|
`Under OpenAI API Key, paste your Phantom AI key, then turn on Override OpenAI Base URL and set it to ${agentBaseUrl(ctx.env, 'cursor')}`,
|
package/dist/setup/apply.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
5
4
|
import { stateDir } from "../core/state.mjs";
|
|
6
5
|
import { readText } from "./shared.mjs";
|
|
7
6
|
import { jsonText } from "./editors.mjs";
|
|
@@ -64,8 +63,6 @@ export function setupPreview(plan) {
|
|
|
64
63
|
else
|
|
65
64
|
parts.push(`${f.action} ${f.file}\n${maskKeys(lineDiff(f.before, f.after))}`);
|
|
66
65
|
}
|
|
67
|
-
for (const r of plan.runs)
|
|
68
|
-
parts.push(`run ${r.cmd} ${r.args.join(' ')}`);
|
|
69
66
|
for (const s of plan.skipped)
|
|
70
67
|
parts.push(`skip ${s.agent}: ${s.reason}`);
|
|
71
68
|
for (const a of plan.agents) {
|
|
@@ -97,11 +94,9 @@ const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
|
97
94
|
* Writes a plan from {@link planSetup}. Before it edits a file it copies it
|
|
98
95
|
* to `<file>.phantom.bak`, unless the file is still as phantom last wrote it: then the
|
|
99
96
|
* copy from before phantom's first change stays. A file that changed after the
|
|
100
|
-
* plan read it is left alone and listed in `skipped`.
|
|
101
|
-
* commands.
|
|
102
|
-
* @param exec Runs an agent's CLI, such as `claude mcp add`.
|
|
97
|
+
* plan read it is left alone and listed in `skipped`.
|
|
103
98
|
*/
|
|
104
|
-
export function applySetup(env, plan
|
|
99
|
+
export function applySetup(env, plan) {
|
|
105
100
|
const backups = readBackups(env);
|
|
106
101
|
const skipped = [...plan.skipped];
|
|
107
102
|
const files = [];
|
|
@@ -138,10 +133,7 @@ export function applySetup(env, plan, exec = (cmd, args) => execFileSync(cmd, ar
|
|
|
138
133
|
}
|
|
139
134
|
if (plan.files.some((f) => !f.skill))
|
|
140
135
|
writeBackups(env, backups);
|
|
141
|
-
|
|
142
|
-
exec(r.cmd, r.args);
|
|
143
|
-
const agents = plan.agents.map((a) => ({ ...a, mcp_added: plan.mcp.includes(a.agent) }));
|
|
144
|
-
return { agents, files, ...(skipped.length ? { skipped } : {}), ...(plan.notes.length ? { notes: plan.notes } : {}) };
|
|
136
|
+
return { agents: plan.agents, files, ...(skipped.length ? { skipped } : {}), ...(plan.notes.length ? { notes: plan.notes } : {}) };
|
|
145
137
|
}
|
|
146
138
|
/**
|
|
147
139
|
* Points Claude Code at Phantom AI, or removes phantom's settings with `off`.
|
|
@@ -155,7 +147,7 @@ export function claudeProvider(env, opts = {}) {
|
|
|
155
147
|
const before = readText(edit.file);
|
|
156
148
|
return { ...edit, before, agent: 'claude', action: before === null ? 'create' : edit.after === null ? 'remove' : 'update' };
|
|
157
149
|
});
|
|
158
|
-
applySetup(env, { agents: [], files,
|
|
150
|
+
applySetup(env, { agents: [], files, skipped: [], notes: [] });
|
|
159
151
|
return provider.result;
|
|
160
152
|
}
|
|
161
153
|
/** A plan as `--dry-run` reports it: each file that would change, with its diff, and nothing written. */
|
|
@@ -170,12 +162,12 @@ export function previewSetup(plan) {
|
|
|
170
162
|
}
|
|
171
163
|
/**
|
|
172
164
|
* Installs the phantom-ai skill for one agent, or for every agent found
|
|
173
|
-
* (see {@link SETUP_AGENTS}), and with `
|
|
165
|
+
* (see {@link SETUP_AGENTS}), and with `provider` sets that up
|
|
174
166
|
* too, without asking: {@link planSetup}, then {@link applySetup}.
|
|
175
167
|
* @throws {@link CliError} when no agent is found, the agent is unknown, or `provider` is set without an agent that supports it.
|
|
176
168
|
*/
|
|
177
169
|
export function setupAgents(env, opts = {}) {
|
|
178
|
-
return applySetup(env, planSetup(env, opts)
|
|
170
|
+
return applySetup(env, planSetup(env, opts));
|
|
179
171
|
}
|
|
180
172
|
/**
|
|
181
173
|
* Puts back the files `phantom setup` changed, from the copies it kept
|
package/dist/setup/command.mjs
CHANGED
|
@@ -13,7 +13,6 @@ export function tableSetup(d) {
|
|
|
13
13
|
const p = a.provider;
|
|
14
14
|
return [
|
|
15
15
|
`${a.agent.padEnd(16)}${a.skill ? `skill ${r.dry_run || r.applied === false ? 'goes' : 'installed'} at ${a.skill}` : 'no skill (this agent does not read skills)'}`,
|
|
16
|
-
...(a.mcp_added ? [`${pad}MCP server added`] : a.mcp ? [`${pad}MCP (optional): ${a.mcp}`] : []),
|
|
17
16
|
...(!p
|
|
18
17
|
? []
|
|
19
18
|
: p.manual
|
|
@@ -74,10 +73,9 @@ export async function setupCommand({ env, io, flags, table }) {
|
|
|
74
73
|
}
|
|
75
74
|
// Plain `phantom setup` sets up everything it finds and asks first. Any of
|
|
76
75
|
// the older flags runs just what they name, without asking.
|
|
77
|
-
const guided = agent === undefined && provider === undefined
|
|
76
|
+
const guided = agent === undefined && provider === undefined;
|
|
78
77
|
const plan = planSetup(env, {
|
|
79
78
|
agent,
|
|
80
|
-
mcp: Boolean(flags['mcp']),
|
|
81
79
|
provider: guided || provider === true ? true : provider === 'off' ? 'off' : false,
|
|
82
80
|
model: typeof model === 'string' ? model : undefined,
|
|
83
81
|
guided,
|
package/dist/setup/plan.mjs
CHANGED
|
@@ -18,7 +18,7 @@ function skillSource() {
|
|
|
18
18
|
const isSkip = (err) => err instanceof CliError && err.code === 'setup_unreadable';
|
|
19
19
|
/**
|
|
20
20
|
* Works out what setup would change, and writes nothing: the skill for each
|
|
21
|
-
* agent ({@link SETUP_AGENTS}),
|
|
21
|
+
* agent ({@link SETUP_AGENTS}), each agent's model
|
|
22
22
|
* settings with `provider` ({@link SetupAgentSpec.provider}), and the shell
|
|
23
23
|
* block ({@link shellBlock}) for agents that read the key from the
|
|
24
24
|
* environment. A config file setup can't edit safely is left out and listed
|
|
@@ -61,8 +61,6 @@ export function planSetup(env, opts = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
const ctx = { env, home, onPath: opts.onPath, model: opts.model, shellRc: shellRcFile(env, home) };
|
|
63
63
|
const files = [];
|
|
64
|
-
const runs = [];
|
|
65
|
-
const mcpAdds = [];
|
|
66
64
|
const skipped = [];
|
|
67
65
|
const agents = [];
|
|
68
66
|
const add = (agent, file, after, skill = false, mode) => {
|
|
@@ -87,25 +85,6 @@ export function planSetup(env, opts = {}) {
|
|
|
87
85
|
const skill = spec.skills ? path.join(home, spec.skills, 'phantom-ai', 'SKILL.md') : null;
|
|
88
86
|
if (skill)
|
|
89
87
|
add(agent, skill, skillText, true);
|
|
90
|
-
let mcp = null;
|
|
91
|
-
if (spec.mcp) {
|
|
92
|
-
try {
|
|
93
|
-
const m = spec.mcp(ctx, Boolean(opts.mcp));
|
|
94
|
-
mcp = m.mcp;
|
|
95
|
-
if (opts.mcp) {
|
|
96
|
-
if (m.run)
|
|
97
|
-
runs.push({ agent, cmd: m.run[0], args: m.run[1] });
|
|
98
|
-
for (const e of m.edits ?? [])
|
|
99
|
-
add(agent, e.file, e.after);
|
|
100
|
-
mcpAdds.push(agent);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
if (!isSkip(err))
|
|
105
|
-
throw err;
|
|
106
|
-
skipped.push({ agent, reason: err.message });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
88
|
let result;
|
|
110
89
|
if (provider && spec.provider) {
|
|
111
90
|
try {
|
|
@@ -126,7 +105,7 @@ export function planSetup(env, opts = {}) {
|
|
|
126
105
|
skipped.push({ agent, reason: err.message });
|
|
127
106
|
}
|
|
128
107
|
}
|
|
129
|
-
agents.push({ agent, skill,
|
|
108
|
+
agents.push({ agent, skill, ...(result ? { provider: result } : {}) });
|
|
130
109
|
}
|
|
131
110
|
if (shellOn.size || shellOff.length) {
|
|
132
111
|
try {
|
|
@@ -138,5 +117,5 @@ export function planSetup(env, opts = {}) {
|
|
|
138
117
|
skipped.push({ agent: 'shell', reason: err.message });
|
|
139
118
|
}
|
|
140
119
|
}
|
|
141
|
-
return { agents, files,
|
|
120
|
+
return { agents, files, skipped, notes };
|
|
142
121
|
}
|
package/dist/setup/shared.mjs
CHANGED
|
@@ -19,14 +19,6 @@ export function agentBaseUrl(env, agent) {
|
|
|
19
19
|
const origin = httpsOnly(env.PHANTOM_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
20
20
|
return agent === 'claude-code' ? `${origin}/claude-code` : `${origin}/${agent}/v1`;
|
|
21
21
|
}
|
|
22
|
-
export const MCP_ARGS = ['-y', '@phantomcodes/cli', 'mcp'];
|
|
23
|
-
/** MCP for an agent whose CLI adds servers: `<agent> mcp add phantom -- npx ...`. */
|
|
24
|
-
export function mcpByCli(agent) {
|
|
25
|
-
return () => ({
|
|
26
|
-
mcp: `${agent} mcp add phantom -- npx ${MCP_ARGS.join(' ')}`,
|
|
27
|
-
run: [agent, ['mcp', 'add', 'phantom', '--', 'npx', ...MCP_ARGS]],
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
22
|
/** The end of every line phantom adds to a TOML, YAML or shell file. */
|
|
31
23
|
export const SETUP_MARK = '# phantom setup';
|
|
32
24
|
/** The start of a line phantom turned into a comment because it replaced it. `--provider off` puts it back. */
|
package/dist/spend/command.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { die } from "../core/errors.mjs";
|
|
3
3
|
import { flagNum, out } from "../core/helpers.mjs";
|
|
4
|
+
import { getLineup, putLineup, resetLineup, LINEUP_TIERS } from "./lineup.mjs";
|
|
4
5
|
import { getRoute, putRoute, patchRoute, clearRoute, testRoute, parseCondition } from "./routing.mjs";
|
|
5
6
|
/** `--table` output for `route`. */
|
|
6
7
|
export function tableRoute(d) {
|
|
@@ -103,3 +104,69 @@ export async function routeCommand({ argv, io, apiKey, subcommand, flags, table,
|
|
|
103
104
|
}
|
|
104
105
|
return 0;
|
|
105
106
|
}
|
|
107
|
+
/** `--table` output for `lineup`. */
|
|
108
|
+
export function tableLineup(d) {
|
|
109
|
+
const { lineup, tiers, choices } = d;
|
|
110
|
+
return LINEUP_TIERS.flatMap((tier) => {
|
|
111
|
+
const pick = lineup?.[tier];
|
|
112
|
+
const fallback = lineup?.fallbacks?.[tier];
|
|
113
|
+
return [
|
|
114
|
+
`${tier.padEnd(15)} ${tiers[tier].model} ${pick ? '(your pick)' : '(default)'}`,
|
|
115
|
+
...(fallback ? [`${''.padEnd(15)} falls back to ${fallback}`] : []),
|
|
116
|
+
...(choices ? choices[tier].map((c) => `${''.padEnd(15)} - ${c.model}${c.default ? ' (default)' : ''}`) : []),
|
|
117
|
+
];
|
|
118
|
+
}).join('\n');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Changes one tier's entry in `into` from a flag value: a model id sets it,
|
|
122
|
+
* and `default` clears it so the tier runs its default again.
|
|
123
|
+
*/
|
|
124
|
+
function applyPick(into, tier, flag, value) {
|
|
125
|
+
if (value === undefined)
|
|
126
|
+
return;
|
|
127
|
+
if (value === true || !value)
|
|
128
|
+
die(`--${flag} needs a model id, or "default" to clear it`);
|
|
129
|
+
if (value === 'default')
|
|
130
|
+
delete into[tier];
|
|
131
|
+
else
|
|
132
|
+
into[tier] = value;
|
|
133
|
+
}
|
|
134
|
+
/** `phantom lineup`: the model phantom-1 runs in each tier, and a fallback for each. Run by `run()`. */
|
|
135
|
+
export async function lineupCommand({ io, apiKey, subcommand, flags, table, baseUrl }) {
|
|
136
|
+
if (!subcommand || subcommand === 'get') {
|
|
137
|
+
out(await getLineup(apiKey, baseUrl), table, tableLineup, io.stdout);
|
|
138
|
+
}
|
|
139
|
+
else if (subcommand === 'set') {
|
|
140
|
+
// PUT replaces the whole lineup, so start from what the key has now.
|
|
141
|
+
const read = await getLineup(apiKey, baseUrl);
|
|
142
|
+
const current = read.lineup ?? {};
|
|
143
|
+
const { fallbacks: currentFallbacks, ...picks } = current;
|
|
144
|
+
const fallbacks = { ...currentFallbacks };
|
|
145
|
+
const model = { economy: flags['economy'], standard: flags['standard'], frontier: flags['frontier'] };
|
|
146
|
+
const fallback = {
|
|
147
|
+
economy: flags['economy-fallback'],
|
|
148
|
+
standard: flags['standard-fallback'],
|
|
149
|
+
frontier: flags['frontier-fallback'],
|
|
150
|
+
};
|
|
151
|
+
for (const tier of LINEUP_TIERS) {
|
|
152
|
+
applyPick(picks, tier, tier, model[tier]);
|
|
153
|
+
applyPick(fallbacks, tier, `${tier}-fallback`, fallback[tier]);
|
|
154
|
+
// A fallback the key already had can't be its tier's own model, which the server refuses. Drop it when this change makes it so.
|
|
155
|
+
const runs = picks[tier] ?? read.choices?.[tier].find((c) => c.default)?.model;
|
|
156
|
+
if (fallback[tier] === undefined && fallbacks[tier] === runs)
|
|
157
|
+
delete fallbacks[tier];
|
|
158
|
+
}
|
|
159
|
+
if (LINEUP_TIERS.every((tier) => model[tier] === undefined && fallback[tier] === undefined)) {
|
|
160
|
+
die('lineup set needs --economy, --standard or --frontier, or --economy-fallback, --standard-fallback or --frontier-fallback, each with a model id or "default"');
|
|
161
|
+
}
|
|
162
|
+
const next = Object.keys(fallbacks).length ? { ...picks, fallbacks } : picks;
|
|
163
|
+
out(await putLineup(apiKey, next, baseUrl), table, tableLineup, io.stdout);
|
|
164
|
+
}
|
|
165
|
+
else if (subcommand === 'reset') {
|
|
166
|
+
out(await resetLineup(apiKey, baseUrl), table, tableLineup, io.stdout);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
die(`Unknown lineup subcommand: ${subcommand}`);
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { request } from "../core/request.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* A key's lineup decides which model `phantom-1` runs in each tier. The server
|
|
4
|
+
* checks every pick against the tier's choices (lib/lineup.ts in the Phantom
|
|
5
|
+
* repo), so the CLI passes model ids through.
|
|
6
|
+
*/
|
|
7
|
+
/** The three tiers `phantom-1` picks between, cheapest first. */
|
|
8
|
+
export const LINEUP_TIERS = ['economy', 'standard', 'frontier'];
|
|
9
|
+
/** The key's lineup, the model each tier runs, and each tier's choices (GET /key/lineup). */
|
|
10
|
+
export function getLineup(apiKey, baseUrl) {
|
|
11
|
+
return request('GET', '/key/lineup', apiKey, undefined, baseUrl);
|
|
12
|
+
}
|
|
13
|
+
/** Replaces the key's whole lineup (PUT /key/lineup). The server checks it before saving. */
|
|
14
|
+
export function putLineup(apiKey, lineup, baseUrl) {
|
|
15
|
+
return request('PUT', '/key/lineup', apiKey, lineup, baseUrl);
|
|
16
|
+
}
|
|
17
|
+
/** Puts every tier back to its default (DELETE /key/lineup). */
|
|
18
|
+
export function resetLineup(apiKey, baseUrl) {
|
|
19
|
+
return request('DELETE', '/key/lineup', apiKey, undefined, baseUrl);
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { die } from "../core/errors.mjs";
|
|
2
|
+
import { out } from "../core/helpers.mjs";
|
|
3
|
+
import { request } from "../core/request.mjs";
|
|
4
|
+
import { workspaceGet } from "../workspace/command.mjs";
|
|
5
|
+
/** The dates `--from` and `--to` ask for, as a query string. The server checks them. */
|
|
6
|
+
function query(dates) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
for (const [name, value] of Object.entries(dates)) {
|
|
9
|
+
if (value === true)
|
|
10
|
+
die(`--${name} requires a date as YYYY-MM-DD`);
|
|
11
|
+
if (value !== undefined)
|
|
12
|
+
params.set(name, value);
|
|
13
|
+
}
|
|
14
|
+
const text = params.toString();
|
|
15
|
+
return text ? `?${text}` : '';
|
|
16
|
+
}
|
|
17
|
+
/** What the key spent (GET /key/spend). */
|
|
18
|
+
export function getSpend(apiKey, dates = {}, baseUrl) {
|
|
19
|
+
return request('GET', `/key/spend${query(dates)}`, apiKey, undefined, baseUrl);
|
|
20
|
+
}
|
|
21
|
+
const dollars = (n) => `$${n.toFixed(4)}`;
|
|
22
|
+
const calls = (n) => `${n} ${n === 1 ? 'call' : 'calls'}`;
|
|
23
|
+
/** `--table` output for `spend`. */
|
|
24
|
+
export function tableSpend(d) {
|
|
25
|
+
const s = d;
|
|
26
|
+
const lines = [
|
|
27
|
+
`range ${s.from} to ${s.to} (UTC)`,
|
|
28
|
+
`spent ${dollars(s.total.spent_usd)} (${calls(s.total.calls)})`,
|
|
29
|
+
];
|
|
30
|
+
if (s.total.plan_usd > 0)
|
|
31
|
+
lines.push(`paid by plan ${dollars(s.total.plan_usd)}`);
|
|
32
|
+
if (!s.routing.available)
|
|
33
|
+
lines.push(`phantom-1 saved not available: ${s.routing.reason}`);
|
|
34
|
+
else if (s.routing.calls === 0)
|
|
35
|
+
lines.push('phantom-1 saved no calls went to phantom-1');
|
|
36
|
+
else
|
|
37
|
+
lines.push(`phantom-1 saved ${dollars(s.routing.saved_usd)} (${calls(s.routing.calls)} cost ${dollars(s.routing.spent_usd)}; on ${s.routing.baseline_model} they would cost ${dollars(s.routing.baseline_usd)})`);
|
|
38
|
+
lines.push('', 'by key');
|
|
39
|
+
for (const k of s.keys) {
|
|
40
|
+
const saved = k.saved_usd === null ? '' : ` saved ${dollars(k.saved_usd)}`;
|
|
41
|
+
lines.push(` ${k.name.padEnd(16)}${dollars(k.spent_usd)} ${calls(k.calls)}${saved}${k.active ? '' : ' (revoked or rotated)'}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push('', 'by model');
|
|
44
|
+
for (const m of s.models)
|
|
45
|
+
lines.push(` ${m.model.padEnd(40)}${dollars(m.spent_usd)} ${calls(m.calls)}`);
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `phantom spend`: what the key spent, by model and day, and what phantom-1
|
|
50
|
+
* saved. `--workspace` shows every key in the workspace by name, with the
|
|
51
|
+
* workspace connection `phantom login` saved. Run by `run()`.
|
|
52
|
+
*/
|
|
53
|
+
export async function spendCommand(input) {
|
|
54
|
+
const { io, apiKey, table, baseUrl, flags } = input;
|
|
55
|
+
const dates = { from: flags['from'], to: flags['to'] };
|
|
56
|
+
const result = flags['workspace']
|
|
57
|
+
? (await workspaceGet(input, `/spend${query(dates)}`))
|
|
58
|
+
: await getSpend(apiKey, dates, baseUrl);
|
|
59
|
+
out(result, table, tableSpend, io.stdout);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
package/dist/spend/routing.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function patchRoute(apiKey, fields, baseUrl) {
|
|
|
16
16
|
export function clearRoute(apiKey, baseUrl) {
|
|
17
17
|
return request('DELETE', '/key/route', apiKey, undefined, baseUrl);
|
|
18
18
|
}
|
|
19
|
-
/** Which model a request would run on now, and why (POST /key/route/test). Nothing is charged
|
|
19
|
+
/** Which model a request would run on now, and why (POST /key/route/test). Nothing is charged. A route policy picks without calling a model; `phantom-1`, `phantom-1:zdr`, or `auto` on a key with no policy can call the model that picks the tier. `phantom-1:free` never does. */
|
|
20
20
|
export function testRoute(apiKey, opts, baseUrl) {
|
|
21
21
|
return request('POST', '/key/route/test', apiKey, opts, baseUrl);
|
|
22
22
|
}
|
|
@@ -27,6 +27,11 @@ function credentials(input) {
|
|
|
27
27
|
die('This workspace belongs to a different API address. Run phantom login for this address.', 'workspace_origin');
|
|
28
28
|
return saved;
|
|
29
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
|
+
}
|
|
30
35
|
/** Device authorization for accounts or guests, saving keys locally without copying a secret through the browser. */
|
|
31
36
|
export async function workspaceLogin(input) {
|
|
32
37
|
const { env, io, flags } = input;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantomcodes/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI inference for your coding agents. Route models, control spending, and fund your workspace.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"phantom",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"url": "https://github.com/connortessaro/phantom/issues"
|
|
66
66
|
},
|
|
67
67
|
"license": "SEE LICENSE IN LICENSE",
|
|
68
|
-
"author": "
|
|
68
|
+
"author": "Harbor Financial LLC",
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^24.19.1",
|
|
71
71
|
"@vitest/coverage-v8": "^5.0.3",
|
|
@@ -101,6 +101,44 @@ phantom route test --table
|
|
|
101
101
|
Routing affects `auto` requests. Use `PHANTOM_KEY_NAME=coding` to manage the same
|
|
102
102
|
key used by an agent with `--key coding`. Route tests are free.
|
|
103
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
|
+
|
|
104
142
|
## Advanced tasks
|
|
105
143
|
|
|
106
144
|
Use `phantom --help --all` for receipts, MCP and direct-key purchases.
|