acdev 1.0.8 → 1.0.10

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/src/config.js CHANGED
@@ -6,20 +6,15 @@ import { checkGhAuth, githubTokenFromEnv, originRemoteInfo } from './gh-auth.js'
6
6
  import { normalizeJiraBaseUrl } from './jira.js';
7
7
  import {
8
8
  DEFAULT_MODEL,
9
- DEFAULT_OPENROUTER_MODEL,
10
- LLM_PROVIDERS,
11
9
  NO_MODEL,
12
10
  isValidModelId,
13
11
  isNoModel,
14
- isValidLlmProvider,
15
- defaultModelForProvider,
16
- isModelIdForProvider,
12
+ isClaudeCatalogId,
17
13
  curatedModelOptions,
18
14
  } from './models.js';
19
- import { checkOpenRouterAuth } from './openrouter-auth.js';
20
15
  import { dataDir } from './paths.js';
21
16
 
22
- export { DEFAULT_MODEL, DEFAULT_OPENROUTER_MODEL, CLAUDE_MODEL_OPTIONS, MODEL_OPTIONS, OPENROUTER_MODEL_OPTIONS, LLM_PROVIDERS, NO_MODEL } from './models.js';
17
+ export { DEFAULT_MODEL, CLAUDE_MODEL_OPTIONS, MODEL_OPTIONS, NO_MODEL } from './models.js';
23
18
 
24
19
  /** Tool names the agent may be granted via config. */
25
20
  export const KNOWN_TOOLS = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
@@ -69,10 +64,7 @@ const DEFAULTS = {
69
64
  maxAgentTurns: 30,
70
65
  allowedTools: [...KNOWN_TOOLS],
71
66
  agentTimeoutMs: 900_000,
72
- llmProvider: /** @type {'claude' | 'openrouter'} */ ('claude'),
73
67
  model: DEFAULT_MODEL,
74
- /** Last valid model selection per LLM provider (for restore on provider switch). */
75
- lastModelsByProvider: /** @type {Record<string, string>} */ ({}),
76
68
  ticketSource: /** @type {'github' | 'jira'} */ ('github'),
77
69
  jiraBaseUrl: '',
78
70
  /** PR body phrase for Jira tickets, e.g. "Relates to PROJ-123". */
@@ -82,76 +74,29 @@ const DEFAULTS = {
82
74
  };
83
75
 
84
76
  /**
77
+ * Migrate legacy OpenRouter model ids to a Claude model.
85
78
  * @param {unknown} raw
86
- * @returns {Record<string, string>}
87
- */
88
- function normalizeLastModelsByProvider(raw) {
89
- /** @type {Record<string, string>} */
90
- const out = {};
91
- if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
92
- return out;
93
- }
94
- for (const provider of LLM_PROVIDERS) {
95
- const value = /** @type {Record<string, unknown>} */ (raw)[provider];
96
- if (typeof value !== 'string' || isNoModel(value) || !isValidModelId(value)) {
97
- continue;
98
- }
99
- const id = value.trim();
100
- if (isModelIdForProvider(id, provider)) {
101
- out[provider] = id;
102
- }
103
- }
104
- return out;
105
- }
106
-
107
- /**
108
- * @param {object} config
109
- * @param {'claude' | 'openrouter'} provider
110
- * @param {string} model
111
- */
112
- function rememberModelForProvider(config, provider, model) {
113
- if (!isValidLlmProvider(provider)) return;
114
- if (isNoModel(model) || !isValidModelId(model)) return;
115
- const id = String(model).trim();
116
- if (!isModelIdForProvider(id, provider)) return;
117
- if (
118
- !config.lastModelsByProvider ||
119
- typeof config.lastModelsByProvider !== 'object' ||
120
- config.lastModelsByProvider === DEFAULTS.lastModelsByProvider
121
- ) {
122
- config.lastModelsByProvider = {
123
- ...(config.lastModelsByProvider &&
124
- typeof config.lastModelsByProvider === 'object' &&
125
- config.lastModelsByProvider !== DEFAULTS.lastModelsByProvider
126
- ? config.lastModelsByProvider
127
- : {}),
128
- };
129
- }
130
- config.lastModelsByProvider[provider] = id;
131
- }
132
-
133
- /**
134
- * Pick model for a provider: preferred if in catalog, else last stored, else no selection.
135
- * @param {object} config
136
- * @param {'claude' | 'openrouter'} provider
137
- * @param {string} [preferredModel]
79
+ * @param {string} currentModel
138
80
  * @returns {string}
139
81
  */
