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/.acdev/.env.example +0 -3
- package/README.md +11 -21
- package/bin/acdev.js +3 -13
- package/package.json +1 -2
- package/public/app.js +71 -388
- package/public/index.html +19 -33
- package/public/styles.css +32 -31
- package/src/agent.js +64 -127
- package/src/config.js +45 -151
- package/src/git.js +62 -0
- package/src/models.js +35 -256
- package/src/server.js +72 -52
- package/src/store.js +2 -0
- package/src/openrouter-auth.js +0 -37
package/src/models.js
CHANGED
|
@@ -15,7 +15,6 @@ import { join } from 'node:path';
|
|
|
15
15
|
/**
|
|
16
16
|
* Curated Claude Agent SDK / Claude Code model ids used as defaults + fallback.
|
|
17
17
|
* Prefer documented Code aliases and Anthropic API ids (not invented snapshots).
|
|
18
|
-
* OpenRouter slugs (provider/model) are excluded — those belong in OPENROUTER_MODEL_OPTIONS.
|
|
19
18
|
*/
|
|
20
19
|
export const CLAUDE_MODEL_OPTIONS = [
|
|
21
20
|
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
@@ -44,49 +43,23 @@ export const MODEL_OPTIONS = CLAUDE_MODEL_OPTIONS;
|
|
|
44
43
|
|
|
45
44
|
export const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
46
45
|
|
|
47
|
-
/**
|
|
48
|
-
* OpenRouter slugs commonly used with acdev. Curated list is merged with the
|
|
49
|
-
* live OpenRouter catalog; anthropic/* entries are agent-compatible.
|
|
50
|
-
*/
|
|
51
|
-
export const OPENROUTER_AGENT_MODEL_PREFIX = 'anthropic/';
|
|
52
|
-
|
|
53
|
-
/** Curated OpenRouter model ids used as defaults + fallback when the live API is unavailable. */
|
|
54
|
-
export const OPENROUTER_MODEL_OPTIONS = [
|
|
55
|
-
{ id: 'anthropic/claude-sonnet-4', label: 'Claude Sonnet 4 (Anthropic)' },
|
|
56
|
-
{ id: 'anthropic/claude-opus-4', label: 'Claude Opus 4 (Anthropic)' },
|
|
57
|
-
{ id: 'anthropic/claude-3.5-sonnet', label: 'Claude 3.5 Sonnet (Anthropic)' },
|
|
58
|
-
{ id: 'anthropic/claude-3.7-sonnet', label: 'Claude 3.7 Sonnet (Anthropic)' },
|
|
59
|
-
{ id: 'openai/gpt-4o', label: 'GPT-4o (OpenAI)' },
|
|
60
|
-
{ id: 'openai/gpt-4o-mini', label: 'GPT-4o Mini (OpenAI)' },
|
|
61
|
-
{ id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro Preview (Google)' },
|
|
62
|
-
{ id: 'qwen/qwen3-235b-a22b', label: 'Qwen3 235B (Qwen)' },
|
|
63
|
-
{ id: 'deepseek/deepseek-chat-v3-0324', label: 'DeepSeek Chat V3 (DeepSeek)' },
|
|
64
|
-
{ id: 'meta-llama/llama-3.3-70b-instruct', label: 'Llama 3.3 70B Instruct (Meta)' },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
export const DEFAULT_OPENROUTER_MODEL = 'anthropic/claude-sonnet-4';
|
|
68
|
-
|
|
69
46
|
/** Config value meaning no model selected (jobs blocked until user picks one). */
|
|
70
47
|
export const NO_MODEL = '-';
|
|
71
48
|
|
|
72
|
-
/**
|
|
73
|
-
export const LLM_PROVIDERS = /** @type {const} */ (['claude', 'openrouter']);
|
|
74
|
-
|
|
75
|
-
/** Loose model id shape accepted by config (Claude aliases, API ids, OpenRouter slugs). */
|
|
49
|
+
/** Claude direct path ids: Anthropic API ids and Claude Code aliases (no `/`). */
|
|
76
50
|
export const MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:\/-]{0,127}$/;
|
|
77
51
|
|
|
78
52
|
const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models';
|
|
79
|
-
const OPENROUTER_MODELS_URL = 'https://openrouter.ai/api/v1/models';
|
|
80
53
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
81
54
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
82
55
|
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
83
56
|
|
|
84
|
-
/** @typedef {{ id: string, name?: string, label?: string
|
|
85
|
-
/** @typedef {'anthropic' | '
|
|
86
|
-
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource
|
|
57
|
+
/** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
|
|
58
|
+
/** @typedef {'anthropic' | 'fallback'} ModelsSource */
|
|
59
|
+
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource }} ModelsListResult */
|
|
87
60
|
|
|
88
|
-
/** @type {
|
|
89
|
-
|
|
61
|
+
/** @type {{ expiresAt: number, result: Omit<ModelsListResult, 'selected'> } | null} */
|
|
62
|
+
let cache = null;
|
|
90
63
|
|
|
91
64
|
/** @type {typeof fetch | null} */
|
|
92
65
|
let fetchImpl = null;
|
|
@@ -125,23 +98,7 @@ export function _resetCredentialsTokenResolver() {
|
|
|
125
98
|
}
|
|
126
99
|
|
|
127
100
|
export function _resetModelsCache() {
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @param {unknown} value
|
|
133
|
-
* @returns {value is LlmProvider}
|
|
134
|
-
*/
|
|
135
|
-
export function isValidLlmProvider(value) {
|
|
136
|
-
return value === 'claude' || value === 'openrouter';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Default model id for an LLM provider.
|
|
141
|
-
* @param {LlmProvider} [provider]
|
|
142
|
-
*/
|
|
143
|
-
export function defaultModelForProvider(provider = 'claude') {
|
|
144
|
-
return provider === 'openrouter' ? DEFAULT_OPENROUTER_MODEL : DEFAULT_MODEL;
|
|
101
|
+
cache = null;
|
|
145
102
|
}
|
|
146
103
|
|
|
147
104
|
/**
|
|
@@ -181,7 +138,7 @@ function toOption(id, displayName) {
|
|
|
181
138
|
}
|
|
182
139
|
|
|
183
140
|
/**
|
|
184
|
-
* Claude
|
|
141
|
+
* Claude catalog ids: Anthropic API ids and Claude Code aliases (no provider/ prefix).
|
|
185
142
|
* @param {string} id
|
|
186
143
|
* @returns {boolean}
|
|
187
144
|
*/
|
|
@@ -190,147 +147,45 @@ export function isClaudeCatalogId(id) {
|
|
|
190
147
|
}
|
|
191
148
|
|
|
192
149
|
/**
|
|
193
|
-
*
|
|
194
|
-
* @param {string} id
|
|
195
|
-
* @returns {boolean}
|
|
196
|
-
*/
|
|
197
|
-
export function isOpenRouterCatalogId(id) {
|
|
198
|
-
return isValidModelId(id) && String(id).includes('/');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Whether an OpenRouter slug can be used for acdev agent runs.
|
|
203
|
-
* Non-anthropic slugs may appear in OpenRouter's catalog but fail at runtime
|
|
204
|
-
* because the Claude Agent SDK speaks Anthropic Messages API semantics.
|
|
205
|
-
* @param {string} modelId
|
|
206
|
-
* @returns {boolean}
|
|
207
|
-
*/
|
|
208
|
-
export function isOpenRouterAgentCompatibleModel(modelId) {
|
|
209
|
-
if (!isOpenRouterCatalogId(modelId)) return false;
|
|
210
|
-
return String(modelId).trim().toLowerCase().startsWith(OPENROUTER_AGENT_MODEL_PREFIX);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Mark whether each OpenRouter slug is supported for acdev agent jobs.
|
|
215
|
-
* @param {ModelOption} model
|
|
216
|
-
* @returns {ModelOption}
|
|
217
|
-
*/
|
|
218
|
-
export function annotateOpenRouterModelCompatibility(model) {
|
|
219
|
-
if (!model?.id) return model;
|
|
220
|
-
return {
|
|
221
|
-
...model,
|
|
222
|
-
agentCompatible: isOpenRouterAgentCompatibleModel(model.id),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @param {ModelOption[]} models
|
|
150
|
+
* Curated fallback list.
|
|
228
151
|
* @returns {ModelOption[]}
|
|
229
152
|
*/
|
|
230
|
-
function
|
|
231
|
-
return
|
|
153
|
+
export function curatedModelOptions() {
|
|
154
|
+
return CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
232
155
|
}
|
|
233
156
|
|
|
234
157
|
/**
|
|
235
|
-
*
|
|
236
|
-
* filtered to anthropic/* — runtime enqueue still validates agent compatibility.
|
|
237
|
-
* @param {ModelOption[]} models
|
|
238
|
-
* @param {ModelOption[]} curated
|
|
239
|
-
* @returns {ModelOption[]}
|
|
240
|
-
*/
|
|
241
|
-
function ensureOpenRouterModels(models, curated) {
|
|
242
|
-
const list = models.length > 0 ? models : curated;
|
|
243
|
-
return annotateOpenRouterModelsCompatibility(list);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Improve OpenRouter agent failure messages for the job UI.
|
|
248
|
-
* @param {string} message
|
|
249
|
-
* @param {string} [model]
|
|
250
|
-
* @returns {string}
|
|
251
|
-
*/
|
|
252
|
-
export function enhanceOpenRouterAgentError(message, model) {
|
|
253
|
-
const raw = String(message || '').trim();
|
|
254
|
-
if (!raw) return raw;
|
|
255
|
-
|
|
256
|
-
const id = String(model || '').trim();
|
|
257
|
-
const routingFailure =
|
|
258
|
-
/no allowed providers are available/i.test(raw) ||
|
|
259
|
-
/not allowed by provider/i.test(raw) ||
|
|
260
|
-
/model.*not allowed/i.test(raw);
|
|
261
|
-
|
|
262
|
-
if (id && !isOpenRouterAgentCompatibleModel(id)) {
|
|
263
|
-
return [
|
|
264
|
-
`OpenRouter model "${id}" is not supported for acdev agent runs.`,
|
|
265
|
-
'Agent jobs use the Claude Agent SDK via OpenRouter\'s Anthropic-compatible API.',
|
|
266
|
-
'Choose an anthropic/* model (e.g. anthropic/claude-sonnet-4).',
|
|
267
|
-
raw !== id ? `Provider error: ${raw}` : '',
|
|
268
|
-
]
|
|
269
|
-
.filter(Boolean)
|
|
270
|
-
.join(' ');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (routingFailure) {
|
|
274
|
-
return [
|
|
275
|
-
raw,
|
|
276
|
-
'If you use OpenRouter provider allowlists, clear Settings → Privacy → Providers or add the model\'s upstream provider.',
|
|
277
|
-
'For acdev, anthropic/* models on OpenRouter are the most reliable choice.',
|
|
278
|
-
].join(' ');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return raw;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Curated fallback list for a provider.
|
|
286
|
-
* @param {LlmProvider} provider
|
|
287
|
-
* @returns {ModelOption[]}
|
|
288
|
-
*/
|
|
289
|
-
export function curatedModelOptions(provider) {
|
|
290
|
-
return provider === 'openrouter'
|
|
291
|
-
? OPENROUTER_MODEL_OPTIONS.map((m) => ({ ...m }))
|
|
292
|
-
: CLAUDE_MODEL_OPTIONS.map((m) => ({ ...m }));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Whether a model id belongs in a provider's curated catalog.
|
|
158
|
+
* Whether a model id belongs in the curated catalog.
|
|
297
159
|
* @param {string} modelId
|
|
298
|
-
* @param {LlmProvider} provider
|
|
299
160
|
* @returns {boolean}
|
|
300
161
|
*/
|
|
301
|
-
export function isModelInCuratedCatalog(modelId
|
|
162
|
+
export function isModelInCuratedCatalog(modelId) {
|
|
302
163
|
if (isNoModel(modelId)) return false;
|
|
303
164
|
const id = String(modelId).trim();
|
|
304
|
-
return curatedModelOptions(
|
|
165
|
+
return curatedModelOptions().some((m) => m.id === id);
|
|
305
166
|
}
|
|
306
167
|
|
|
307
168
|
/**
|
|
308
|
-
* Whether a model id is valid for
|
|
309
|
-
* Claude: direct ids without `/`. OpenRouter: provider/model slugs with `/`.
|
|
169
|
+
* Whether a model id is valid for Claude (direct ids without `/`).
|
|
310
170
|
* @param {string} modelId
|
|
311
|
-
* @param {LlmProvider} provider
|
|
312
171
|
* @returns {boolean}
|
|
313
172
|
*/
|
|
314
|
-
export function isModelIdForProvider(modelId
|
|
173
|
+
export function isModelIdForProvider(modelId) {
|
|
315
174
|
if (isNoModel(modelId) || !isValidModelId(modelId)) return false;
|
|
316
|
-
|
|
317
|
-
return provider === 'openrouter' ? isOpenRouterCatalogId(id) : isClaudeCatalogId(id);
|
|
175
|
+
return isClaudeCatalogId(String(modelId).trim());
|
|
318
176
|
}
|
|
319
177
|
|
|
320
178
|
/**
|
|
321
|
-
* Drop ids that do not belong in
|
|
179
|
+
* Drop ids that do not belong in the Claude catalog.
|
|
322
180
|
* @param {ModelOption[]} models
|
|
323
|
-
* @param {LlmProvider} provider
|
|
324
181
|
* @returns {ModelOption[]}
|
|
325
182
|
*/
|
|
326
|
-
function
|
|
327
|
-
|
|
328
|
-
provider === 'openrouter' ? isOpenRouterCatalogId : isClaudeCatalogId;
|
|
329
|
-
return models.filter((m) => m?.id && keep(m.id));
|
|
183
|
+
function filterClaudeModels(models) {
|
|
184
|
+
return models.filter((m) => m?.id && isClaudeCatalogId(m.id));
|
|
330
185
|
}
|
|
331
186
|
|
|
332
187
|
/**
|
|
333
|
-
* Merge curated options (stable order / aliases) with live rows
|
|
188
|
+
* Merge curated options (stable order / aliases) with live rows.
|
|
334
189
|
* Same ids keep curated position but prefer live display names.
|
|
335
190
|
* @param {ModelOption[]} curated
|
|
336
191
|
* @param {ModelOption[]} live
|
|
@@ -365,7 +220,7 @@ function mergeModelLists(curated, live) {
|
|
|
365
220
|
}
|
|
366
221
|
|
|
367
222
|
/**
|
|
368
|
-
* Keep selected only when it exists in the
|
|
223
|
+
* Keep selected only when it exists in the model catalog.
|
|
369
224
|
* @param {ModelOption[]} models
|
|
370
225
|
* @param {string} selected
|
|
371
226
|
* @returns {string}
|
|
@@ -539,69 +394,14 @@ export async function fetchAnthropicModels() {
|
|
|
539
394
|
}
|
|
540
395
|
|
|
541
396
|
/**
|
|
542
|
-
*
|
|
543
|
-
* @
|
|
544
|
-
*/
|
|
545
|
-
async function parseOpenRouterModelsResponse(res) {
|
|
546
|
-
if (!res.ok) {
|
|
547
|
-
throw new Error(`OpenRouter Models API HTTP ${res.status}`);
|
|
548
|
-
}
|
|
549
|
-
const body = await res.json();
|
|
550
|
-
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
551
|
-
/** @type {ModelOption[]} */
|
|
552
|
-
const models = [];
|
|
553
|
-
for (const row of rows) {
|
|
554
|
-
const id = typeof row?.id === 'string' ? row.id.trim() : '';
|
|
555
|
-
if (!isValidModelId(id)) continue;
|
|
556
|
-
const display =
|
|
557
|
-
typeof row.name === 'string'
|
|
558
|
-
? row.name
|
|
559
|
-
: typeof row?.id === 'string'
|
|
560
|
-
? row.id
|
|
561
|
-
: '';
|
|
562
|
-
models.push(toOption(id, display));
|
|
563
|
-
}
|
|
564
|
-
if (models.length === 0) {
|
|
565
|
-
throw new Error('OpenRouter Models API returned no models');
|
|
566
|
-
}
|
|
567
|
-
return models;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Fetch live models from OpenRouter (no cache).
|
|
572
|
-
* @returns {Promise<ModelOption[]>}
|
|
573
|
-
*/
|
|
574
|
-
export async function fetchOpenRouterModels() {
|
|
575
|
-
const env = envResolver();
|
|
576
|
-
const apiKey = (env.OPENROUTER_API_KEY || '').trim();
|
|
577
|
-
if (!apiKey) {
|
|
578
|
-
throw new Error('No OpenRouter API key for models list');
|
|
579
|
-
}
|
|
580
|
-
const doFetch = fetchImpl || globalThis.fetch;
|
|
581
|
-
if (typeof doFetch !== 'function') {
|
|
582
|
-
throw new Error('fetch is not available');
|
|
583
|
-
}
|
|
584
|
-
const res = await doFetch(OPENROUTER_MODELS_URL, {
|
|
585
|
-
method: 'GET',
|
|
586
|
-
headers: {
|
|
587
|
-
Authorization: `Bearer ${apiKey}`,
|
|
588
|
-
'HTTP-Referer': 'https://github.com/acdev',
|
|
589
|
-
'X-Title': 'acdev',
|
|
590
|
-
},
|
|
591
|
-
});
|
|
592
|
-
return parseOpenRouterModelsResponse(res);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* @param {LlmProvider} provider
|
|
597
|
-
* @param {{ selected?: string, force?: boolean }} opts
|
|
397
|
+
* List Claude models for the UI.
|
|
398
|
+
* @param {{ selected?: string, force?: boolean }} [opts]
|
|
598
399
|
* @returns {Promise<ModelsListResult>}
|
|
599
400
|
*/
|
|
600
|
-
async function
|
|
401
|
+
export async function listModels(opts = {}) {
|
|
601
402
|
const selectedRaw = opts.selected;
|
|
602
403
|
const force = opts.force === true;
|
|
603
404
|
const now = Date.now();
|
|
604
|
-
const cache = cacheByProvider.get(provider);
|
|
605
405
|
|
|
606
406
|
if (!force && cache && cache.expiresAt > now) {
|
|
607
407
|
const selected = reconcileModelForProvider(cache.result.models, selectedRaw);
|
|
@@ -609,45 +409,24 @@ async function listModelsForProvider(provider, opts = {}) {
|
|
|
609
409
|
...cache.result,
|
|
610
410
|
models: cache.result.models,
|
|
611
411
|
selected,
|
|
612
|
-
provider,
|
|
613
412
|
};
|
|
614
413
|
}
|
|
615
414
|
|
|
616
|
-
const curated = curatedModelOptions(
|
|
415
|
+
const curated = curatedModelOptions();
|
|
617
416
|
|
|
618
417
|
try {
|
|
619
|
-
const liveRaw =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
: await fetchAnthropicModels();
|
|
623
|
-
const live = filterModelsForProvider(liveRaw, provider);
|
|
624
|
-
let models = filterModelsForProvider(mergeModelLists(curated, live), provider);
|
|
625
|
-
if (provider === 'openrouter') {
|
|
626
|
-
models = ensureOpenRouterModels(models, curated);
|
|
627
|
-
}
|
|
418
|
+
const liveRaw = await fetchAnthropicModels();
|
|
419
|
+
const live = filterClaudeModels(liveRaw);
|
|
420
|
+
const models = filterClaudeModels(mergeModelLists(curated, live));
|
|
628
421
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return { ...result, selected, provider };
|
|
422
|
+
const result = { models, source: /** @type {ModelsSource} */ ('anthropic') };
|
|
423
|
+
cache = { expiresAt: now + CACHE_TTL_MS, result };
|
|
424
|
+
return { ...result, selected };
|
|
633
425
|
} catch {
|
|
634
|
-
|
|
635
|
-
if (provider === 'openrouter') {
|
|
636
|
-
models = ensureOpenRouterModels(models, curated);
|
|
637
|
-
}
|
|
426
|
+
const models = curated;
|
|
638
427
|
const selected = reconcileModelForProvider(models, selectedRaw);
|
|
639
428
|
const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
|
|
640
|
-
|
|
641
|
-
return { ...result, selected
|
|
429
|
+
cache = { expiresAt: now + 30_000, result };
|
|
430
|
+
return { ...result, selected };
|
|
642
431
|
}
|
|
643
432
|
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* List models for the UI.
|
|
647
|
-
* @param {{ selected?: string, force?: boolean, provider?: LlmProvider }} [opts]
|
|
648
|
-
* @returns {Promise<ModelsListResult>}
|
|
649
|
-
*/
|
|
650
|
-
export async function listModels(opts = {}) {
|
|
651
|
-
const provider = isValidLlmProvider(opts.provider) ? opts.provider : 'claude';
|
|
652
|
-
return listModelsForProvider(provider, opts);
|
|
653
|
-
}
|
package/src/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { Store } from './store.js';
|
|
@@ -18,7 +19,8 @@ import {
|
|
|
18
19
|
getDiff,
|
|
19
20
|
pushBranch,
|
|
20
21
|
detectIssueType,
|
|
21
|
-
|
|
22
|
+
parsePreferredBranchName,
|
|
23
|
+
resolveDesiredBranchName,
|
|
22
24
|
sanitizeBranchCommits,
|
|
23
25
|
listChangedFiles,
|
|
24
26
|
applyFileExclusions,
|
|
@@ -38,8 +40,7 @@ import { splitIssueUrls } from './urls.js';
|
|
|
38
40
|
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
39
41
|
import { checkGhAuth } from './gh-auth.js';
|
|
40
42
|
import { checkClaudeAuth } from './claude-auth.js';
|
|
41
|
-
import {
|
|
42
|
-
import { isValidLlmProvider, isValidModelId, isNoModel, NO_MODEL, isOpenRouterAgentCompatibleModel, enhanceOpenRouterAgentError } from './models.js';
|
|
43
|
+
import { isValidModelId, isNoModel, NO_MODEL } from './models.js';
|
|
43
44
|
|
|
44
45
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
46
|
|
|
@@ -175,7 +176,6 @@ export function normalizeReviewComments(body) {
|
|
|
175
176
|
* resolveJiraCredentials?: Function,
|
|
176
177
|
* checkGhAuth?: typeof checkGhAuth,
|
|
177
178
|
* checkClaudeAuth?: typeof checkClaudeAuth,
|
|
178
|
-
* checkOpenRouterAuth?: typeof checkOpenRouterAuth,
|
|
179
179
|
* },
|
|
180
180
|
* }} options
|
|
181
181
|
*/
|
|
@@ -191,18 +191,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
191
191
|
const doApplyFileExclusions = deps.applyFileExclusions || applyFileExclusions;
|
|
192
192
|
const doCheckGhAuth = deps.checkGhAuth || checkGhAuth;
|
|
193
193
|
const doCheckClaudeAuth = deps.checkClaudeAuth || checkClaudeAuth;
|
|
194
|
-
const doCheckOpenRouterAuth = deps.checkOpenRouterAuth || checkOpenRouterAuth;
|
|
195
|
-
|
|
196
|
-
function currentLlmProvider() {
|
|
197
|
-
return isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
|
|
198
|
-
}
|
|
199
194
|
|
|
200
195
|
function formatAgentJobError(err) {
|
|
201
|
-
|
|
202
|
-
if (currentLlmProvider() === 'openrouter') {
|
|
203
|
-
message = enhanceOpenRouterAgentError(message, config.model);
|
|
204
|
-
}
|
|
205
|
-
return message;
|
|
196
|
+
return err instanceof Error ? err.message : String(err);
|
|
206
197
|
}
|
|
207
198
|
|
|
208
199
|
/**
|
|
@@ -242,35 +233,14 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
242
233
|
};
|
|
243
234
|
}
|
|
244
235
|
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
code: 'openrouter_auth_required',
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
if (!isOpenRouterAgentCompatibleModel(model)) {
|
|
257
|
-
return {
|
|
258
|
-
status: 400,
|
|
259
|
-
error:
|
|
260
|
-
`Model "${model}" is not supported for OpenRouter agent runs. acdev uses the Claude Agent SDK (Anthropic-compatible API). Choose an anthropic/* model such as anthropic/claude-sonnet-4.`,
|
|
261
|
-
code: 'openrouter_model_incompatible',
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
const claude = doCheckClaudeAuth();
|
|
266
|
-
if (!claude.ok) {
|
|
267
|
-
return {
|
|
268
|
-
status: 400,
|
|
269
|
-
error:
|
|
270
|
-
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
271
|
-
code: 'claude_auth_required',
|
|
272
|
-
};
|
|
273
|
-
}
|
|
236
|
+
const claude = doCheckClaudeAuth();
|
|
237
|
+
if (!claude.ok) {
|
|
238
|
+
return {
|
|
239
|
+
status: 400,
|
|
240
|
+
error:
|
|
241
|
+
'Claude is not authenticated. Add an API key or OAuth token in Settings → Authentication, run claude auth login, or start with --stub-agent.',
|
|
242
|
+
code: 'claude_auth_required',
|
|
243
|
+
};
|
|
274
244
|
}
|
|
275
245
|
}
|
|
276
246
|
|
|
@@ -283,7 +253,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
283
253
|
stubAgent: useStubAgent,
|
|
284
254
|
ghAuth: doCheckGhAuth(),
|
|
285
255
|
claudeAuth: doCheckClaudeAuth(),
|
|
286
|
-
openrouterAuth: doCheckOpenRouterAuth(),
|
|
287
256
|
});
|
|
288
257
|
}
|
|
289
258
|
|
|
@@ -374,7 +343,34 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
374
343
|
issueType = detectIssueType(issueDetails);
|
|
375
344
|
}
|
|
376
345
|
|
|
377
|
-
const desiredBranchName =
|
|
346
|
+
const desiredBranchName = resolveDesiredBranchName(
|
|
347
|
+
job.preferredBranchName,
|
|
348
|
+
issueType,
|
|
349
|
+
issueTitle
|
|
350
|
+
);
|
|
351
|
+
// #region agent log
|
|
352
|
+
try {
|
|
353
|
+
fs.appendFileSync(
|
|
354
|
+
'/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
|
|
355
|
+
`${JSON.stringify({
|
|
356
|
+
sessionId: '473a78',
|
|
357
|
+
runId: 'pre-fix',
|
|
358
|
+
hypothesisId: 'C',
|
|
359
|
+
location: 'src/server.js:resolveDesiredBranchName',
|
|
360
|
+
message: 'resolved worktree branch name',
|
|
361
|
+
data: {
|
|
362
|
+
preferred: job.preferredBranchName ?? null,
|
|
363
|
+
issueType,
|
|
364
|
+
usedCustom: Boolean(job.preferredBranchName),
|
|
365
|
+
desiredBranchName,
|
|
366
|
+
},
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
})}\n`
|
|
369
|
+
);
|
|
370
|
+
} catch {
|
|
371
|
+
// ignore debug log failures
|
|
372
|
+
}
|
|
373
|
+
// #endregion
|
|
378
374
|
const worktreeId = worktreeIdForJob(job);
|
|
379
375
|
if (worktreeId == null) {
|
|
380
376
|
throw new Error('Job is missing issueNumber / jiraKey for worktree path');
|
|
@@ -603,11 +599,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
603
599
|
req.query.refresh === '1' ||
|
|
604
600
|
req.query.refresh === 'true' ||
|
|
605
601
|
req.query.force === '1';
|
|
606
|
-
const
|
|
607
|
-
req.query.provider === 'openrouter' || req.query.provider === 'claude'
|
|
608
|
-
? req.query.provider
|
|
609
|
-
: currentLlmProvider();
|
|
610
|
-
const result = await listModels({ selected: config.model, force, provider });
|
|
602
|
+
const result = await listModels({ selected: config.model, force });
|
|
611
603
|
res.json(result);
|
|
612
604
|
} catch (err) {
|
|
613
605
|
res.status(500).json({ error: err.message });
|
|
@@ -631,7 +623,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
631
623
|
applySecretField(envPatch, 'GH_TOKEN', patch.ghToken);
|
|
632
624
|
applySecretField(envPatch, 'ANTHROPIC_API_KEY', patch.anthropicApiKey);
|
|
633
625
|
applySecretField(envPatch, 'CLAUDE_CODE_OAUTH_TOKEN', patch.claudeOauthToken);
|
|
634
|
-
applySecretField(envPatch, 'OPENROUTER_API_KEY', patch.openrouterApiKey);
|
|
635
626
|
if (patch.jiraBaseUrl !== undefined && typeof patch.jiraBaseUrl === 'string') {
|
|
636
627
|
// Also mirror base URL into env for convenience when set via Settings
|
|
637
628
|
const trimmed = patch.jiraBaseUrl.trim();
|
|
@@ -649,7 +640,6 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
649
640
|
ghToken: _gh,
|
|
650
641
|
anthropicApiKey: _ak,
|
|
651
642
|
claudeOauthToken: _oa,
|
|
652
|
-
openrouterApiKey: _or,
|
|
653
643
|
...configPatch
|
|
654
644
|
} = patch;
|
|
655
645
|
updateConfig(repoRoot, config, configPatch);
|
|
@@ -696,6 +686,35 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
696
686
|
});
|
|
697
687
|
}
|
|
698
688
|
|
|
689
|
+
const preferredParsed = parsePreferredBranchName(req.body?.branchName);
|
|
690
|
+
if (!preferredParsed.ok) {
|
|
691
|
+
return res.status(400).json({ error: preferredParsed.error });
|
|
692
|
+
}
|
|
693
|
+
const preferredBranchName = preferredParsed.value;
|
|
694
|
+
// #region agent log
|
|
695
|
+
try {
|
|
696
|
+
fs.appendFileSync(
|
|
697
|
+
'/Users/giancarlogarcia/Documents/Personal/Projects-2026/agent-mcp/.cursor/debug-473a78.log',
|
|
698
|
+
`${JSON.stringify({
|
|
699
|
+
sessionId: '473a78',
|
|
700
|
+
runId: 'pre-fix',
|
|
701
|
+
hypothesisId: 'A',
|
|
702
|
+
location: 'src/server.js:POST /api/issues',
|
|
703
|
+
message: 'enqueue preferred branch parse',
|
|
704
|
+
data: {
|
|
705
|
+
rawProvided: req.body?.branchName != null,
|
|
706
|
+
rawType: typeof req.body?.branchName,
|
|
707
|
+
preferredBranchName: preferredBranchName ?? null,
|
|
708
|
+
urlCount: urls.length,
|
|
709
|
+
},
|
|
710
|
+
timestamp: Date.now(),
|
|
711
|
+
})}\n`
|
|
712
|
+
);
|
|
713
|
+
} catch {
|
|
714
|
+
// ignore debug log failures
|
|
715
|
+
}
|
|
716
|
+
// #endregion
|
|
717
|
+
|
|
699
718
|
const ticketSource =
|
|
700
719
|
req.body?.ticketSource === 'jira' || req.body?.ticketSource === 'github'
|
|
701
720
|
? req.body.ticketSource
|
|
@@ -790,6 +809,7 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
790
809
|
issueNumber: item.number,
|
|
791
810
|
ticketSource: item.ticketSource,
|
|
792
811
|
jiraKey: item.jiraKey,
|
|
812
|
+
...(preferredBranchName ? { preferredBranchName } : {}),
|
|
793
813
|
});
|
|
794
814
|
created.push(job);
|
|
795
815
|
}
|
package/src/store.js
CHANGED
|
@@ -55,6 +55,7 @@ export class Store {
|
|
|
55
55
|
* issueNumber?: number,
|
|
56
56
|
* ticketSource?: 'github' | 'jira',
|
|
57
57
|
* jiraKey?: string,
|
|
58
|
+
* preferredBranchName?: string,
|
|
58
59
|
* }} data
|
|
59
60
|
* @returns {Job}
|
|
60
61
|
*/
|
|
@@ -68,6 +69,7 @@ export class Store {
|
|
|
68
69
|
issueNumber: data.issueNumber,
|
|
69
70
|
ticketSource,
|
|
70
71
|
...(data.jiraKey ? { jiraKey: data.jiraKey } : {}),
|
|
72
|
+
...(data.preferredBranchName ? { preferredBranchName: data.preferredBranchName } : {}),
|
|
71
73
|
status: 'queued',
|
|
72
74
|
createdAt: now,
|
|
73
75
|
updatedAt: now,
|
package/src/openrouter-auth.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** @typedef {{ ok: true } | { ok: false, reason: 'missing' }} OpenRouterAuthResult */
|
|
2
|
-
|
|
3
|
-
/** @type {() => NodeJS.ProcessEnv} */
|
|
4
|
-
let envResolver = () => process.env;
|
|
5
|
-
|
|
6
|
-
/** @param {() => NodeJS.ProcessEnv} fn */
|
|
7
|
-
export function _setEnvResolver(fn) {
|
|
8
|
-
envResolver = fn;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function _resetEnvResolver() {
|
|
12
|
-
envResolver = () => process.env;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Whether OpenRouter can authenticate agent runs.
|
|
17
|
-
* @returns {OpenRouterAuthResult}
|
|
18
|
-
*/
|
|
19
|
-
export function checkOpenRouterAuth() {
|
|
20
|
-
const key = (envResolver().OPENROUTER_API_KEY || '').trim();
|
|
21
|
-
if (key) return { ok: true };
|
|
22
|
-
return { ok: false, reason: 'missing' };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Human-readable startup warning for a failed {@link checkOpenRouterAuth}.
|
|
27
|
-
* Soft-auth: server still starts so Settings can configure the key.
|
|
28
|
-
* @param {OpenRouterAuthResult} [_result]
|
|
29
|
-
*/
|
|
30
|
-
export function formatOpenRouterAuthError(_result) {
|
|
31
|
-
return [
|
|
32
|
-
'⚠ OpenRouter is not authenticated — server will still start.',
|
|
33
|
-
' Open Settings → Authentication to add your OpenRouter API key',
|
|
34
|
-
' (or set OPENROUTER_API_KEY in `.acdev/.env`).',
|
|
35
|
-
' For UI-only testing without auth: pass `--stub-agent`.',
|
|
36
|
-
].join('\n');
|
|
37
|
-
}
|