@phantomcodes/cli 1.2.2 → 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 +38 -14
- package/dist/cli/help.mjs +26 -5
- package/dist/cli/run.mjs +57 -28
- 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 +154 -57
- 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 +364 -41
- package/package.json +1 -1
- package/skills/phantom-ai/SKILL.md +3 -2
package/dist/spend/command.mjs
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
+
import { unknownSub } from "../cli/start.mjs";
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { die } from "../core/errors.mjs";
|
|
3
|
-
import { flagNum, out } from "../core/helpers.mjs";
|
|
4
|
+
import { flagNum, out, pickIo, showsScreen } from "../core/helpers.mjs";
|
|
5
|
+
import { colorOn, paint } from "../core/style.mjs";
|
|
6
|
+
import { choose } from "../core/pick.mjs";
|
|
7
|
+
import { hideHints, lineupScreen, priceHint, routeScreen, routeTestScreen, TIER_ABOUT } from "../cli/screens.mjs";
|
|
4
8
|
import { getLineup, putLineup, resetLineup, LINEUP_TIERS } from "./lineup.mjs";
|
|
5
9
|
import { getRoute, putRoute, patchRoute, clearRoute, testRoute, parseCondition } from "./routing.mjs";
|
|
6
|
-
/** `--table` output for `route`. */
|
|
7
|
-
export function tableRoute(d) {
|
|
8
|
-
const p = d.route_policy;
|
|
9
|
-
if (!p)
|
|
10
|
-
return 'route none (model "auto" is refused)';
|
|
11
|
-
const rules = p.rules ?? [];
|
|
12
|
-
return [
|
|
13
|
-
`models ${p.models.join(', ')}`,
|
|
14
|
-
`applies_to ${p.applies_to ?? 'auto'}`,
|
|
15
|
-
`on_empty ${p.on_empty ?? 'stop'}`,
|
|
16
|
-
`fallback ${p.fallback_on_error ?? false}`,
|
|
17
|
-
`sticky ${p.stick_minutes ?? 5} min${(p.stick_by_prompt ?? true) ? '' : ' (x-phantom-session only)'}`,
|
|
18
|
-
...(rules.length ? rules.map((r, i) => `rule ${String(i + 1).padEnd(11)}if ${Object.entries(r.if).map(([k, v]) => `${k}=${v}`).join(' ')} use ${r.use}`) : ['rules none (auto runs the first model)']),
|
|
19
|
-
].join('\n');
|
|
20
|
-
}
|
|
21
|
-
/** `--table` output for `route test`. */
|
|
22
|
-
export function tableRouteTest(d) {
|
|
23
|
-
const t = d;
|
|
24
|
-
return [`model ${t.model}`, `reason ${t.reason}`].join('\n');
|
|
25
|
-
}
|
|
26
10
|
/** `phantom route`: the key's route policy, its rules, and a dry run. Run by `run()`. */
|
|
27
|
-
export async function routeCommand(
|
|
11
|
+
export async function routeCommand(input) {
|
|
12
|
+
const { argv, io, apiKey, subcommand, flags, baseUrl, env } = input;
|
|
13
|
+
const table = showsScreen(input);
|
|
14
|
+
const style = { env, color: colorOn(env, io.stdoutIsTTY) };
|
|
15
|
+
const tableRoute = (d) => routeScreen(d, style);
|
|
16
|
+
const tableRouteTest = (d) => routeTestScreen(d, style);
|
|
28
17
|
if (!subcommand || subcommand === 'get') {
|
|
29
18
|
out(await getRoute(apiKey, baseUrl), table, tableRoute, io.stdout);
|
|
30
19
|
}
|
|
@@ -100,23 +89,10 @@ export async function routeCommand({ argv, io, apiKey, subcommand, flags, table,
|
|
|
100
89
|
out(await clearRoute(apiKey, baseUrl), table, tableRoute, io.stdout);
|
|
101
90
|
}
|
|
102
91
|
else {
|
|
103
|
-
die(
|
|
92
|
+
die(unknownSub('route', String(subcommand), ['get', 'set', 'rule', 'test', 'clear']), 'route_usage');
|
|
104
93
|
}
|
|
105
94
|
return 0;
|
|
106
95
|
}
|
|
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
96
|
/**
|
|
121
97
|
* Changes one tier's entry in `into` from a flag value: a model id sets it,
|
|
122
98
|
* and `default` clears it so the tier runs its default again.
|
|
@@ -132,7 +108,16 @@ function applyPick(into, tier, flag, value) {
|
|
|
132
108
|
into[tier] = value;
|
|
133
109
|
}
|
|
134
110
|
/** `phantom lineup`: the model phantom-1 runs in each tier, and a fallback for each. Run by `run()`. */
|
|
135
|
-
export async function lineupCommand(
|
|
111
|
+
export async function lineupCommand(input) {
|
|
112
|
+
const { io, apiKey, subcommand, flags, baseUrl, env } = input;
|
|
113
|
+
const table = showsScreen(input);
|
|
114
|
+
const style = { env, color: colorOn(env, io.stdoutIsTTY) };
|
|
115
|
+
const tableLineup = (d) => lineupScreen(d, style);
|
|
116
|
+
// lineup set with no flags, at a terminal: pick a tier, then its model, from lists with prices.
|
|
117
|
+
// lineup alone, or set with no flags, at a terminal: the table, then a picker to change it, instead of a command to type.
|
|
118
|
+
const pick = (!subcommand || subcommand === 'set') && !LINEUP_FLAGS.some((name) => flags[name] !== undefined) ? pickIo(input) : null;
|
|
119
|
+
if (pick)
|
|
120
|
+
return lineupPicker(input, pick, tableLineup);
|
|
136
121
|
if (!subcommand || subcommand === 'get') {
|
|
137
122
|
out(await getLineup(apiKey, baseUrl), table, tableLineup, io.stdout);
|
|
138
123
|
}
|
|
@@ -166,7 +151,59 @@ export async function lineupCommand({ io, apiKey, subcommand, flags, table, base
|
|
|
166
151
|
out(await resetLineup(apiKey, baseUrl), table, tableLineup, io.stdout);
|
|
167
152
|
}
|
|
168
153
|
else {
|
|
169
|
-
die(
|
|
154
|
+
die(unknownSub('lineup', String(subcommand), ['get', 'set', 'reset']), 'lineup_usage');
|
|
170
155
|
}
|
|
171
156
|
return 0;
|
|
172
157
|
}
|
|
158
|
+
/** The flags `lineup set` reads; with none of them, at a terminal, it opens the picker instead. */
|
|
159
|
+
const LINEUP_FLAGS = ['economy', 'standard', 'frontier', 'economy-fallback', 'standard-fallback', 'frontier-fallback'];
|
|
160
|
+
/**
|
|
161
|
+
* `phantom lineup set` at a terminal: choose a tier, then a model for it from
|
|
162
|
+
* a list with prices (the default marked), and it saves at once. It loops
|
|
163
|
+
* back to the tiers so you can change another; Esc there is done.
|
|
164
|
+
*/
|
|
165
|
+
async function lineupPicker(input, pick, show) {
|
|
166
|
+
const { io, apiKey, baseUrl } = input;
|
|
167
|
+
let read = await getLineup(apiKey, baseUrl);
|
|
168
|
+
// The table's "→ phantom lineup set" line would point at the picker that's already open.
|
|
169
|
+
const hintsWere = hideHints(true);
|
|
170
|
+
// The marker stays on the tier you last changed, so changing it again (or the next one) is one move.
|
|
171
|
+
let last = 0;
|
|
172
|
+
try {
|
|
173
|
+
for (;;) {
|
|
174
|
+
io.stdout(show(read) + '\n\n');
|
|
175
|
+
const tier = await choose({ ...pick, esc: 'back' }, 'Which tier do you want to change?', [
|
|
176
|
+
...LINEUP_TIERS.map((t) => ({ label: t[0].toUpperCase() + t.slice(1), hint: `${TIER_ABOUT[t]} · now ${read.tiers[t].model}`, value: t })),
|
|
177
|
+
{ label: 'Put every tier back to its default', value: 'reset' },
|
|
178
|
+
], last);
|
|
179
|
+
if (tier === null)
|
|
180
|
+
return 0;
|
|
181
|
+
if (tier === 'reset') {
|
|
182
|
+
read = { ...(await resetLineup(apiKey, baseUrl)), choices: read.choices };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const options = read.choices?.[tier] ?? [];
|
|
186
|
+
const now = read.tiers[tier].model;
|
|
187
|
+
const model = await choose({ ...pick, esc: 'back' }, `Which model should ${tier} run?`, options.map((c) => ({ label: c.model, hint: priceHint(c), value: c.model })), Math.max(0, options.findIndex((c) => c.model === now)));
|
|
188
|
+
if (model === null)
|
|
189
|
+
continue;
|
|
190
|
+
const { fallbacks, ...picks } = read.lineup ?? {};
|
|
191
|
+
const chosen = options.find((c) => c.model === model);
|
|
192
|
+
if (chosen?.default)
|
|
193
|
+
delete picks[tier];
|
|
194
|
+
else
|
|
195
|
+
picks[tier] = model;
|
|
196
|
+
// A fallback can't be the tier's own model, which the server refuses.
|
|
197
|
+
const keep = { ...fallbacks };
|
|
198
|
+
if (keep[tier] === model)
|
|
199
|
+
delete keep[tier];
|
|
200
|
+
read = { ...(await putLineup(apiKey, Object.keys(keep).length ? { ...picks, fallbacks: keep } : picks, baseUrl)), choices: read.choices };
|
|
201
|
+
last = LINEUP_TIERS.indexOf(tier);
|
|
202
|
+
// Said on its own line, so the change is plain before the table redraws.
|
|
203
|
+
io.stdout(`\n ${paint(input.env, colorOn(input.env, io.stdoutIsTTY)).lime('✓')} ${tier[0].toUpperCase() + tier.slice(1)} now runs ${model}.\n`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
hideHints(hintsWere);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/spend/report.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { die } from "../core/errors.mjs";
|
|
2
|
-
import { out } from "../core/helpers.mjs";
|
|
2
|
+
import { out, showsScreen } from "../core/helpers.mjs";
|
|
3
|
+
import { colorOn } from "../core/style.mjs";
|
|
4
|
+
import { barsGrowing, motionOn, play } from "../core/motion.mjs";
|
|
5
|
+
import { spendScreen } from "../cli/screens.mjs";
|
|
3
6
|
import { request } from "../core/request.mjs";
|
|
4
7
|
import { workspaceGet } from "../workspace/command.mjs";
|
|
5
8
|
/** The dates `--from` and `--to` ask for, as a query string. The server checks them. */
|
|
@@ -51,11 +54,17 @@ export function tableSpend(d) {
|
|
|
51
54
|
* workspace connection `phantom login` saved. Run by `run()`.
|
|
52
55
|
*/
|
|
53
56
|
export async function spendCommand(input) {
|
|
54
|
-
const { io, apiKey,
|
|
57
|
+
const { io, apiKey, baseUrl, flags, env } = input;
|
|
55
58
|
const dates = { from: flags['from'], to: flags['to'] };
|
|
56
59
|
const result = flags['workspace']
|
|
57
60
|
? (await workspaceGet(input, `/spend${query(dates)}`))
|
|
58
61
|
: await getSpend(apiKey, dates, baseUrl);
|
|
59
|
-
|
|
62
|
+
const screen = showsScreen(input);
|
|
63
|
+
// The bars grow in, the way the site's cost bars do; JSON, pipes and NO_COLOR get the result at once.
|
|
64
|
+
if (screen && motionOn(env, io.stdoutIsTTY)) {
|
|
65
|
+
await play(io.stdout, barsGrowing(spendScreen(result, { env, color: true }).split('\n')), { rows: process.stdout.rows, skipOnKey: true });
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
out(result, screen, (d) => spendScreen(d, { env, color: colorOn(env, io.stdoutIsTTY) }), io.stdout);
|
|
60
69
|
return 0;
|
|
61
70
|
}
|