@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 +12 -2
- package/dist/cli/args.d.ts +29 -1
- package/dist/cli/args.js +113 -0
- package/dist/cli/cli.js +23 -2
- package/dist/cli/commands.d.ts +7 -0
- package/dist/cli/commands.js +97 -4
- package/dist/config/config-loader.d.ts +2 -0
- package/dist/config/config-loader.js +49 -7
- package/dist/config/config-writer.js +9 -0
- package/dist/config/profiles.d.ts +30 -0
- package/dist/config/profiles.js +224 -0
- package/dist/config/provider-presets.d.ts +5 -0
- package/dist/config/provider-presets.js +18 -0
- package/dist/config/schema.d.ts +28 -1
- package/dist/config/schema.js +11 -0
- package/dist/credentials/credentials.d.ts +4 -0
- package/dist/credentials/credentials.js +7 -1
- package/dist/github/github-client.d.ts +7 -0
- package/dist/github/github-client.js +85 -0
- package/dist/loop/run-loop-adapter.js +4 -1
- package/dist/loop/take-over-flow.d.ts +14 -0
- package/dist/loop/take-over-flow.js +22 -6
- package/dist/orchestrator/orchestrator.js +16 -4
- package/dist/state/pr-context-store.d.ts +20 -0
- package/dist/state/pr-context-store.js +60 -0
- package/dist/subagents/planner.d.ts +2 -4
- package/dist/subagents/planner.js +11 -10
- package/dist/subagents/reviewer.d.ts +2 -4
- package/dist/subagents/reviewer.js +16 -12
- package/dist/subagents/worker.d.ts +5 -5
- package/dist/subagents/worker.js +22 -9
- package/package.json +2 -2
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**:
|
|
80
|
-
|
|
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
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -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
|
|
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);
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
-
|
|
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') {
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|