gitdone-agent 0.6.8 → 0.6.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.
Files changed (2) hide show
  1. package/index.js +67 -1
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
27
27
  // Reported to the server on every sync so the web UI can flag outdated agents.
28
28
  // Keep in lockstep with packages/agent/package.json "version" AND
29
29
  // src/lib/agentVersion.ts LATEST_AGENT_VERSION.
30
- const AGENT_VERSION = '0.6.8'
30
+ const AGENT_VERSION = '0.6.10'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -783,6 +783,18 @@ function ensureAiMcpConfig(cfg) {
783
783
  return AI_MCP_CONFIG_PATH
784
784
  }
785
785
 
786
+ // Sanitise the model value from a command payload into a safe `--model`
787
+ // argument. gitDone sends a stable CLI alias ("opus"/"sonnet"/"haiku"), but we
788
+ // also allow a full model id (letters, digits, dot, dash). Anything else — or a
789
+ // missing value — yields null, so the agent falls back to the machine's own
790
+ // default model. Bounds the length as a belt-and-braces guard (gd-354).
791
+ function aiModelArg(raw) {
792
+ if (typeof raw !== 'string') return null
793
+ const m = raw.trim()
794
+ if (!m || m.length > 100) return null
795
+ return /^[A-Za-z0-9._-]+$/.test(m) ? m : null
796
+ }
797
+
786
798
  // Run a headless Claude Code session for an `ai_run` command and stream its
787
799
  // output back. Long-running and fire-and-forget: it wires up async handlers and
788
800
  // returns immediately so the agent's snapshot loop is never blocked.
@@ -790,6 +802,7 @@ function runAiCommand(cfg, cmd, repoPath) {
790
802
  const runId = cmd.payload?.runId
791
803
  const prompt = cmd.payload?.prompt ?? ''
792
804
  const allowCommit = cmd.payload?.allowCommit === true
805
+ const model = aiModelArg(cmd.payload?.model)
793
806
  if (!runId || !prompt) {
794
807
  reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_run payload')
795
808
  return
@@ -819,6 +832,8 @@ function runAiCommand(cfg, cmd, repoPath) {
819
832
  '-p',
820
833
  '--output-format', 'stream-json',
821
834
  '--verbose',
835
+ // Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
836
+ ...(model ? ['--model', model] : []),
822
837
  '--permission-mode', 'acceptEdits',
823
838
  '--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
824
839
  // Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
@@ -961,6 +976,7 @@ async function runAiChat(cfg, cmd, repoPath) {
961
976
  const images = Array.isArray(cmd.payload?.images) ? cmd.payload.images.filter((u) => typeof u === 'string' && u) : []
962
977
  const claudeSessionId = cmd.payload?.claudeSessionId || null
963
978
  const allowCommit = cmd.payload?.allowCommit === true
979
+ const model = aiModelArg(cmd.payload?.model)
964
980
  // A turn may be text-only, image-only, or both — but needs at least one.
965
981
  if (!sessionId || (!prompt && images.length === 0)) {
966
982
  reportCommandResult(cfg, cmd.id, 'error', 'invalid ai_chat payload')
@@ -988,6 +1004,9 @@ async function runAiChat(cfg, cmd, repoPath) {
988
1004
  // Stream the reply token-by-token so the console can show it being written
989
1005
  // live, not only once each block is complete (gd-302).
990
1006
  '--include-partial-messages',
1007
+ // Model resolved for this session (picker / project default); omitted →
1008
+ // machine default (gd-354). Kept consistent across the session's turns.
1009
+ ...(model ? ['--model', model] : []),
991
1010
  '--permission-mode', 'acceptEdits',
992
1011
  '--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
993
1012
  // Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
@@ -1241,13 +1260,60 @@ async function executeCommand(cfg, cmd, repoPath) {
1241
1260
  }
1242
1261
 
1243
1262
  // Report discovered repos + machine info, get back which repos to track.
1263
+ // ─── Claude plan usage (gd-355) ────────────────────────────────────────────
1264
+ // Claude Code persists an OAuth token at ~/.claude/.credentials.json and keeps
1265
+ // it fresh. We reuse it to ask Anthropic for the SAME plan-usage numbers the
1266
+ // /usage command shows (account-level: a 5-hour "session" window + a weekly
1267
+ // limit). The АИ Конзола then renders these live. Best-effort: on a missing or
1268
+ // expired token, being offline, or any error we return null and the console
1269
+ // simply keeps the last snapshot. Cached ~60s so the 30s sync loop doesn't hit
1270
+ // the endpoint twice as often as it changes.
1271
+ let usageCache = { at: 0, data: null }
1272
+ async function readClaudeUsage() {
1273
+ const now = Date.now()
1274
+ if (usageCache.data && now - usageCache.at < 60_000) return usageCache.data
1275
+ try {
1276
+ const creds = JSON.parse(readFileSync(join(homedir(), '.claude', '.credentials.json'), 'utf8'))
1277
+ const token = creds?.claudeAiOauth?.accessToken
1278
+ if (!token) return null
1279
+ const ctrl = new AbortController()
1280
+ const to = setTimeout(() => ctrl.abort(), 8000)
1281
+ let res
1282
+ try {
1283
+ res = await fetch('https://api.anthropic.com/api/oauth/usage', {
1284
+ headers: {
1285
+ Authorization: `Bearer ${token}`,
1286
+ 'Content-Type': 'application/json',
1287
+ 'anthropic-beta': 'oauth-2025-04-20',
1288
+ 'User-Agent': 'claude-cli',
1289
+ },
1290
+ signal: ctrl.signal,
1291
+ })
1292
+ } finally { clearTimeout(to) }
1293
+ if (!res.ok) return null // 401 = token expired; Claude Code refreshes it — skip this tick
1294
+ const j = await res.json()
1295
+ const data = {
1296
+ sessionPct: Math.round(j?.five_hour?.utilization ?? 0),
1297
+ sessionResetsAt: j?.five_hour?.resets_at ?? null,
1298
+ weeklyPct: Math.round(j?.seven_day?.utilization ?? 0),
1299
+ weeklyResetsAt: j?.seven_day?.resets_at ?? null,
1300
+ }
1301
+ usageCache = { at: now, data }
1302
+ return data
1303
+ } catch {
1304
+ return null // no creds file / malformed / offline — stay quiet
1305
+ }
1306
+ }
1307
+
1244
1308
  async function sync(cfg, discovered) {
1309
+ const usage = await readClaudeUsage()
1245
1310
  const data = await api(cfg, '/api/v1/agent/sync', {
1246
1311
  machineId: cfg.machineId,
1247
1312
  hostname: cfg.hostname,
1248
1313
  agentVersion: AGENT_VERSION,
1249
1314
  roots: cfg.roots,
1250
1315
  repos: discovered,
1316
+ ...(usage ? { usage } : {}),
1251
1317
  })
1252
1318
  return data.tracked ?? []
1253
1319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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": {