@phantomcodes/cli 1.2.4 → 1.2.6
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 -6
- package/dist/agents/command.mjs +11 -3
- package/dist/cli/run.mjs +16 -10
- package/dist/cli/screens.mjs +9 -4
- package/dist/core/helpers.mjs +1 -1
- package/dist/core/pick.mjs +5 -3
- package/dist/core/version.mjs +1 -1
- package/dist/setup/agents.mjs +445 -1
- package/dist/setup/command.mjs +1 -1
- package/dist/setup/screen.mjs +1 -1
- package/dist/setup/shared.mjs +4 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -62,9 +62,11 @@ The Phantom CLI detects supported agents, previews the configuration changes, th
|
|
|
62
62
|
applying them. It connects their model calls to Phantom AI and wires up the saved
|
|
63
63
|
key. Restart your agent, or open a new terminal, to use the connection.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
Saved setup covers Claude Code, Codex, opencode, Continue, Goose, Aider, GitHub
|
|
66
|
+
Copilot CLI, Cline, Gemini CLI, Crush, Qwen Code, Kilo Code, Kimi Code, Droid,
|
|
67
|
+
Mistral Vibe and Zed. Cursor, VS Code, JetBrains AI Assistant, Warp and Trae
|
|
68
|
+
keep these settings on their own settings screens, so setup prints the steps;
|
|
69
|
+
pi gets the agent skill.
|
|
68
70
|
|
|
69
71
|
Preview without changing files with `phantom setup --dry-run`. The Phantom CLI keeps backups;
|
|
70
72
|
undo the changes with `phantom setup --restore`.
|
|
@@ -79,9 +81,11 @@ phantom workspace status
|
|
|
79
81
|
phantom codex
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
Both setup options require **Node 24+** and your coding agent installed. Connect,
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
Both setup options require **Node 24+** and your coding agent installed. Connect, then
|
|
85
|
+
launch **Codex** (`phantom codex`), **Claude Code** (`phantom claude`), **pi**
|
|
86
|
+
(`phantom pi`), or **any other compatible agent**. Each runs phantom-1 unless you
|
|
87
|
+
pick a model (`--model` after `--`), and phantom-1 runs on free models until you add
|
|
88
|
+
credit. Free calls leave out web search, which costs money on any model.
|
|
85
89
|
|
|
86
90
|
Login opens a device approval page and saves a key privately. Funding opens
|
|
87
91
|
checkout; check your workspace balance after payment. Agent runs use temporary
|
package/dist/agents/command.mjs
CHANGED
|
@@ -6,7 +6,10 @@ import { die, NO_KEY } from "../core/errors.mjs";
|
|
|
6
6
|
import { DEFAULT_BASE_URL, httpsOnly, request } from "../core/request.mjs";
|
|
7
7
|
import { keyId, readNamedKey, resolveApiKey } from "../keys/saved.mjs";
|
|
8
8
|
import { workspaceLogin } from "../workspace/command.mjs";
|
|
9
|
-
|
|
9
|
+
/** The model an agent runs when the user names none: phantom-1, which runs free on a key with no credit. */
|
|
10
|
+
const DEFAULT_MODEL = 'phantom-1';
|
|
11
|
+
/** phantom-1 as Claude Code names it: its picker keeps only ids that say "claude", so Phantom lists them as `claude-code/<id>`. */
|
|
12
|
+
const CLAUDE_DEFAULT_MODEL = `claude-code/${DEFAULT_MODEL}`;
|
|
10
13
|
/** 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. */
|
|
11
14
|
async function keyStatus(apiKey, baseUrl) {
|
|
12
15
|
const s = await request('GET', '/key/balance', apiKey, undefined, baseUrl);
|
|
@@ -23,11 +26,16 @@ export const AGENTS = {
|
|
|
23
26
|
// --settings outranks settings.json, so an env block or apiKeyHelper
|
|
24
27
|
// `phantom setup` wrote can't send Claude Code past the proxy. The helper
|
|
25
28
|
// reads the token from the environment, so it stays off the command line.
|
|
29
|
+
// It runs with this phantom's own Node: the curl installer puts none on PATH.
|
|
30
|
+
const helper = `${JSON.stringify(process.execPath)} -p "process.env.PHANTOM_AGENT_TOKEN || ''"`;
|
|
31
|
+
// phantom-1 unless the user picks a model. It goes in the environment, which
|
|
32
|
+
// Claude Code ranks below --model and below a model in its settings file.
|
|
33
|
+
const model = hasModel(args) || env.ANTHROPIC_MODEL ? {} : { ANTHROPIC_MODEL: CLAUDE_DEFAULT_MODEL };
|
|
26
34
|
const vars = { ANTHROPIC_BASE_URL: proxy, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1', ENABLE_TOOL_SEARCH: 'true' };
|
|
27
35
|
const { ANTHROPIC_AUTH_TOKEN: _auth, ...rest } = env;
|
|
28
36
|
return {
|
|
29
|
-
args: ['--settings', JSON.stringify({ apiKeyHelper:
|
|
30
|
-
env: { ...rest, ANTHROPIC_BASE_URL: proxy, ANTHROPIC_API_KEY: token, PHANTOM_AGENT_TOKEN: token },
|
|
37
|
+
args: ['--settings', JSON.stringify({ apiKeyHelper: helper, env: vars }), ...args],
|
|
38
|
+
env: { ...rest, ...model, ANTHROPIC_BASE_URL: proxy, ANTHROPIC_API_KEY: token, PHANTOM_AGENT_TOKEN: token },
|
|
31
39
|
};
|
|
32
40
|
},
|
|
33
41
|
},
|
package/dist/cli/run.mjs
CHANGED
|
@@ -21,16 +21,21 @@ import { LEGACY_SETUP_HINT, legacySetupFound } from "../setup/legacy.mjs";
|
|
|
21
21
|
* @param node The Node version it runs on; one too old to reject forged
|
|
22
22
|
* eMudhra certificates gets a warning on stderr first.
|
|
23
23
|
*/
|
|
24
|
+
/** Retired flags, accepted so the command can say what replaced them, and never suggested for a typo. */
|
|
25
|
+
const RETIRED_FLAGS = ['pay', 'mpp'];
|
|
26
|
+
/** The flags every command takes, and the retired ones. */
|
|
27
|
+
const EVERY_COMMAND_FLAGS = ['table', 'json', 'help', 'all', 'version', 'mcp', ...RETIRED_FLAGS];
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* such as --yse
|
|
28
|
-
*
|
|
29
|
+
* The flags a command reads: each one its help names, plus those of the
|
|
30
|
+
* command its subcommand names (`workspace fund` reads `fund`'s), plus
|
|
31
|
+
* {@link EVERY_COMMAND_FLAGS}. A typo such as --yse, or a flag only another
|
|
32
|
+
* command takes (balance --key), is refused instead of quietly ignored, which
|
|
33
|
+
* in CI would otherwise drop the flag without a word.
|
|
29
34
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
function flagsOf(command, subcommand) {
|
|
36
|
+
const helps = [command.help, COMMANDS.find((c) => c.name === subcommand)?.help ?? ''].join('\n');
|
|
37
|
+
return new Set([...[...helps.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]), ...EVERY_COMMAND_FLAGS]);
|
|
38
|
+
}
|
|
34
39
|
export async function run(argv = process.argv.slice(2), env = process.env, io = {
|
|
35
40
|
stdout: (s) => process.stdout.write(s),
|
|
36
41
|
stderr: (s) => process.stderr.write(s),
|
|
@@ -92,10 +97,11 @@ export async function run(argv = process.argv.slice(2), env = process.env, io =
|
|
|
92
97
|
const ownFlags = parseFlags(flagArgs.includes('--') ? flagArgs.slice(0, flagArgs.indexOf('--')) : flagArgs);
|
|
93
98
|
// claude, codex and pi hand their arguments to the agent, so they check their own.
|
|
94
99
|
const passesArgs = command.handler === COMMANDS.find((c) => c.name === 'claude')?.handler;
|
|
100
|
+
const known = flagsOf(command, subcommand);
|
|
95
101
|
for (const name of passesArgs ? [] : Object.keys(ownFlags)) {
|
|
96
|
-
if (
|
|
102
|
+
if (known.has(name))
|
|
97
103
|
continue;
|
|
98
|
-
const guess = closestCommand(name, [...
|
|
104
|
+
const guess = closestCommand(name, [...known].filter((flag) => !RETIRED_FLAGS.includes(flag)));
|
|
99
105
|
die(`Unknown flag --${name} for phantom ${argv[0]}.${guess ? ` Did you mean --${guess}?` : ''} See phantom ${argv[0]} --help.`, 'unknown_flag');
|
|
100
106
|
}
|
|
101
107
|
// A person at a terminal reads each command's screen; a pipe (scripts, agents) or --json gets JSON.
|
package/dist/cli/screens.mjs
CHANGED
|
@@ -109,15 +109,19 @@ export function spendScreen(s, style = {}) {
|
|
|
109
109
|
lines.push(...(s.scope === 'workspace' ? next(p, 'Pick the models phantom-1 runs:', 'phantom lineup') : next(p, 'Every key in the workspace:', 'phantom spend --workspace')));
|
|
110
110
|
return lines.join('\n');
|
|
111
111
|
}
|
|
112
|
+
/** `text` cut to `width` characters, ending in … when it was longer. */
|
|
113
|
+
const clip = (text, width) => (text.length > width ? `${text.slice(0, width - 1)}…` : text);
|
|
112
114
|
/** The keys, one row each: name, the start of the key, what it spent, and its limit. */
|
|
113
115
|
function keyRows(p, keys) {
|
|
114
116
|
if (!keys.length)
|
|
115
117
|
return [` ${p.dim('No keys yet.')}`];
|
|
116
118
|
return keys.map((k) => {
|
|
117
119
|
const limit = k.limit_usd != null ? `limit ${usd(k.limit_usd)}` : k.budget_usd != null ? `${usd(k.budget_usd)} every ${plural(k.period_days ?? 30, 'day')}` : 'no limit';
|
|
118
|
-
|
|
120
|
+
// A revoked or rotated key gets a hollow mark and the word off at the end, so the columns stay in line.
|
|
121
|
+
const off = k.is_active === false;
|
|
122
|
+
const mark = off ? p.dim('○') : p.lime('●');
|
|
119
123
|
// The id's first 8 characters: enough for --id, which also takes the name.
|
|
120
|
-
return ` ${mark} ${(k.name ?? 'Key'
|
|
124
|
+
return ` ${mark} ${clip(k.name ?? 'Key', 20).padEnd(21)}${p.dim((k.id ?? '').slice(0, 8).padEnd(10))}${p.dim((k.prefix ?? '').slice(0, 18).padEnd(20))}${usd(k.spent_usd ?? 0).padStart(8)} ${p.dim(limit)}${off ? ` ${p.dim('off')}` : ''}`;
|
|
121
125
|
});
|
|
122
126
|
}
|
|
123
127
|
/** `phantom workspace`: the shared balance, each key, and the last few charges. */
|
|
@@ -161,7 +165,7 @@ export function routeScreen(r, style = {}) {
|
|
|
161
165
|
const lines = ['', ...caption(p, w, 'ROUTING', p.dim('how model "auto" picks for this key'))];
|
|
162
166
|
const policy = r.route_policy;
|
|
163
167
|
if (!policy) {
|
|
164
|
-
lines.push(' No route policy, so
|
|
168
|
+
lines.push(' No route policy, so model "auto" runs phantom-1, which picks a model for each request.');
|
|
165
169
|
lines.push(...next(p, 'Make one:', 'phantom route set --models a,b,c'));
|
|
166
170
|
return lines.join('\n');
|
|
167
171
|
}
|
|
@@ -317,7 +321,8 @@ export function savedKeysScreen(keys, style = {}) {
|
|
|
317
321
|
lines.push(` ${p.dim('None saved yet.')}`);
|
|
318
322
|
for (const k of keys) {
|
|
319
323
|
const credit = k.credit_balance_usd === undefined ? '' : usd(k.credit_balance_usd).padStart(10);
|
|
320
|
-
|
|
324
|
+
const off = k.active === false;
|
|
325
|
+
lines.push(` ${off ? p.dim('○') : p.lime('●')} ${clip(k.name, 20).padEnd(21)}${p.dim(k.id.slice(0, 12).padEnd(14))}${credit}${off ? ` ${p.dim('off')}` : ''}`);
|
|
321
326
|
}
|
|
322
327
|
lines.push(...next(p, 'Save another:', 'phantom keys save <name>'));
|
|
323
328
|
return lines.join('\n');
|
package/dist/core/helpers.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export function pickIo(input) {
|
|
|
12
12
|
if (!(io.keys ? io.isTTY : canPick(env, io.isTTY)))
|
|
13
13
|
return null;
|
|
14
14
|
// A command started from the menu goes back to it on Esc; one typed on its own quits.
|
|
15
|
-
return { write: io.stderr, env, keys: io.keys, esc: io.fromMenu ? 'back' : '
|
|
15
|
+
return { write: io.stderr, env, keys: io.keys, esc: io.fromMenu ? 'back' : 'cancel', columns: process.stderr.columns };
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Whether a command should draw its screen rather than print JSON: with
|
package/dist/core/pick.mjs
CHANGED
|
@@ -96,6 +96,8 @@ function block(write) {
|
|
|
96
96
|
}
|
|
97
97
|
/** The question line: a lime ? and the question in bold, then the answer once there is one. */
|
|
98
98
|
const asked = (p, question, answer = '') => ` ${p.lime('?')} ${p.bold(question)}${answer ? ` ${p.cyan(answer)}` : ''}`;
|
|
99
|
+
/** The answer a question shows after Esc: back, quit, or cancelled. */
|
|
100
|
+
const escAnswer = (esc) => (esc === 'quit' ? 'quit' : esc === 'cancel' ? 'cancelled' : 'back');
|
|
99
101
|
/** The hint under a picker: which keys do what. */
|
|
100
102
|
const keysHint = (p, words) => ` ${p.dim(words)}`;
|
|
101
103
|
/**
|
|
@@ -129,7 +131,7 @@ export async function choose(io, question, choices, start = 0) {
|
|
|
129
131
|
return choices[at].value;
|
|
130
132
|
}
|
|
131
133
|
else if (key === 'escape' || key === 'q' || key === 'end') {
|
|
132
|
-
draw([asked(p, question, io.esc
|
|
134
|
+
draw([asked(p, question, escAnswer(io.esc))]);
|
|
133
135
|
return null;
|
|
134
136
|
}
|
|
135
137
|
else if (key === 'ctrl-c') {
|
|
@@ -190,7 +192,7 @@ export async function checklist(io, question, items) {
|
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
else if (key === 'escape' || key === 'q' || key === 'end') {
|
|
193
|
-
draw([asked(p, question, io.esc
|
|
195
|
+
draw([asked(p, question, escAnswer(io.esc))]);
|
|
194
196
|
return null;
|
|
195
197
|
}
|
|
196
198
|
else if (key === 'ctrl-c') {
|
|
@@ -270,7 +272,7 @@ export async function askText(io, question, opts = {}) {
|
|
|
270
272
|
problem = why;
|
|
271
273
|
}
|
|
272
274
|
else if (key === 'escape' || key === 'end') {
|
|
273
|
-
draw([asked(p, question, io.esc
|
|
275
|
+
draw([asked(p, question, escAnswer(io.esc))]);
|
|
274
276
|
return null;
|
|
275
277
|
}
|
|
276
278
|
else if (key === 'ctrl-c') {
|
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.6';
|
package/dist/setup/agents.mjs
CHANGED
|
@@ -6,6 +6,13 @@ import { die } from "../core/errors.mjs";
|
|
|
6
6
|
import { resolveApiKey } from "../keys/saved.mjs";
|
|
7
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
|
+
/** The most output a model call may ask for, for agents that need a number. */
|
|
10
|
+
const SETUP_OUTPUT_TOKENS = 32_000;
|
|
11
|
+
/** A JSON edit's result, or '' when only `$schema` (or nothing) is left, so `off` deletes a file phantom made. */
|
|
12
|
+
function unlessEmpty(file, after) {
|
|
13
|
+
const left = jsonObject(file, after);
|
|
14
|
+
return Object.keys(left).every((k) => k === '$schema') ? '' : after;
|
|
15
|
+
}
|
|
9
16
|
// ── Manual Provider Helper ───────────────────────────────────────────────────
|
|
10
17
|
/** An agent whose model settings live on its own settings screen: setup prints the steps. */
|
|
11
18
|
export function manualProvider(where, steps, note) {
|
|
@@ -88,7 +95,8 @@ export function claudePlan(env, opts) {
|
|
|
88
95
|
ANTHROPIC_API_KEY: '',
|
|
89
96
|
ENABLE_TOOL_SEARCH: 'true',
|
|
90
97
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1',
|
|
91
|
-
|
|
98
|
+
// phantom-1 unless a model is named, as Codex gets: it runs free until there is credit.
|
|
99
|
+
ANTHROPIC_MODEL: opts.model ?? 'claude-code/phantom-1',
|
|
92
100
|
};
|
|
93
101
|
const changes = [[['apiKeyHelper'], `${phantomCommand(opts.onPath ?? onPath)} key show`], ...Object.entries(set).map(([k, v]) => [['env', k], v])];
|
|
94
102
|
// A token outranks apiKeyHelper, and would send a non-Phantom key.
|
|
@@ -311,6 +319,312 @@ export function clineProvider(ctx, off) {
|
|
|
311
319
|
},
|
|
312
320
|
};
|
|
313
321
|
}
|
|
322
|
+
// ── Gemini CLI ───────────────────────────────────────────────────────────────
|
|
323
|
+
/** Gemini CLI's folder: `.gemini` in `$GEMINI_CLI_HOME`, else in the home folder. */
|
|
324
|
+
export function geminiDir(env, home) {
|
|
325
|
+
return path.join(env.GEMINI_CLI_HOME?.trim() ? env.GEMINI_CLI_HOME : home, '.gemini');
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Gemini CLI speaks Google's API, which Phantom AI serves at `/gemini-cli`.
|
|
329
|
+
* The shell block sets GOOGLE_GEMINI_BASE_URL to it and GEMINI_API_KEY to
|
|
330
|
+
* the Phantom key; `settings.json` in Gemini CLI's folder picks API key
|
|
331
|
+
* sign-in (`security.auth.selectedType`) and the model (`model.name`).
|
|
332
|
+
* `--provider off` takes out a phantom model; the sign-in choice stays, and
|
|
333
|
+
* `phantom setup --restore` puts the file back as it was.
|
|
334
|
+
*/
|
|
335
|
+
export function geminiProvider(ctx, off) {
|
|
336
|
+
const file = path.join(geminiDir(ctx.env, ctx.home), 'settings.json');
|
|
337
|
+
const text = readText(file);
|
|
338
|
+
const settings = jsonObject(file, text);
|
|
339
|
+
if (off) {
|
|
340
|
+
const ours = typeof settings.model?.name === 'string' && settings.model.name.startsWith('phantom-');
|
|
341
|
+
const alone = Object.keys(settings.model ?? {}).length === 1;
|
|
342
|
+
return offResult('Gemini CLI', file, text, ours ? jsonEdit(file, text, [[alone ? ['model'] : ['model', 'name'], undefined]]) : text);
|
|
343
|
+
}
|
|
344
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
345
|
+
const changes = [
|
|
346
|
+
[['security', 'auth', 'selectedType'], 'gemini-api-key'],
|
|
347
|
+
[['model', 'name'], model],
|
|
348
|
+
];
|
|
349
|
+
return {
|
|
350
|
+
edits: [{ file, after: jsonEdit(file, text, changes) }],
|
|
351
|
+
result: {
|
|
352
|
+
settings: file,
|
|
353
|
+
on: true,
|
|
354
|
+
base_url: agentBaseUrl(ctx.env, 'gemini-cli'),
|
|
355
|
+
model,
|
|
356
|
+
warnings: ['Gemini CLI reads its key from the shell; open a new terminal before running it. Other programs that read GEMINI_API_KEY get the Phantom key too'],
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// ── Crush ────────────────────────────────────────────────────────────────────
|
|
361
|
+
/** Crush's config folder: `$CRUSH_GLOBAL_CONFIG`, else `crush` in the config folder. */
|
|
362
|
+
export function crushDir(env, home) {
|
|
363
|
+
return env.CRUSH_GLOBAL_CONFIG?.trim() ? env.CRUSH_GLOBAL_CONFIG : path.join(configHome(env, home), 'crush');
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Crush: `crush.json` in Crush's config folder gets a `phantom` provider at
|
|
367
|
+
* `/coding-agent/v1` whose key is `$PHANTOM_API_KEY`, which Crush expands
|
|
368
|
+
* when it loads, and phantom-1 as both its large and small model. Crush 0.99
|
|
369
|
+
* reads this file beside a `crushrc`.
|
|
370
|
+
*/
|
|
371
|
+
export function crushProvider(ctx, off) {
|
|
372
|
+
const file = path.join(crushDir(ctx.env, ctx.home), 'crush.json');
|
|
373
|
+
const text = readText(file);
|
|
374
|
+
const config = jsonObject(file, text);
|
|
375
|
+
const sizes = ['large', 'small'];
|
|
376
|
+
if (off) {
|
|
377
|
+
if (!config.providers?.phantom)
|
|
378
|
+
return offResult('Crush', file, text, text);
|
|
379
|
+
const changes = [Object.keys(config.providers).length === 1 ? [['providers'], undefined] : [['providers', 'phantom'], undefined]];
|
|
380
|
+
const mine = Object.keys(config.models ?? {}).filter((k) => sizes.includes(k) && config.models?.[k]?.provider === 'phantom');
|
|
381
|
+
if (mine.length && mine.length === Object.keys(config.models ?? {}).length)
|
|
382
|
+
changes.push([['models'], undefined]);
|
|
383
|
+
else
|
|
384
|
+
for (const k of mine)
|
|
385
|
+
changes.push([['models', k], undefined]);
|
|
386
|
+
return offResult('Crush', file, text, unlessEmpty(file, jsonEdit(file, text, changes)));
|
|
387
|
+
}
|
|
388
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
389
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
390
|
+
const changes = [
|
|
391
|
+
...(!text?.trim() ? [[['$schema'], 'https://charm.land/crush.json']] : []),
|
|
392
|
+
[
|
|
393
|
+
['providers', 'phantom'],
|
|
394
|
+
{
|
|
395
|
+
name: 'Phantom AI',
|
|
396
|
+
type: 'openai-compat',
|
|
397
|
+
base_url: base,
|
|
398
|
+
api_key: '$PHANTOM_API_KEY',
|
|
399
|
+
models: [{ id: model, name: model, context_window: SETUP_CONTEXT_WINDOW, default_max_tokens: SETUP_OUTPUT_TOKENS }],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
...sizes.map((size) => [['models', size], { provider: 'phantom', model }]),
|
|
403
|
+
];
|
|
404
|
+
return { edits: [{ file, after: jsonEdit(file, text, changes) }], result: { settings: file, on: true, base_url: base, model, warnings: [] } };
|
|
405
|
+
}
|
|
406
|
+
// ── Qwen Code ────────────────────────────────────────────────────────────────
|
|
407
|
+
/** Qwen Code's folder: `$QWEN_HOME`, else `~/.qwen`. */
|
|
408
|
+
export function qwenDir(env, home) {
|
|
409
|
+
return env.QWEN_HOME?.trim() ? env.QWEN_HOME : path.join(home, '.qwen');
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Qwen Code: `settings.json` in its folder gets a model under
|
|
413
|
+
* `modelProviders.openai` at `/coding-agent/v1` that reads its key from
|
|
414
|
+
* PHANTOM_API_KEY (`envKey`), is picked as the model (`model.name`), and the
|
|
415
|
+
* OpenAI-compatible sign-in (`security.auth.selectedType`), so Qwen Code
|
|
416
|
+
* starts without asking. Its other models stay.
|
|
417
|
+
*/
|
|
418
|
+
export function qwenProvider(ctx, off) {
|
|
419
|
+
const file = path.join(qwenDir(ctx.env, ctx.home), 'settings.json');
|
|
420
|
+
const text = readText(file);
|
|
421
|
+
const config = jsonObject(file, text);
|
|
422
|
+
const list = config.modelProviders?.openai ?? [];
|
|
423
|
+
if (!Array.isArray(list))
|
|
424
|
+
unreadable(file, 'has modelProviders.openai as something other than a list');
|
|
425
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
426
|
+
const ours = (m) => m?.baseUrl === base;
|
|
427
|
+
const others = list.filter((m) => !ours(m));
|
|
428
|
+
const mineIds = list.filter(ours).map((m) => m.id);
|
|
429
|
+
if (off) {
|
|
430
|
+
if (others.length === list.length)
|
|
431
|
+
return offResult('Qwen Code', file, text, text);
|
|
432
|
+
const changes = [];
|
|
433
|
+
if (others.length)
|
|
434
|
+
changes.push([['modelProviders', 'openai'], others]);
|
|
435
|
+
else
|
|
436
|
+
changes.push(Object.keys(config.modelProviders ?? {}).length === 1 ? [['modelProviders'], undefined] : [['modelProviders', 'openai'], undefined]);
|
|
437
|
+
if (mineIds.includes(config.model?.name))
|
|
438
|
+
changes.push(Object.keys(config.model ?? {}).length === 1 ? [['model'], undefined] : [['model', 'name'], undefined]);
|
|
439
|
+
return offResult('Qwen Code', file, text, unlessEmpty(file, jsonEdit(file, text, changes)));
|
|
440
|
+
}
|
|
441
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
442
|
+
const entry = { id: model, name: `Phantom AI ${model}`, baseUrl: base, envKey: 'PHANTOM_API_KEY', generationConfig: { contextWindowSize: SETUP_CONTEXT_WINDOW } };
|
|
443
|
+
const changes = [
|
|
444
|
+
[['modelProviders', 'openai'], [entry, ...others]],
|
|
445
|
+
[['security', 'auth', 'selectedType'], 'openai'],
|
|
446
|
+
[['model', 'name'], model],
|
|
447
|
+
];
|
|
448
|
+
return { edits: [{ file, after: jsonEdit(file, text, changes) }], result: { settings: file, on: true, base_url: base, model, warnings: [] } };
|
|
449
|
+
}
|
|
450
|
+
// ── Kilo Code ────────────────────────────────────────────────────────────────
|
|
451
|
+
/** Kilo Code's config file: `kilo.jsonc` (or `kilo.json`) in `$XDG_CONFIG_HOME/kilo`, else `~/.config/kilo`. */
|
|
452
|
+
export function kiloFile(env, home) {
|
|
453
|
+
const dir = path.join(configHome(env, home), 'kilo');
|
|
454
|
+
return existsSync(path.join(dir, 'kilo.jsonc')) ? path.join(dir, 'kilo.jsonc') : path.join(dir, 'kilo.json');
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Kilo Code's CLI, a fork of opencode: its global config gets the same
|
|
458
|
+
* `phantom` provider opencode does, with the key as `{env:PHANTOM_API_KEY}`,
|
|
459
|
+
* and `phantom/<model>` as the model. Kilo resolves `{env:...}` only in the
|
|
460
|
+
* global file, which is the one written here.
|
|
461
|
+
*/
|
|
462
|
+
export function kiloProvider(ctx, off) {
|
|
463
|
+
const file = kiloFile(ctx.env, ctx.home);
|
|
464
|
+
const text = readText(file);
|
|
465
|
+
const config = jsonObject(file, text);
|
|
466
|
+
if (off) {
|
|
467
|
+
if (!config.provider?.phantom)
|
|
468
|
+
return offResult('Kilo Code', file, text, text);
|
|
469
|
+
const changes = [Object.keys(config.provider).length === 1 ? [['provider'], undefined] : [['provider', 'phantom'], undefined]];
|
|
470
|
+
if (typeof config.model === 'string' && config.model.startsWith('phantom/'))
|
|
471
|
+
changes.push([['model'], undefined]);
|
|
472
|
+
return offResult('Kilo Code', file, text, unlessEmpty(file, jsonEdit(file, text, changes)));
|
|
473
|
+
}
|
|
474
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
475
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
476
|
+
const changes = [
|
|
477
|
+
...(!text?.trim() ? [[['$schema'], 'https://app.kilo.ai/config.json']] : []),
|
|
478
|
+
[
|
|
479
|
+
['provider', 'phantom'],
|
|
480
|
+
{
|
|
481
|
+
npm: '@ai-sdk/openai-compatible',
|
|
482
|
+
name: 'Phantom AI',
|
|
483
|
+
options: { baseURL: base, apiKey: '{env:PHANTOM_API_KEY}' },
|
|
484
|
+
// Kilo turns off compaction for a model without limits.
|
|
485
|
+
models: { [model]: { name: model, tool_call: true, reasoning: true, limit: { context: SETUP_CONTEXT_WINDOW, output: SETUP_OUTPUT_TOKENS } } },
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
[['model'], `phantom/${model}`],
|
|
489
|
+
];
|
|
490
|
+
return { edits: [{ file, after: jsonEdit(file, text, changes) }], result: { settings: file, on: true, base_url: base, model: `phantom/${model}`, warnings: [] } };
|
|
491
|
+
}
|
|
492
|
+
// ── Kimi Code ────────────────────────────────────────────────────────────────
|
|
493
|
+
/** Kimi Code's folder: `$KIMI_CODE_HOME`, else `~/.kimi-code`. */
|
|
494
|
+
export function kimiDir(env, home) {
|
|
495
|
+
return env.KIMI_CODE_HOME?.trim() ? env.KIMI_CODE_HOME : path.join(home, '.kimi-code');
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Kimi Code CLI: `config.toml` in its folder gets a `phantom` provider at
|
|
499
|
+
* `/coding-agent/v1` that reads its key from PHANTOM_API_KEY
|
|
500
|
+
* (`api_key_env`), a `phantom/<model>` model, and that model as
|
|
501
|
+
* `default_model`. Each run starts from the file without phantom's lines, so
|
|
502
|
+
* a new `--model` replaces the old one.
|
|
503
|
+
*/
|
|
504
|
+
export function kimiProvider(ctx, off) {
|
|
505
|
+
const file = path.join(kimiDir(ctx.env, ctx.home), 'config.toml');
|
|
506
|
+
const text = readText(file);
|
|
507
|
+
const before = text === null ? null : tomlRemoveOurs(file, text);
|
|
508
|
+
if (off)
|
|
509
|
+
return offResult('Kimi Code', file, text, before);
|
|
510
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
511
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
512
|
+
const alias = `phantom/${model}`;
|
|
513
|
+
let t = tomlSet(file, before ?? '', [], 'default_model', JSON.stringify(alias));
|
|
514
|
+
for (const [k, v] of [['type', '"openai"'], ['base_url', JSON.stringify(base)], ['api_key_env', '"PHANTOM_API_KEY"']])
|
|
515
|
+
t = tomlSet(file, t, ['providers', 'phantom'], k, v);
|
|
516
|
+
for (const [k, v] of [['provider', '"phantom"'], ['model', JSON.stringify(model)], ['max_context_size', String(SETUP_CONTEXT_WINDOW)], ['capabilities', '["thinking", "tool_use"]']]) {
|
|
517
|
+
t = tomlSet(file, t, ['models', alias], k, v);
|
|
518
|
+
}
|
|
519
|
+
return { edits: [{ file, after: t }], result: { settings: file, on: true, base_url: base, model: alias, warnings: [] } };
|
|
520
|
+
}
|
|
521
|
+
// ── Droid ────────────────────────────────────────────────────────────────────
|
|
522
|
+
/** The name phantom gives its model in Droid. Droid calls it `custom:Phantom-AI-<n>`. */
|
|
523
|
+
const DROID_NAME = 'Phantom AI';
|
|
524
|
+
/**
|
|
525
|
+
* Factory's Droid: `~/.factory/settings.json` gets a custom model at
|
|
526
|
+
* `/coding-agent/v1` (chat completions) with the key as `${PHANTOM_API_KEY}`,
|
|
527
|
+
* which Droid expands, and that model as `model`. Droid names a custom
|
|
528
|
+
* model by its place in `customModels`, so phantom's goes last, or stays
|
|
529
|
+
* where it was, and the user's own keep their names.
|
|
530
|
+
*/
|
|
531
|
+
export function droidProvider(ctx, off) {
|
|
532
|
+
const file = path.join(ctx.home, '.factory', 'settings.json');
|
|
533
|
+
const text = readText(file);
|
|
534
|
+
const config = jsonObject(file, text);
|
|
535
|
+
const list = config.customModels ?? [];
|
|
536
|
+
if (!Array.isArray(list))
|
|
537
|
+
unreadable(file, 'has customModels as something other than a list');
|
|
538
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
539
|
+
const at = list.findIndex((m) => m?.baseUrl === base);
|
|
540
|
+
const mine = (m) => typeof m === 'string' && m.startsWith(`custom:${DROID_NAME.replace(/ /g, '-')}-`);
|
|
541
|
+
if (off) {
|
|
542
|
+
if (at === -1)
|
|
543
|
+
return offResult('Droid', file, text, text);
|
|
544
|
+
const rest = list.filter((_, i) => i !== at);
|
|
545
|
+
const changes = [[['customModels'], rest.length ? rest : undefined]];
|
|
546
|
+
if (mine(config.model))
|
|
547
|
+
changes.push([['model'], undefined]);
|
|
548
|
+
return offResult('Droid', file, text, unlessEmpty(file, jsonEdit(file, text, changes)));
|
|
549
|
+
}
|
|
550
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
551
|
+
const entry = { model, displayName: DROID_NAME, baseUrl: base, apiKey: '${PHANTOM_API_KEY}', provider: 'generic-chat-completion-api', maxOutputTokens: SETUP_OUTPUT_TOKENS };
|
|
552
|
+
const index = at === -1 ? list.length : at;
|
|
553
|
+
const next = [...list];
|
|
554
|
+
next[index] = entry;
|
|
555
|
+
const id = `custom:${DROID_NAME.replace(/ /g, '-')}-${index}`;
|
|
556
|
+
const changes = [
|
|
557
|
+
[['customModels'], next],
|
|
558
|
+
[['model'], id],
|
|
559
|
+
];
|
|
560
|
+
return { edits: [{ file, after: jsonEdit(file, text, changes) }], result: { settings: file, on: true, base_url: base, model: id, warnings: [] } };
|
|
561
|
+
}
|
|
562
|
+
// ── Mistral Vibe ─────────────────────────────────────────────────────────────
|
|
563
|
+
/**
|
|
564
|
+
* Mistral Vibe: `~/.vibe/config.toml` gets a `phantom` provider at
|
|
565
|
+
* `/coding-agent/v1` that reads its key from PHANTOM_API_KEY
|
|
566
|
+
* (`api_key_env_var`), a model on it, and that model as `active_model`.
|
|
567
|
+
* Providers and models are lists of tables (`[[providers]]`), so phantom's
|
|
568
|
+
* go at the end as marked lines, which `--provider off` takes out again.
|
|
569
|
+
*/
|
|
570
|
+
export function vibeProvider(ctx, off) {
|
|
571
|
+
const file = path.join(ctx.home, '.vibe', 'config.toml');
|
|
572
|
+
const text = readText(file);
|
|
573
|
+
const before = text === null ? null : tomlRemoveOurs(file, text);
|
|
574
|
+
if (off)
|
|
575
|
+
return offResult('Mistral Vibe', file, text, before);
|
|
576
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
577
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
578
|
+
const block = [
|
|
579
|
+
'[[providers]]',
|
|
580
|
+
'name = "phantom"',
|
|
581
|
+
`api_base = ${JSON.stringify(base)}`,
|
|
582
|
+
'api_key_env_var = "PHANTOM_API_KEY"',
|
|
583
|
+
'backend = "generic"',
|
|
584
|
+
'api_style = "openai"',
|
|
585
|
+
'',
|
|
586
|
+
'[[models]]',
|
|
587
|
+
`name = ${JSON.stringify(model)}`,
|
|
588
|
+
'provider = "phantom"',
|
|
589
|
+
`alias = ${JSON.stringify(model)}`,
|
|
590
|
+
].map((l) => (l ? `${l} ${SETUP_MARK}` : l));
|
|
591
|
+
const body = (before ?? '').replace(/\s*$/, '');
|
|
592
|
+
const t = tomlSet(file, `${body ? `${body}\n\n` : ''}${block.join('\n')}\n`, [], 'active_model', JSON.stringify(model));
|
|
593
|
+
return { edits: [{ file, after: t }], result: { settings: file, on: true, base_url: base, model, warnings: [] } };
|
|
594
|
+
}
|
|
595
|
+
// ── Zed ──────────────────────────────────────────────────────────────────────
|
|
596
|
+
/**
|
|
597
|
+
* Zed's agent panel: `settings.json` in Zed's config folder gets an OpenAI
|
|
598
|
+
* compatible provider named `phantom` at `/coding-agent/v1`, and its model
|
|
599
|
+
* as the agent's default. Zed reads the key for a provider named `phantom`
|
|
600
|
+
* from PHANTOM_API_KEY by itself, so no key goes in the file.
|
|
601
|
+
*/
|
|
602
|
+
export function zedProvider(ctx, off) {
|
|
603
|
+
const file = path.join(configHome(ctx.env, ctx.home), 'zed', 'settings.json');
|
|
604
|
+
const text = readText(file);
|
|
605
|
+
const config = jsonObject(file, text);
|
|
606
|
+
if (off) {
|
|
607
|
+
if (!config.language_models?.openai_compatible?.phantom)
|
|
608
|
+
return offResult('Zed', file, text, text);
|
|
609
|
+
const changes = [[['language_models', 'openai_compatible', 'phantom'], undefined]];
|
|
610
|
+
if (config.agent?.default_model?.provider === 'phantom')
|
|
611
|
+
changes.push([['agent', 'default_model'], undefined]);
|
|
612
|
+
return offResult('Zed', file, text, jsonEdit(file, text, changes));
|
|
613
|
+
}
|
|
614
|
+
const base = agentBaseUrl(ctx.env, 'coding-agent');
|
|
615
|
+
const model = ctx.model ?? SETUP_MODEL;
|
|
616
|
+
const changes = [
|
|
617
|
+
[
|
|
618
|
+
['language_models', 'openai_compatible', 'phantom'],
|
|
619
|
+
{ api_url: base, available_models: [{ name: model, display_name: `Phantom AI ${model}`, max_tokens: SETUP_CONTEXT_WINDOW, capabilities: { tools: true } }] },
|
|
620
|
+
],
|
|
621
|
+
[['agent', 'default_model'], { provider: 'phantom', model }],
|
|
622
|
+
];
|
|
623
|
+
return {
|
|
624
|
+
edits: [{ file, after: jsonEdit(file, text, changes) }],
|
|
625
|
+
result: { settings: file, on: true, base_url: base, model, warnings: ['Zed reads PHANTOM_API_KEY from your login shell; quit and reopen Zed'] },
|
|
626
|
+
};
|
|
627
|
+
}
|
|
314
628
|
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
315
629
|
/**
|
|
316
630
|
* The agents `phantom setup` knows, in the order setup runs them. Adding an agent
|
|
@@ -418,6 +732,136 @@ export const SETUP_AGENTS = {
|
|
|
418
732
|
home: '.cline',
|
|
419
733
|
provider: clineProvider,
|
|
420
734
|
},
|
|
735
|
+
/** Gemini CLI. Its address and key come from the shell block; settings.json picks the sign-in and model. */
|
|
736
|
+
gemini: {
|
|
737
|
+
label: 'Gemini CLI',
|
|
738
|
+
/** ~/.gemini, or under $GEMINI_CLI_HOME */
|
|
739
|
+
home: '.gemini',
|
|
740
|
+
detect: (env, home) => existsSync(geminiDir(env, home)),
|
|
741
|
+
/** ~/.agents/skills, which Gemini CLI reads as well as ~/.gemini/skills */
|
|
742
|
+
skills: '.agents/skills',
|
|
743
|
+
provider: geminiProvider,
|
|
744
|
+
shellEnv: (ctx) => ({
|
|
745
|
+
GOOGLE_GEMINI_BASE_URL: agentBaseUrl(ctx.env, 'gemini-cli'),
|
|
746
|
+
GEMINI_API_KEY: '$PHANTOM_API_KEY',
|
|
747
|
+
}),
|
|
748
|
+
},
|
|
749
|
+
/** Crush. */
|
|
750
|
+
crush: {
|
|
751
|
+
label: 'Crush',
|
|
752
|
+
/** ~/.config/crush, or $CRUSH_GLOBAL_CONFIG */
|
|
753
|
+
home: '.config/crush',
|
|
754
|
+
detect: (env, home) => existsSync(crushDir(env, home)),
|
|
755
|
+
/** ~/.agents/skills */
|
|
756
|
+
skills: '.agents/skills',
|
|
757
|
+
provider: crushProvider,
|
|
758
|
+
shellEnv: () => ({}),
|
|
759
|
+
},
|
|
760
|
+
/** Qwen Code. */
|
|
761
|
+
qwen: {
|
|
762
|
+
label: 'Qwen Code',
|
|
763
|
+
/** ~/.qwen, or $QWEN_HOME */
|
|
764
|
+
home: '.qwen',
|
|
765
|
+
detect: (env, home) => existsSync(qwenDir(env, home)),
|
|
766
|
+
/** ~/.agents/skills */
|
|
767
|
+
skills: '.agents/skills',
|
|
768
|
+
provider: qwenProvider,
|
|
769
|
+
shellEnv: () => ({}),
|
|
770
|
+
},
|
|
771
|
+
/** Kilo Code's CLI. */
|
|
772
|
+
kilo: {
|
|
773
|
+
label: 'Kilo Code',
|
|
774
|
+
/** ~/.config/kilo, or under $XDG_CONFIG_HOME */
|
|
775
|
+
home: '.config/kilo',
|
|
776
|
+
detect: (env, home) => existsSync(path.join(configHome(env, home), 'kilo')),
|
|
777
|
+
/** ~/.agents/skills */
|
|
778
|
+
skills: '.agents/skills',
|
|
779
|
+
provider: kiloProvider,
|
|
780
|
+
shellEnv: () => ({}),
|
|
781
|
+
},
|
|
782
|
+
/** Kimi Code CLI. */
|
|
783
|
+
kimi: {
|
|
784
|
+
label: 'Kimi Code',
|
|
785
|
+
/** ~/.kimi-code, or $KIMI_CODE_HOME */
|
|
786
|
+
home: '.kimi-code',
|
|
787
|
+
detect: (env, home) => existsSync(kimiDir(env, home)),
|
|
788
|
+
/** ~/.agents/skills */
|
|
789
|
+
skills: '.agents/skills',
|
|
790
|
+
provider: kimiProvider,
|
|
791
|
+
shellEnv: () => ({}),
|
|
792
|
+
},
|
|
793
|
+
/** Factory's Droid. */
|
|
794
|
+
droid: {
|
|
795
|
+
label: 'Droid',
|
|
796
|
+
/** ~/.factory */
|
|
797
|
+
home: '.factory',
|
|
798
|
+
/** ~/.factory/skills */
|
|
799
|
+
skills: '.factory/skills',
|
|
800
|
+
provider: droidProvider,
|
|
801
|
+
shellEnv: () => ({}),
|
|
802
|
+
},
|
|
803
|
+
/** Mistral Vibe. */
|
|
804
|
+
vibe: {
|
|
805
|
+
label: 'Mistral Vibe',
|
|
806
|
+
/** ~/.vibe */
|
|
807
|
+
home: '.vibe',
|
|
808
|
+
provider: vibeProvider,
|
|
809
|
+
shellEnv: () => ({}),
|
|
810
|
+
},
|
|
811
|
+
/** Zed's agent panel. It reads PHANTOM_API_KEY from the login shell. */
|
|
812
|
+
zed: {
|
|
813
|
+
label: 'Zed',
|
|
814
|
+
/** ~/.config/zed, or under $XDG_CONFIG_HOME */
|
|
815
|
+
home: '.config/zed',
|
|
816
|
+
detect: (env, home) => existsSync(path.join(configHome(env, home), 'zed')),
|
|
817
|
+
/** ~/.agents/skills */
|
|
818
|
+
skills: '.agents/skills',
|
|
819
|
+
provider: zedProvider,
|
|
820
|
+
shellEnv: () => ({}),
|
|
821
|
+
},
|
|
822
|
+
/** VS Code's chat. Its Custom Endpoint models are added on its Language Models screen. */
|
|
823
|
+
vscode: {
|
|
824
|
+
label: 'VS Code',
|
|
825
|
+
/** ~/.vscode */
|
|
826
|
+
home: '.vscode',
|
|
827
|
+
provider: manualProvider('VS Code, Chat: Manage Language Models', (ctx) => [
|
|
828
|
+
'In VS Code, run Chat: Manage Language Models from the Command Palette, then Add Models, Custom Endpoint.',
|
|
829
|
+
'Name the group Phantom AI, paste your Phantom AI key (copy it with "Copy my key" in phantom\'s menu, or phantom keys show --copy), and pick Chat Completions.',
|
|
830
|
+
`In the chatLanguageModels.json file VS Code opens, set the model's id to ${ctx.model ?? SETUP_MODEL}, its url to ${agentBaseUrl(ctx.env, 'coding-agent')}/chat/completions, and toolCalling to true, then save.`,
|
|
831
|
+
]),
|
|
832
|
+
},
|
|
833
|
+
/** JetBrains AI Assistant, in IntelliJ IDEA, PyCharm and the other JetBrains editors. */
|
|
834
|
+
jetbrains: {
|
|
835
|
+
label: 'JetBrains AI Assistant',
|
|
836
|
+
/** ~/Library/Application Support/JetBrains on macOS, ~/.config/JetBrains elsewhere */
|
|
837
|
+
home: 'Library/Application Support/JetBrains',
|
|
838
|
+
detect: (env, home) => existsSync(path.join(home, 'Library', 'Application Support', 'JetBrains')) || existsSync(path.join(configHome(env, home), 'JetBrains')),
|
|
839
|
+
provider: manualProvider('Settings, Tools, AI Assistant, Providers & API keys', (ctx) => [
|
|
840
|
+
'In your JetBrains editor, open Settings, then Tools, AI Assistant, Providers & API keys.',
|
|
841
|
+
`Under OpenAI-compatible, set the URL to ${agentBaseUrl(ctx.env, 'coding-agent')}, paste your Phantom AI key (copy it with "Copy my key" in phantom's menu, or phantom keys show --copy), and turn on Tool calling.`,
|
|
842
|
+
`Pick ${ctx.model ?? SETUP_MODEL} in the AI Assistant chat.`,
|
|
843
|
+
]),
|
|
844
|
+
},
|
|
845
|
+
/** Warp. Its custom inference endpoint is set on its settings screen. */
|
|
846
|
+
warp: {
|
|
847
|
+
label: 'Warp',
|
|
848
|
+
/** ~/.warp */
|
|
849
|
+
home: '.warp',
|
|
850
|
+
provider: manualProvider('Warp Settings', (ctx) => [
|
|
851
|
+
'In Warp, open Settings and search for "inference endpoint".',
|
|
852
|
+
`Set the endpoint to ${agentBaseUrl(ctx.env, 'coding-agent')}, paste your Phantom AI key (copy it with "Copy my key" in phantom's menu, or phantom keys show --copy), and add the model ${ctx.model ?? SETUP_MODEL}.`,
|
|
853
|
+
], 'Warp uses a custom endpoint for agents that run on this computer only.'),
|
|
854
|
+
},
|
|
855
|
+
/** Trae. Its custom models are added on its settings screen. */
|
|
856
|
+
trae: {
|
|
857
|
+
label: 'Trae',
|
|
858
|
+
/** ~/.trae */
|
|
859
|
+
home: '.trae',
|
|
860
|
+
provider: manualProvider('Trae Settings, Models', (ctx) => [
|
|
861
|
+
'In Trae, open Settings, then Models, Add Model, Custom Model.',
|
|
862
|
+
`Pick the OpenAI API format, set the request URL to ${agentBaseUrl(ctx.env, 'coding-agent')}, the model id to ${ctx.model ?? SETUP_MODEL}, and paste your Phantom AI key (copy it with "Copy my key" in phantom's menu, or phantom keys show --copy).`,
|
|
863
|
+
]),
|
|
864
|
+
},
|
|
421
865
|
};
|
|
422
866
|
export const SETUP_NAMES = Object.keys(SETUP_AGENTS);
|
|
423
867
|
/** `a, b or c`. */
|
package/dist/setup/command.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export async function setupCommand(input) {
|
|
|
168
168
|
// At a terminal: a choice instead of typing y or n, with a look at every line first if wanted.
|
|
169
169
|
if (pick && !io.ask) {
|
|
170
170
|
for (;;) {
|
|
171
|
-
const answer = await choose({ ...pick, esc: ticks || io.fromMenu ? 'back' : '
|
|
171
|
+
const answer = await choose({ ...pick, esc: ticks || io.fromMenu ? 'back' : 'cancel' }, 'Make these changes?', [
|
|
172
172
|
{ label: 'Make these changes', value: 'yes' },
|
|
173
173
|
{ label: 'Show every line first', value: 'diff' },
|
|
174
174
|
{ label: 'Cancel', hint: 'change nothing', value: 'no' },
|
package/dist/setup/screen.mjs
CHANGED
|
@@ -208,7 +208,7 @@ function doneSummary(agent, a, files, skipped, p) {
|
|
|
208
208
|
return files ? 'skill installed.' : p.dim('already set up.');
|
|
209
209
|
}
|
|
210
210
|
/** The command that starts each agent that runs in a terminal. Editors (Cursor, Continue, Cline) are opened, not run. */
|
|
211
|
-
const START = { claude: 'claude', codex: 'codex', pi: 'pi', opencode: 'opencode', goose: 'goose', aider: 'aider', copilot: 'copilot' };
|
|
211
|
+
const START = { claude: 'claude', codex: 'codex', pi: 'pi', opencode: 'opencode', goose: 'goose', aider: 'aider', copilot: 'copilot', gemini: 'gemini', crush: 'crush', qwen: 'qwen', kilo: 'kilo', kimi: 'kimi', droid: 'droid', vibe: 'vibe' };
|
|
212
212
|
/**
|
|
213
213
|
* The one thing to do after setup, in corner brackets: run the agent. When an
|
|
214
214
|
* agent reads its key from the shell (Codex, Goose and others), the step says
|
package/dist/setup/shared.mjs
CHANGED
|
@@ -11,13 +11,14 @@ export function onPath(bin) {
|
|
|
11
11
|
export const SETUP_MODEL = 'phantom-1';
|
|
12
12
|
/**
|
|
13
13
|
* The address Phantom AI serves a coding agent at, from PHANTOM_BASE_URL
|
|
14
|
-
* without its `/v1`: `/claude-code` (Claude Code adds `/v1` itself),
|
|
15
|
-
* `/
|
|
14
|
+
* without its `/v1`: `/claude-code` (Claude Code adds `/v1` itself),
|
|
15
|
+
* `/gemini-cli` (Gemini CLI adds Google's `/v1beta`), or `/codex/v1`,
|
|
16
|
+
* `/cursor/v1` and `/coding-agent/v1` for the others.
|
|
16
17
|
* @throws {@link CliError} when PHANTOM_BASE_URL is not https ({@link httpsOnly}).
|
|
17
18
|
*/
|
|
18
19
|
export function agentBaseUrl(env, agent) {
|
|
19
20
|
const origin = httpsOnly(env.PHANTOM_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
20
|
-
return agent === 'claude-code' ? `${origin}
|
|
21
|
+
return agent === 'claude-code' || agent === 'gemini-cli' ? `${origin}/${agent}` : `${origin}/${agent}/v1`;
|
|
21
22
|
}
|
|
22
23
|
/** The end of every line phantom adds to a TOML, YAML or shell file. */
|
|
23
24
|
export const SETUP_MARK = '# phantom setup';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantomcodes/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "AI inference for your coding agents. Route models, control spending, and fund your workspace.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"phantom",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"license": "SEE LICENSE IN LICENSE",
|
|
67
67
|
"author": "Harbor Financial LLC",
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@types/node": "^24.19.
|
|
69
|
+
"@types/node": "^24.19.2",
|
|
70
70
|
"@vitest/coverage-v8": "^5.0.3",
|
|
71
71
|
"typescript": "^6.0.3",
|
|
72
|
-
"vitest": "^5.0.
|
|
72
|
+
"vitest": "^5.0.5"
|
|
73
73
|
},
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"access": "public"
|