gitdone-agent 0.8.0 → 0.8.1

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.
Files changed (2) hide show
  1. package/index.js +107 -25
  2. 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.0'
32
+ const AGENT_VERSION = '0.8.1'
32
33
 
33
34
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
34
35
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -2388,19 +2389,14 @@ async function readClaudeUsage() {
2388
2389
  }
2389
2390
 
2390
2391
  // ─── Codex (ChatGPT) plan usage (gd-492) ───────────────────────────────────
2391
- // Codex knows its own rate limits but does NOT put them in the `codex exec
2392
- // --json` stdout stream (upstream openai/codex#14728). It DOES write them to the
2393
- // session rollout it keeps on disk:
2394
- // ~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<thread>.jsonl
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.
2392
+ // Ask Codex itself for the ChatGPT account rate limits, instead of guessing from
2393
+ // a previous run's transcript. The app-server API is the same local surface the
2394
+ // clients use for account/rateLimits/read; it returns primary/secondary windows
2395
+ // when the account has both a short and a weekly/general Codex bucket.
2399
2396
  //
2400
- // Unlike the Claude numbers fetched live from Anthropic on every tick these
2401
- // are only as fresh as the user's last Codex run, which is why we report the
2402
- // rollout line's OWN timestamp as observedAt instead of "now". The console can
2403
- // then say how old they are rather than implying they're live.
2397
+ // If that live read fails, fall back to the last rollout file so old Codex
2398
+ // installs still show something. Rollout values are telemetry only and may not
2399
+ // include every limit the CLI enforces.
2404
2400
 
2405
2401
  // Newest entry in a dir, by name (the sessions tree is zero-padded YYYY/MM/DD,
2406
2402
  // so the lexicographic max IS the newest) or by mtime for the files themselves.
@@ -2435,8 +2431,20 @@ function readTail(file, bytes) {
2435
2431
  return start > 0 ? text.slice(text.indexOf('\n') + 1) : text
2436
2432
  }
2437
2433
 
