@developerz.ai/aitm 0.0.4 → 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);
@@ -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,6 +13,9 @@ 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',
@@ -50,10 +53,13 @@ export class ConfigLoader {
50
53
  const project = await this.readProject();
51
54
  const claudeUser = await this.readClaudeUserMcp();
52
55
  const claudeProject = await this.readClaudeProjectMcp();
53
- 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);
54
59
  if (apiKey === undefined || apiKeySource === undefined) {
55
- throw new Error('No OpenRouter API key found. Set OPENROUTER_API_KEY env, or add ' +
56
- '"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>`.');
57
63
  }
58
64
  const { mcpServers, mcpServerSources } = this.resolveMcpServers({
59
65
  aitmGlobal: global?.mcpServers,
@@ -64,7 +70,9 @@ export class ConfigLoader {
64
70
  return {
65
71
  openrouterApiKey: apiKey,
66
72
  apiKeySource,
67
- 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),
68
76
  maxPrs: pick(cliOverrides.maxPrs, project?.maxPrs, global?.maxPrs, DEFAULTS.maxPrs),
69
77
  maxSessions: pickNullable(cliOverrides.maxSessions, project?.maxSessions, global?.maxSessions, DEFAULTS.maxSessions),
70
78
  autoMerge: pick(cliOverrides.autoMerge, project?.autoMerge, global?.autoMerge, DEFAULTS.autoMerge),
@@ -178,27 +186,58 @@ export class ConfigLoader {
178
186
  }
179
187
  return validated;
180
188
  }
181
- 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) {
182
218
  if (project?.openrouterApiKey) {
183
219
  return { apiKey: project.openrouterApiKey, apiKeySource: 'project' };
184
220
  }
185
221
  if (global?.openrouterApiKey) {
186
222
  return { apiKey: global.openrouterApiKey, apiKeySource: 'global' };
187
223
  }
224
+ if (profile?.openrouterApiKey) {
225
+ return { apiKey: profile.openrouterApiKey, apiKeySource: 'profile' };
226
+ }
188
227
  const envKey = this.env.OPENROUTER_API_KEY;
189
228
  if (envKey) {
190
229
  return { apiKey: envKey, apiKeySource: 'env' };
191
230
  }
192
231
  return { apiKey: undefined, apiKeySource: undefined };
193
232
  }
194
- resolveModels(global, project, cliOverrides) {
233
+ resolveModels(global, project, profile, cliOverrides) {
195
234
  const merged = {
196
235
  generic: DEFAULT_MODELS.generic,
197
236
  smart: DEFAULT_MODELS.smart,
198
237
  coding: DEFAULT_MODELS.coding,
199
238
  fast: DEFAULT_MODELS.fast,
200
239
  };
201
- for (const src of [global?.models, project?.models]) {
240
+ for (const src of [profile?.models, global?.models, project?.models]) {
202
241
  if (!src)
203
242
  continue;
204
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
+ }
@@ -0,0 +1,224 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { ZodError } from 'zod';
4
+ import { atomicWrite } from "../fs/atomic-write.js";
5
+ import { PROVIDER_PRESETS } from "./provider-presets.js";
6
+ import { ConfigFileSchema } from "./schema.js";
7
+ const GLOBAL_FILE = '.aitm.json';
8
+ export class ProfileManager {
9
+ homeDir;
10
+ constructor(homeDir) {
11
+ this.homeDir = homeDir;
12
+ }
13
+ async list() {
14
+ const validated = await this.readValidated();
15
+ return {
16
+ activeProfile: validated.activeProfile,
17
+ profiles: validated.profiles ?? {},
18
+ };
19
+ }
20
+ async use(name) {
21
+ const file = await this.readRaw();
22
+ const profiles = asObject(file.profiles);
23
+ if (!(name in profiles)) {
24
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
25
+ }
26
+ file.activeProfile = name;
27
+ await this.persist(file);
28
+ }
29
+ async add(name, input = {}) {
30
+ if (name.trim() === '')
31
+ throw new Error('Profile name must be non-empty.');
32
+ const file = await this.readRaw();
33
+ const profiles = ensureObject(file, 'profiles');
34
+ if (name in profiles) {
35
+ throw new Error(`Profile "${name}" already exists. Use \`aitm profile set ${name} <key> <value>\` to modify it.`);
36
+ }
37
+ const profile = input.preset
38
+ ? jsonClone(PROVIDER_PRESETS[input.preset])
39
+ : {};
40
+ if (input.baseURL !== undefined)
41
+ profile.baseURL = input.baseURL;
42
+ if (input.apiKey !== undefined)
43
+ profile.openrouterApiKey = input.apiKey;
44
+ profiles[name] = profile;
45
+ if (file.activeProfile === undefined)
46
+ file.activeProfile = name;
47
+ const validated = await this.persist(file);
48
+ return validated.profiles?.[name] ?? {};
49
+ }
50
+ async set(name, key, value) {
51
+ const file = await this.readRaw();
52
+ const profiles = asObject(file.profiles);
53
+ if (!(name in profiles)) {
54
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
55
+ }
56
+ const profile = asObject(profiles[name]);
57
+ setDotted(profile, splitKey(key), parseValue(value));
58
+ profiles[name] = profile;
59
+ file.profiles = profiles;
60
+ const validated = await this.persist(file);
61
+ return validated.profiles?.[name] ?? {};
62
+ }
63
+ async get(name, key) {
64
+ const { profiles } = await this.list();
65
+ const profile = profiles[name];
66
+ if (profile === undefined) {
67
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
68
+ }
69
+ return getDotted(profile, splitKey(key));
70
+ }
71
+ async remove(name) {
72
+ const file = await this.readRaw();
73
+ const profiles = asObject(file.profiles);
74
+ if (!(name in profiles)) {
75
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
76
+ }
77
+ delete profiles[name];
78
+ if (file.activeProfile === name)
79
+ delete file.activeProfile;
80
+ await this.persist(file);
81
+ }
82
+ async show(name) {
83
+ const { activeProfile, profiles } = await this.list();
84
+ const target = name ?? activeProfile;
85
+ if (target === undefined) {
86
+ throw new Error('No profile specified and no active profile set. Pass a name or run `aitm profile use <name>`.');
87
+ }
88
+ const profile = profiles[target];
89
+ if (profile === undefined) {
90
+ throw new Error(unknownProfileMessage(target, Object.keys(profiles)));
91
+ }
92
+ return { name: target, profile };
93
+ }
94
+ filePath() {
95
+ return join(this.homeDir, GLOBAL_FILE);
96
+ }
97
+ async readRaw() {
98
+ const path = this.filePath();
99
+ let raw;
100
+ try {
101
+ raw = await readFile(path, 'utf8');
102
+ }
103
+ catch (err) {
104
+ if (isNotFound(err))
105
+ return {};
106
+ throw err;
107
+ }
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(raw);
111
+ }
112
+ catch (err) {
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ throw new Error(`${path}: invalid JSON — ${msg}`);
115
+ }
116
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
+ throw new Error(`${path}: expected a JSON object at the top level`);
118
+ }
119
+ return parsed;
120
+ }
121
+ async readValidated() {
122
+ return this.validate(await this.readRaw());
123
+ }
124
+ validate(file) {
125
+ try {
126
+ return ConfigFileSchema.parse(file);
127
+ }
128
+ catch (err) {
129
+ if (err instanceof ZodError)
130
+ throw new Error(`${this.filePath()}: ${formatZodError(err)}`);
131
+ throw err;
132
+ }
133
+ }
134
+ async persist(file) {
135
+ const validated = this.validate(file);
136
+ await atomicWrite(this.filePath(), `${JSON.stringify(validated, null, 2)}\n`);
137
+ return validated;
138
+ }
139
+ }
140
+ function asObject(value) {
141
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
142
+ ? value
143
+ : {};
144
+ }
145
+ function ensureObject(parent, key) {
146
+ const next = asObject(parent[key]);
147
+ parent[key] = next;
148
+ return next;
149
+ }
150
+ function jsonClone(value) {
151
+ return JSON.parse(JSON.stringify(value));
152
+ }
153
+ const ALLOWED_PROFILE_ROOT_KEYS = new Set([
154
+ 'openrouterApiKey',
155
+ 'baseURL',
156
+ 'models',
157
+ ]);
158
+ const FORBIDDEN_KEY_SEGMENTS = new Set([
159
+ '__proto__',
160
+ 'prototype',
161
+ 'constructor',
162
+ ]);
163
+ const KEY_SURFACE_HINT = 'Allowed keys: openrouterApiKey, baseURL, models.<tier>.';
164
+ function splitKey(key) {
165
+ const parts = key.split('.');
166
+ if (parts.length === 0 || parts.some((p) => p === '')) {
167
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
168
+ }
169
+ if (parts.some((p) => FORBIDDEN_KEY_SEGMENTS.has(p))) {
170
+ throw new Error(`Invalid profile key: "${key}" — reserved segment. ${KEY_SURFACE_HINT}`);
171
+ }
172
+ const [first, ...rest] = parts;
173
+ if (first === undefined || !ALLOWED_PROFILE_ROOT_KEYS.has(first)) {
174
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
175
+ }
176
+ if (first === 'models' ? rest.length !== 1 : rest.length !== 0) {
177
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
178
+ }
179
+ return [first, ...rest];
180
+ }
181
+ function setDotted(obj, parts, value) {
182
+ const [first, ...rest] = parts;
183
+ if (first === undefined)
184
+ return;
185
+ if (rest.length === 0) {
186
+ obj[first] = value;
187
+ return;
188
+ }
189
+ const sub = asObject(obj[first]);
190
+ obj[first] = sub;
191
+ setDotted(sub, rest, value);
192
+ }
193
+ function getDotted(obj, parts) {
194
+ let cur = obj;
195
+ for (const p of parts) {
196
+ if (cur === null || typeof cur !== 'object' || Array.isArray(cur))
197
+ return undefined;
198
+ cur = cur[p];
199
+ }
200
+ return cur;
201
+ }
202
+ function parseValue(v) {
203
+ if (typeof v !== 'string')
204
+ return v;
205
+ try {
206
+ return JSON.parse(v);
207
+ }
208
+ catch {
209
+ return v;
210
+ }
211
+ }
212
+ function unknownProfileMessage(name, available) {
213
+ const list = available.length > 0 ? available.slice().sort().join(', ') : '(none)';
214
+ return `Unknown profile "${name}". Available: ${list}. Create it with \`aitm profile add ${name}\`.`;
215
+ }
216
+ function isNotFound(err) {
217
+ return (typeof err === 'object' &&
218
+ err !== null &&
219
+ 'code' in err &&
220
+ err.code === 'ENOENT');
221
+ }
222
+ function formatZodError(err) {
223
+ return err.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`).join('; ');
224
+ }
@@ -0,0 +1,5 @@
1
+ import type { Profile } from './schema.ts';
2
+ export type PresetName = 'openrouter' | 'zai';
3
+ export declare const PROVIDER_PRESETS: Readonly<Record<PresetName, Profile>>;
4
+ export declare const PRESET_NAMES: readonly PresetName[];
5
+ export declare function isPresetName(s: string): s is PresetName;
@@ -0,0 +1,18 @@
1
+ export const PROVIDER_PRESETS = {
2
+ openrouter: {
3
+ baseURL: 'https://openrouter.ai/api/v1',
4
+ },
5
+ zai: {
6
+ baseURL: 'https://api.z.ai/api/coding/paas/v4',
7
+ models: {
8
+ generic: 'glm-5.2',
9
+ smart: 'glm-5.2',
10
+ coding: 'glm-5.2',
11
+ fast: 'glm-5-turbo',
12
+ },
13
+ },
14
+ };
15
+ export const PRESET_NAMES = Object.keys(PROVIDER_PRESETS);
16
+ export function isPresetName(s) {
17
+ return Object.hasOwn(PROVIDER_PRESETS, s);
18
+ }
@@ -18,8 +18,31 @@ export declare const MergeMethodSchema: z.ZodEnum<{
18
18
  merge: "merge";
19
19
  rebase: "rebase";
20
20
  }>;
21
+ export declare const ProfileSchema: z.ZodObject<{
22
+ openrouterApiKey: z.ZodOptional<z.ZodString>;
23
+ baseURL: z.ZodOptional<z.ZodURL>;
24
+ models: z.ZodOptional<z.ZodObject<{
25
+ generic: z.ZodOptional<z.ZodString>;
26
+ smart: z.ZodOptional<z.ZodString>;
27
+ coding: z.ZodOptional<z.ZodString>;
28
+ fast: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$loose>>;
30
+ }, z.core.$loose>;
31
+ export type Profile = z.infer<typeof ProfileSchema>;
21
32
  export declare const ConfigFileSchema: z.ZodObject<{
22
33
  openrouterApiKey: z.ZodOptional<z.ZodString>;
34
+ activeProfile: z.ZodOptional<z.ZodString>;
35
+ profiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
36
+ openrouterApiKey: z.ZodOptional<z.ZodString>;
37
+ baseURL: z.ZodOptional<z.ZodURL>;
38
+ models: z.ZodOptional<z.ZodObject<{
39
+ generic: z.ZodOptional<z.ZodString>;
40
+ smart: z.ZodOptional<z.ZodString>;
41
+ coding: z.ZodOptional<z.ZodString>;
42
+ fast: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$loose>>;
44
+ }, z.core.$loose>>>;
45
+ baseURL: z.ZodOptional<z.ZodURL>;
23
46
  models: z.ZodOptional<z.ZodObject<{
24
47
  generic: z.ZodOptional<z.ZodString>;
25
48
  smart: z.ZodOptional<z.ZodString>;
@@ -71,7 +94,9 @@ export type CliOverrides = {
71
94
  };
72
95
  export type ResolvedConfig = {
73
96
  openrouterApiKey: string;
74
- apiKeySource: 'project' | 'global' | 'env';
97
+ apiKeySource: 'project' | 'global' | 'env' | 'profile';
98
+ activeProfile?: string | undefined;
99
+ baseURL?: string | undefined;
75
100
  models: Required<Pick<CapabilityModels, 'generic' | 'smart' | 'coding' | 'fast'>>;
76
101
  maxPrs: number;
77
102
  maxSessions: number | null;
@@ -10,9 +10,19 @@ export const CapabilityModelsSchema = z
10
10
  .passthrough();
11
11
  export const LogLevelSchema = z.enum(['debug', 'info', 'warn', 'error']);
12
12
  export const MergeMethodSchema = z.enum(['squash', 'merge', 'rebase']);
13
+ export const ProfileSchema = z
14
+ .object({
15
+ openrouterApiKey: z.string().optional(),
16
+ baseURL: z.url().optional(),
17
+ models: CapabilityModelsSchema.optional(),
18
+ })
19
+ .passthrough();
13
20
  export const ConfigFileSchema = z
14
21
  .object({
15
22
  openrouterApiKey: z.string().optional(),
23
+ activeProfile: z.string().optional(),
24
+ profiles: z.record(z.string(), ProfileSchema).optional(),
25
+ baseURL: z.url().optional(),
16
26
  models: CapabilityModelsSchema.optional(),
17
27
  maxPrs: z.number().int().positive().optional(),
18
28
  maxSessions: z.number().int().positive().nullable().optional(),
@@ -3,6 +3,10 @@ import type { Capability, ResolvedConfig } from '../config/schema.ts';
3
3
  export type Role = 'planner' | 'worker' | 'reviewer' | 'orchestrator';
4
4
  export declare const ROLE_CAPABILITY: Readonly<Record<Role, Capability>>;
5
5
  export type ModelHandles = Record<Role, LanguageModel>;
6
+ export declare function providerSettings(resolved: ResolvedConfig): {
7
+ apiKey: string;
8
+ baseURL?: string;
9
+ };
6
10
  export declare class Credentials {
7
11
  private readonly resolved;
8
12
  private providerInstance;
@@ -6,6 +6,12 @@ export const ROLE_CAPABILITY = {
6
6
  reviewer: 'smart',
7
7
  orchestrator: 'fast',
8
8
  };
9
+ export function providerSettings(resolved) {
10
+ return {
11
+ apiKey: resolved.openrouterApiKey,
12
+ ...(resolved.baseURL ? { baseURL: resolved.baseURL } : {}),
13
+ };
14
+ }
9
15
  export class Credentials {
10
16
  resolved;
11
17
  providerInstance;
@@ -37,7 +43,7 @@ export class Credentials {
37
43
  provider() {
38
44
  if (!this.providerInstance) {
39
45
  Credentials.assertApiKeyPresent(this.resolved);
40
- this.providerInstance = createOpenRouter({ apiKey: this.resolved.openrouterApiKey });
46
+ this.providerInstance = createOpenRouter(providerSettings(this.resolved));
41
47
  }
42
48
  return this.providerInstance;
43
49
  }
@@ -1,4 +1,5 @@
1
- import { generateText, hasToolCall, Output, stepCountIs, ToolLoopAgent } from 'ai';
1
+ import { submittedOutput } from '@developerz.ai/ai-claude-compat';
2
+ import { generateText, hasToolCall, stepCountIs, ToolLoopAgent, tool } from 'ai';
2
3
  import { ExecaError, execa } from 'execa';
3
4
  import { z } from 'zod';
4
5
  import { makePlannerTool, makeReviewerTool, makeWorkerTool, } from "./subagent-tools.js";
@@ -133,15 +134,26 @@ export class Orchestrator {
133
134
  const result = await generateText({
134
135
  model: this.init.credentials.modelFor('orchestrator'),
135
136
  prompt: this.buildPrPrompt(group, delivery),
136
- output: Output.object({ schema: PrCompositionSchema, name: 'PrComposition' }),
137
+ tools: {
138
+ submit: tool({
139
+ description: 'Submit the composed pull-request title and body (the PrComposition schema).',
140
+ inputSchema: PrCompositionSchema,
141
+ execute: async (composition) => composition,
142
+ }),
143
+ },
144
+ toolChoice: { type: 'tool', toolName: 'submit' },
137
145
  });
138
- return result.experimental_output;
146
+ const out = submittedOutput(result, PrCompositionSchema);
147
+ if (!out) {
148
+ throw new Error('orchestrator did not submit a PR composition');
149
+ }
150
+ return out;
139
151
  }
140
152
  buildPrPrompt(group, delivery) {
141
153
  return [
142
154
  this.buildSystemPrompt(),
143
155
  '',
144
- 'Compose the pull-request title and body for this PR group. Return JSON.',
156
+ 'Compose the pull-request title and body for this PR group, then call the submit tool with it.',
145
157
  '- title: conventional-commit style, ≤72 chars',
146
158
  '- body: short summary + bulleted file changes + relevant rolling context',
147
159
  '',
@@ -1,9 +1,8 @@
1
1
  import type { GlobInput, GlobOutput, GrepInput, GrepOutput, ReadFileInput, ReadFileOutput } from '@developerz.ai/ai-claude-compat';
2
- import { type DeepPartial, Output, type Tool, type ToolLoopAgent } from 'ai';
2
+ import { type Tool, type ToolLoopAgent } from 'ai';
3
3
  import { type Plan } from '../plan/schema.ts';
4
4
  import type { SubagentInit } from './factory.ts';
5
- type PlannerOutput = Output.Output<Plan, DeepPartial<Plan>, never>;
6
- export type PlannerAgent = ToolLoopAgent<never, PlannerTools, PlannerOutput>;
5
+ export type PlannerAgent = ToolLoopAgent<never, PlannerTools>;
7
6
  export type PlannerTools = {
8
7
  readFile: Tool<ReadFileInput, ReadFileOutput>;
9
8
  grep: Tool<GrepInput, GrepOutput>;
@@ -28,4 +27,3 @@ export type PlannerResult = {
28
27
  export declare const PLANNER_SYSTEM_PREFIX: string;
29
28
  export declare function createPlannerAgent(init: SubagentInit<PlannerTools>): PlannerAgent;
30
29
  export declare function runPlanner(agent: PlannerAgent, input: PlannerInput): Promise<PlannerResult>;
31
- export {};
@@ -1,5 +1,5 @@
1
- import { createSubagent } from '@developerz.ai/ai-claude-compat';
2
- import { Output } from 'ai';
1
+ import { createSubagent, submittedOutput } from '@developerz.ai/ai-claude-compat';
2
+ import { tool } from 'ai';
3
3
  import { PlanSchema } from "../plan/schema.js";
4
4
  export const PLANNER_SYSTEM_PREFIX = [
5
5
  '',
@@ -15,14 +15,18 @@ export const PLANNER_SYSTEM_PREFIX = [
15
15
  '- Prefer parallelizable siblings over a single linear chain.',
16
16
  '- Do not invent files. Do not propose work outside the repo.',
17
17
  '',
18
- 'Return JSON that matches the Plan schema exactly.',
18
+ 'When the plan is ready, call the `submit` tool exactly once with the Plan (matching the Plan schema).',
19
19
  ].join('\n');
20
20
  export function createPlannerAgent(init) {
21
21
  return createSubagent({
22
22
  model: init.model,
23
23
  tools: init.tools,
24
24
  systemPrompt: init.systemPrompt,
25
- output: plannerOutput(),
25
+ submit: tool({
26
+ description: 'Submit the finished plan as an ordered list of PR groups (the Plan schema).',
27
+ inputSchema: PlanSchema,
28
+ execute: async (plan) => plan,
29
+ }),
26
30
  ...(init.maxSteps !== undefined ? { maxSteps: init.maxSteps } : {}),
27
31
  }, 20);
28
32
  }
@@ -32,8 +36,8 @@ export async function runPlanner(agent, input) {
32
36
  }
33
37
  try {
34
38
  const result = await agent.generate({ prompt: buildUserPrompt(input) });
35
- const raw = result.experimental_output;
36
- if (!raw.groups || raw.groups.length === 0) {
39
+ const raw = submittedOutput(result, PlanSchema);
40
+ if (!raw || raw.groups.length === 0) {
37
41
  return { kind: 'blocked', reason: 'planner returned an empty group list' };
38
42
  }
39
43
  return { kind: 'ok', plan: capGroups(raw, input.maxPrs) };
@@ -42,16 +46,13 @@ export async function runPlanner(agent, input) {
42
46
  return { kind: 'error', error: err instanceof Error ? err.message : String(err) };
43
47
  }
44
48
  }
45
- function plannerOutput() {
46
- return Output.object({ schema: PlanSchema, name: 'Plan' });
47
- }
48
49
  function buildUserPrompt(input) {
49
50
  const lines = [`Goal: ${input.goal}`];
50
51
  if (input.criteria?.trim()) {
51
52
  lines.push(`Acceptance criteria: ${input.criteria}`);
52
53
  }
53
54
  lines.push(`maxPrs: ${input.maxPrs}`);
54
- lines.push('Survey the repo with the read-only tools, then emit the Plan JSON.');
55
+ lines.push('Survey the repo with the read-only tools, then call submit with the Plan.');
55
56
  return lines.join('\n');
56
57
  }
57
58
  function capGroups(plan, maxPrs) {
@@ -1,4 +1,4 @@
1
- import { type DeepPartial, Output, type Tool, type ToolLoopAgent } from 'ai';
1
+ import { type Tool, type ToolLoopAgent } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import type { ReviewThread } from '../github/schema.ts';
4
4
  import type { SubagentInit } from './factory.ts';
@@ -24,8 +24,7 @@ export declare const ThreadResolutionOutputSchema: z.ZodObject<{
24
24
  reason: z.ZodOptional<z.ZodString>;
25
25
  }, z.core.$strip>;
26
26
  export type ThreadResolutionOutput = z.infer<typeof ThreadResolutionOutputSchema>;
27
- type ReviewerAgentOutput = Output.Output<ThreadResolutionOutput, DeepPartial<ThreadResolutionOutput>, never>;
28
- export type ReviewerAgent = ToolLoopAgent<never, ReviewerTools, ReviewerAgentOutput>;
27
+ export type ReviewerAgent = ToolLoopAgent<never, ReviewerTools>;
29
28
  export type ReviewerInput = {
30
29
  pr: number;
31
30
  threads: ReviewThread[];
@@ -57,4 +56,3 @@ export type ReviewerResult = {
57
56
  export declare const REVIEWER_SYSTEM_PREFIX: string;
58
57
  export declare function createReviewerAgent(init: SubagentInit<ReviewerTools>): ReviewerAgent;
59
58
  export declare function runReviewer(agent: ReviewerAgent, input: ReviewerInput): Promise<ReviewerResult>;
60
- export {};
@@ -1,5 +1,5 @@
1
- import { createSubagent } from '@developerz.ai/ai-claude-compat';
2
- import { Output } from 'ai';
1
+ import { createSubagent, submittedOutput, } from '@developerz.ai/ai-claude-compat';
2
+ import { tool } from 'ai';
3
3
  import { z } from 'zod';
4
4
  export const ThreadResolutionOutputSchema = z.object({
5
5
  kind: z.enum(['fixed', 'replied', 'wontfix']),
@@ -9,24 +9,24 @@ export const ThreadResolutionOutputSchema = z.object({
9
9
  export const REVIEWER_SYSTEM_PREFIX = [
10
10
  '',
11
11
  'You are the Reviewer subagent. You receive ONE unresolved PR review thread at a time and',
12
- 'decide between three outcomes, emitting a ThreadResolutionOutput JSON that names the choice.',
12
+ 'decide between three outcomes, then call the `submit` tool with a ThreadResolutionOutput naming the choice.',
13
13
  '',
14
14
  '- "fixed": the reviewer is right and a code change is needed. Use your tools (grep/glob/',
15
15
  ' readFile to locate, editFile/multiEdit to change, writeFile to rewrite, bash for the rest)',
16
16
  ' to make the fix inside the worktree. DO NOT run `git commit` yourself — the runner commits',
17
17
  ' every staged change after you finish. Reply on the thread via the github tool explaining',
18
- ' the fix and resolve the thread, then emit { kind: "fixed", commitMessage } where',
18
+ ' the fix and resolve the thread, then submit { kind: "fixed", commitMessage } where',
19
19
  ' commitMessage is the subject line the runner will pass to `git commit`.',
20
20
  '- "replied": the comment is a question or clarification request and no code change is needed.',
21
- ' Answer it via github.replyToThread. Do not edit code. Emit { kind: "replied" }.',
21
+ ' Answer it via github.replyToThread. Do not edit code. Submit { kind: "replied" }.',
22
22
  '- "wontfix": the suggestion is stale, out of scope, or you disagree. Reply with the reason',
23
- ' via github.replyToThread, resolve the thread via github.resolveThread, and emit',
23
+ ' via github.replyToThread, resolve the thread via github.resolveThread, and submit',
24
24
  ' { kind: "wontfix", reason }.',
25
25
  '',
26
26
  'Rules:',
27
27
  '- Stay inside the worktree. No work outside the repo.',
28
28
  '- Resolve the thread for "fixed" and "wontfix" outcomes; "replied" leaves it open.',
29
- '- Return JSON that matches the ThreadResolutionOutput schema exactly.',
29
+ '- When done, call `submit` once with a value matching the ThreadResolutionOutput schema.',
30
30
  ].join('\n');
31
31
  const reviewerInitRegistry = new WeakMap();
32
32
  export function createReviewerAgent(init) {
@@ -34,9 +34,10 @@ export function createReviewerAgent(init) {
34
34
  model: init.model,
35
35
  tools: init.tools,
36
36
  systemPrompt: init.systemPrompt,
37
- output: Output.object({
38
- schema: ThreadResolutionOutputSchema,
39
- name: 'ThreadResolution',
37
+ submit: tool({
38
+ description: 'Submit the resolution for this review thread (the ThreadResolutionOutput schema).',
39
+ inputSchema: ThreadResolutionOutputSchema,
40
+ execute: async (resolution) => resolution,
40
41
  }),
41
42
  ...(init.maxSteps !== undefined ? { maxSteps: init.maxSteps } : {}),
42
43
  }, 20);
@@ -67,7 +68,10 @@ export async function runReviewer(agent, input) {
67
68
  }
68
69
  async function resolveOneThread(agent, init, input, thread) {
69
70
  const result = await agent.generate({ prompt: buildThreadPrompt(input, thread) });
70
- const out = result.experimental_output;
71
+ const out = submittedOutput(result, ThreadResolutionOutputSchema);
72
+ if (!out) {
73
+ return { threadId: thread.id, kind: 'wontfix', reason: 'reviewer did not submit a resolution' };
74
+ }
71
75
  switch (out.kind) {
72
76
  case 'fixed': {
73
77
  const message = out.commitMessage?.trim() || `fix: address review thread ${thread.id}`;
@@ -92,7 +96,7 @@ function buildThreadPrompt(input, thread) {
92
96
  for (const c of thread.comments) {
93
97
  lines.push(` @${c.author}: ${c.body}`);
94
98
  }
95
- lines.push('', 'Decide the outcome, take the action, then emit the ThreadResolutionOutput JSON.');
99
+ lines.push('', 'Decide the outcome, take the action, then call submit with the ThreadResolutionOutput.');
96
100
  return lines.join('\n');
97
101
  }
98
102
  async function commitFix(bash, worktreePath, message) {
@@ -1,5 +1,5 @@
1
1
  import type { BashInput, BashOutput, EditFileInput, EditFileOutput, GlobInput, GlobOutput, GrepInput, GrepOutput, MultiBashInput, MultiBashOutput, MultiEditInput, MultiEditOutput, ReadFileInput, ReadFileOutput, WriteFileInput, WriteFileOutput } from '@developerz.ai/ai-claude-compat';
2
- import { type DeepPartial, Output, type Tool, type ToolLoopAgent } from 'ai';
2
+ import { type Tool, type ToolLoopAgent } from 'ai';
3
3
  import { z } from 'zod';
4
4
  import type { PrGroup } from '../state/schema.ts';
5
5
  import type { SubagentInit } from './factory.ts';
@@ -36,8 +36,7 @@ export declare const FileManifestSchema: z.ZodObject<{
36
36
  draftCommitMessage: z.ZodString;
37
37
  }, z.core.$strip>;
38
38
  export type FileManifest = z.infer<typeof FileManifestSchema>;
39
- type WorkerOutput = Output.Output<FileManifest, DeepPartial<FileManifest>, never>;
40
- export type WorkerAgent = ToolLoopAgent<never, WorkerTools, WorkerOutput>;
39
+ export type WorkerAgent = ToolLoopAgent<never, WorkerTools>;
41
40
  export type WorkerInput = {
42
41
  group: PrGroup;
43
42
  worktreePath: string;
@@ -70,4 +69,3 @@ export type WorkerResult = {
70
69
  export declare const WORKER_SYSTEM_PREFIX: string;
71
70
  export declare function createWorkerAgent(init: SubagentInit<WorkerTools>): WorkerAgent;
72
71
  export declare function runWorker(agent: WorkerAgent, input: WorkerInput): Promise<WorkerResult>;
73
- export {};
@@ -1,5 +1,5 @@
1
- import { composeSystemPrompt, createSubagent } from '@developerz.ai/ai-claude-compat';
2
- import { generateText, Output, stepCountIs, } from 'ai';
1
+ import { composeSystemPrompt, createSubagent, submittedOutput, } from '@developerz.ai/ai-claude-compat';
2
+ import { generateText, stepCountIs, tool } from 'ai';
3
3
  import { z } from 'zod';
4
4
  export const FileManifestEntrySchema = z.object({
5
5
  path: z.string().min(1),
@@ -16,8 +16,8 @@ export const WORKER_SYSTEM_PREFIX = [
16
16
  'land in a single pull request on a dedicated branch. Work in two phases.',
17
17
  '',
18
18
  'Phase 1 — manifest. Use your read-only tools (readFile with optional offset/limit, grep,',
19
- 'glob) to ground yourself in the existing code, then emit a FileManifest JSON listing every',
20
- 'file to create/modify/delete plus a one-line draft commit message. Do not edit yet.',
19
+ 'glob) to ground yourself in the existing code, then call the `submit` tool with a FileManifest',
20
+ 'listing every file to create/modify/delete plus a one-line draft commit message. Do not edit yet.',
21
21
  '',
22
22
  'Phase 2 — edits. Each manifest entry is handed to a dedicated editor subagent in',
23
23
  'parallel by the runtime; you do not execute Phase 2 yourself.',
@@ -26,7 +26,7 @@ export const WORKER_SYSTEM_PREFIX = [
26
26
  '- Stay inside the worktree provided. No work outside the repo.',
27
27
  '- One responsibility per file. If a file has multiple unrelated edits, split it.',
28
28
  '- draftCommitMessage is a hint to the Orchestrator; keep the subject under 72 chars.',
29
- '- Return the FileManifest JSON exactly matching the schema.',
29
+ '- When the manifest is complete, call `submit` once with the FileManifest (matching the schema).',
30
30
  ].join('\n');
31
31
  const EDITOR_SYSTEM_PREFIX = [
32
32
  '',
@@ -50,7 +50,11 @@ export function createWorkerAgent(init) {
50
50
  model: init.model,
51
51
  tools: init.tools,
52
52
  systemPrompt: init.systemPrompt,
53
- output: Output.object({ schema: FileManifestSchema, name: 'FileManifest' }),
53
+ submit: tool({
54
+ description: 'Submit the file manifest (the FileManifest schema) for this PR group.',
55
+ inputSchema: FileManifestSchema,
56
+ execute: async (manifest) => manifest,
57
+ }),
54
58
  ...(init.maxSteps !== undefined ? { maxSteps: init.maxSteps } : {}),
55
59
  }, 30);
56
60
  workerInitRegistry.set(agent, init);
@@ -91,7 +95,7 @@ export async function runWorker(agent, input) {
91
95
  }
92
96
  async function planManifest(agent, input) {
93
97
  const result = await agent.generate({ prompt: buildManifestPrompt(input) });
94
- return result.experimental_output;
98
+ return submittedOutput(result, FileManifestSchema) ?? { files: [], draftCommitMessage: '' };
95
99
  }
96
100
  function buildManifestPrompt(input) {
97
101
  const lines = [
@@ -106,7 +110,7 @@ function buildManifestPrompt(input) {
106
110
  if (input.rollingContext.trim()) {
107
111
  lines.push('', 'Rolling context from prior PRs:', input.rollingContext);
108
112
  }
109
- lines.push('', 'Survey the repo, then emit the FileManifest JSON.');
113
+ lines.push('', 'Survey the repo, then call submit with the FileManifest.');
110
114
  return lines.join('\n');
111
115
  }
112
116
  async function runEditor(init, file, input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developerz.ai/aitm",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Autonomous task orchestrator. Goal in, merged PRs out.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@ai-sdk/mcp": "^1.0.42",
45
- "@developerz.ai/ai-claude-compat": "0.0.4",
45
+ "@developerz.ai/ai-claude-compat": "0.0.5",
46
46
  "@openrouter/ai-sdk-provider": "^2.9.0",
47
47
  "ai": "^6.0.182",
48
48
  "execa": "^9.6.1",