acdev 1.0.1 → 1.0.3
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 +8 -2
- package/README.md +59 -21
- package/bin/acdev.js +4 -24
- package/package.json +1 -1
- package/public/app.js +551 -129
- package/public/index.html +195 -63
- package/public/styles.css +81 -21
- package/src/afterPrRules.js +63 -8
- package/src/claude-auth.js +30 -0
- package/src/config.js +128 -88
- package/src/env.js +1 -1
- package/src/gh-auth.js +123 -6
- package/src/jira.js +171 -0
- package/src/models.js +366 -0
- package/src/server.js +44 -11
package/src/models.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Claude model options for the Settings UI.
|
|
3
|
+
*
|
|
4
|
+
* Prefer Anthropic Models API (`GET /v1/models`) when an API key, auth token,
|
|
5
|
+
* or Claude Code OAuth credential is available. Claude Agent SDK and Claude CLI
|
|
6
|
+
* do not expose a list endpoint. Subscription-only login without a readable
|
|
7
|
+
* credential falls back to a curated Claude Code / Agent SDK list.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Curated Claude Agent SDK / Claude Code model ids used as defaults + fallback.
|
|
17
|
+
* Prefer documented Code aliases and Anthropic API ids (not invented snapshots).
|
|
18
|
+
*/
|
|
19
|
+
export const MODEL_OPTIONS = [
|
|
20
|
+
{ id: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
21
|
+
{ id: 'claude-opus-5', label: 'Opus 5' },
|
|
22
|
+
{ id: 'claude-fable-5', label: 'Fable 5' },
|
|
23
|
+
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5' },
|
|
24
|
+
{ id: 'sonnet', label: 'sonnet (latest)' },
|
|
25
|
+
{ id: 'opus', label: 'opus (latest)' },
|
|
26
|
+
{ id: 'haiku', label: 'haiku (latest)' },
|
|
27
|
+
{ id: 'fable', label: 'fable (latest)' },
|
|
28
|
+
{ id: 'best', label: 'best' },
|
|
29
|
+
{ id: 'opusplan', label: 'opusplan' },
|
|
30
|
+
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6' },
|
|
31
|
+
{ id: 'claude-opus-4-8', label: 'Opus 4.8' },
|
|
32
|
+
{ id: 'claude-opus-4-7', label: 'Opus 4.7' },
|
|
33
|
+
{ id: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
34
|
+
{ id: 'claude-sonnet-4-5', label: 'Sonnet 4.5' },
|
|
35
|
+
{ id: 'claude-opus-4-5', label: 'Opus 4.5' },
|
|
36
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5 (20251001)' },
|
|
37
|
+
{ id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5 (20250929)' },
|
|
38
|
+
{ id: 'claude-opus-4-5-20251101', label: 'Opus 4.5 (20251101)' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_MODEL = 'claude-sonnet-5';
|
|
42
|
+
|
|
43
|
+
/** Loose model id shape accepted by config (API ids + Claude Code aliases). */
|
|
44
|
+
export const MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
|
|
45
|
+
|
|
46
|
+
const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models';
|
|
47
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
48
|
+
const CACHE_TTL_MS = 5 * 60_000;
|
|
49
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
50
|
+
|
|
51
|
+
/** @typedef {{ id: string, name?: string, label?: string }} ModelOption */
|
|
52
|
+
/** @typedef {'anthropic' | 'fallback'} ModelsSource */
|
|
53
|
+
/** @typedef {{ models: ModelOption[], selected: string, source: ModelsSource }} ModelsListResult */
|
|
54
|
+
|
|
55
|
+
/** @type {{ expiresAt: number, result: Omit<ModelsListResult, 'selected'> } | null} */
|
|
56
|
+
let cache = null;
|
|
57
|
+
|
|
58
|
+
/** @type {typeof fetch | null} */
|
|
59
|
+
let fetchImpl = null;
|
|
60
|
+
|
|
61
|
+
/** @type {() => NodeJS.ProcessEnv} */
|
|
62
|
+
let envResolver = () => process.env;
|
|
63
|
+
|
|
64
|
+
/** @type {() => string | null} */
|
|
65
|
+
let credentialsTokenResolver = defaultCredentialsTokenResolver;
|
|
66
|
+
|
|
67
|
+
/** @param {typeof fetch} fn */
|
|
68
|
+
export function _setFetchImpl(fn) {
|
|
69
|
+
fetchImpl = fn;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function _resetFetchImpl() {
|
|
73
|
+
fetchImpl = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {() => NodeJS.ProcessEnv} fn */
|
|
77
|
+
export function _setEnvResolver(fn) {
|
|
78
|
+
envResolver = fn;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function _resetEnvResolver() {
|
|
82
|
+
envResolver = () => process.env;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {() => string | null} fn */
|
|
86
|
+
export function _setCredentialsTokenResolver(fn) {
|
|
87
|
+
credentialsTokenResolver = fn;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function _resetCredentialsTokenResolver() {
|
|
91
|
+
credentialsTokenResolver = defaultCredentialsTokenResolver;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function _resetModelsCache() {
|
|
95
|
+
cache = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} value
|
|
100
|
+
* @returns {value is string}
|
|
101
|
+
*/
|
|
102
|
+
export function isValidModelId(value) {
|
|
103
|
+
return typeof value === 'string' && MODEL_ID_RE.test(value.trim());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} id
|
|
108
|
+
* @param {string} [displayName]
|
|
109
|
+
* @returns {ModelOption}
|
|
110
|
+
*/
|
|
111
|
+
function toOption(id, displayName) {
|
|
112
|
+
const name = (displayName || '').trim();
|
|
113
|
+
return name ? { id, name, label: name } : { id, label: id };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Merge curated options (stable order / aliases) with live Anthropic rows.
|
|
118
|
+
* Same ids keep curated position but prefer live display names.
|
|
119
|
+
* @param {ModelOption[]} curated
|
|
120
|
+
* @param {ModelOption[]} live
|
|
121
|
+
* @returns {ModelOption[]}
|
|
122
|
+
*/
|
|
123
|
+
function mergeModelLists(curated, live) {
|
|
124
|
+
const liveById = new Map();
|
|
125
|
+
for (const m of live) {
|
|
126
|
+
if (m?.id) liveById.set(m.id, m);
|
|
127
|
+
}
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
/** @type {ModelOption[]} */
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const m of curated) {
|
|
132
|
+
if (!m?.id || seen.has(m.id)) continue;
|
|
133
|
+
seen.add(m.id);
|
|
134
|
+
const liveHit = liveById.get(m.id);
|
|
135
|
+
if (liveHit) {
|
|
136
|
+
const label = liveHit.label || liveHit.name || m.label || m.id;
|
|
137
|
+
const name = liveHit.name || liveHit.label || m.name;
|
|
138
|
+
out.push(name ? { id: m.id, name, label } : { id: m.id, label });
|
|
139
|
+
} else {
|
|
140
|
+
out.push({ ...m });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const m of live) {
|
|
144
|
+
if (!m?.id || seen.has(m.id)) continue;
|
|
145
|
+
seen.add(m.id);
|
|
146
|
+
out.push({ ...m });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {ModelOption[]} models
|
|
153
|
+
* @param {string} selected
|
|
154
|
+
* @returns {ModelOption[]}
|
|
155
|
+
*/
|
|
156
|
+
function ensureSelected(models, selected) {
|
|
157
|
+
if (!selected || models.some((m) => m.id === selected)) return models;
|
|
158
|
+
return [toOption(selected), ...models];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract Claude Code OAuth access token from a credentials JSON blob.
|
|
163
|
+
* @param {string} raw
|
|
164
|
+
* @returns {string | null}
|
|
165
|
+
*/
|
|
166
|
+
export function parseClaudeCodeOauthAccessToken(raw) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(String(raw || '').trim());
|
|
169
|
+
const tok = parsed?.claudeAiOauth?.accessToken;
|
|
170
|
+
if (typeof tok === 'string' && tok.trim()) return tok.trim();
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read plaintext Claude Code credentials file (`~/.claude/.credentials.json`
|
|
179
|
+
* or `$CLAUDE_CONFIG_DIR/.credentials.json`).
|
|
180
|
+
* @returns {string | null}
|
|
181
|
+
*/
|
|
182
|
+
function readClaudeCodeCredentialsFile() {
|
|
183
|
+
const env = envResolver();
|
|
184
|
+
/** @type {string[]} */
|
|
185
|
+
const dirs = [];
|
|
186
|
+
const configDir = (env.CLAUDE_CONFIG_DIR || '').trim();
|
|
187
|
+
if (configDir) dirs.push(configDir);
|
|
188
|
+
dirs.push(join(homedir(), '.claude'));
|
|
189
|
+
|
|
190
|
+
for (const dir of dirs) {
|
|
191
|
+
const path = join(dir, '.credentials.json');
|
|
192
|
+
try {
|
|
193
|
+
if (!existsSync(path)) continue;
|
|
194
|
+
const tok = parseClaudeCodeOauthAccessToken(readFileSync(path, 'utf8'));
|
|
195
|
+
if (tok) return tok;
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore unreadable paths
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read Claude Code OAuth token from macOS Keychain (browser `claude auth login`).
|
|
205
|
+
* @returns {string | null}
|
|
206
|
+
*/
|
|
207
|
+
function readClaudeCodeKeychainToken() {
|
|
208
|
+
if (process.platform !== 'darwin') return null;
|
|
209
|
+
try {
|
|
210
|
+
const raw = execFileSync(
|
|
211
|
+
'security',
|
|
212
|
+
['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'],
|
|
213
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
|
|
214
|
+
);
|
|
215
|
+
return parseClaudeCodeOauthAccessToken(raw);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Claude Code subscription OAuth token from local credential stores.
|
|
223
|
+
* @returns {string | null}
|
|
224
|
+
*/
|
|
225
|
+
function defaultCredentialsTokenResolver() {
|
|
226
|
+
return readClaudeCodeCredentialsFile() || readClaudeCodeKeychainToken();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build auth headers for Anthropic Models API from env or Claude Code login.
|
|
231
|
+
* @returns {{ headers: Record<string, string>, auth: string } | null}
|
|
232
|
+
*/
|
|
233
|
+
function resolveAnthropicAuth() {
|
|
234
|
+
const env = envResolver();
|
|
235
|
+
const apiKey = (env.ANTHROPIC_API_KEY || '').trim();
|
|
236
|
+
if (apiKey) {
|
|
237
|
+
return {
|
|
238
|
+
auth: 'api-key',
|
|
239
|
+
headers: {
|
|
240
|
+
'x-api-key': apiKey,
|
|
241
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const bearer = (
|
|
246
|
+
env.ANTHROPIC_AUTH_TOKEN ||
|
|
247
|
+
env.CLAUDE_CODE_OAUTH_TOKEN ||
|
|
248
|
+
''
|
|
249
|
+
).trim();
|
|
250
|
+
if (bearer) {
|
|
251
|
+
return {
|
|
252
|
+
auth: 'bearer',
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: `Bearer ${bearer}`,
|
|
255
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const stored = (credentialsTokenResolver() || '').trim();
|
|
261
|
+
if (stored) {
|
|
262
|
+
return {
|
|
263
|
+
auth: 'claude-code-login',
|
|
264
|
+
headers: {
|
|
265
|
+
Authorization: `Bearer ${stored}`,
|
|
266
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @param {Response} res
|
|
275
|
+
* @returns {Promise<ModelOption[]>}
|
|
276
|
+
*/
|
|
277
|
+
async function parseModelsResponse(res) {
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
throw new Error(`Anthropic Models API HTTP ${res.status}`);
|
|
280
|
+
}
|
|
281
|
+
const body = await res.json();
|
|
282
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
283
|
+
/** @type {ModelOption[]} */
|
|
284
|
+
const models = [];
|
|
285
|
+
for (const row of rows) {
|
|
286
|
+
const id = typeof row?.id === 'string' ? row.id.trim() : '';
|
|
287
|
+
if (!isValidModelId(id)) continue;
|
|
288
|
+
const display =
|
|
289
|
+
typeof row.display_name === 'string'
|
|
290
|
+
? row.display_name
|
|
291
|
+
: typeof row.name === 'string'
|
|
292
|
+
? row.name
|
|
293
|
+
: '';
|
|
294
|
+
models.push(toOption(id, display));
|
|
295
|
+
}
|
|
296
|
+
if (models.length === 0) {
|
|
297
|
+
throw new Error('Anthropic Models API returned no models');
|
|
298
|
+
}
|
|
299
|
+
return models;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Fetch live models from Anthropic (no cache).
|
|
304
|
+
* @returns {Promise<ModelOption[]>}
|
|
305
|
+
*/
|
|
306
|
+
export async function fetchAnthropicModels() {
|
|
307
|
+
const auth = resolveAnthropicAuth();
|
|
308
|
+
if (!auth) {
|
|
309
|
+
throw new Error('No Anthropic API credentials for models list');
|
|
310
|
+
}
|
|
311
|
+
const doFetch = fetchImpl || globalThis.fetch;
|
|
312
|
+
if (typeof doFetch !== 'function') {
|
|
313
|
+
throw new Error('fetch is not available');
|
|
314
|
+
}
|
|
315
|
+
const res = await doFetch(ANTHROPIC_MODELS_URL, {
|
|
316
|
+
method: 'GET',
|
|
317
|
+
headers: auth.headers,
|
|
318
|
+
});
|
|
319
|
+
return parseModelsResponse(res);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* List models for the UI.
|
|
324
|
+
* @param {{ selected?: string, force?: boolean }} [opts]
|
|
325
|
+
* @returns {Promise<ModelsListResult>}
|
|
326
|
+
*/
|
|
327
|
+
export async function listModels(opts = {}) {
|
|
328
|
+
const selectedRaw = opts.selected;
|
|
329
|
+
const selected = isValidModelId(selectedRaw)
|
|
330
|
+
? String(selectedRaw).trim()
|
|
331
|
+
: DEFAULT_MODEL;
|
|
332
|
+
const force = opts.force === true;
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
|
|
335
|
+
if (!force && cache && cache.expiresAt > now) {
|
|
336
|
+
return {
|
|
337
|
+
...cache.result,
|
|
338
|
+
models: ensureSelected(cache.result.models, selected),
|
|
339
|
+
selected,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const live = await fetchAnthropicModels();
|
|
345
|
+
// Keep curated Claude Code aliases available alongside API ids.
|
|
346
|
+
const models = ensureSelected(
|
|
347
|
+
mergeModelLists(
|
|
348
|
+
MODEL_OPTIONS.map((m) => ({ ...m })),
|
|
349
|
+
live
|
|
350
|
+
),
|
|
351
|
+
selected
|
|
352
|
+
);
|
|
353
|
+
const result = { models, source: /** @type {ModelsSource} */ ('anthropic') };
|
|
354
|
+
cache = { expiresAt: now + CACHE_TTL_MS, result };
|
|
355
|
+
return { ...result, selected };
|
|
356
|
+
} catch {
|
|
357
|
+
const models = ensureSelected(
|
|
358
|
+
MODEL_OPTIONS.map((m) => ({ ...m })),
|
|
359
|
+
selected
|
|
360
|
+
);
|
|
361
|
+
const result = { models, source: /** @type {ModelsSource} */ ('fallback') };
|
|
362
|
+
// Short cache on fallback so we retry Anthropic soon after auth is saved.
|
|
363
|
+
cache = { expiresAt: now + 30_000, result };
|
|
364
|
+
return { ...result, selected };
|
|
365
|
+
}
|
|
366
|
+
}
|
package/src/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from './agent.js';
|
|
34
34
|
import { publicConfig, updateConfig } from './config.js';
|
|
35
35
|
import { upsertEnvVars } from './env.js';
|
|
36
|
+
import { listModels } from './models.js';
|
|
36
37
|
import { splitIssueUrls } from './urls.js';
|
|
37
38
|
import { usageFromLogs, withJobUsage } from './usage.js';
|
|
38
39
|
|
|
@@ -76,6 +77,23 @@ function jobDedupeKey(job) {
|
|
|
76
77
|
}
|
|
77
78
|
return `github:${job.issueUrl}`;
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Empty string = leave the existing secret unchanged; null = clear.
|
|
83
|
+
* @param {Record<string, string | null>} envPatch
|
|
84
|
+
* @param {string} envKey
|
|
85
|
+
* @param {unknown} value
|
|
86
|
+
*/
|
|
87
|
+
function applySecretField(envPatch, envKey, value) {
|
|
88
|
+
if (value === undefined) return;
|
|
89
|
+
if (value === null) {
|
|
90
|
+
envPatch[envKey] = null;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'string' && value.trim()) {
|
|
94
|
+
envPatch[envKey] = value.trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
79
97
|
/**
|
|
80
98
|
* Normalize + validate POST /api/jobs/:id/review body.
|
|
81
99
|
* @param {unknown} body
|
|
@@ -146,6 +164,8 @@ export function normalizeReviewComments(body) {
|
|
|
146
164
|
* listChangedFiles?: typeof listChangedFiles,
|
|
147
165
|
* applyFileExclusions?: typeof applyFileExclusions,
|
|
148
166
|
* transitionJiraIssue?: Function,
|
|
167
|
+
* addJiraIssueLabel?: Function,
|
|
168
|
+
* closeJiraIssue?: Function,
|
|
149
169
|
* addIssueLabel?: Function,
|
|
150
170
|
* closeIssue?: Function,
|
|
151
171
|
* resolveJiraCredentials?: Function,
|
|
@@ -447,11 +467,25 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
447
467
|
}
|
|
448
468
|
});
|
|
449
469
|
|
|
470
|
+
app.get('/api/models', async (req, res) => {
|
|
471
|
+
try {
|
|
472
|
+
const force =
|
|
473
|
+
req.query.refresh === '1' ||
|
|
474
|
+
req.query.refresh === 'true' ||
|
|
475
|
+
req.query.force === '1';
|
|
476
|
+
const result = await listModels({ selected: config.model, force });
|
|
477
|
+
res.json(result);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
res.status(500).json({ error: err.message });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
450
483
|
app.patch('/api/config', (req, res) => {
|
|
451
484
|
try {
|
|
452
485
|
const patch = req.body || {};
|
|
453
486
|
|
|
454
|
-
//
|
|
487
|
+
// Secrets → .acdev/.env (never persisted in config.json).
|
|
488
|
+
// Empty string = leave unchanged; null = clear.
|
|
455
489
|
/** @type {Record<string, string | null>} */
|
|
456
490
|
const envPatch = {};
|
|
457
491
|
if (patch.jiraEmail !== undefined) {
|
|
@@ -459,16 +493,10 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
459
493
|
typeof patch.jiraEmail === 'string' ? patch.jiraEmail.trim() : '';
|
|
460
494
|
envPatch.JIRA_EMAIL = email || null;
|
|
461
495
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (patch.jiraApiToken === null) {
|
|
467
|
-
envPatch.JIRA_API_TOKEN = null;
|
|
468
|
-
} else if (token) {
|
|
469
|
-
envPatch.JIRA_API_TOKEN = token;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
496
|
+
applySecretField(envPatch, 'JIRA_API_TOKEN', patch.jiraApiToken);
|
|
497
|
+
applySecretField(envPatch, 'GH_TOKEN', patch.ghToken);
|
|
498
|
+
applySecretField(envPatch, 'ANTHROPIC_API_KEY', patch.anthropicApiKey);
|
|
499
|
+
applySecretField(envPatch, 'CLAUDE_CODE_OAUTH_TOKEN', patch.claudeOauthToken);
|
|
472
500
|
if (patch.jiraBaseUrl !== undefined && typeof patch.jiraBaseUrl === 'string') {
|
|
473
501
|
// Also mirror base URL into env for convenience when set via Settings
|
|
474
502
|
const trimmed = patch.jiraBaseUrl.trim();
|
|
@@ -483,6 +511,9 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
483
511
|
const {
|
|
484
512
|
jiraEmail: _e,
|
|
485
513
|
jiraApiToken: _t,
|
|
514
|
+
ghToken: _gh,
|
|
515
|
+
anthropicApiKey: _ak,
|
|
516
|
+
claudeOauthToken: _oa,
|
|
486
517
|
...configPatch
|
|
487
518
|
} = patch;
|
|
488
519
|
updateConfig(repoRoot, config, configPatch);
|
|
@@ -814,6 +845,8 @@ export function createServer({ repoRoot, config, store, useStubAgent = false, de
|
|
|
814
845
|
appendLog: (j, type, payload) => appendLog(j, type, payload),
|
|
815
846
|
deps: {
|
|
816
847
|
transitionJiraIssue: deps.transitionJiraIssue,
|
|
848
|
+
addJiraIssueLabel: deps.addJiraIssueLabel,
|
|
849
|
+
closeJiraIssue: deps.closeJiraIssue,
|
|
817
850
|
addIssueLabel: deps.addIssueLabel,
|
|
818
851
|
closeIssue: deps.closeIssue,
|
|
819
852
|
resolveJiraCredentials: deps.resolveJiraCredentials,
|