2438
- // One window of a Codex rate limit → the shape the server stores.
2439
- function codexWindow(w) {
2434
+ function codexAppWindow(w) {
2435
+ if (!w || typeof w !== 'object') return null
2436
+ const pct = Number(w.usedPercent)
2437
+ if (!Number.isFinite(pct)) return null
2438
+ const resets = Number(w.resetsAt)
2439
+ return {
2440
+ pct: Math.round(pct),
2441
+ windowMin: Number.isFinite(Number(w.windowDurationMins)) ? Math.round(Number(w.windowDurationMins)) : null,
2442
+ resetsAt: Number.isFinite(resets) && resets > 0 ? new Date(resets * 1000).toISOString() : null,
2443
+ }
2444
+ }
2445
+
2446
+ // One rollout window of a Codex rate limit → the shape the server stores.
2447
+ function codexRolloutWindow(w) {
2440
2448
  if (!w || typeof w !== 'object') return null
2441
2449
  const pct = Number(w.used_percent)
2442
2450
  if (!Number.isFinite(pct)) return null
@@ -2449,10 +2457,77 @@ function codexWindow(w) {
2449
2457
  }
2450
2458
  }
2451
2459
 
2452
- let codexUsageCache = { at: 0, data: null }
2453
- function readCodexUsage() {
2454
- const now = Date.now()
2455
- if (codexUsageCache.data && now - codexUsageCache.at < 60_000) return codexUsageCache.data
2460
+ function codexUsageFromSnapshot(snapshot) {
2461
+ if (!snapshot || typeof snapshot !== 'object') return null
2462
+ const primary = codexAppWindow(snapshot.primary)
2463
+ const secondary = codexAppWindow(snapshot.secondary)
2464
+ if (!primary && !secondary) return null
2465
+ return {
2466
+ primary,
2467
+ secondary,
2468
+ planType: typeof snapshot.planType === 'string' ? snapshot.planType : null,
2469
+ observedAt: new Date().toISOString(),
2470
+ }
2471
+ }
2472
+
2473
+ function readCodexUsageFromAppServer() {
2474
+ return new Promise((resolve) => {
2475
+ const { path: codexPath, shell, found } = findCodex()
2476
+ if (!found) return resolve(null)
2477
+
2478
+ let settled = false
2479
+ let child
2480
+ const finish = (value) => {
2481
+ if (settled) return
2482
+ settled = true
2483
+ clearTimeout(timer)
2484
+ try { child?.kill() } catch { /* best-effort */ }
2485
+ resolve(value)
2486
+ }
2487
+ const timer = setTimeout(() => finish(null), 12_000)
2488
+
2489
+ try {
2490
+ child = spawn(codexPath, ['app-server', '--stdio'], {
2491
+ stdio: ['pipe', 'pipe', 'ignore'],
2492
+ shell,
2493
+ windowsHide: true,
2494
+ })
2495
+ } catch {
2496
+ return finish(null)
2497
+ }
2498
+
2499
+ child.on('error', () => finish(null))
2500
+ child.on('exit', () => finish(null))
2501
+
2502
+ const rl = readline.createInterface({ input: child.stdout })
2503
+ rl.on('line', (line) => {
2504
+ let msg
2505
+ try { msg = JSON.parse(line) } catch { return }
2506
+ if (msg.id !== 2) return
2507
+ if (msg.error) return finish(null)
2508
+ const result = msg.result
2509
+ const byId = result?.rateLimitsByLimitId
2510
+ const snapshot = byId?.codex || result?.rateLimits
2511
+ finish(codexUsageFromSnapshot(snapshot))
2512
+ })
2513
+
2514
+ const send = (message) => {
2515
+ try { child.stdin.write(`${JSON.stringify(message)}\n`) } catch { finish(null) }
2516
+ }
2517
+ send({
2518
+ method: 'initialize',
2519
+ id: 1,
2520
+ params: {
2521
+ clientInfo: { name: 'gitdone-agent', title: 'gitDone Agent', version: AGENT_VERSION },
2522
+ capabilities: { experimentalApi: true },
2523
+ },
2524
+ })
2525
+ send({ method: 'initialized', params: {} })
2526
+ send({ method: 'account/rateLimits/read', id: 2 })
2527
+ })
2528
+ }
2529
+
2530
+ function readCodexUsageFromRollout() {
2456
2531
  try {
2457
2532
  const root = join(homedir(), '.codex', 'sessions')
2458
2533
  if (!existsSync(root)) return null
@@ -2477,18 +2552,16 @@ function readCodexUsage() {
2477
2552
  try { ev = JSON.parse(line) } catch { continue }
2478
2553
  const rl = ev?.payload?.rate_limits
2479
2554
  if (!rl || typeof rl !== 'object') continue
2480
- const primary = codexWindow(rl.primary)
2481
- const secondary = codexWindow(rl.secondary)
2555
+ const primary = codexRolloutWindow(rl.primary)
2556
+ const secondary = codexRolloutWindow(rl.secondary)
2482
2557
  if (!primary && !secondary) continue
2483
- const data = {
2558
+ return {
2484
2559
  primary,
2485
2560
  secondary,
2486
2561
  planType: typeof rl.plan_type === 'string' ? rl.plan_type : null,
2487
2562
  // When these numbers were actually true — the run that produced them.
2488
2563
  observedAt: typeof ev.timestamp === 'string' ? ev.timestamp : new Date(statSync(file).mtimeMs).toISOString(),
2489
2564
  }
2490
- codexUsageCache = { at: now, data }
2491
- return data
2492
2565
  }
2493
2566
  }
2494
2567
  }
@@ -2498,6 +2571,15 @@ function readCodexUsage() {
2498
2571
  }
2499
2572
  }
2500
2573
 
2574
+ let codexUsageCache = { at: 0, data: null }
2575
+ async function readCodexUsage() {
2576
+ const now = Date.now()
2577
+ if (codexUsageCache.data && now - codexUsageCache.at < 60_000) return codexUsageCache.data
2578
+ const data = await readCodexUsageFromAppServer() || readCodexUsageFromRollout()
2579
+ if (data) codexUsageCache = { at: now, data }
2580
+ return data
2581
+ }
2582
+
2501
2583
  // The discovered repo list barely ever changes, but re-upserting every repo's
2502
2584
  // row on the server each tick is pure idle write load at scale. So we ship the
2503
2585
  // full `repos` list only when the discovered set actually changed, or every
@@ -2513,7 +2595,7 @@ async function sync(cfg, discovered) {
2513
2595
  const usage = await readClaudeUsage()
2514
2596
  // gd-492: the ChatGPT side's own limits, so a Codex console shows ITS numbers
2515
2597
  // instead of Claude's. Both ride along — one machine can run both engines.
2516
- const codexUsage = readCodexUsage()
2598
+ const codexUsage = await readCodexUsage()
2517
2599
  const sig = createHash('sha1').update(JSON.stringify(discovered)).digest('hex')
2518
2600
  const full = sig !== syncRepoSig || syncFullCountdown <= 0
2519
2601
  const data = await api(cfg, '/api/v1/agent/sync', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {