gitdone-agent 0.8.0 → 0.8.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/index.js +135 -31
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// from ~/.gitdone-agent/config.json, so the autostart entry needs no args.
|
|
11
11
|
|
|
12
12
|
import { execSync, execFileSync, spawn } from 'node:child_process'
|
|
13
|
+
import readline from 'node:readline'
|
|
13
14
|
import {
|
|
14
15
|
existsSync, writeFileSync, readFileSync, unlinkSync, renameSync,
|
|
15
16
|
mkdirSync, copyFileSync, appendFileSync, readdirSync, rmSync, statSync,
|
|
@@ -28,7 +29,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
28
29
|
// Reported to the server on every sync so the web UI can flag outdated agents.
|
|
29
30
|
// Keep in lockstep with packages/agent/package.json "version" AND
|
|
30
31
|
// src/lib/agentVersion.ts LATEST_AGENT_VERSION.
|
|
31
|
-
const AGENT_VERSION = '0.8.
|
|
32
|
+
const AGENT_VERSION = '0.8.3'
|
|
32
33
|
|
|
33
34
|
const AGENT_DIR = join(homedir(), '.gitdone-agent')
|
|
34
35
|
const CONFIG_PATH = join(AGENT_DIR, 'config.json')
|
|
@@ -1470,6 +1471,17 @@ function aiProviderArg(raw) {
|
|
|
1470
1471
|
return raw === 'codex' ? 'codex' : 'claude'
|
|
1471
1472
|
}
|
|
1472
1473
|
|
|
1474
|
+
// Reasoning effort — how hard the model thinks per turn (gd-507). Both CLIs
|
|
1475
|
+
// take the same five levels, by different spellings (gd-510): Claude Code as
|
|
1476
|
+
// `--effort <level>`, Codex as a `model_reasoning_effort` config override.
|
|
1477
|
+
// Whitelisted rather than pattern-matched, since an unknown level makes either
|
|
1478
|
+
// CLI refuse to start. null (incl. a gitDone older than gd-507) → don't pass
|
|
1479
|
+
// it, and the machine's own setting decides.
|
|
1480
|
+
const AI_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1481
|
+
function aiEffortArg(raw) {
|
|
1482
|
+
return typeof raw === 'string' && AI_EFFORT_LEVELS.includes(raw.trim()) ? raw.trim() : null
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1473
1485
|
function cliNotFoundMessage(provider, hostname) {
|
|
1474
1486
|
return provider === 'codex'
|
|
1475
1487
|
? `Codex CLI (ChatGPT) не е намерен на този компютър (${hostname}). Инсталирай го (npm i -g @openai/codex), влез с ChatGPT акаунт през „codex login", увери се, че „codex" е в PATH, после рестартирай агента.`
|
|
@@ -1494,7 +1506,7 @@ function cliNotFoundMessage(provider, hostname) {
|
|
|
1494
1506
|
// The commit policy is NOT here: Codex has no tool-level deny list to mirror
|
|
1495
1507
|
// Claude's --disallowedTools, so it rides along in the prompt (codexPrompt).
|
|
1496
1508
|
function codexExecArgs(cfg, opts) {
|
|
1497
|
-
const { model, resumeId } = opts
|
|
1509
|
+
const { model, effort, resumeId } = opts
|
|
1498
1510
|
const flags = [
|
|
1499
1511
|
'--json',
|
|
1500
1512
|
'--skip-git-repo-check',
|
|
@@ -1517,6 +1529,9 @@ function codexExecArgs(cfg, opts) {
|
|
|
1517
1529
|
// it, which killed every resumed turn with "unexpected argument".)
|
|
1518
1530
|
'--dangerously-bypass-approvals-and-sandbox',
|
|
1519
1531
|
...(model ? ['--model', model] : []),
|
|
1532
|
+
// Reasoning effort chosen in gitDone (gd-507). There is no flag for it, so
|
|
1533
|
+
// it goes in as a config override like the MCP settings below.
|
|
1534
|
+
...(effort ? ['-c', `model_reasoning_effort="${effort}"`] : []),
|
|
1520
1535
|
'-c', `mcp_servers.gitdone.url="${cfg.url}/api/mcp"`,
|
|
1521
1536
|
'-c', 'mcp_servers.gitdone.bearer_token_env_var="GITDONE_KEY"',
|
|
1522
1537
|
// Tags the AI agents this run registers with THIS computer (gd-308).
|
|
@@ -1550,6 +1565,7 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1550
1565
|
const prompt = cmd.payload?.prompt ?? ''
|
|
1551
1566
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
1552
1567
|
const model = aiModelArg(cmd.payload?.model)
|
|
1568
|
+
const effort = aiEffortArg(cmd.payload?.effort)
|
|
1553
1569
|
const provider = aiProviderArg(cmd.payload?.provider)
|
|
1554
1570
|
const isCodex = provider === 'codex'
|
|
1555
1571
|
// gd-466: a token-reset resume asks us to continue the CLI's own conversation.
|
|
@@ -1585,7 +1601,7 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1585
1601
|
// Piping it to stdin is robust for both the native exe and the shim; `codex
|
|
1586
1602
|
// exec -` reads its prompt from stdin the same way.
|
|
1587
1603
|
const args = isCodex
|
|
1588
|
-
? codexExecArgs(cfg, { model, resumeId })
|
|
1604
|
+
? codexExecArgs(cfg, { model, effort, resumeId })
|
|
1589
1605
|
: [
|
|
1590
1606
|
'-p',
|
|
1591
1607
|
'--output-format', 'stream-json',
|
|
@@ -1599,6 +1615,8 @@ function runAiCommand(cfg, cmd, repoPath) {
|
|
|
1599
1615
|
...(resumeId ? ['--resume', resumeId] : []),
|
|
1600
1616
|
// Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
|
|
1601
1617
|
...(model ? ['--model', model] : []),
|
|
1618
|
+
// Reasoning effort chosen in gitDone (gd-510); omitted → machine default.
|
|
1619
|
+
...(effort ? ['--effort', effort] : []),
|
|
1602
1620
|
'--permission-mode', 'acceptEdits',
|
|
1603
1621
|
'--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
|
|
1604
1622
|
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
@@ -1893,7 +1911,7 @@ async function finishChatTurn(cfg, entry, outcome) {
|
|
|
1893
1911
|
// Spawn the persistent claude process for one session and wire its stream
|
|
1894
1912
|
// handlers once. Turns come and go via entry.turn; the process stays.
|
|
1895
1913
|
function spawnChatProc(cfg, opts) {
|
|
1896
|
-
const { sessionId, repoPath, model, allowCommit, resumeId, claudePath, shell, settingsPath, mcpConfigPath } = opts
|
|
1914
|
+
const { sessionId, repoPath, model, effort, allowCommit, resumeId, claudePath, shell, settingsPath, mcpConfigPath } = opts
|
|
1897
1915
|
|
|
1898
1916
|
// Room in the pool: evict the least-recently-used idle process first.
|
|
1899
1917
|
if (chatProcs.size >= CHAT_PROC_MAX) {
|
|
@@ -1913,6 +1931,9 @@ function spawnChatProc(cfg, opts) {
|
|
|
1913
1931
|
// Model resolved for this session (picker / project default); omitted →
|
|
1914
1932
|
// machine default (gd-354). Kept consistent across the session's turns.
|
|
1915
1933
|
...(model ? ['--model', model] : []),
|
|
1934
|
+
// Same for the session's reasoning effort (gd-510) — frozen at spawn, so a
|
|
1935
|
+
// change in gitDone reaches the pooled process on its next respawn.
|
|
1936
|
+
...(effort ? ['--effort', effort] : []),
|
|
1916
1937
|
'--permission-mode', 'acceptEdits',
|
|
1917
1938
|
'--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
|
|
1918
1939
|
// Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
|
|
@@ -2004,7 +2025,7 @@ function killTree(child) {
|
|
|
2004
2025
|
// chatProcs under the same entry shape, so ai_chat_stop and the stuck-turn
|
|
2005
2026
|
// sweeper keep working untouched.
|
|
2006
2027
|
function runCodexChatTurn(cfg, cmd, repoPath, opts) {
|
|
2007
|
-
const { sessionId, prompt, resumeId, model, allowCommit } = opts
|
|
2028
|
+
const { sessionId, prompt, resumeId, model, effort, allowCommit } = opts
|
|
2008
2029
|
|
|
2009
2030
|
const { path: codexPath, shell, found } = findCodex()
|
|
2010
2031
|
if (!found) {
|
|
@@ -2015,7 +2036,7 @@ function runCodexChatTurn(cfg, cmd, repoPath, opts) {
|
|
|
2015
2036
|
return
|
|
2016
2037
|
}
|
|
2017
2038
|
|
|
2018
|
-
const args = codexExecArgs(cfg, { model, resumeId })
|
|
2039
|
+
const args = codexExecArgs(cfg, { model, effort, resumeId })
|
|
2019
2040
|
const childEnv = { ...process.env, GITDONE_URL: cfg.url, GITDONE_KEY: cfg.key, GITDONE_SESSION_ID: sessionId }
|
|
2020
2041
|
|
|
2021
2042
|
let child
|
|
@@ -2106,6 +2127,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2106
2127
|
const claudeSessionId = cmd.payload?.claudeSessionId || null
|
|
2107
2128
|
const allowCommit = cmd.payload?.allowCommit === true
|
|
2108
2129
|
const model = aiModelArg(cmd.payload?.model)
|
|
2130
|
+
const effort = aiEffortArg(cmd.payload?.effort)
|
|
2109
2131
|
const provider = aiProviderArg(cmd.payload?.provider)
|
|
2110
2132
|
// A turn may be text-only, image-only, or both — but needs at least one.
|
|
2111
2133
|
if (!sessionId || (!prompt && images.length === 0)) {
|
|
@@ -2140,6 +2162,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2140
2162
|
imgDir: dl?.dir ?? null,
|
|
2141
2163
|
resumeId: claudeSessionId,
|
|
2142
2164
|
model,
|
|
2165
|
+
effort,
|
|
2143
2166
|
allowCommit,
|
|
2144
2167
|
})
|
|
2145
2168
|
return
|
|
@@ -2159,7 +2182,7 @@ async function runAiChat(cfg, cmd, repoPath) {
|
|
|
2159
2182
|
let mcpConfigPath
|
|
2160
2183
|
try { mcpConfigPath = ensureAiMcpConfig(cfg) } catch (e) { log(`✗ ai mcp config setup failed: ${e.message}`) }
|
|
2161
2184
|
try {
|
|
2162
|
-
entry = spawnChatProc(cfg, { sessionId, repoPath, model, allowCommit, resumeId: claudeSessionId, claudePath, shell, settingsPath, mcpConfigPath })
|
|
2185
|
+
entry = spawnChatProc(cfg, { sessionId, repoPath, model, effort, allowCommit, resumeId: claudeSessionId, claudePath, shell, settingsPath, mcpConfigPath })
|
|
2163
2186
|
} catch (err) {
|
|
2164
2187
|
postSessionEvents(cfg, sessionId, [{ role: 'SYSTEM', text: `Грешка при стартиране: ${err.message}` }], 'error')
|
|
2165
2188
|
reportCommandResult(cfg, cmd.id, 'error', err.message)
|
|
@@ -2388,19 +2411,14 @@ async function readClaudeUsage() {
|
|
|
2388
2411
|
}
|
|
2389
2412
|
|
|
2390
2413
|
// ─── Codex (ChatGPT) plan usage (gd-492) ───────────────────────────────────
|
|
2391
|
-
// Codex
|
|
2392
|
-
//
|
|
2393
|
-
//
|
|
2394
|
-
//
|
|
2395
|
-
// where `token_count` lines carry:
|
|
2396
|
-
// payload.rate_limits = { primary: { used_percent, window_minutes, resets_at },
|
|
2397
|
-
// secondary: …|null, plan_type, credits, … }
|
|
2398
|
-
// So we read the newest rollout and take its LAST rate_limits record.
|
|
2414
|
+
// Ask Codex itself for the ChatGPT account rate limits, instead of guessing from
|
|
2415
|
+
// a previous run's transcript. The app-server API is the same local surface the
|
|
2416
|
+
// clients use for account/rateLimits/read; it returns primary/secondary windows
|
|
2417
|
+
// when the account has both a short and a weekly/general Codex bucket.
|
|
2399
2418
|
//
|
|
2400
|
-
//
|
|
2401
|
-
//
|
|
2402
|
-
//
|
|
2403
|
-
// then say how old they are rather than implying they're live.
|
|
2419
|
+
// If that live read fails, fall back to the last rollout file so old Codex
|
|
2420
|
+
// installs still show something. Rollout values are telemetry only and may not
|
|
2421
|
+
// include every limit the CLI enforces.
|
|
2404
2422
|
|
|
2405
2423
|
// Newest entry in a dir, by name (the sessions tree is zero-padded YYYY/MM/DD,
|
|
2406
2424
|
// so the lexicographic max IS the newest) or by mtime for the files themselves.
|
|
@@ -2435,8 +2453,20 @@ function readTail(file, bytes) {
|
|
|
2435
2453
|
return start > 0 ? text.slice(text.indexOf('\n') + 1) : text
|
|
2436
2454
|
}
|
|
2437
2455
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2456
|
+
function codexAppWindow(w) {
|
|
2457
|
+
if (!w || typeof w !== 'object') return null
|
|
2458
|
+
const pct = Number(w.usedPercent)
|
|
2459
|
+
if (!Number.isFinite(pct)) return null
|
|
2460
|
+
const resets = Number(w.resetsAt)
|
|
2461
|
+
return {
|
|
2462
|
+
pct: Math.round(pct),
|
|
2463
|
+
windowMin: Number.isFinite(Number(w.windowDurationMins)) ? Math.round(Number(w.windowDurationMins)) : null,
|
|
2464
|
+
resetsAt: Number.isFinite(resets) && resets > 0 ? new Date(resets * 1000).toISOString() : null,
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// One rollout window of a Codex rate limit → the shape the server stores.
|
|
2469
|
+
function codexRolloutWindow(w) {
|
|
2440
2470
|
if (!w || typeof w !== 'object') return null
|
|
2441
2471
|
const pct = Number(w.used_percent)
|
|
2442
2472
|
if (!Number.isFinite(pct)) return null
|
|
@@ -2449,10 +2479,77 @@ function codexWindow(w) {
|
|
|
2449
2479
|
}
|
|
2450
2480
|
}
|
|
2451
2481
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2482
|
+
function codexUsageFromSnapshot(snapshot) {
|
|
2483
|
+
if (!snapshot || typeof snapshot !== 'object') return null
|
|
2484
|
+
const primary = codexAppWindow(snapshot.primary)
|
|
2485
|
+
const secondary = codexAppWindow(snapshot.secondary)
|
|
2486
|
+
if (!primary && !secondary) return null
|
|
2487
|
+
return {
|
|
2488
|
+
primary,
|
|
2489
|
+
secondary,
|
|
2490
|
+
planType: typeof snapshot.planType === 'string' ? snapshot.planType : null,
|
|
2491
|
+
observedAt: new Date().toISOString(),
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function readCodexUsageFromAppServer() {
|
|
2496
|
+
return new Promise((resolve) => {
|
|
2497
|
+
const { path: codexPath, shell, found } = findCodex()
|
|
2498
|
+
if (!found) return resolve(null)
|
|
2499
|
+
|
|
2500
|
+
let settled = false
|
|
2501
|
+
let child
|
|
2502
|
+
const finish = (value) => {
|
|
2503
|
+
if (settled) return
|
|
2504
|
+
settled = true
|
|
2505
|
+
clearTimeout(timer)
|
|
2506
|
+
try { child?.kill() } catch { /* best-effort */ }
|
|
2507
|
+
resolve(value)
|
|
2508
|
+
}
|
|
2509
|
+
const timer = setTimeout(() => finish(null), 12_000)
|
|
2510
|
+
|
|
2511
|
+
try {
|
|
2512
|
+
child = spawn(codexPath, ['app-server', '--stdio'], {
|
|
2513
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
2514
|
+
shell,
|
|
2515
|
+
windowsHide: true,
|
|
2516
|
+
})
|
|
2517
|
+
} catch {
|
|
2518
|
+
return finish(null)
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
child.on('error', () => finish(null))
|
|
2522
|
+
child.on('exit', () => finish(null))
|
|
2523
|
+
|
|
2524
|
+
const rl = readline.createInterface({ input: child.stdout })
|
|
2525
|
+
rl.on('line', (line) => {
|
|
2526
|
+
let msg
|
|
2527
|
+
try { msg = JSON.parse(line) } catch { return }
|
|
2528
|
+
if (msg.id !== 2) return
|
|
2529
|
+
if (msg.error) return finish(null)
|
|
2530
|
+
const result = msg.result
|
|
2531
|
+
const byId = result?.rateLimitsByLimitId
|
|
2532
|
+
const snapshot = byId?.codex || result?.rateLimits
|
|
2533
|
+
finish(codexUsageFromSnapshot(snapshot))
|
|
2534
|
+
})
|
|
2535
|
+
|
|
2536
|
+
const send = (message) => {
|
|
2537
|
+
try { child.stdin.write(`${JSON.stringify(message)}\n`) } catch { finish(null) }
|
|
2538
|
+
}
|
|
2539
|
+
send({
|
|
2540
|
+
method: 'initialize',
|
|
2541
|
+
id: 1,
|
|
2542
|
+
params: {
|
|
2543
|
+
clientInfo: { name: 'gitdone-agent', title: 'gitDone Agent', version: AGENT_VERSION },
|
|
2544
|
+
capabilities: { experimentalApi: true },
|
|
2545
|
+
},
|
|
2546
|
+
})
|
|
2547
|
+
send({ method: 'initialized', params: {} })
|
|
2548
|
+
send({ method: 'account/rateLimits/read', id: 2 })
|
|
2549
|
+
})
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
function readCodexUsageFromRollout() {
|
|
2456
2553
|
try {
|
|
2457
2554
|
const root = join(homedir(), '.codex', 'sessions')
|
|
2458
2555
|
if (!existsSync(root)) return null
|
|
@@ -2477,18 +2574,16 @@ function readCodexUsage() {
|
|
|
2477
2574
|
try { ev = JSON.parse(line) } catch { continue }
|
|
2478
2575
|
const rl = ev?.payload?.rate_limits
|
|
2479
2576
|
if (!rl || typeof rl !== 'object') continue
|
|
2480
|
-
const primary =
|
|
2481
|
-
const secondary =
|
|
2577
|
+
const primary = codexRolloutWindow(rl.primary)
|
|
2578
|
+
const secondary = codexRolloutWindow(rl.secondary)
|
|
2482
2579
|
if (!primary && !secondary) continue
|
|
2483
|
-
|
|
2580
|
+
return {
|
|
2484
2581
|
primary,
|
|
2485
2582
|
secondary,
|
|
2486
2583
|
planType: typeof rl.plan_type === 'string' ? rl.plan_type : null,
|
|
2487
2584
|
// When these numbers were actually true — the run that produced them.
|
|
2488
2585
|
observedAt: typeof ev.timestamp === 'string' ? ev.timestamp : new Date(statSync(file).mtimeMs).toISOString(),
|
|
2489
2586
|
}
|
|
2490
|
-
codexUsageCache = { at: now, data }
|
|
2491
|
-
return data
|
|
2492
2587
|
}
|
|
2493
2588
|
}
|
|
2494
2589
|
}
|
|
@@ -2498,6 +2593,15 @@ function readCodexUsage() {
|
|
|
2498
2593
|
}
|
|
2499
2594
|
}
|
|
2500
2595
|
|
|
2596
|
+
let codexUsageCache = { at: 0, data: null }
|
|
2597
|
+
async function readCodexUsage() {
|
|
2598
|
+
const now = Date.now()
|
|
2599
|
+
if (codexUsageCache.data && now - codexUsageCache.at < 60_000) return codexUsageCache.data
|
|
2600
|
+
const data = await readCodexUsageFromAppServer() || readCodexUsageFromRollout()
|
|
2601
|
+
if (data) codexUsageCache = { at: now, data }
|
|
2602
|
+
return data
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2501
2605
|
// The discovered repo list barely ever changes, but re-upserting every repo's
|
|
2502
2606
|
// row on the server each tick is pure idle write load at scale. So we ship the
|
|
2503
2607
|
// full `repos` list only when the discovered set actually changed, or every
|
|
@@ -2513,7 +2617,7 @@ async function sync(cfg, discovered) {
|
|
|
2513
2617
|
const usage = await readClaudeUsage()
|
|
2514
2618
|
// gd-492: the ChatGPT side's own limits, so a Codex console shows ITS numbers
|
|
2515
2619
|
// instead of Claude's. Both ride along — one machine can run both engines.
|
|
2516
|
-
const codexUsage = readCodexUsage()
|
|
2620
|
+
const codexUsage = await readCodexUsage()
|
|
2517
2621
|
const sig = createHash('sha1').update(JSON.stringify(discovered)).digest('hex')
|
|
2518
2622
|
const full = sig !== syncRepoSig || syncFullCountdown <= 0
|
|
2519
2623
|
const data = await api(cfg, '/api/v1/agent/sync', {
|