140
- function resolveModelForProvider(config, provider, preferredModel) {
141
- const candidate =
142
- preferredModel != null ? preferredModel : config.model;
143
- if (!isNoModel(candidate) && isModelIdForProvider(candidate, provider)) {
144
- return String(candidate).trim();
145
- }
146
- const stored = config.lastModelsByProvider?.[provider];
147
- if (
148
- typeof stored === 'string' &&
149
- !isNoModel(stored) &&
150
- isModelIdForProvider(stored, provider)
151
- ) {
82
+ function migrateLegacyModel(raw, currentModel) {
83
+ if (isNoModel(currentModel)) return NO_MODEL;
84
+ if (isClaudeCatalogId(currentModel)) return String(currentModel).trim();
85
+
86
+ const stored =
87
+ raw != null &&
88
+ typeof raw === 'object' &&
89
+ !Array.isArray(raw) &&
90
+ typeof /** @type {Record<string, unknown>} */ (raw).lastModelsByProvider === 'object'
91
+ ? /** @type {Record<string, unknown>} */ (
92
+ /** @type {Record<string, unknown>} */ (raw).lastModelsByProvider
93
+ ).claude
94
+ : null;
95
+
96
+ if (typeof stored === 'string' && isClaudeCatalogId(stored)) {
152
97
  return stored.trim();
153
98
  }
154
- return NO_MODEL;
99
+ return DEFAULT_MODEL;
155
100
  }
156
101
 
157
102
  /**
@@ -294,9 +239,7 @@ function persistable(config) {
294
239
  maxAgentTurns: config.maxAgentTurns,
295
240
  allowedTools: config.allowedTools,
296
241
  agentTimeoutMs: config.agentTimeoutMs,
297
- llmProvider: isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude',
298
242
  model: config.model,
299
- lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
300
243
  ticketSource: config.ticketSource,
301
244
  jiraBaseUrl: config.jiraBaseUrl || '',
302
245
  jiraPrLinkPhrase: config.jiraPrLinkPhrase || 'Relates to',
@@ -336,11 +279,9 @@ export function loadConfig(repoRoot) {
336
279
  const initial = {
337
280
  ...DEFAULTS,
338
281
  allowedTools: [...KNOWN_TOOLS],
339
- lastModelsByProvider: {},
340
282
  jiraRules: structuredClone(DEFAULT_JIRA_RULES),
341
283
  githubRules: structuredClone(DEFAULT_GITHUB_RULES),
342
284
  };
343
- rememberModelForProvider(initial, 'claude', initial.model);
344
285
  fs.writeFileSync(configPath, JSON.stringify(persistable(initial), null, 2) + '\n', 'utf8');
345
286
  return initial;
346
287
  }
@@ -350,29 +291,34 @@ export function loadConfig(repoRoot) {
350
291
  ...DEFAULTS,
351
292
  ...raw,
352
293
  allowedTools: raw.allowedTools ?? [...KNOWN_TOOLS],
353
- lastModelsByProvider: normalizeLastModelsByProvider(raw.lastModelsByProvider),
354
294
  jiraRules: normalizeJiraRules(raw.jiraRules),
355
295
  githubRules: normalizeGithubRules(raw.githubRules),
356
296
  };
357
297
 
298
+ let needsSave = false;
299
+
358
300
  if (isNoModel(config.model)) {
359
301
  if (String(config.model).trim() !== NO_MODEL) {
360
302
  config.model = NO_MODEL;
361
- saveConfig(repoRoot, persistable(config));
303
+ needsSave = true;
362
304
  } else {
363
305
  config.model = NO_MODEL;
364
306
  }
365
307
  } else if (!isValidModelId(config.model)) {
366
- config.model = defaultModelForProvider(
367
- isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude'
368
- );
369
- saveConfig(repoRoot, persistable(config));
308
+ config.model = DEFAULT_MODEL;
309
+ needsSave = true;
370
310
  } else {
371
- config.model = String(config.model).trim();
311
+ const migrated = migrateLegacyModel(raw, config.model);
312
+ if (migrated !== String(config.model).trim()) {
313
+ config.model = migrated;
314
+ needsSave = true;
315
+ } else {
316
+ config.model = String(config.model).trim();
317
+ }
372
318
  }
373
319
 
374
- if (!isValidLlmProvider(config.llmProvider)) {
375
- config.llmProvider = 'claude';
320
+ if (raw.llmProvider === 'openrouter' || raw.lastModelsByProvider != null) {
321
+ needsSave = true;
376
322
  }
377
323
 
378
324
  if (!ALLOWED_TICKET_SOURCES.has(config.ticketSource)) {
@@ -387,10 +333,9 @@ export function loadConfig(repoRoot) {
387
333
  config.jiraPrLinkPhrase = 'Relates to';
388
334
  }
389
335
 
390
- const loadedProvider = isValidLlmProvider(config.llmProvider)
391
- ? config.llmProvider
392
- : 'claude';
393
- rememberModelForProvider(config, loadedProvider, config.model);
336
+ if (needsSave) {
337
+ saveConfig(repoRoot, persistable(config));
338
+ }
394
339
 
395
340
  return config;
396
341
  }
@@ -404,23 +349,6 @@ export function loadConfig(repoRoot) {
404
349
  * @returns {typeof DEFAULTS}
405
350
  */
406
351
  export function updateConfig(repoRoot, config, patch) {
407
- if (!config.lastModelsByProvider || typeof config.lastModelsByProvider !== 'object') {
408
- config.lastModelsByProvider = normalizeLastModelsByProvider(config.lastModelsByProvider);
409
- }
410
-
411
- const prevProvider = isValidLlmProvider(config.llmProvider)
412
- ? config.llmProvider
413
- : 'claude';
414
- let providerChanged = false;
415
- const willChangeProvider =
416
- patch.llmProvider !== undefined &&
417
- isValidLlmProvider(patch.llmProvider) &&
418
- patch.llmProvider !== prevProvider;
419
-
420
- if (willChangeProvider) {
421
- rememberModelForProvider(config, prevProvider, config.model);
422
- }
423
-
424
352
  if (patch.model != null) {
425
353
  if (isNoModel(patch.model)) {
426
354
  config.model = NO_MODEL;
@@ -428,33 +356,13 @@ export function updateConfig(repoRoot, config, patch) {
428
356
  throw new Error(
429
357
  `Invalid model "${patch.model}". Expected a non-empty model id or "${NO_MODEL}" for no selection`
430
358
  );
431
- } else {
432
- config.model = String(patch.model).trim();
433
- }
434
- }
435
-
436
- if (patch.llmProvider !== undefined) {
437
- if (!isValidLlmProvider(patch.llmProvider)) {
359
+ } else if (!isClaudeCatalogId(String(patch.model).trim())) {
438
360
  throw new Error(
439
- `Invalid llmProvider "${patch.llmProvider}". Allowed: ${LLM_PROVIDERS.join(', ')}`
361
+ `Invalid model "${patch.model}". Expected a Claude model id (no provider/ prefix)`
440
362
  );
363
+ } else {
364
+ config.model = String(patch.model).trim();
441
365
  }
442
- providerChanged = willChangeProvider;
443
- config.llmProvider = patch.llmProvider;
444
- }
445
-
446
- const currentProvider = isValidLlmProvider(config.llmProvider)
447
- ? config.llmProvider
448
- : 'claude';
449
-
450
- if (providerChanged) {
451
- config.model = resolveModelForProvider(config, currentProvider, config.model);
452
- }
453
-
454
- if (patch.model != null && !providerChanged) {
455
- rememberModelForProvider(config, currentProvider, config.model);
456
- } else if (providerChanged && !isNoModel(config.model)) {
457
- rememberModelForProvider(config, currentProvider, config.model);
458
366
  }
459
367
 
460
368
  if (patch.baseBranch !== undefined) {
@@ -545,18 +453,14 @@ export function updateConfig(repoRoot, config, patch) {
545
453
  * stubAgent?: boolean,
546
454
  * ghAuth?: import('./gh-auth.js').GhAuthResult,
547
455
  * claudeAuth?: import('./claude-auth.js').ClaudeAuthResult,
548
- * openrouterAuth?: import('./openrouter-auth.js').OpenRouterAuthResult,
549
456
  * }} [opts]
550
457
  */
551
458
  export function publicConfig(config, opts = {}) {
552
- const llmProvider = isValidLlmProvider(config.llmProvider)
553
- ? config.llmProvider
554
- : 'claude';
555
459
  const model = isNoModel(config.model)
556
460
  ? NO_MODEL
557
- : isValidModelId(config.model)
461
+ : isValidModelId(config.model) && isClaudeCatalogId(config.model)
558
462
  ? String(config.model).trim()
559
- : defaultModelForProvider(llmProvider);
463
+ : DEFAULT_MODEL;
560
464
  const repoName = opts.repoRoot ? path.basename(opts.repoRoot) : undefined;
561
465
  const ticketSource = ALLOWED_TICKET_SOURCES.has(config.ticketSource)
562
466
  ? config.ticketSource
@@ -581,19 +485,15 @@ export function publicConfig(config, opts = {}) {
581
485
 
582
486
  const ghAuth = opts.ghAuth ?? checkGhAuth();
583
487
  const claudeAuth = opts.claudeAuth ?? checkClaudeAuth();
584
- const openrouterAuth = opts.openrouterAuth ?? checkOpenRouterAuth();
585
488
  const origin = originRemoteInfo(opts.repoRoot);
586
489
  const ghTokenMask = maskSecret(githubTokenFromEnv());
587
490
  const anthropicMask = maskSecret(process.env.ANTHROPIC_API_KEY);
588
491
  const claudeOauthMask = maskSecret(process.env.CLAUDE_CODE_OAUTH_TOKEN);
589
- const openrouterMask = maskSecret(process.env.OPENROUTER_API_KEY);
590
492
 
591
- const curatedModels = curatedModelOptions(llmProvider);
493
+ const curatedModels = curatedModelOptions();
592
494
 
593
495
  return {
594
- llmProvider,
595
496
  model,
596
- lastModelsByProvider: normalizeLastModelsByProvider(config.lastModelsByProvider),
597
497
  baseBranch: config.baseBranch,
598
498
  testCommand: config.testCommand ?? null,
599
499
  maxAgentTurns: config.maxAgentTurns,
@@ -625,13 +525,7 @@ export function publicConfig(config, opts = {}) {
625
525
  anthropicApiKeyMasked: anthropicMask.masked,
626
526
  claudeOauthTokenSet: claudeOauthMask.set,
627
527
  claudeOauthTokenMasked: claudeOauthMask.masked,
628
- openrouterAuthOk: openrouterAuth.ok === true,
629
- openrouterApiKeySet: openrouterMask.set,
630
- openrouterApiKeyMasked: openrouterMask.masked,
631
- llmAuthOk:
632
- llmProvider === 'openrouter'
633
- ? openrouterAuth.ok === true
634
- : claudeAuth.ok === true,
528
+ llmAuthOk: claudeAuth.ok === true,
635
529
  stubAgent: opts.stubAgent === true,
636
530
  ...(repoName ? { repoName } : {}),
637
531
  };
package/src/git.js CHANGED
@@ -103,6 +103,68 @@ export function buildBranchName(type, title) {
103
103
  return `${type}/${slugifyTitle(title)}`;
104
104
  }
105
105
 
106
+ const PREFERRED_BRANCH_MAX = 100;
107
+
108
+ /**
109
+ * Sanitize a user-supplied git branch name. Preserves case (e.g. NC-2133).
110
+ * Empty / whitespace-only input returns ''.
111
+ * @param {unknown} raw
112
+ */
113
+ export function normalizePreferredBranchName(raw) {
114
+ if (raw == null) return '';
115
+ let name = String(raw).trim().replace(/\s+/g, '-');
116
+ if (!name) return '';
117
+
118
+ name = name
119
+ .replace(/[~^:?*\[\\]+/g, '-')
120
+ .replace(/@{/g, '-at-')
121
+ .replace(/[^A-Za-z0-9._/-]+/g, '-')
122
+ .replace(/\/+/g, '/')
123
+ .replace(/\.{2,}/g, '.')
124
+ .replace(/-{2,}/g, '-')
125
+ .replace(/^[-./]+|[-./]+$/g, '');
126
+
127
+ if (/\.lock$/i.test(name)) {
128
+ name = name.replace(/\.lock$/i, '').replace(/[-./]+$/g, '');
129
+ }
130
+
131
+ name = name.slice(0, PREFERRED_BRANCH_MAX).replace(/[-./]+$/g, '');
132
+ if (!name || name === '@' || /^HEAD$/i.test(name)) return '';
133
+ return name;
134
+ }
135
+
136
+ /**
137
+ * Parse optional branchName from an API body.
138
+ * Omitted / blank → no preference (caller uses feat/fix slug).
139
+ * Provided but invalid after sanitize → error.
140
+ * @param {unknown} raw
141
+ * @returns {{ ok: true, value?: string } | { ok: false, error: string }}
142
+ */
143
+ export function parsePreferredBranchName(raw) {
144
+ if (raw == null) return { ok: true };
145
+ if (typeof raw !== 'string') {
146
+ return { ok: false, error: 'branchName must be a string' };
147
+ }
148
+ if (!raw.trim()) return { ok: true };
149
+ const value = normalizePreferredBranchName(raw);
150
+ if (!value) {
151
+ return { ok: false, error: 'branchName is not a valid git branch name' };
152
+ }
153
+ return { ok: true, value };
154
+ }
155
+
156
+ /**
157
+ * Prefer a user-supplied branch name; otherwise feat/fix slug from the ticket.
158
+ * @param {string | undefined | null} preferred
159
+ * @param {'feat' | 'fix'} issueType
160
+ * @param {string} issueTitle
161
+ */
162
+ export function resolveDesiredBranchName(preferred, issueType, issueTitle) {
163
+ const custom = normalizePreferredBranchName(preferred);
164
+ if (custom) return custom;
165
+ return buildBranchName(issueType, issueTitle);
166
+ }
167
+
106
168
  /**
107
169
  * @param {string} repoRoot
108
170
  * @param {string | number} worktreeId GitHub issue number or Jira key