@phantomcodes/cli 1.2.5 → 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 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
- Codex, Claude Code, opencode, Continue, Goose, Aider, GitHub Copilot CLI, Cline
66
- and Gemini CLI support saved setup. Cursor gets instructions for its settings screen; pi gets
67
- the agent skill.
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`.
@@ -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.5';
2
+ export const VERSION = '1.2.6';
@@ -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) {
@@ -350,6 +357,274 @@ export function geminiProvider(ctx, off) {
350
357
  },
351
358
  };
352
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
+ }
353
628
  // ── Registry ─────────────────────────────────────────────────────────────────
354
629
  /**
355
630
  * The agents `phantom setup` knows, in the order setup runs them. Adding an agent
@@ -471,6 +746,122 @@ export const SETUP_AGENTS = {
471
746
  GEMINI_API_KEY: '$PHANTOM_API_KEY',
472
747
  }),
473
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
+ },
474
865
  };
475
866
  export const SETUP_NAMES = Object.keys(SETUP_AGENTS);
476
867
  /** `a, b or c`. */
@@ -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', gemini: 'gemini' };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phantomcodes/cli",
3
- "version": "1.2.5",
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",