@developerz.ai/aitm 0.0.3 → 0.0.5

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
@@ -76,8 +76,18 @@ aitm config set autoMerge true --project
76
76
  aitm config list
77
77
  ```
78
78
 
79
- - **Provider**: OpenRouter only (`OPENROUTER_API_KEY`). Any OpenRouter-routed
80
- model id works per role no Anthropic SDK involved.
79
+ - **Provider**: any OpenAI-compatible endpoint via one credential — OpenRouter by
80
+ default, or set `baseURL` to run on z.ai GLM, a self-hosted gateway, etc. No
81
+ Anthropic SDK. **Profiles** switch the whole provider in one command:
82
+
83
+ ```bash
84
+ aitm profile add z.ai --preset zai --api-key "<your z.ai key>"
85
+ aitm profile use z.ai # ✅ verified end-to-end on z.ai GLM (glm-5.2 / glm-5-turbo)
86
+ aitm profile use openrouter
87
+ ```
88
+
89
+ See [providers](https://github.com/developerz-ai/ai-task-master/blob/main/packages/aitm/docs/providers.md)
90
+ and [`aitm profile`](https://github.com/developerz-ai/ai-task-master/blob/main/packages/aitm/docs/commands/profile.md).
81
91
  - **Coding style**: `aitm` reads your repo's `CLAUDE.md` / `AGENTS.md` and feeds
82
92
  it to subagents as a style signal (the provider stays OpenRouter).
83
93
  - **MCP**: `aitm` is an MCP **client** — declare `mcpServers` in config and their
@@ -1,3 +1,4 @@
1
+ import { type PresetName } from '../config/provider-presets.ts';
1
2
  export type StartArgs = {
2
3
  kind: 'start';
3
4
  goal: string;
@@ -31,7 +32,34 @@ export type ConfigArgs = {
31
32
  kind: 'config-list';
32
33
  scope: 'global' | 'project';
33
34
  };
34
- export type ParsedArgs = StartArgs | MergePrArgs | ConfigArgs | {
35
+ export type ProfileArgs = {
36
+ kind: 'profile-list';
37
+ } | {
38
+ kind: 'profile-use';
39
+ name: string;
40
+ } | {
41
+ kind: 'profile-add';
42
+ name: string;
43
+ preset?: PresetName;
44
+ baseURL?: string;
45
+ apiKey?: string;
46
+ } | {
47
+ kind: 'profile-set';
48
+ name: string;
49
+ key: string;
50
+ value: string;
51
+ } | {
52
+ kind: 'profile-get';
53
+ name: string;
54
+ key: string;
55
+ } | {
56
+ kind: 'profile-remove';
57
+ name: string;
58
+ } | {
59
+ kind: 'profile-show';
60
+ name?: string;
61
+ };
62
+ export type ParsedArgs = StartArgs | MergePrArgs | ConfigArgs | ProfileArgs | {
35
63
  kind: 'help';
36
64
  };
37
65
  export declare function parseArgs(argv: ReadonlyArray<string>): ParsedArgs;
package/dist/cli/args.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isPresetName } from "../config/provider-presets.js";
1
2
  const HELP = { kind: 'help' };
2
3
  export function parseArgs(argv) {
3
4
  const [command, ...rest] = argv;
@@ -12,6 +13,8 @@ export function parseArgs(argv) {
12
13
  return parseMergePr(rest);
13
14
  case 'config':
14
15
  return parseConfig(rest);
16
+ case 'profile':
17
+ return parseProfile(rest);
15
18
  default:
16
19
  return HELP;
17
20
  }
@@ -194,6 +197,116 @@ function parseConfig(args) {
194
197
  return HELP;
195
198
  }
196
199
  }
200
+ function parseProfile(args) {
201
+ const sub = args[0];
202
+ if (sub === undefined)
203
+ return HELP;
204
+ const tail = args.slice(1);
205
+ switch (sub) {
206
+ case 'list':
207
+ return tail.length === 0 ? { kind: 'profile-list' } : HELP;
208
+ case 'use': {
209
+ const name = onlyName(tail);
210
+ return name === null ? HELP : { kind: 'profile-use', name };
211
+ }
212
+ case 'remove': {
213
+ const name = onlyName(tail);
214
+ return name === null ? HELP : { kind: 'profile-remove', name };
215
+ }
216
+ case 'show': {
217
+ if (tail.length === 0)
218
+ return { kind: 'profile-show' };
219
+ const name = onlyName(tail);
220
+ return name === null ? HELP : { kind: 'profile-show', name };
221
+ }
222
+ case 'get': {
223
+ if (tail.length !== 2)
224
+ return HELP;
225
+ const [name, key] = tail;
226
+ if (name === undefined ||
227
+ key === undefined ||
228
+ name.startsWith('--') ||
229
+ key.startsWith('--')) {
230
+ return HELP;
231
+ }
232
+ return { kind: 'profile-get', name, key };
233
+ }
234
+ case 'set': {
235
+ if (tail.length !== 3)
236
+ return HELP;
237
+ const [name, key, value] = tail;
238
+ if (name === undefined || key === undefined || value === undefined)
239
+ return HELP;
240
+ if (name.startsWith('--') || key.startsWith('--'))
241
+ return HELP;
242
+ return { kind: 'profile-set', name, key, value };
243
+ }
244
+ case 'add':
245
+ return parseProfileAdd(tail);
246
+ default:
247
+ return HELP;
248
+ }
249
+ }
250
+ function parseProfileAdd(tail) {
251
+ const positionals = [];
252
+ let preset;
253
+ let baseURL;
254
+ let apiKey;
255
+ let i = 0;
256
+ while (i < tail.length) {
257
+ const raw = tail[i];
258
+ if (raw === undefined)
259
+ break;
260
+ const { flag, inlineValue, consumed } = splitFlag(raw);
261
+ if (flag === '--preset') {
262
+ const v = takeValue(tail, i, inlineValue);
263
+ if (v === null || !isPresetName(v))
264
+ return HELP;
265
+ preset = v;
266
+ i += consumed(inlineValue !== null);
267
+ }
268
+ else if (flag === '--base-url') {
269
+ const v = takeValue(tail, i, inlineValue);
270
+ if (v === null || (inlineValue === null && v.startsWith('--')))
271
+ return HELP;
272
+ baseURL = v;
273
+ i += consumed(inlineValue !== null);
274
+ }
275
+ else if (flag === '--api-key') {
276
+ const v = takeValue(tail, i, inlineValue);
277
+ if (v === null || (inlineValue === null && v.startsWith('--')))
278
+ return HELP;
279
+ apiKey = v;
280
+ i += consumed(inlineValue !== null);
281
+ }
282
+ else if (raw.startsWith('--')) {
283
+ return HELP;
284
+ }
285
+ else {
286
+ positionals.push(raw);
287
+ i += 1;
288
+ }
289
+ }
290
+ const name = positionals[0];
291
+ if (name === undefined || positionals.length > 1)
292
+ return HELP;
293
+ const out = { kind: 'profile-add', name };
294
+ if (preset !== undefined)
295
+ out.preset = preset;
296
+ if (baseURL !== undefined)
297
+ out.baseURL = baseURL;
298
+ if (apiKey !== undefined)
299
+ out.apiKey = apiKey;
300
+ return out;
301
+ }
302
+ function onlyName(tail) {
303
+ if (tail.length !== 1)
304
+ return null;
305
+ const name = tail[0];
306
+ if (name === undefined || name.startsWith('--'))
307
+ return null;
308
+ return name;
309
+ }
197
310
  function parseNonNegativeInt(s) {
198
311
  if (s === undefined || s === null)
199
312
  return null;
package/dist/cli/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { realpathSync } from 'node:fs';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { parseArgs } from "./args.js";
5
- import { runConfig, runMergePr, runStart } from "./commands.js";
5
+ import { runConfig, runMergePr, runProfile, runStart } from "./commands.js";
6
6
  export async function main(argv, ctx = {}) {
7
7
  const stdout = ctx.stdout ?? ((chunk) => process.stdout.write(chunk));
8
8
  const stderr = ctx.stderr ?? ((chunk) => process.stderr.write(chunk));
@@ -17,6 +17,14 @@ export async function main(argv, ctx = {}) {
17
17
  case 'config-get':
18
18
  case 'config-list':
19
19
  return emit(await runConfig(parsed, buildConfigCtx(ctx, stdout)), stderr);
20
+ case 'profile-list':
21
+ case 'profile-use':
22
+ case 'profile-add':
23
+ case 'profile-set':
24
+ case 'profile-get':
25
+ case 'profile-remove':
26
+ case 'profile-show':
27
+ return emit(await runProfile(parsed, buildProfileCtx(ctx, stdout)), stderr);
20
28
  case 'help':
21
29
  stdout(`${HELP_TEXT}\n`);
22
30
  return 0;
@@ -60,6 +68,12 @@ function buildConfigCtx(ctx, stdout) {
60
68
  out.homeDir = ctx.homeDir;
61
69
  return out;
62
70
  }
71
+ function buildProfileCtx(ctx, stdout) {
72
+ const out = { stdout };
73
+ if (ctx.homeDir !== undefined)
74
+ out.homeDir = ctx.homeDir;
75
+ return out;
76
+ }
63
77
  function emit(exit, stderr) {
64
78
  if (exit.message !== undefined && exit.message !== '')
65
79
  stderr(`${exit.message}\n`);
@@ -76,6 +90,13 @@ Usage:
76
90
  aitm config unset <key> [--project]
77
91
  aitm config get <key> [--project]
78
92
  aitm config list [--project]
93
+ aitm profile list
94
+ aitm profile use <name>
95
+ aitm profile add <name> [--preset openrouter|zai] [--base-url <url>] [--api-key <key>]
96
+ aitm profile set <name> <key> <value>
97
+ aitm profile get <name> <key>
98
+ aitm profile remove <name>
99
+ aitm profile show [<name>]
79
100
  aitm help | --help | -h
80
101
 
81
102
  Exit codes:
@@ -83,7 +104,7 @@ Exit codes:
83
104
  1 precondition failure or run blocked
84
105
  2 cancelled
85
106
 
86
- Docs: docs/commands/start.md, docs/commands/merge-pr.md, docs/commands/config.md`;
107
+ Docs: docs/commands/start.md, docs/commands/merge-pr.md, docs/commands/config.md, docs/commands/profile.md`;
87
108
  if (isEntrypoint(import.meta.url, process.argv[1])) {
88
109
  main(process.argv.slice(2)).then((code) => {
89
110
  process.exit(code);
@@ -72,6 +72,10 @@ export type ConfigCtx = {
72
72
  homeDir?: string;
73
73
  stdout?: (chunk: string) => void;
74
74
  };
75
+ export type ProfileCtx = {
76
+ homeDir?: string;
77
+ stdout?: (chunk: string) => void;
78
+ };
75
79
  export declare function runStart(args: Extract<ParsedArgs, {
76
80
  kind: 'start';
77
81
  }>, ctx?: StartCtx): Promise<CommandExit>;
@@ -81,3 +85,6 @@ export declare function runMergePr(args: Extract<ParsedArgs, {
81
85
  export declare function runConfig(args: Extract<ParsedArgs, {
82
86
  kind: `config-${string}`;
83
87
  }>, ctx?: ConfigCtx): Promise<CommandExit>;
88
+ export declare function runProfile(args: Extract<ParsedArgs, {
89
+ kind: `profile-${string}`;
90
+ }>, ctx?: ProfileCtx): Promise<CommandExit>;
@@ -3,6 +3,7 @@ import { join, resolve as resolvePath } from 'node:path';
3
3
  import { AgentConfigDetector } from "../agent-config/agent-config-detector.js";
4
4
  import { ConfigLoader } from "../config/config-loader.js";
5
5
  import { ConfigWriter } from "../config/config-writer.js";
6
+ import { ProfileManager } from "../config/profiles.js";
6
7
  import { Credentials } from "../credentials/credentials.js";
7
8
  import { DEFAULT_MODELS } from "../credentials/defaults.js";
8
9
  import { GitHubClient } from "../github/github-client.js";
@@ -257,10 +258,7 @@ export async function runConfig(args, ctx = {}) {
257
258
  }
258
259
  case 'config-list': {
259
260
  const file = await writer.list(args.scope);
260
- const safe = file.openrouterApiKey
261
- ? { ...file, openrouterApiKey: maskSecret(file.openrouterApiKey) }
262
- : file;
263
- stdout(`${JSON.stringify(safe, null, 2)}\n`);
261
+ stdout(`${JSON.stringify(redactConfigKeys(file), null, 2)}\n`);
264
262
  return { code: 0 };
265
263
  }
266
264
  default:
@@ -274,6 +272,65 @@ export async function runConfig(args, ctx = {}) {
274
272
  return { code: 1, message: errMsg(err) };
275
273
  }
276
274
  }
275
+ export async function runProfile(args, ctx = {}) {
276
+ const homeDir = ctx.homeDir ?? homedir();
277
+ const stdout = ctx.stdout ?? ((chunk) => process.stdout.write(chunk));
278
+ const manager = new ProfileManager(homeDir);
279
+ try {
280
+ switch (args.kind) {
281
+ case 'profile-list': {
282
+ const listing = await manager.list();
283
+ stdout(formatProfileList(listing.activeProfile, listing.profiles));
284
+ return { code: 0 };
285
+ }
286
+ case 'profile-use': {
287
+ await manager.use(args.name);
288
+ stdout(`Active profile is now "${args.name}".\n`);
289
+ return { code: 0 };
290
+ }
291
+ case 'profile-add': {
292
+ const input = {};
293
+ if (args.preset !== undefined)
294
+ input.preset = args.preset;
295
+ if (args.baseURL !== undefined)
296
+ input.baseURL = args.baseURL;
297
+ if (args.apiKey !== undefined)
298
+ input.apiKey = args.apiKey;
299
+ await manager.add(args.name, input);
300
+ const activated = (await manager.list()).activeProfile === args.name;
301
+ stdout(activated
302
+ ? `Created and activated profile "${args.name}".\n`
303
+ : `Created profile "${args.name}". Run \`aitm profile use ${args.name}\` to activate it.\n`);
304
+ return { code: 0 };
305
+ }
306
+ case 'profile-set':
307
+ await manager.set(args.name, args.key, args.value);
308
+ return { code: 0 };
309
+ case 'profile-get': {
310
+ const value = await manager.get(args.name, args.key);
311
+ stdout(`${formatConfigValue(value)}\n`);
312
+ return { code: 0 };
313
+ }
314
+ case 'profile-remove':
315
+ await manager.remove(args.name);
316
+ stdout(`Removed profile "${args.name}".\n`);
317
+ return { code: 0 };
318
+ case 'profile-show': {
319
+ const { name, profile } = await manager.show(args.name);
320
+ stdout(`${name}\n${JSON.stringify(redactProfile(profile), null, 2)}\n`);
321
+ return { code: 0 };
322
+ }
323
+ default:
324
+ return {
325
+ code: 1,
326
+ message: `Unknown profile subcommand: ${args.kind}`,
327
+ };
328
+ }
329
+ }
330
+ catch (err) {
331
+ return { code: 1, message: errMsg(err) };
332
+ }
333
+ }
277
334
  function toCliOverrides(args) {
278
335
  const out = {};
279
336
  if (args.maxPrs !== undefined)
@@ -362,6 +419,38 @@ function formatConfigValue(value) {
362
419
  return value;
363
420
  return JSON.stringify(value, null, 2);
364
421
  }
422
+ function formatProfileList(active, profiles) {
423
+ const names = Object.keys(profiles).sort();
424
+ if (names.length === 0) {
425
+ return 'No profiles configured. Create one with `aitm profile add <name> --preset zai`.\n';
426
+ }
427
+ const lines = names.map((name) => {
428
+ const p = profiles[name] ?? {};
429
+ const marker = name === active ? '*' : ' ';
430
+ const base = p.baseURL ?? '(provider default)';
431
+ const key = p.openrouterApiKey ? maskSecret(p.openrouterApiKey) : '(no key)';
432
+ return `${marker} ${name}\t${base}\t${key}`;
433
+ });
434
+ return `${lines.join('\n')}\n`;
435
+ }
436
+ function redactProfile(profile) {
437
+ return profile.openrouterApiKey
438
+ ? { ...profile, openrouterApiKey: maskSecret(profile.openrouterApiKey) }
439
+ : profile;
440
+ }
441
+ function redactConfigKeys(file) {
442
+ const out = file.openrouterApiKey
443
+ ? { ...file, openrouterApiKey: maskSecret(file.openrouterApiKey) }
444
+ : { ...file };
445
+ if (out.profiles) {
446
+ const profiles = {};
447
+ for (const [name, profile] of Object.entries(out.profiles)) {
448
+ profiles[name] = redactProfile(profile);
449
+ }
450
+ out.profiles = profiles;
451
+ }
452
+ return out;
453
+ }
365
454
  const defaultAuthStatus = (cwd) => new GitHubClient(cwd).authStatus();
366
455
  async function defaultRunLoop(input) {
367
456
  return runLoopAdapter(input);
@@ -370,9 +459,11 @@ async function defaultRunMergeFlow(input) {
370
459
  const { runTakeOverFlow } = await import("../loop/take-over-flow.js");
371
460
  const { execa } = await import('execa');
372
461
  const { githubThreadTool } = await import("../tools/github-thread-tool.js");
462
+ const { PrContextStore } = await import("../state/pr-context-store.js");
373
463
  const worktreePath = input.cwd;
374
464
  const baseBranch = await input.github.defaultBranch();
375
465
  const styleContents = input.agentConfig.contents;
466
+ const prContext = new PrContextStore(resolvePath(input.cwd, '.ai-task-master'));
376
467
  const workerTools = localEditTools(worktreePath);
377
468
  const github = githubThreadTool({ github: input.github });
378
469
  const result = await runTakeOverFlow({
@@ -380,6 +471,7 @@ async function defaultRunMergeFlow(input) {
380
471
  worktreePath,
381
472
  baseBranch,
382
473
  github: input.github,
474
+ prContext,
383
475
  mergeMethod: input.runState.options.mergeMethod,
384
476
  push: async (cwd) => {
385
477
  const r = await execa('git', ['push'], { cwd });
@@ -393,6 +485,7 @@ async function defaultRunMergeFlow(input) {
393
485
  workerModel: input.credentials.modelFor('worker'),
394
486
  workerTools,
395
487
  styleContents,
488
+ ...(input.resolved.formatCommand ? { formatCommand: input.resolved.formatCommand } : {}),
396
489
  },
397
490
  });
398
491
  if (result.kind === 'merged') {
@@ -19,6 +19,8 @@ export declare class ConfigLoader {
19
19
  private readMcpEnvelope;
20
20
  private resolveMcpServers;
21
21
  private readConfigFile;
22
+ private resolveActiveProfile;
23
+ private resolveBaseURL;
22
24
  private resolveApiKey;
23
25
  private resolveModels;
24
26
  }
@@ -13,12 +13,16 @@ const CLAUDE_PROJECT_MCP_FILE = '.mcp.json';
13
13
  const CLAUDE_USER_FILE = '.claude.json';
14
14
  const KNOWN_KEYS = new Set([
15
15
  'openrouterApiKey',
16
+ 'activeProfile',
17
+ 'profiles',
18
+ 'baseURL',
16
19
  'models',
17
20
  'maxPrs',
18
21
  'maxSessions',
19
22
  'autoMerge',
20
23
  'mergeMethod',
21
24
  'stylePath',
25
+ 'formatCommand',
22
26
  'logLevel',
23
27
  'concurrency',
24
28
  'mcpServers',
@@ -29,6 +33,7 @@ const DEFAULTS = {
29
33
  autoMerge: true,
30
34
  mergeMethod: 'squash',
31
35
  stylePath: null,
36
+ formatCommand: null,
32
37
  logLevel: 'info',
33
38
  concurrency: 1,
34
39
  };
@@ -48,10 +53,13 @@ export class ConfigLoader {
48
53
  const project = await this.readProject();
49
54
  const claudeUser = await this.readClaudeUserMcp();
50
55
  const claudeProject = await this.readClaudeProjectMcp();
51
- const { apiKey, apiKeySource } = this.resolveApiKey(global, project);
56
+ const active = this.resolveActiveProfile(global);
57
+ const profile = active?.profile;
58
+ const { apiKey, apiKeySource } = this.resolveApiKey(global, project, profile);
52
59
  if (apiKey === undefined || apiKeySource === undefined) {
53
- throw new Error('No OpenRouter API key found. Set OPENROUTER_API_KEY env, or add ' +
54
- '"openrouterApiKey" to ~/.aitm.json or ./.ai-task-master/config.json.');
60
+ throw new Error('No OpenRouter API key found. Set OPENROUTER_API_KEY env, add ' +
61
+ '"openrouterApiKey" to ~/.aitm.json or ./.ai-task-master/config.json, or ' +
62
+ 'create a profile with `aitm profile add <name> --api-key <key>`.');
55
63
  }
56
64
  const { mcpServers, mcpServerSources } = this.resolveMcpServers({
57
65
  aitmGlobal: global?.mcpServers,
@@ -62,12 +70,15 @@ export class ConfigLoader {
62
70
  return {
63
71
  openrouterApiKey: apiKey,
64
72
  apiKeySource,
65
- models: this.resolveModels(global, project, cliOverrides),
73
+ ...(active ? { activeProfile: active.name } : {}),
74
+ baseURL: this.resolveBaseURL(global, project, profile),
75
+ models: this.resolveModels(global, project, profile, cliOverrides),
66
76
  maxPrs: pick(cliOverrides.maxPrs, project?.maxPrs, global?.maxPrs, DEFAULTS.maxPrs),
67
77
  maxSessions: pickNullable(cliOverrides.maxSessions, project?.maxSessions, global?.maxSessions, DEFAULTS.maxSessions),
68
78
  autoMerge: pick(cliOverrides.autoMerge, project?.autoMerge, global?.autoMerge, DEFAULTS.autoMerge),
69
79
  mergeMethod: pick(cliOverrides.mergeMethod, project?.mergeMethod, global?.mergeMethod, DEFAULTS.mergeMethod),
70
80
  stylePath: pickNullable(cliOverrides.stylePath, project?.stylePath, global?.stylePath, DEFAULTS.stylePath),
81
+ formatCommand: pickNullable(undefined, project?.formatCommand, global?.formatCommand, DEFAULTS.formatCommand),
71
82
  logLevel: pick(undefined, project?.logLevel, global?.logLevel, DEFAULTS.logLevel),
72
83
  concurrency: pick(cliOverrides.concurrency, project?.concurrency, global?.concurrency, DEFAULTS.concurrency),
73
84
  mcpServers,
@@ -175,27 +186,58 @@ export class ConfigLoader {
175
186
  }
176
187
  return validated;
177
188
  }
178
- resolveApiKey(global, project) {
189
+ resolveActiveProfile(global) {
190
+ const name = global?.activeProfile;
191
+ if (!name)
192
+ return undefined;
193
+ const profile = global?.profiles?.[name];
194
+ if (!profile) {
195
+ this.warn(`activeProfile "${name}" is set in ~/.aitm.json but no such profile exists — ignoring it. ` +
196
+ 'Run `aitm profile list` to see available profiles.');
197
+ return undefined;
198
+ }
199
+ return { name, profile };
200
+ }
201
+ resolveBaseURL(global, project, profile) {
202
+ if (project?.baseURL)
203
+ return project.baseURL;
204
+ if (global?.baseURL)
205
+ return global.baseURL;
206
+ if (profile?.baseURL)
207
+ return profile.baseURL;
208
+ const env = this.env.OPENROUTER_BASE_URL?.trim();
209
+ if (!env)
210
+ return undefined;
211
+ const parsed = z.url().safeParse(env);
212
+ if (!parsed.success) {
213
+ throw new Error(`OPENROUTER_BASE_URL is not a valid URL: ${JSON.stringify(env)}`);
214
+ }
215
+ return parsed.data;
216
+ }
217
+ resolveApiKey(global, project, profile) {
179
218
  if (project?.openrouterApiKey) {
180
219
  return { apiKey: project.openrouterApiKey, apiKeySource: 'project' };
181
220
  }
182
221
  if (global?.openrouterApiKey) {
183
222
  return { apiKey: global.openrouterApiKey, apiKeySource: 'global' };
184
223
  }
224
+ if (profile?.openrouterApiKey) {
225
+ return { apiKey: profile.openrouterApiKey, apiKeySource: 'profile' };
226
+ }
185
227
  const envKey = this.env.OPENROUTER_API_KEY;
186
228
  if (envKey) {
187
229
  return { apiKey: envKey, apiKeySource: 'env' };
188
230
  }
189
231
  return { apiKey: undefined, apiKeySource: undefined };
190
232
  }
191
- resolveModels(global, project, cliOverrides) {
233
+ resolveModels(global, project, profile, cliOverrides) {
192
234
  const merged = {
193
235
  generic: DEFAULT_MODELS.generic,
194
236
  smart: DEFAULT_MODELS.smart,
195
237
  coding: DEFAULT_MODELS.coding,
196
238
  fast: DEFAULT_MODELS.fast,
197
239
  };
198
- for (const src of [global?.models, project?.models]) {
240
+ for (const src of [profile?.models, global?.models, project?.models]) {
199
241
  if (!src)
200
242
  continue;
201
243
  if (src.generic)
@@ -6,8 +6,10 @@ import { ConfigFileSchema } from "./schema.js";
6
6
  const GLOBAL_FILE = '.aitm.json';
7
7
  const PROJECT_DIR = '.ai-task-master';
8
8
  const PROJECT_FILE = 'config.json';
9
+ const PROFILE_MANAGED_KEYS = new Set(['activeProfile', 'profiles']);
9
10
  const KNOWN_KEYS = new Set([
10
11
  'openrouterApiKey',
12
+ 'baseURL',
11
13
  'models',
12
14
  'maxPrs',
13
15
  'maxSessions',
@@ -28,6 +30,7 @@ export class ConfigWriter {
28
30
  async set(scope, key, value) {
29
31
  const parts = splitKey(key);
30
32
  const top = parts[0];
33
+ assertNotProfileManaged(top);
31
34
  if (!KNOWN_KEYS.has(top)) {
32
35
  throw new Error(unknownKeyMessage(top));
33
36
  }
@@ -37,6 +40,7 @@ export class ConfigWriter {
37
40
  }
38
41
  async unset(scope, key) {
39
42
  const parts = splitKey(key);
43
+ assertNotProfileManaged(parts[0]);
40
44
  const file = await this.readRaw(scope);
41
45
  unsetDottedKey(file, parts);
42
46
  return this.validateAndPersist(scope, file);
@@ -158,6 +162,11 @@ function getDottedKey(obj, parts) {
158
162
  }
159
163
  return cur;
160
164
  }
165
+ function assertNotProfileManaged(top) {
166
+ if (PROFILE_MANAGED_KEYS.has(top)) {
167
+ throw new Error(`"${top}" is managed by \`aitm profile …\`. Use the profile commands instead.`);
168
+ }
169
+ }
161
170
  function unknownKeyMessage(top) {
162
171
  const allowed = [...KNOWN_KEYS].sort().join(', ');
163
172
  return `Unknown config key "${top}". Allowed top-level keys: ${allowed}`;
@@ -0,0 +1,30 @@
1
+ import { type PresetName } from './provider-presets.ts';
2
+ import { type Profile } from './schema.ts';
3
+ export type AddProfileInput = {
4
+ preset?: PresetName;
5
+ baseURL?: string;
6
+ apiKey?: string;
7
+ };
8
+ export type ProfileListing = {
9
+ activeProfile: string | undefined;
10
+ profiles: Record<string, Profile>;
11
+ };
12
+ export declare class ProfileManager {
13
+ private readonly homeDir;
14
+ constructor(homeDir: string);
15
+ list(): Promise<ProfileListing>;
16
+ use(name: string): Promise<void>;
17
+ add(name: string, input?: AddProfileInput): Promise<Profile>;
18
+ set(name: string, key: string, value: unknown): Promise<Profile>;
19
+ get(name: string, key: string): Promise<unknown>;
20
+ remove(name: string): Promise<void>;
21
+ show(name?: string): Promise<{
22
+ name: string;
23
+ profile: Profile;
24
+ }>;
25
+ private filePath;
26
+ private readRaw;
27
+ private readValidated;
28
+ private validate;
29
+ private persist;
30
+ }