Skip to content

Commit 52c71da

Browse files
committed
feat: add jam models set command, read default model from bridge file
1 parent 062c93b commit 52c71da

4 files changed

Lines changed: 77 additions & 11 deletions

File tree

src/commands/completion.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function generateBashCompletion(): string {
4848
' return 0',
4949
' ;;',
5050
' models)',
51-
' COMPREPLY=( $(compgen -W "list" -- "${cur}") )',
51+
' COMPREPLY=( $(compgen -W "list set" -- "${cur}") )',
5252
' return 0',
5353
' ;;',
5454
' history)',
@@ -85,7 +85,7 @@ function generateZshCompletion(): string {
8585
' local -a auth_cmds config_cmds models_cmds history_cmds completion_cmds',
8686
" auth_cmds=('login' 'logout')",
8787
" config_cmds=('show' 'init')",
88-
" models_cmds=('list')",
88+
" models_cmds=('list' 'set')",
8989
" history_cmds=('list' 'show')",
9090
" completion_cmds=('install')",
9191
'',
@@ -127,7 +127,7 @@ function generatePowerShellCompletion(): string {
127127
' $subcommands = @{',
128128
" 'auth' = @('login', 'logout')",
129129
" 'config' = @('show', 'init')",
130-
" 'models' = @('list')",
130+
" 'models' = @('list', 'set')",
131131
" 'history' = @('list', 'show')",
132132
" 'completion' = @('install')",
133133
' }',

src/commands/models.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,58 @@ import { loadConfig, getActiveProfile } from '../config/loader.js';
22
import { createProvider } from '../providers/factory.js';
33
import { JamError } from '../utils/errors.js';
44
import type { CliOverrides } from '../config/schema.js';
5+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
6+
import { join } from 'node:path';
7+
import { homedir } from 'node:os';
8+
9+
export async function runModelsSet(model: string, options: CliOverrides): Promise<void> {
10+
try {
11+
// Validate: list available models and check if the requested one exists
12+
const config = await loadConfig(process.cwd(), options);
13+
const profile = getActiveProfile(config);
14+
const adapter = await createProvider(profile);
15+
16+
let available: string[];
17+
try {
18+
available = await adapter.listModels();
19+
} catch {
20+
available = [];
21+
}
22+
23+
if (available.length > 0 && !available.includes(model)) {
24+
process.stderr.write(`Warning: "${model}" not found in available models for ${profile.provider}.\n`);
25+
process.stderr.write(`Available: ${available.join(', ')}\n\n`);
26+
process.stderr.write(`Setting it anyway — it may work if the provider accepts it.\n\n`);
27+
}
28+
29+
// Read or create ~/.jam/config.json
30+
const configDir = join(homedir(), '.jam');
31+
const configPath = join(configDir, 'config.json');
32+
33+
let existing: Record<string, unknown> = {};
34+
try {
35+
existing = JSON.parse(await readFile(configPath, 'utf-8')) as Record<string, unknown>;
36+
} catch { /* file doesn't exist yet */ }
37+
38+
// Merge the model into the active profile
39+
const profileName = (existing['defaultProfile'] as string) ?? 'default';
40+
const profiles = (existing['profiles'] ?? {}) as Record<string, Record<string, unknown>>;
41+
const activeProfile = profiles[profileName] ?? {};
42+
43+
profiles[profileName] = { ...activeProfile, model };
44+
existing['profiles'] = profiles;
45+
46+
await mkdir(configDir, { recursive: true });
47+
await writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
48+
49+
process.stdout.write(`Model set to "${model}" for profile "${profileName}".\n`);
50+
process.stdout.write(`Config: ${configPath}\n`);
51+
} catch (err) {
52+
const jamErr = JamError.fromUnknown(err);
53+
process.stderr.write(`Error: ${jamErr.message}\n`);
54+
process.exit(1);
55+
}
56+
}
557

658
export async function runModelsList(options: CliOverrides): Promise<void> {
759
try {

src/config/loader.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,30 +144,35 @@ async function isCopilotCliInstalled(): Promise<boolean> {
144144
* The VSCode extension writes this file when the proxy starts.
145145
* Falls back to JAM_VSCODE_LM_PORT env var.
146146
*/
147-
function readBridgePort(): string | null {
147+
interface BridgeInfo {
148+
port: string;
149+
model?: string;
150+
}
151+
152+
function readBridgeInfo(): BridgeInfo | null {
148153
if (process.env['JAM_VSCODE_LM_PORT']) {
149-
return process.env['JAM_VSCODE_LM_PORT'];
154+
return { port: process.env['JAM_VSCODE_LM_PORT'] };
150155
}
151156
try {
152157
const bridgePath = join(homedir(), '.jam', 'copilot-bridge.json');
153-
const data = JSON.parse(readFileSync(bridgePath, 'utf-8')) as { port?: number; pid?: number };
158+
const data = JSON.parse(readFileSync(bridgePath, 'utf-8')) as { port?: number; pid?: number; model?: string };
154159
if (data.port) {
155160
// Verify the VSCode process is still running
156161
if (data.pid) {
157162
try { process.kill(data.pid, 0); } catch { return null; } // process gone
158163
}
159-
return String(data.port);
164+
return { port: String(data.port), model: data.model };
160165
}
161166
} catch { /* file doesn't exist or invalid */ }
162167
return null;
163168
}
164169

165170
async function detectBestProvider(): Promise<{ provider: string; model?: string } | null> {
166171
// 1. VSCode Copilot proxy (env var or bridge file)
167-
const bridgePort = readBridgePort();
168-
if (bridgePort) {
169-
process.env['JAM_VSCODE_LM_PORT'] = bridgePort; // propagate for downstream use
170-
return { provider: 'copilot' };
172+
const bridge = readBridgeInfo();
173+
if (bridge) {
174+
process.env['JAM_VSCODE_LM_PORT'] = bridge.port; // propagate for downstream use
175+
return { provider: 'copilot', model: bridge.model };
171176
}
172177

173178
// 2. Copilot CLI (@github/copilot)

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,15 @@ models
331331
await runModelsList({ profile: g.profile, provider: g.provider, baseUrl: g.baseUrl });
332332
});
333333

334+
models
335+
.command('set <model>')
336+
.description('Set the default model for the current provider')
337+
.action(async (model: string) => {
338+
const g = globalOpts();
339+
const { runModelsSet } = await import('./commands/models.js');
340+
await runModelsSet(model, { profile: g.profile, provider: g.provider, baseUrl: g.baseUrl });
341+
});
342+
334343
// ── history ───────────────────────────────────────────────────────────────────
335344
const history = program.command('history').description('Manage chat session history');
336345

0 commit comments

Comments
 (0)