From e97572d839a330da15f490a7521adaf3b664f7b4 Mon Sep 17 00:00:00 2001 From: Prathik Shetty Date: Mon, 21 Sep 2026 22:22:12 +0530 Subject: [PATCH 1/5] test(cli): Cover naming a provider by the command it runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--provider claude-code` is a name only this project uses; the CLI people already have installed is `claude`. These tests state the naming the launcher should have — the id is the command — and keep the former names resolving from the flag, `AI_PROVIDER` and a saved default, so nobody's config has to be rewritten. --- .../src/launcher/__tests__/preflight.test.ts | 8 +- .../__tests__/provider-options.test.ts | 10 +- .../__tests__/provider-picker.test.ts | 18 ++-- .../__tests__/provider-settings.test.ts | 28 +++--- .../src/launcher/__tests__/providers.test.ts | 92 +++++++++++-------- .../src/launcher/__tests__/selection.test.ts | 35 ++++--- 6 files changed, 107 insertions(+), 84 deletions(-) diff --git a/packages/mukti-cli/src/launcher/__tests__/preflight.test.ts b/packages/mukti-cli/src/launcher/__tests__/preflight.test.ts index 55e266d..58d0324 100644 --- a/packages/mukti-cli/src/launcher/__tests__/preflight.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/preflight.test.ts @@ -38,18 +38,18 @@ function probes() { } test('preflight probes the provider when it is given no readiness', async () => { - const antigravity = providerById('antigravity')!; + const antigravity = providerById('agy')!; const { calls, run } = probes(); const result = await runPreflight({ provider: antigravity, run }); - assert.equal(result.provider.id, 'antigravity'); + assert.equal(result.provider.id, 'agy'); assert.equal(result.version, '1.1.22'); assert.deepEqual(calls, ['agy --version', 'agy models']); }); test('preflight reuses readiness the launcher already established', async () => { - const antigravity = providerById('antigravity')!; + const antigravity = providerById('agy')!; const { calls, run } = probes(); const status: ProviderStatus = { provider: antigravity, @@ -64,7 +64,7 @@ test('preflight reuses readiness the launcher already established', async () => }); test('preflight still runs the mode-specific checks it is given', async () => { - const antigravity = providerById('antigravity')!; + const antigravity = providerById('agy')!; const { run } = probes(); let ran = false; diff --git a/packages/mukti-cli/src/launcher/__tests__/provider-options.test.ts b/packages/mukti-cli/src/launcher/__tests__/provider-options.test.ts index 4a43b1e..96e94e9 100644 --- a/packages/mukti-cli/src/launcher/__tests__/provider-options.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/provider-options.test.ts @@ -23,14 +23,14 @@ test('no flags means nothing chosen, nothing saved', () => { }); test('a provider can be named, with or without saving it', () => { - assert.deepEqual(parseProviderOptions(['--provider', 'antigravity']), { + assert.deepEqual(parseProviderOptions(['--provider', 'agy']), { kind: 'ok', - options: { choose: false, provider: 'antigravity', save: false }, + options: { choose: false, provider: 'agy', save: false }, rest: [], }); - assert.deepEqual(parseProviderOptions(['--provider', 'antigravity', '--save']), { + assert.deepEqual(parseProviderOptions(['--provider', 'agy', '--save']), { kind: 'ok', - options: { choose: false, provider: 'antigravity', save: true }, + options: { choose: false, provider: 'agy', save: true }, rest: [], }); }); @@ -78,7 +78,7 @@ test('saving without naming a provider is refused', () => { }); test('naming a provider and asking to choose one is refused', () => { - const result = parseProviderOptions(['--provider', 'antigravity', '--choose']); + const result = parseProviderOptions(['--provider', 'agy', '--choose']); assert.equal(result.kind, 'error'); assert.match(result.kind === 'error' ? result.message : '', /--choose/); diff --git a/packages/mukti-cli/src/launcher/__tests__/provider-picker.test.ts b/packages/mukti-cli/src/launcher/__tests__/provider-picker.test.ts index c527011..5e76e16 100644 --- a/packages/mukti-cli/src/launcher/__tests__/provider-picker.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/provider-picker.test.ts @@ -17,8 +17,8 @@ import type { ProviderStatus } from '../providers.ts'; import { isInteractive, pickProvider } from '../provider-picker.ts'; import { providerById } from '../providers.ts'; -const CLAUDE = providerById('claude-code')!; -const ANTIGRAVITY = providerById('antigravity')!; +const CLAUDE = providerById('claude')!; +const ANTIGRAVITY = providerById('agy')!; const ready = (provider = CLAUDE): ProviderStatus => ({ provider, @@ -65,7 +65,7 @@ test('every supported CLI is offered, with its status and its trade-off', async assert.deepEqual( harness.offered.map((option) => option.value), - ['claude-code', 'antigravity'] + ['claude', 'agy'] ); const [claude, antigravity] = harness.offered; assert.match(claude.hint ?? '', new RegExp(CLAUDE.tradeOff)); @@ -97,26 +97,26 @@ test('the first ready CLI is selected to begin with', async () => { const picked = await pickProvider(statuses, harness.prompts); - assert.equal(picked?.provider.id, 'antigravity'); + assert.equal(picked?.provider.id, 'agy'); }); test('keeping the pick asks to remember it', async () => { - const harness = prompts({ confirm: true, select: 'antigravity' }); + const harness = prompts({ confirm: true, select: 'agy' }); const picked = await pickProvider([ready(CLAUDE), ready(ANTIGRAVITY)], harness.prompts); - assert.equal(picked?.provider.id, 'antigravity'); + assert.equal(picked?.provider.id, 'agy'); assert.equal(picked?.remember, true); assert.equal(harness.asked.length, 2); assert.match(harness.asked[1], /confirm:/); }); test('declining to remember still uses the pick for this launch', async () => { - const harness = prompts({ confirm: false, select: 'antigravity' }); + const harness = prompts({ confirm: false, select: 'agy' }); const picked = await pickProvider([ready(CLAUDE), ready(ANTIGRAVITY)], harness.prompts); - assert.equal(picked?.provider.id, 'antigravity'); + assert.equal(picked?.provider.id, 'agy'); assert.equal(picked?.remember, false); }); @@ -128,7 +128,7 @@ test('cancelling the choice picks nothing', async () => { }); test('cancelling the remember question picks nothing', async () => { - const harness = prompts({ confirm: CANCELLED, select: 'antigravity' }); + const harness = prompts({ confirm: CANCELLED, select: 'agy' }); assert.equal(await pickProvider([ready(CLAUDE), ready(ANTIGRAVITY)], harness.prompts), undefined); }); diff --git a/packages/mukti-cli/src/launcher/__tests__/provider-settings.test.ts b/packages/mukti-cli/src/launcher/__tests__/provider-settings.test.ts index 53940f3..30e6432 100644 --- a/packages/mukti-cli/src/launcher/__tests__/provider-settings.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/provider-settings.test.ts @@ -27,7 +27,7 @@ import { writeStoredProvider, } from '../provider-settings.ts'; -const SUPPORTED = ['claude-code', 'antigravity'] as const; +const SUPPORTED = ['claude', 'agy'] as const; let root: string; @@ -57,9 +57,9 @@ test('a missing file means no default, and says nothing', () => { }); test('a valid file yields its provider', () => { - write({ provider: 'antigravity', version: 1 }); + write({ provider: 'agy', version: 1 }); - assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'antigravity' }); + assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'agy' }); }); test('malformed JSON means no default, and names the file', () => { @@ -72,7 +72,7 @@ test('malformed JSON means no default, and names the file', () => { }); test('a version this launcher does not know means no default', () => { - write({ provider: 'antigravity', version: 99 }); + write({ provider: 'agy', version: 99 }); const result = readStoredProvider(root, SUPPORTED); @@ -96,36 +96,36 @@ test('a file with no provider means no default', () => { }); test('writing records the provider and the format version', () => { - writeStoredProvider(root, 'antigravity'); + writeStoredProvider(root, 'agy'); assert.deepEqual(JSON.parse(readFileSync(providerSettingsPath(root), 'utf8')), { - provider: 'antigravity', + provider: 'agy', version: 1, }); - assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'antigravity' }); + assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'agy' }); }); test('writing creates the home directory but nothing else', () => { const fresh = join(root, 'not-yet'); - writeStoredProvider(fresh, 'claude-code'); + writeStoredProvider(fresh, 'claude'); assert.deepEqual(readdirSync(fresh), ['config.json']); }); test('writing leaves no temporary file behind', () => { - writeStoredProvider(root, 'claude-code'); + writeStoredProvider(root, 'claude'); assert.deepEqual(readdirSync(root), ['config.json']); }); test('writing preserves settings this launcher does not know about', () => { - write({ provider: 'claude-code', somethingLater: { kept: true }, version: 1 }); + write({ provider: 'claude', somethingLater: { kept: true }, version: 1 }); - writeStoredProvider(root, 'antigravity'); + writeStoredProvider(root, 'agy'); assert.deepEqual(JSON.parse(readFileSync(providerSettingsPath(root), 'utf8')), { - provider: 'antigravity', + provider: 'agy', somethingLater: { kept: true }, version: 1, }); @@ -134,9 +134,9 @@ test('writing preserves settings this launcher does not know about', () => { test('writing over a malformed file replaces it rather than failing', () => { write('{not json'); - writeStoredProvider(root, 'antigravity'); + writeStoredProvider(root, 'agy'); - assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'antigravity' }); + assert.deepEqual(readStoredProvider(root, SUPPORTED), { provider: 'agy' }); }); test('an unreadable settings path is reported, not thrown', () => { diff --git a/packages/mukti-cli/src/launcher/__tests__/providers.test.ts b/packages/mukti-cli/src/launcher/__tests__/providers.test.ts index c22f8fe..a3eda0a 100644 --- a/packages/mukti-cli/src/launcher/__tests__/providers.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/providers.test.ts @@ -46,10 +46,18 @@ function scripted(answers: Record>) { test('supports Claude Code and Antigravity, in that detection order', () => { assert.deepEqual( SUPPORTED_PROVIDERS.map((p) => p.id), - ['claude-code', 'antigravity'] + ['claude', 'agy'] ); }); +// `--provider claude` is the whole point of the id: it is what the user types +// to run the CLI, so it cannot drift from the command being run. +test('each provider is named after the command it runs', () => { + for (const provider of SUPPORTED_PROVIDERS) { + assert.equal(provider.id, provider.binary); + } +}); + const BOTH_READY_ANSWERS = { 'agy --version': { stdout: '1.1.22\n' }, 'agy models': { stdout: 'gemini-3.8-flash-high\tGemini\n' }, @@ -80,34 +88,34 @@ function resolve( test('an explicit provider wins over AI_PROVIDER, the saved default and what is ready', async () => { const result = await resolve({ - env: 'claude-code', - explicit: 'antigravity', + env: 'claude', + explicit: 'agy', run: scripted(BOTH_READY_ANSWERS).run, - saved: 'claude-code', + saved: 'claude', }); assert.equal(result.kind, 'chosen'); - assert.equal(result.kind === 'chosen' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'chosen' && result.provider.id, 'agy'); assert.equal(result.kind === 'chosen' && result.source, 'option'); }); test('AI_PROVIDER wins over the saved default', async () => { const result = await resolve({ - env: 'antigravity', + env: 'agy', run: scripted(BOTH_READY_ANSWERS).run, - saved: 'claude-code', + saved: 'claude', }); - assert.equal(result.kind === 'chosen' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'chosen' && result.provider.id, 'agy'); assert.equal(result.kind === 'chosen' && result.source, 'env'); }); test('the saved default is used when nothing is named, and only it is probed', async () => { const probes = scripted(BOTH_READY_ANSWERS); - const result = await resolve({ run: probes.run, saved: 'antigravity' }); + const result = await resolve({ run: probes.run, saved: 'agy' }); - assert.equal(result.kind === 'chosen' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'chosen' && result.provider.id, 'agy'); assert.equal(result.kind === 'chosen' && result.source, 'saved'); assert.equal(result.kind === 'chosen' && result.status?.readiness, 'ready'); assert.deepEqual(probes.calls, ['agy --version', 'agy models']); @@ -117,16 +125,16 @@ test('AI_PROVIDER=openrouter is set aside and the ready CLIs decide', async () = const result = await resolve({ env: 'openrouter', run: scripted(ONLY_AGY_READY).run }); assert.equal(result.kind, 'detected'); - assert.equal(result.kind === 'detected' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'detected' && result.provider.id, 'agy'); assert.equal(result.kind === 'detected' && result.ignoredEnv, 'openrouter'); }); test('choosing again skips AI_PROVIDER and the saved default', async () => { const result = await resolve({ choose: true, - env: 'claude-code', + env: 'claude', run: scripted(BOTH_READY_ANSWERS).run, - saved: 'claude-code', + saved: 'claude', }); assert.equal(result.kind, 'pick'); @@ -153,16 +161,22 @@ test('an unknown explicit provider is refused', async () => { ); }); -test('a provider can be named by the command it runs as', async () => { +// Providers are named after the commands they run, but were called +// `claude-code` and `antigravity` first: scripts and saved defaults written +// then must keep resolving to the same CLI. +test('a provider can still be named by the name it used to go by', async () => { for (const [name, id] of [ - ['agy', 'antigravity'], - ['claude', 'claude-code'], + ['antigravity', 'agy'], + ['claude-code', 'claude'], ]) { const explicit = await resolve({ explicit: name, run: scripted(BOTH_READY_ANSWERS).run }); assert.equal(explicit.kind === 'chosen' && explicit.provider.id, id); const env = await resolve({ env: name, run: scripted(BOTH_READY_ANSWERS).run }); assert.equal(env.kind === 'chosen' && env.provider.id, id); + + const saved = await resolve({ run: scripted(BOTH_READY_ANSWERS).run, saved: name }); + assert.equal(saved.kind === 'chosen' && saved.provider.id, id); } }); @@ -175,18 +189,18 @@ test('an unknown AI_PROVIDER is refused', async () => { }); test('a saved default that is signed out is reported, and resolution moves on', async () => { - const result = await resolve({ run: scripted(CLAUDE_SIGNED_OUT).run, saved: 'claude-code' }); + const result = await resolve({ run: scripted(CLAUDE_SIGNED_OUT).run, saved: 'claude' }); assert.equal(result.kind, 'detected'); - assert.equal(result.kind === 'detected' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'detected' && result.provider.id, 'agy'); assert.equal(result.kind === 'detected' && result.stale?.readiness, 'signed-out'); - assert.equal(result.kind === 'detected' && result.stale?.provider.id, 'claude-code'); + assert.equal(result.kind === 'detected' && result.stale?.provider.id, 'claude'); }); test('a saved default that is no longer installed is reported, and resolution moves on', async () => { - const result = await resolve({ run: scripted(ONLY_AGY_READY).run, saved: 'claude-code' }); + const result = await resolve({ run: scripted(ONLY_AGY_READY).run, saved: 'claude' }); - assert.equal(result.kind === 'detected' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'detected' && result.provider.id, 'agy'); assert.equal(result.kind === 'detected' && result.stale?.readiness, 'missing'); }); @@ -198,7 +212,7 @@ test('nothing ready, but something installed, reports which CLI to sign in to', result.kind === 'none' ? result.statuses.filter((x) => x.readiness === 'signed-out') : []; assert.deepEqual( signedOut.map((x) => x.provider.id), - ['claude-code'] + ['claude'] ); }); @@ -216,7 +230,7 @@ test('a single ready CLI is used without asking', async () => { const result = await resolve({ run: scripted(ONLY_AGY_READY).run }); assert.equal(result.kind, 'detected'); - assert.equal(result.kind === 'detected' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'detected' && result.provider.id, 'agy'); assert.deepEqual(result.kind === 'detected' ? result.others : null, []); assert.equal(result.kind === 'detected' && result.status.readiness, 'ready'); }); @@ -226,8 +240,8 @@ test('several ready CLIs are picked between when a terminal is available', async assert.equal(result.kind, 'pick'); assert.deepEqual(result.kind === 'pick' ? result.statuses.map((x) => x.provider.id) : null, [ - 'claude-code', - 'antigravity', + 'claude', + 'agy', ]); }); @@ -238,9 +252,9 @@ test('several ready CLIs without a terminal use the first and say how to choose' }); assert.equal(result.kind, 'detected'); - assert.equal(result.kind === 'detected' && result.provider.id, 'claude-code'); + assert.equal(result.kind === 'detected' && result.provider.id, 'claude'); assert.deepEqual(result.kind === 'detected' ? result.others.map((x) => x.provider.id) : null, [ - 'antigravity', + 'agy', ]); assert.equal(result.kind === 'detected' && result.canChoose, true); }); @@ -250,7 +264,7 @@ test('several ready CLIs without a terminal use the first and say how to choose' test('a signed-out CLI is never chosen automatically', async () => { const result = await resolve({ interactive: false, run: scripted(CLAUDE_SIGNED_OUT).run }); - assert.equal(result.kind === 'detected' && result.provider.id, 'antigravity'); + assert.equal(result.kind === 'detected' && result.provider.id, 'agy'); }); test('the no-provider remediation names every supported option', () => { @@ -272,7 +286,7 @@ test('an unsupported value is explained with every supported id', () => { }); test('the Antigravity sign-in probe lists models and never runs a completion', async () => { - const agy = providerById('antigravity')!; + const agy = providerById('agy')!; const signedIn = scripted({ 'agy models': { stdout: 'gemini-3.8-flash-high\tGemini\n' } }); const signedOut = scripted({ 'agy models': { status: 1, stderr: 'not signed in' } }); @@ -285,7 +299,7 @@ test('the Antigravity sign-in probe lists models and never runs a completion', a }); test('the Claude sign-in probe reads `claude auth status`', async () => { - const claude = providerById('claude-code')!; + const claude = providerById('claude')!; assert.equal( await claude.isAuthenticated( @@ -306,7 +320,7 @@ test('the Claude sign-in probe reads `claude auth status`', async () => { }); test('a CLI that cannot run or exits non-zero reports no version', async () => { - const agy = providerById('antigravity')!; + const agy = providerById('agy')!; assert.equal(await agy.version(scripted({}).run), undefined); assert.equal(await agy.version(scripted({ 'agy --version': { status: 2 } }).run), undefined); @@ -317,19 +331,19 @@ test('a CLI that cannot run or exits non-zero reports no version', async () => { }); test('each provider names its own sign-in step', () => { - assert.match(providerById('claude-code')!.signInRemediation, /claude login/); - assert.match(providerById('antigravity')!.signInRemediation, /`agy`/); - assert.doesNotMatch(providerById('antigravity')!.signInRemediation, /claude/); + assert.match(providerById('claude')!.signInRemediation, /claude login/); + assert.match(providerById('agy')!.signInRemediation, /`agy`/); + assert.doesNotMatch(providerById('agy')!.signInRemediation, /claude/); }); test('Antigravity discloses its cost, its latency, what it leaves behind and what can reach it', () => { - const disclosure = providerById('antigravity')!.disclosure ?? ''; + const disclosure = providerById('agy')!.disclosure ?? ''; assert.match(disclosure, /15–25k input tokens/); assert.match(disclosure, /30–60 seconds/); assert.match(disclosure, /Antigravity history/); assert.match(disclosure, /global agy rules and hooks/); - assert.equal(providerById('claude-code')!.disclosure, undefined); + assert.equal(providerById('claude')!.disclosure, undefined); }); const BOTH_READY = { @@ -340,7 +354,7 @@ const BOTH_READY = { }; test('readiness tells missing, signed out and ready apart', async () => { - const claude = providerById('claude-code')!; + const claude = providerById('claude')!; assert.equal((await probeProvider(claude, scripted({}).run)).readiness, 'missing'); assert.equal( @@ -359,7 +373,7 @@ test('readiness tells missing, signed out and ready apart', async () => { test('a CLI that is not installed is not asked about sign-in', async () => { const probes = scripted({}); - await probeProvider(providerById('antigravity')!, probes.run); + await probeProvider(providerById('agy')!, probes.run); assert.deepEqual(probes.calls, ['agy --version']); }); @@ -369,7 +383,7 @@ test('scanning covers every supported provider, in registry order', async () => assert.deepEqual( statuses.map((s) => s.provider.id), - ['claude-code', 'antigravity'] + ['claude', 'agy'] ); assert.deepEqual( statuses.map((s) => s.readiness), diff --git a/packages/mukti-cli/src/launcher/__tests__/selection.test.ts b/packages/mukti-cli/src/launcher/__tests__/selection.test.ts index 8e3a70e..9a720a8 100644 --- a/packages/mukti-cli/src/launcher/__tests__/selection.test.ts +++ b/packages/mukti-cli/src/launcher/__tests__/selection.test.ts @@ -79,37 +79,46 @@ function select(options: { } test('a named provider is used, reported as such, and not saved', async () => { - const selection = await select({ explicit: 'antigravity' }); + const selection = await select({ explicit: 'agy' }); assert.equal(selection.kind, 'ready'); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.equal(selection.kind === 'ready' && selection.remember, false); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /--provider/); }); test('a named provider with --save is remembered only once preflight has passed', async () => { - const selection = await select({ explicit: 'antigravity', save: true }); + const selection = await select({ explicit: 'agy', save: true }); assert.equal(selection.kind === 'ready' && selection.remember, true); // Selection itself writes nothing: the launcher saves after preflight. - assert.equal(readStoredProvider(home, ['antigravity', 'claude-code']).provider, undefined); + assert.equal(readStoredProvider(home, ['agy', 'claude']).provider, undefined); }); test('AI_PROVIDER is reported as the source', async () => { - process.env.AI_PROVIDER = 'antigravity'; + process.env.AI_PROVIDER = 'agy'; const selection = await select({}); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /AI_PROVIDER/); }); -test('a saved default is used, and the report says how to change it', async () => { +test('a saved default under a provider’s former name is still used', async () => { writeStoredProvider(home, 'antigravity'); const selection = await select({}); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); + assert.equal(selection.kind === 'ready' && selection.status?.readiness, 'ready'); +}); + +test('a saved default is used, and the report says how to change it', async () => { + writeStoredProvider(home, 'agy'); + + const selection = await select({}); + + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.equal(selection.kind === 'ready' && selection.status?.readiness, 'ready'); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /--choose/); }); @@ -118,7 +127,7 @@ test('a saved default is used, and the report says how to change it', async () = // there is nothing to pick: the remaining one is used and nothing is saved. // The picker's stale notice matters once a third provider exists. test('a saved default that is signed out is explained, and the ready CLI is used', async () => { - writeStoredProvider(home, 'claude-code'); + writeStoredProvider(home, 'claude'); const selection = await select({ answers: { ...ONLY_AGY_READY, ...CLAUDE_SIGNED_OUT }, @@ -127,7 +136,7 @@ test('a saved default that is signed out is explained, and the ready CLI is used const notices = selection.kind === 'ready' ? selection.notices.join('\n') : ''; assert.match(notices, /not signed in/); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.equal(selection.kind === 'ready' && selection.remember, false); }); @@ -136,7 +145,7 @@ test('a pick that is kept is remembered', async () => { pick: (statuses) => Promise.resolve({ provider: statuses[1].provider, remember: true }), }); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.equal(selection.kind === 'ready' && selection.remember, true); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /chosen just now/); }); @@ -144,14 +153,14 @@ test('a pick that is kept is remembered', async () => { test('the only ready CLI is used and reported as the only one', async () => { const selection = await select({ answers: ONLY_AGY_READY }); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'antigravity'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'agy'); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /only/i); }); test('several ready CLIs without a terminal take the first and say how to choose', async () => { const selection = await select({ interactive: false }); - assert.equal(selection.kind === 'ready' && selection.provider.id, 'claude-code'); + assert.equal(selection.kind === 'ready' && selection.provider.id, 'claude'); assert.match(selection.kind === 'ready' ? selection.notices.join('\n') : '', /--choose/); }); From 7bb812e5804558cf2fefbd936fd24789f6c6ca95 Mon Sep 17 00:00:00 2001 From: Prathik Shetty Date: Mon, 21 Sep 2026 22:22:18 +0530 Subject: [PATCH 2/5] feat(cli,scripts): Name each provider after the command it runs The id is what `--provider` takes, what is saved as the default and what is passed on as `AI_PROVIDER`, so it should be the name the user already knows the CLI by: `claude` and `agy`, not `claude-code` and `antigravity`. The former names move to a per-provider `aliases` list and are still accepted everywhere a provider can be named, so saved defaults and older scripts keep working. --- packages/mukti-cli/src/index.ts | 2 +- packages/mukti-cli/src/launcher/providers.ts | 38 +++++++++++++------- scripts/start-local.ts | 2 +- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/mukti-cli/src/index.ts b/packages/mukti-cli/src/index.ts index 05cf209..15ffc24 100644 --- a/packages/mukti-cli/src/index.ts +++ b/packages/mukti-cli/src/index.ts @@ -92,7 +92,7 @@ const HELP = ` --api-port Port for the API (default ${DEFAULT_API_PORT}) --data-dir Where to keep the database and logs (default ~/.mukti, or $MUKTI_HOME) - --provider AI CLI to run on: ${SUPPORTED_PROVIDERS.map((p) => `${p.id} (${p.binary})`).join(' or ')} + --provider AI CLI to run on: ${SUPPORTED_PROVIDERS.map((p) => p.id).join(' or ')} (default: your saved choice, else $AI_PROVIDER, else whichever CLI is ready) --save Save --provider as your default for later runs diff --git a/packages/mukti-cli/src/launcher/providers.ts b/packages/mukti-cli/src/launcher/providers.ts index 6286c4a..d72bd62 100644 --- a/packages/mukti-cli/src/launcher/providers.ts +++ b/packages/mukti-cli/src/launcher/providers.ts @@ -30,6 +30,12 @@ export interface CommandResult { export type ExplicitSource = 'env' | 'option'; export interface LocalCliProvider { + /** + * Names this provider used to answer to. Still accepted from `--provider`, + * `AI_PROVIDER` and a saved default, so commands and config files written + * before the providers were named after their commands keep working. + */ + readonly aliases: readonly string[]; /** Executable looked up on PATH. */ readonly binary: string; /** @@ -37,7 +43,10 @@ export interface LocalCliProvider { * leaves on their machine, told before they discover it. */ readonly disclosure?: string; - /** The `AI_PROVIDER` value, and what `--provider` accepts alongside {@link LocalCliProvider.binary}. */ + /** + * The `AI_PROVIDER` value, and what `--provider` takes: the command the CLI + * runs as, which is the name its users already know it by. + */ readonly id: LocalCliProviderId; readonly installRemediation: string; /** Signed in and usable. Must not consume model tokens. */ @@ -55,7 +64,7 @@ export interface LocalCliProvider { version(run: RunCommand): Promise; } -export type LocalCliProviderId = 'antigravity' | 'claude-code'; +export type LocalCliProviderId = 'agy' | 'claude'; /** How usable a provider is on this machine. */ export type ProviderReadiness = 'missing' | 'ready' | 'signed-out'; @@ -131,8 +140,9 @@ async function versionOf(run: RunCommand, binary: string): Promise [ + provider.id, + ...provider.aliases, +]); + export const NO_PROVIDER_REMEDIATION = [ 'Mukti runs on an AI CLI you have installed and signed in to. Install one of:', ...SUPPORTED_PROVIDERS.map( @@ -236,7 +253,7 @@ export function describeUnsupportedProvider(options: { readonly value: string; }): string { const origin = options.source === 'option' ? '--provider' : 'AI_PROVIDER'; - const ids = SUPPORTED_PROVIDERS.map((p) => `${p.id} (or ${p.binary})`).join(', '); + const ids = SUPPORTED_PROVIDERS.map((p) => p.id).join(', '); return `${origin} is "${options.value}", which is not a supported local AI CLI. Use one of: ${ids}.`; } @@ -255,11 +272,11 @@ export async function probeProvider( } /** - * The provider a `--provider` or `AI_PROVIDER` value names: its id, or the - * command it runs as (`agy`, `claude`) — the name people know the CLI by. + * The provider a `--provider` or `AI_PROVIDER` value names: its id, or one of + * the names it used to go by. */ export function providerById(id: string): LocalCliProvider | undefined { - return SUPPORTED_PROVIDERS.find((p) => p.id === id || p.binary === id); + return SUPPORTED_PROVIDERS.find((p) => p.id === id || p.aliases.includes(id)); } /** @@ -384,10 +401,7 @@ export async function selectProvider(options: { const { home, interactive, pick = pickProvider, run = runCommand } = options; const notices: string[] = []; - const stored = readStoredProvider( - home, - SUPPORTED_PROVIDERS.map((provider) => provider.id) - ); + const stored = readStoredProvider(home, PROVIDER_NAMES); if (stored.warning) { notices.push(stored.warning); } diff --git a/scripts/start-local.ts b/scripts/start-local.ts index 6447a06..5374835 100644 --- a/scripts/start-local.ts +++ b/scripts/start-local.ts @@ -26,7 +26,7 @@ * Node's own type stripping) — hence the explicit `.ts` extensions on the local * imports below, which Node's ESM resolver requires. * - * Usage: bun run start:local [-- --provider claude-code|antigravity] + * Usage: bun run start:local [-- --provider claude|agy] * bun run start:local -- --choose (ask, and offer to remember) * bun run start:local -- --provider agy --save * (or npm run start:local -- …) From 6021f4fde3b161c71e6a847481dc58f19e554c5b Mon Sep 17 00:00:00 2001 From: Prathik Shetty Date: Mon, 21 Sep 2026 22:22:23 +0530 Subject: [PATCH 3/5] test(api): Cover the launcher's provider names as AI_PROVIDER values The launcher passes the provider it chose straight through as `AI_PROVIDER`, and it now names them after their commands, so `claude` and `agy` have to reach the same adapters as `claude-code` and `antigravity` or local mode stops booting. --- .../__tests__/ai-provider.registry.spec.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/mukti-api/src/modules/ai/services/__tests__/ai-provider.registry.spec.ts b/packages/mukti-api/src/modules/ai/services/__tests__/ai-provider.registry.spec.ts index 2446191..7a04c25 100644 --- a/packages/mukti-api/src/modules/ai/services/__tests__/ai-provider.registry.spec.ts +++ b/packages/mukti-api/src/modules/ai/services/__tests__/ai-provider.registry.spec.ts @@ -60,6 +60,18 @@ describe('AiProviderRegistry', () => { expect(r.isLocalCliProvider()).toBe(true); }); + // The launcher asks for these CLIs by the command they run and passes that + // name straight through, so both spellings have to reach the same adapter. + it.each([ + ['claude', ClaudeCliAdapter], + ['agy', AntigravityCliAdapter], + ])('resolves %s, the command name, to its adapter', (provider, adapter) => { + const r = registry({ AI_PROVIDER: provider }); + + expect(r.getActiveLocalCliAdapter()).toBeInstanceOf(adapter); + expect(r.isLocalCliProvider()).toBe(true); + }); + it('runs agy from the workspace named by MUKTI_AGY_WORKSPACE', () => { const workspace = join(root, 'custom-workspace'); const adapter = registry({ @@ -74,7 +86,7 @@ describe('AiProviderRegistry', () => { const r = registry({ AI_PROVIDER: 'gemini-cli' }); await expect(r.onModuleInit()).rejects.toThrow( - 'Unsupported AI_PROVIDER "gemini-cli". Supported values: openrouter, claude-code, antigravity.', + 'Unsupported AI_PROVIDER "gemini-cli". Supported values: openrouter, claude-code, antigravity, agy, claude.', ); expect(r.isLocalCliProvider()).toBe(false); }); From 1a29eb0cfa476528faa863a0a09018f9cc0be4cf Mon Sep 17 00:00:00 2001 From: Prathik Shetty Date: Mon, 21 Sep 2026 22:22:28 +0530 Subject: [PATCH 4/5] feat(api): Accept the launcher's provider names in AI_PROVIDER `claude` and `agy` resolve to the adapters registered as `claude-code` and `antigravity`, so the name the launcher hands over is the name the API understands. Aliasing rather than renaming the adapters leaves every existing `.env` working, and the no-key message now names the CLIs the way users meet them. --- .../ai/services/ai-provider.registry.ts | 28 +++++++++++++++---- .../dialogue/services/dialogue-ai.service.ts | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/mukti-api/src/modules/ai/services/ai-provider.registry.ts b/packages/mukti-api/src/modules/ai/services/ai-provider.registry.ts index 4a474cf..8373bb2 100644 --- a/packages/mukti-api/src/modules/ai/services/ai-provider.registry.ts +++ b/packages/mukti-api/src/modules/ai/services/ai-provider.registry.ts @@ -27,6 +27,18 @@ const OPENROUTER_PROVIDER = 'openrouter'; const DEFAULT_PROVIDER = OPENROUTER_PROVIDER; +/** + * Local CLIs named after the commands they run. The launcher asks for them + * this way (`--provider claude`, `--provider agy`) and passes the name + * straight through as `AI_PROVIDER`, while the adapters were first registered + * under longer names. Both are accepted so neither a saved launcher default + * nor an older `.env` has to be rewritten. + */ +const PROVIDER_ALIASES: Readonly> = { + agy: 'antigravity', + claude: 'claude-code', +}; + @Injectable() export class AiProviderRegistry implements OnModuleInit { private readonly adapters: readonly LocalCliAdapter[]; @@ -55,16 +67,19 @@ export class AiProviderRegistry implements OnModuleInit { return this.adapters.find((a) => a.providerId === provider); } - /** Raw `AI_PROVIDER` value, defaulting to `openrouter` when unset. */ + /** + * The configured `AI_PROVIDER` with any alias resolved, defaulting to + * `openrouter` when unset. + */ getProviderId(): string { // `||` (not `??`) is deliberate: an empty or whitespace-only AI_PROVIDER is // an unset one, and must fall through to the default rather than being // treated as a configured provider name. /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ - return ( - this.configService.get('AI_PROVIDER')?.trim() || DEFAULT_PROVIDER - ); + const configured = + this.configService.get('AI_PROVIDER')?.trim() || DEFAULT_PROVIDER; /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ + return PROVIDER_ALIASES[configured] ?? configured; } /** Whether completions route through a local, user-authenticated CLI. */ @@ -103,7 +118,10 @@ export class AiProviderRegistry implements OnModuleInit { if (!supported.includes(provider)) { throw new Error( - `Unsupported AI_PROVIDER "${provider}". Supported values: ${supported.join(', ')}.`, + `Unsupported AI_PROVIDER "${provider}". Supported values: ${[ + ...supported, + ...Object.keys(PROVIDER_ALIASES), + ].join(', ')}.`, ); } } diff --git a/packages/mukti-api/src/modules/dialogue/services/dialogue-ai.service.ts b/packages/mukti-api/src/modules/dialogue/services/dialogue-ai.service.ts index 2a69905..04a8e68 100644 --- a/packages/mukti-api/src/modules/dialogue/services/dialogue-ai.service.ts +++ b/packages/mukti-api/src/modules/dialogue/services/dialogue-ai.service.ts @@ -394,7 +394,7 @@ export class DialogueAIService { } throw new Error( - 'No AI API key is configured. Add your OpenRouter key in Settings, set OPENROUTER_API_KEY on the server, or run locally with a local-CLI provider (AI_PROVIDER=claude-code or antigravity).', + 'No AI API key is configured. Add your OpenRouter key in Settings, set OPENROUTER_API_KEY on the server, or run locally with a local-CLI provider (AI_PROVIDER=claude or agy).', ); } From ed0651d43dedc862c26ef27680d44e6f5fe4237b Mon Sep 17 00:00:00 2001 From: Prathik Shetty Date: Mon, 21 Sep 2026 22:22:33 +0530 Subject: [PATCH 5/5] docs(cli,config): Document the provider names as claude and agy The quickstart, the local-mode guide and the env example all taught the old names; they now teach the command names, and say once that the former ones are still accepted rather than repeating it at every mention. --- .env.example | 13 +++++++------ README.md | 2 +- docs/local-mode.md | 22 +++++++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 506a6a4..58ab539 100644 --- a/.env.example +++ b/.env.example @@ -21,14 +21,15 @@ OPENROUTER_API_KEY= # ── AI provider ───────────────────────────────────────────────────────────── # openrouter (default): hosted models through OPENROUTER_API_KEY. -# claude-code | antigravity: run on an AI CLI you have installed and signed in -# to (`claude` or `agy`) instead — no API key, no quota. `start:local` and -# `npx muktiai` set this themselves (pick with --provider); set it here only -# when running the API directly in local mode. Antigravity costs roughly 15k -# input tokens and 30–45 seconds per reply — see the README before choosing it. +# claude | agy: run on the AI CLI of that name, installed and signed in, instead +# — no API key, no quota. Each provider is named after its command; the older +# names claude-code and antigravity still work. `start:local` and `npx muktiai` +# set this themselves (pick with --provider); set it here only when running the +# API directly in local mode. Antigravity costs roughly 15k input tokens and +# 30–45 seconds per reply — see the README before choosing it. # AI_PROVIDER=openrouter # -# antigravity only: the directory agy runs from, which also names the agy +# agy only: the directory agy runs from, which also names the agy # project Mukti's replies are filed under. Defaults to ~/.mukti/mukti-socratic. # Never point it inside a repository: agy would pick up that repo's own rules. # MUKTI_AGY_WORKSPACE= diff --git a/README.md b/README.md index b93f6d0..561cb90 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ To override: ```bash npx muktiai --choose # ask again -npx muktiai --provider antigravity --save # set the default, no prompt +npx muktiai --provider agy --save # set the default, no prompt ``` | | Claude Code | Antigravity | diff --git a/docs/local-mode.md b/docs/local-mode.md index 846eeb0..bbf524c 100644 --- a/docs/local-mode.md +++ b/docs/local-mode.md @@ -14,11 +14,15 @@ Both share the same launcher, preflight checks, and saved settings. ## Supported AI CLIs -| CLI | Provider id | Status | -| ------------------------------------------------------------------------------ | ------------- | ----------- | -| [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) (`claude`) | `claude-code` | Supported | -| [Antigravity](https://antigravity.google/docs/cli/reference) (`agy`) | `antigravity` | Supported | -| Codex, OpenCode, Pi, Grok, and more | — | Coming soon | +| CLI | Provider id | Status | +| ------------------------------------------------------------------------------ | ----------- | ----------- | +| [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) (`claude`) | `claude` | Supported | +| [Antigravity](https://antigravity.google/docs/cli/reference) (`agy`) | `agy` | Supported | +| Codex, OpenCode, Pi, Grok, and more | — | Coming soon | + +Each provider is named after the command it runs, so `--provider claude` and `--provider agy` are +all you have to remember. The older names `claude-code` and `antigravity` still work everywhere a +provider can be named. Sign in first: `claude login`, or run `agy` once and follow the prompts. @@ -41,8 +45,8 @@ so. To change it, or to decide without being asked: ```bash npx muktiai --choose # ask again, and offer to remember -npx muktiai --provider antigravity # just this once -npx muktiai --provider antigravity --save # set the default, no prompt +npx muktiai --provider agy # just this once +npx muktiai --provider agy --save # set the default, no prompt ``` From a checkout, pass the same flags after `--`: `bun run start:local -- --choose`. @@ -60,7 +64,7 @@ you how to choose another. The two are not equivalent, and the difference is worth knowing before you choose: -| | Claude Code (`claude-code`) | Antigravity (`antigravity`) | +| | Claude Code (`claude`) | Antigravity (`agy`) | | ------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Time per reply (measured) | about 13–20 seconds | about 30–60 seconds | | Prompt size per reply | Mukti's own prompt and your conversation | about 15–25k input tokens: the same, plus agy's own ~14k-token coding-agent prompt, which cannot be replaced | @@ -108,7 +112,7 @@ npx muktiai [options] --port, --web-port Port for the web app (default 3001) --api-port Port for the API (default 3000) --data-dir Where to keep the database and logs (default ~/.mukti, or $MUKTI_HOME) - --provider AI CLI to run on: claude-code or antigravity + --provider AI CLI to run on: claude or agy --save Save --provider as your default for later runs --choose Ask which AI CLI to use, and offer to save it -h, --help Show help