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/public/index.html
CHANGED
|
@@ -138,6 +138,24 @@
|
|
|
138
138
|
or a browse link. Press <kbd class="enqueue-kbd">Enter</kbd> to add.
|
|
139
139
|
</p>
|
|
140
140
|
|
|
141
|
+
<div class="enqueue-branch-field">
|
|
142
|
+
<div class="enqueue-branch-label-row">
|
|
143
|
+
<label class="field-label" for="preferred-branch">Branch name</label>
|
|
144
|
+
<span class="enqueue-branch-optional">Optional</span>
|
|
145
|
+
</div>
|
|
146
|
+
<input
|
|
147
|
+
id="preferred-branch"
|
|
148
|
+
class="input enqueue-branch-input"
|
|
149
|
+
type="text"
|
|
150
|
+
spellcheck="false"
|
|
151
|
+
autocomplete="off"
|
|
152
|
+
placeholder="your-branch-name"
|
|
153
|
+
>
|
|
154
|
+
<p class="enqueue-input-hint">
|
|
155
|
+
Leave empty to keep feat/fix + ticket title. If set, that name is used instead.
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
141
159
|
<div class="enqueue-info-banner" id="enqueue-info-banner" role="note">
|
|
142
160
|
<svg class="enqueue-info-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
143
161
|
<circle cx="12" cy="12" r="10"/>
|
|
@@ -155,10 +173,6 @@
|
|
|
155
173
|
|
|
156
174
|
<div class="enqueue-actions">
|
|
157
175
|
<div class="agent-picker" id="overview-agent-picker" aria-label="Agent">
|
|
158
|
-
<select id="overview-llm-provider" class="sr-only" aria-hidden="true" tabindex="-1">
|
|
159
|
-
<option value="claude">Claude</option>
|
|
160
|
-
<option value="openrouter">OpenRouter</option>
|
|
161
|
-
</select>
|
|
162
176
|
<input type="hidden" id="overview-model" value="">
|
|
163
177
|
<button
|
|
164
178
|
type="button"
|
|
@@ -171,15 +185,11 @@
|
|
|
171
185
|
<span class="agent-pill-icon agent-pill-icon--claude" id="overview-agent-icon" aria-hidden="true"></span>
|
|
172
186
|
<span class="agent-pill-body">
|
|
173
187
|
<span class="agent-pill-label">Agent</span>
|
|
174
|
-
<span class="agent-pill-value" id="overview-agent-value"
|
|
188
|
+
<span class="agent-pill-value" id="overview-agent-value">—</span>
|
|
175
189
|
</span>
|
|
176
190
|
<svg class="agent-pill-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
177
191
|
</button>
|
|
178
192
|
<div class="agent-picker-panel" id="overview-agent-panel" role="dialog" aria-label="Choose agent model" aria-hidden="true">
|
|
179
|
-
<div class="agent-picker-tabs" role="tablist" aria-label="Provider">
|
|
180
|
-
<button type="button" class="agent-picker-tab is-active" data-provider="claude" role="tab" aria-selected="true">Claude</button>
|
|
181
|
-
<button type="button" class="agent-picker-tab" data-provider="openrouter" role="tab" aria-selected="false">OpenRouter</button>
|
|
182
|
-
</div>
|
|
183
193
|
<label class="sr-only" for="overview-agent-search">Search models</label>
|
|
184
194
|
<input
|
|
185
195
|
id="overview-agent-search"
|
|
@@ -538,21 +548,6 @@
|
|
|
538
548
|
</div>
|
|
539
549
|
</div>
|
|
540
550
|
</div>
|
|
541
|
-
|
|
542
|
-
<div class="card settings-card">
|
|
543
|
-
<div class="rules-heading">OpenRouter authentication</div>
|
|
544
|
-
<p id="settings-openrouter-status" class="auth-status" role="status">Checking OpenRouter auth…</p>
|
|
545
|
-
<div class="settings-grid">
|
|
546
|
-
<div class="settings-field settings-field-full">
|
|
547
|
-
<label class="field-label" for="settings-openrouter-key">OpenRouter API key</label>
|
|
548
|
-
<input id="settings-openrouter-key" class="input" type="password" name="openrouterApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
|
|
549
|
-
<button type="button" class="auth-clear hidden" id="settings-openrouter-key-clear">Clear saved API key</button>
|
|
550
|
-
<p class="field-hint" id="settings-openrouter-key-hint">
|
|
551
|
-
Required when LLM provider is OpenRouter. Get a key at <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">openrouter.ai/keys</a>. Stored as <code>OPENROUTER_API_KEY</code> in <code>.acdev/.env</code>.
|
|
552
|
-
</p>
|
|
553
|
-
</div>
|
|
554
|
-
</div>
|
|
555
|
-
</div>
|
|
556
551
|
</div>
|
|
557
552
|
</div>
|
|
558
553
|
|
|
@@ -649,15 +644,6 @@
|
|
|
649
644
|
</p>
|
|
650
645
|
<div class="card settings-card">
|
|
651
646
|
<div class="settings-grid">
|
|
652
|
-
<div class="settings-field">
|
|
653
|
-
<label class="field-label" for="settings-llm-provider">LLM provider</label>
|
|
654
|
-
<select id="settings-llm-provider" class="input" name="llmProvider">
|
|
655
|
-
<option value="claude">Claude (Anthropic / Claude Code)</option>
|
|
656
|
-
<option value="openrouter">OpenRouter</option>
|
|
657
|
-
</select>
|
|
658
|
-
<p class="field-hint" id="settings-llm-provider-hint">Agent runs use the selected provider and model below.</p>
|
|
659
|
-
</div>
|
|
660
|
-
|
|
661
647
|
<div class="settings-field">
|
|
662
648
|
<label class="field-label" for="settings-base-branch">Base branch</label>
|
|
663
649
|
<input id="settings-base-branch" class="input" type="text" name="baseBranch" autocomplete="off" required>
|
package/public/styles.css
CHANGED
|
@@ -459,6 +459,38 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
459
459
|
color: var(--text-muted);
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
.enqueue-branch-field {
|
|
463
|
+
display: flex;
|
|
464
|
+
flex-direction: column;
|
|
465
|
+
gap: 6px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.enqueue-branch-field .field-label {
|
|
469
|
+
margin-bottom: 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.enqueue-branch-field .enqueue-input-hint {
|
|
473
|
+
margin: 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.enqueue-branch-label-row {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: baseline;
|
|
479
|
+
justify-content: space-between;
|
|
480
|
+
gap: 8px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.enqueue-branch-optional {
|
|
484
|
+
font-size: 11px;
|
|
485
|
+
font-weight: 500;
|
|
486
|
+
color: var(--text-muted);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.enqueue-branch-input {
|
|
490
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
491
|
+
font-size: 13px;
|
|
492
|
+
}
|
|
493
|
+
|
|
462
494
|
.enqueue-kbd {
|
|
463
495
|
display: inline-block;
|
|
464
496
|
padding: 1px 6px;
|
|
@@ -556,10 +588,6 @@ a { color: var(--primary); text-underline-offset: 3px; }
|
|
|
556
588
|
background: linear-gradient(145deg, #e8a87c 0%, #d97706 100%);
|
|
557
589
|
}
|
|
558
590
|
|
|
559
|
-
.agent-pill-icon--openrouter {
|
|
560
|
-
background: linear-gradient(145deg, #7c8cf5 0%, #5b4fe8 100%);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
591
|
.agent-pill-body {
|
|
564
592
|
flex: 1;
|
|
565
593
|
min-width: 0;
|
|
@@ -2298,13 +2326,6 @@ body.diff-fs-open {
|
|
|
2298
2326
|
cursor: pointer;
|
|
2299
2327
|
}
|
|
2300
2328
|
|
|
2301
|
-
.model-picker-option-title-row {
|
|
2302
|
-
display: flex;
|
|
2303
|
-
align-items: center;
|
|
2304
|
-
justify-content: space-between;
|
|
2305
|
-
gap: 8px;
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
2329
|
.model-picker-option-title {
|
|
2309
2330
|
min-width: 0;
|
|
2310
2331
|
overflow: hidden;
|
|
@@ -2312,26 +2333,6 @@ body.diff-fs-open {
|
|
|
2312
2333
|
white-space: nowrap;
|
|
2313
2334
|
}
|
|
2314
2335
|
|
|
2315
|
-
.model-picker-badge {
|
|
2316
|
-
flex-shrink: 0;
|
|
2317
|
-
padding: 1px 6px;
|
|
2318
|
-
border-radius: 999px;
|
|
2319
|
-
font-size: 10px;
|
|
2320
|
-
font-weight: 600;
|
|
2321
|
-
letter-spacing: 0.02em;
|
|
2322
|
-
text-transform: uppercase;
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
.model-picker-badge--agent {
|
|
2326
|
-
color: var(--primary);
|
|
2327
|
-
background: color-mix(in srgb, var(--primary) 14%, transparent);
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
.model-picker-badge--reference {
|
|
2331
|
-
color: var(--text-muted);
|
|
2332
|
-
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
2336
|
.model-picker-option:hover,
|
|
2336
2337
|
.model-picker-option.is-active {
|
|
2337
2338
|
background: var(--surface-2);
|
package/src/agent.js
CHANGED
|
@@ -2,7 +2,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { ensureNoAiAttributionSettings, getIssueTitle } from './git.js';
|
|
5
|
-
import { isValidLlmProvider } from './models.js';
|
|
6
5
|
import { extractUsageFromResult } from './usage.js';
|
|
7
6
|
|
|
8
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -397,66 +396,6 @@ async function fetchIssueTitle(issueUrl, worktreePath) {
|
|
|
397
396
|
*/
|
|
398
397
|
export const DISALLOWED_AGENT_TOOLS = ['Task', 'TaskOutput', 'AskUserQuestion'];
|
|
399
398
|
|
|
400
|
-
const OPENROUTER_ANTHROPIC_BASE_URL = 'https://openrouter.ai/api';
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Run an agent query with provider-specific Anthropic env overrides.
|
|
404
|
-
* OpenRouter uses Claude Agent SDK via Anthropic-compatible "skin" routing.
|
|
405
|
-
* @template T
|
|
406
|
-
* @param {object} config
|
|
407
|
-
* @param {() => Promise<T>} fn
|
|
408
|
-
* @returns {Promise<T>}
|
|
409
|
-
*/
|
|
410
|
-
export async function withLlmProviderEnv(config, fn) {
|
|
411
|
-
const provider = isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
|
|
412
|
-
if (provider !== 'openrouter') {
|
|
413
|
-
return fn();
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const apiKey = (process.env.OPENROUTER_API_KEY || '').trim();
|
|
417
|
-
if (!apiKey) {
|
|
418
|
-
throw new Error(
|
|
419
|
-
'OpenRouter is not authenticated. Add OPENROUTER_API_KEY in Settings → Authentication.'
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** @type {Record<string, string | undefined>} */
|
|
424
|
-
const saved = {
|
|
425
|
-
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
|
|
426
|
-
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
|
|
427
|
-
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
428
|
-
HTTP_REFERER: process.env.HTTP_REFERER,
|
|
429
|
-
X_TITLE: process.env.X_TITLE,
|
|
430
|
-
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS,
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
process.env.ANTHROPIC_BASE_URL = OPENROUTER_ANTHROPIC_BASE_URL;
|
|
434
|
-
process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
435
|
-
process.env.ANTHROPIC_API_KEY = '';
|
|
436
|
-
// OpenRouter rejects Anthropic-only beta headers on some models.
|
|
437
|
-
if (!process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) {
|
|
438
|
-
process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '1';
|
|
439
|
-
}
|
|
440
|
-
if (!process.env.HTTP_REFERER) {
|
|
441
|
-
process.env.HTTP_REFERER = 'https://github.com/acdev';
|
|
442
|
-
}
|
|
443
|
-
if (!process.env.X_TITLE) {
|
|
444
|
-
process.env.X_TITLE = 'acdev';
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
return await fn();
|
|
449
|
-
} finally {
|
|
450
|
-
for (const [key, value] of Object.entries(saved)) {
|
|
451
|
-
if (value === undefined) {
|
|
452
|
-
delete process.env[key];
|
|
453
|
-
} else {
|
|
454
|
-
process.env[key] = value;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
399
|
/**
|
|
461
400
|
* Prefer the first successful SDK result. A known SDK bug can emit a follow-up
|
|
462
401
|
* `error_during_execution` right after success when async subagents finish.
|
|
@@ -483,78 +422,76 @@ export function mergeAgentResult(current, incoming) {
|
|
|
483
422
|
* }} params
|
|
484
423
|
*/
|
|
485
424
|
async function runAgentQuery({ prompt, worktreePath, config, onEvent, queryFn = query }) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
ensureNoAiAttributionSettings(worktreePath);
|
|
495
|
-
|
|
496
|
-
const runLoop = async () => {
|
|
497
|
-
try {
|
|
498
|
-
for await (const message of queryFn({
|
|
499
|
-
// String prompt = single-turn SDK input (still uses stream-json transport).
|
|
500
|
-
prompt,
|
|
501
|
-
options: {
|
|
502
|
-
cwd: worktreePath,
|
|
503
|
-
allowedTools: config.allowedTools,
|
|
504
|
-
disallowedTools: DISALLOWED_AGENT_TOOLS,
|
|
505
|
-
permissionMode: 'acceptEdits',
|
|
506
|
-
maxTurns: config.maxAgentTurns,
|
|
507
|
-
model: config.model || 'claude-sonnet-5',
|
|
508
|
-
// Load only local settings so our empty attribution wins without
|
|
509
|
-
// pulling in unrelated user settings.
|
|
510
|
-
settingSources: ['local'],
|
|
511
|
-
abortController,
|
|
512
|
-
},
|
|
513
|
-
})) {
|
|
514
|
-
onEvent(message);
|
|
515
|
-
lastMessage = message;
|
|
516
|
-
if (message?.type === 'result') {
|
|
517
|
-
resultMessage = mergeAgentResult(resultMessage, message);
|
|
518
|
-
// First success is authoritative — stop before SDK async-task teardown
|
|
519
|
-
// can overwrite it with a spurious streaming-mode error.
|
|
520
|
-
if (message.subtype === 'success') {
|
|
521
|
-
abortController.abort();
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
} catch (err) {
|
|
527
|
-
// Aborting after success can surface as an iterator/abort error.
|
|
528
|
-
if (resultMessage?.subtype === 'success') return;
|
|
529
|
-
throw err;
|
|
530
|
-
}
|
|
531
|
-
};
|
|
425
|
+
const timeoutMs = config.agentTimeoutMs ?? 900_000;
|
|
426
|
+
let lastMessage = null;
|
|
427
|
+
let resultMessage = null;
|
|
428
|
+
const abortController = new AbortController();
|
|
429
|
+
|
|
430
|
+
// Claude Agent SDK defaults to injecting Co-Authored-By trailers into commit
|
|
431
|
+
// instructions unless attribution is cleared via Claude settings.
|
|
432
|
+
ensureNoAiAttributionSettings(worktreePath);
|
|
532
433
|
|
|
533
|
-
|
|
434
|
+
const runLoop = async () => {
|
|
534
435
|
try {
|
|
535
|
-
await
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
436
|
+
for await (const message of queryFn({
|
|
437
|
+
// String prompt = single-turn SDK input (still uses stream-json transport).
|
|
438
|
+
prompt,
|
|
439
|
+
options: {
|
|
440
|
+
cwd: worktreePath,
|
|
441
|
+
allowedTools: config.allowedTools,
|
|
442
|
+
disallowedTools: DISALLOWED_AGENT_TOOLS,
|
|
443
|
+
permissionMode: 'acceptEdits',
|
|
444
|
+
maxTurns: config.maxAgentTurns,
|
|
445
|
+
model: config.model || 'claude-sonnet-5',
|
|
446
|
+
// Load only local settings so our empty attribution wins without
|
|
447
|
+
// pulling in unrelated user settings.
|
|
448
|
+
settingSources: ['local'],
|
|
449
|
+
abortController,
|
|
450
|
+
},
|
|
451
|
+
})) {
|
|
452
|
+
onEvent(message);
|
|
453
|
+
lastMessage = message;
|
|
454
|
+
if (message?.type === 'result') {
|
|
455
|
+
resultMessage = mergeAgentResult(resultMessage, message);
|
|
456
|
+
// First success is authoritative — stop before SDK async-task teardown
|
|
457
|
+
// can overwrite it with a spurious streaming-mode error.
|
|
458
|
+
if (message.subtype === 'success') {
|
|
539
459
|
abortController.abort();
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch (err) {
|
|
465
|
+
// Aborting after success can surface as an iterator/abort error.
|
|
466
|
+
if (resultMessage?.subtype === 'success') return;
|
|
467
|
+
throw err;
|
|
546
468
|
}
|
|
469
|
+
};
|
|
547
470
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
471
|
+
let timeoutId;
|
|
472
|
+
try {
|
|
473
|
+
await Promise.race([
|
|
474
|
+
runLoop(),
|
|
475
|
+
new Promise((_, reject) => {
|
|
476
|
+
timeoutId = setTimeout(() => {
|
|
477
|
+
abortController.abort();
|
|
478
|
+
reject(new Error(`Agent timed out after ${timeoutMs}ms`));
|
|
479
|
+
}, timeoutMs);
|
|
480
|
+
}),
|
|
481
|
+
]);
|
|
482
|
+
} finally {
|
|
483
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
484
|
+
}
|
|
552
485
|
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
486
|
+
if (!resultMessage || resultMessage.subtype !== 'success') {
|
|
487
|
+
const detail = lastMessage ? JSON.stringify(lastMessage) : 'no result received';
|
|
488
|
+
throw new Error(`Agent did not complete successfully: ${detail}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const resultText = resultMessage.result ?? '';
|
|
492
|
+
const meta = extractPrMetadata(resultText);
|
|
493
|
+
const usage = extractUsageFromResult(resultMessage);
|
|
494
|
+
return { resultText, meta, usage };
|
|
558
495
|
}
|
|
559
496
|
|
|
560
497
|
function stubAgentResult(onEvent, title, body) {
|