@ucsandman/legcli 0.8.0 → 0.10.0
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/CHANGELOG.md +121 -0
- package/NOTICE +8 -0
- package/README.md +639 -560
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +43 -12
- package/docs/DECISIONS.md +20 -2
- package/docs/ERRORS.md +205 -0
- package/docs/README.md +5 -1
- package/docs/REUSE.md +1 -1
- package/docs/VOCABULARY.md +22 -0
- package/docs/board-guide.md +33 -1
- package/docs/cli-contracts.md +36 -1
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +23 -1
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/docs/history.md +172 -0
- package/docs/runtime-tap.md +156 -0
- package/fixtures/verified.json +1 -1
- package/package.json +7 -3
- package/scripts/build-docs-site.mjs +18 -4
- package/scripts/check-branding.mjs +118 -0
- package/scripts/check-claims.mjs +1 -1
- package/scripts/license-sign.mjs +1 -1
- package/scripts/limits-table.mjs +1 -1
- package/scripts/live-limits.mjs +1 -1
- package/scripts/npm-publish-gate.mjs +114 -0
- package/scripts/probe.mjs +4 -3
- package/scripts/seed-fake-cards.mjs +4 -3
- package/scripts/seed-floor-board.mjs +5 -4
- package/scripts/seed-wes-board.mjs +5 -4
- package/scripts/stripe-setup.mjs +1 -1
- package/scripts/sync-harness-engine.mjs +159 -0
- package/scripts/sync-leg-agents.mjs +127 -0
- package/src/accounts.mjs +6 -4
- package/src/adapters/codex.mjs +1 -1
- package/src/attach.mjs +125 -23
- package/src/auth.mjs +2 -2
- package/src/board/board.css +23 -1
- package/src/board/board.js +17 -5
- package/src/board/history.js +377 -0
- package/src/board/index.html +33 -0
- package/src/board/sessions.js +95 -7
- package/src/bundle.mjs +54 -8
- package/src/chain.mjs +1 -1
- package/src/contract.mjs +4 -3
- package/src/fsx.mjs +5 -2
- package/src/handoff.mjs +6 -6
- package/src/harness/cli.mjs +281 -0
- package/src/harness/fingerprint.mjs +68 -0
- package/src/harness/index.mjs +407 -0
- package/src/harness/registry.mjs +124 -0
- package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
- package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
- package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
- package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
- package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
- package/src/history/cli.mjs +159 -0
- package/src/history/common.mjs +119 -0
- package/src/history/index.mjs +429 -0
- package/src/history/providers/agy.mjs +91 -0
- package/src/history/providers/claude.mjs +161 -0
- package/src/history/providers/codex.mjs +133 -0
- package/src/history/providers/copilot.mjs +94 -0
- package/src/history/providers/grok.mjs +138 -0
- package/src/history/worktrees.mjs +116 -0
- package/src/hook.mjs +49 -49
- package/src/land.mjs +7 -35
- package/src/launcher.mjs +38 -26
- package/src/ledger.mjs +6 -6
- package/src/license.mjs +10 -9
- package/src/live-capture.mjs +1 -1
- package/src/mergequeue.mjs +5 -5
- package/src/orchestrator.mjs +28 -4
- package/src/preferences.mjs +37 -3
- package/src/redact.mjs +24 -6
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +2 -2
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +224 -18
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +15 -3
- package/src/share.mjs +2 -2
- package/src/stations/agent.mjs +1 -1
- package/src/sync/dashclaw.mjs +4 -4
- package/src/synthesis.mjs +165 -0
- package/src/taps/agy.mjs +2 -2
- package/src/taps/claude-usage.mjs +1 -1
- package/src/taps/claude.mjs +177 -170
- package/src/taps/codex.mjs +286 -286
- package/src/taps/grok.mjs +2 -2
- package/src/taps/mod.mjs +340 -0
- package/src/trust.mjs +205 -36
- package/src/usage.mjs +5 -1
- package/src/worktree.mjs +6 -5
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +0 -1
- package/fixtures/live/agy/supervisor.log +0 -2
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/out.log +0 -1
- package/fixtures/live/claude/supervisor.log +0 -2
- package/fixtures/live/codex/err.log +0 -1
- package/fixtures/live/codex/out.log +0 -8
- package/fixtures/live/codex/supervisor.log +0 -2
- package/fixtures/live/grok/err.log +0 -32
- package/fixtures/live/grok/out.log +0 -7
- package/fixtures/live/grok/supervisor.log +0 -2
package/src/taps/claude.mjs
CHANGED
|
@@ -1,170 +1,177 @@
|
|
|
1
|
-
// claude tap — how `
|
|
2
|
-
// Nothing in ~/.claude is edited: the session gets one extra settings file via
|
|
3
|
-
// `--settings` (hooks merge with the user's; statusLine is the only key that
|
|
4
|
-
// replaces, so
|
|
5
|
-
// Sources: code.claude.com/docs/en/hooks (StopFailure `error: rate_limit`),
|
|
6
|
-
// docs/en/statusline (rate_limits.five_hour/seven_day used_percentage,
|
|
7
|
-
// resets_at), docs/en/settings (`--settings` sits above user settings).
|
|
8
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
-
import { join, dirname, resolve } from 'node:path'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
import { sessionDir, updateSession, appendEvent, readSession, workRoot } from '../sessions.mjs'
|
|
12
|
-
import { recordUsage, markLimited, WARN_PCT } from '../usage.mjs'
|
|
13
|
-
import { writeJsonAtomic } from '../fsx.mjs'
|
|
14
|
-
import { LAYOUT } from '../accounts.mjs'
|
|
15
|
-
|
|
16
|
-
const HOOK = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'hook.mjs')
|
|
17
|
-
const q = (p) => `"${String(p).replace(/\\/g, '\\\\')}"`
|
|
18
|
-
|
|
19
|
-
// The user's own statusLine command, if any, so it keeps running.
|
|
20
|
-
export function userStatusLine(configDir = LAYOUT.claude.home()) {
|
|
21
|
-
for (const f of ['settings.json', 'settings.local.json']) {
|
|
22
|
-
const p = join(configDir, f)
|
|
23
|
-
if (!existsSync(p)) continue
|
|
24
|
-
try {
|
|
25
|
-
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
26
|
-
if (s.statusLine?.type === 'command' && s.statusLine.command) return s.statusLine
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
return null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function settingsFor(sessionId, { statusLine = null } = {}) {
|
|
33
|
-
const cmd = (kind) => ({ type: 'command', command: `node ${q(HOOK)} ${kind} --session ${sessionId}`, timeout: 20 })
|
|
34
|
-
const settings = {
|
|
35
|
-
hooks: {
|
|
36
|
-
SessionStart: [{ hooks: [cmd('claude-hook')] }],
|
|
37
|
-
UserPromptSubmit: [{ hooks: [cmd('claude-hook')] }],
|
|
38
|
-
PostToolUse: [{ matcher: 'Edit|Write|MultiEdit|NotebookEdit', hooks: [cmd('claude-hook')] }],
|
|
39
|
-
Stop: [{ hooks: [cmd('claude-hook')] }],
|
|
40
|
-
StopFailure: [{ hooks: [cmd('claude-hook')] }],
|
|
41
|
-
SessionEnd: [{ hooks: [cmd('claude-hook')] }],
|
|
42
|
-
},
|
|
43
|
-
statusLine: { type: 'command', command: `node ${q(HOOK)} claude-statusline --session ${sessionId}`, padding: statusLine?.padding ?? 0 },
|
|
44
|
-
//
|
|
45
|
-
// waiting in the session for the reset.
|
|
46
|
-
autoContinueAtUsageLimit: false,
|
|
47
|
-
}
|
|
48
|
-
return settings
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function writeSettings(sessionId, opts) {
|
|
52
|
-
const file = join(sessionDir(sessionId), 'claude-settings.json')
|
|
53
|
-
writeJsonAtomic(file, settingsFor(sessionId, opts))
|
|
54
|
-
return file
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function textOf(content) {
|
|
58
|
-
if (typeof content === 'string') return content
|
|
59
|
-
if (Array.isArray(content)) return content.filter((c) => c?.type === 'text' && c.text).map((c) => c.text).join('\n')
|
|
60
|
-
return ''
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (j.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
updateSession(sessionId, base)
|
|
135
|
-
return p.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
export function
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
1
|
+
// claude tap — how `leg claude` sees inside a normal interactive Claude Code.
|
|
2
|
+
// Nothing in ~/.claude is edited: the session gets one extra settings file via
|
|
3
|
+
// `--settings` (hooks merge with the user's; statusLine is the only key that
|
|
4
|
+
// replaces, so Leg's status line runs the user's own command first).
|
|
5
|
+
// Sources: code.claude.com/docs/en/hooks (StopFailure `error: rate_limit`),
|
|
6
|
+
// docs/en/statusline (rate_limits.five_hour/seven_day used_percentage,
|
|
7
|
+
// resets_at), docs/en/settings (`--settings` sits above user settings).
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { join, dirname, resolve } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { sessionDir, updateSession, appendEvent, readSession, workRoot } from '../sessions.mjs'
|
|
12
|
+
import { recordUsage, markLimited, WARN_PCT } from '../usage.mjs'
|
|
13
|
+
import { writeJsonAtomic } from '../fsx.mjs'
|
|
14
|
+
import { LAYOUT } from '../accounts.mjs'
|
|
15
|
+
|
|
16
|
+
const HOOK = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'hook.mjs')
|
|
17
|
+
const q = (p) => `"${String(p).replace(/\\/g, '\\\\')}"`
|
|
18
|
+
|
|
19
|
+
// The user's own statusLine command, if any, so it keeps running.
|
|
20
|
+
export function userStatusLine(configDir = LAYOUT.claude.home()) {
|
|
21
|
+
for (const f of ['settings.json', 'settings.local.json']) {
|
|
22
|
+
const p = join(configDir, f)
|
|
23
|
+
if (!existsSync(p)) continue
|
|
24
|
+
try {
|
|
25
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
26
|
+
if (s.statusLine?.type === 'command' && s.statusLine.command) return s.statusLine
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function settingsFor(sessionId, { statusLine = null } = {}) {
|
|
33
|
+
const cmd = (kind) => ({ type: 'command', command: `node ${q(HOOK)} ${kind} --session ${sessionId}`, timeout: 20 })
|
|
34
|
+
const settings = {
|
|
35
|
+
hooks: {
|
|
36
|
+
SessionStart: [{ hooks: [cmd('claude-hook')] }],
|
|
37
|
+
UserPromptSubmit: [{ hooks: [cmd('claude-hook')] }],
|
|
38
|
+
PostToolUse: [{ matcher: 'Edit|Write|MultiEdit|NotebookEdit', hooks: [cmd('claude-hook')] }],
|
|
39
|
+
Stop: [{ hooks: [cmd('claude-hook')] }],
|
|
40
|
+
StopFailure: [{ hooks: [cmd('claude-hook')] }],
|
|
41
|
+
SessionEnd: [{ hooks: [cmd('claude-hook')] }],
|
|
42
|
+
},
|
|
43
|
+
statusLine: { type: 'command', command: `node ${q(HOOK)} claude-statusline --session ${sessionId}`, padding: statusLine?.padding ?? 0 },
|
|
44
|
+
// Leg owns the limit: it hands the work to the next option instead of
|
|
45
|
+
// waiting in the session for the reset.
|
|
46
|
+
autoContinueAtUsageLimit: false,
|
|
47
|
+
}
|
|
48
|
+
return settings
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeSettings(sessionId, opts) {
|
|
52
|
+
const file = join(sessionDir(sessionId), 'claude-settings.json')
|
|
53
|
+
writeJsonAtomic(file, settingsFor(sessionId, opts))
|
|
54
|
+
return file
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function textOf(content) {
|
|
58
|
+
if (typeof content === 'string') return content
|
|
59
|
+
if (Array.isArray(content)) return content.filter((c) => c?.type === 'text' && c.text).map((c) => c.text).join('\n')
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Human/assistant messages from Claude Code transcript lines (jsonl). Shared
|
|
64
|
+
// with history discovery, which hands in the tail of a file it never reads whole.
|
|
65
|
+
export function messagesFromLines(lines, limit = 8) {
|
|
66
|
+
if (!(limit > 0)) return []
|
|
67
|
+
const out = []
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (!line) continue
|
|
70
|
+
let j
|
|
71
|
+
try { j = JSON.parse(line) } catch { continue }
|
|
72
|
+
if (j.type !== 'user' && j.type !== 'assistant') continue
|
|
73
|
+
if (j.isSidechain) continue
|
|
74
|
+
const text = textOf(j.message?.content).trim()
|
|
75
|
+
if (!text || /^<[a-z-]+>/.test(text)) continue
|
|
76
|
+
out.push({ role: j.type, text: text.slice(0, 1500), ts: j.timestamp ?? null })
|
|
77
|
+
}
|
|
78
|
+
return out.slice(-limit)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Last human/assistant messages from a Claude Code transcript (jsonl).
|
|
82
|
+
export function transcriptTail(path, limit = 8) {
|
|
83
|
+
if (!path || !existsSync(path)) return []
|
|
84
|
+
return messagesFromLines(readFileSync(path, 'utf8').split('\n'), limit)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function firstPrompt(path) {
|
|
88
|
+
const t = transcriptTail(path, 1000).find((m) => m.role === 'user')
|
|
89
|
+
return t ? t.text.slice(0, 500) : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function limitsFrom(rl) {
|
|
93
|
+
if (!rl) return null
|
|
94
|
+
const w = (x) => (x && Number.isFinite(x.used_percentage) ? { pct: x.used_percentage, resets_at: x.resets_at ?? null } : null)
|
|
95
|
+
return { five_hour: w(rl.five_hour), seven_day: w(rl.seven_day) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Hook payload → session record. Returns a short line for the hook log.
|
|
99
|
+
export function handleHook(sessionId, p) {
|
|
100
|
+
const s = readSession(sessionId)
|
|
101
|
+
if (!s) return 'no session'
|
|
102
|
+
const base = { agent_session_id: p.session_id ?? s.agent_session_id, transcript_path: p.transcript_path ?? s.transcript_path, last_activity: new Date().toISOString() }
|
|
103
|
+
switch (p.hook_event_name) {
|
|
104
|
+
case 'SessionStart':
|
|
105
|
+
// reducer: one hook process per tool call, so read `cur` inside the lock
|
|
106
|
+
updateSession(sessionId, (cur) => ({ ...base, status: cur.status === 'starting' ? 'running' : cur.status }), { event: { type: 'agent_ready', summary: `claude session ${p.session_id ?? '?'} (${p.source ?? 'startup'})` } })
|
|
107
|
+
return 'session start'
|
|
108
|
+
case 'UserPromptSubmit': {
|
|
109
|
+
updateSession(sessionId, (cur) => ({ ...base, task: cur.task ?? (p.prompt ? String(p.prompt).slice(0, 500) : null), turns: (cur.turns ?? 0) + 1 }), { event: { type: 'turn', summary: `prompt: ${String(p.prompt ?? '').slice(0, 120)}` } })
|
|
110
|
+
return 'prompt'
|
|
111
|
+
}
|
|
112
|
+
case 'PostToolUse': {
|
|
113
|
+
const file = p.tool_input?.file_path ?? p.tool_input?.notebook_path ?? null
|
|
114
|
+
if (!file) return 'tool (no file)'
|
|
115
|
+
const rel = relTo(workRoot(s), file)
|
|
116
|
+
// reducer: concurrent Edit/Write hooks each add their own file without
|
|
117
|
+
// the last writer overwriting the others' additions
|
|
118
|
+
updateSession(sessionId, (cur) => ({ ...base, files_touched: cur.files_touched.includes(rel) ? cur.files_touched : [...cur.files_touched, rel].slice(-200) }))
|
|
119
|
+
return `touched ${rel}`
|
|
120
|
+
}
|
|
121
|
+
case 'Stop':
|
|
122
|
+
// reducer: never turn a 'limit'/'handing_off' back to 'running' by racing
|
|
123
|
+
updateSession(sessionId, (cur) => ({ ...base, status: cur.status === 'starting' ? 'running' : cur.status }), { event: { type: 'turn_done', summary: String(p.last_assistant_message ?? '').slice(0, 160) || 'turn done' } })
|
|
124
|
+
return 'stop'
|
|
125
|
+
case 'StopFailure': {
|
|
126
|
+
if (p.error === 'rate_limit') {
|
|
127
|
+
// a simulated wall (baton sessions simulate-limit) clears after two minutes so a test never walls the real login for hours
|
|
128
|
+
const simulated = Boolean(p.leg_simulated || p.baton_simulated)
|
|
129
|
+
const u = markLimited('claude', s.account, { reason: 'rate_limit', source: simulated ? 'leg simulate-limit' : 'claude StopFailure', resets_at: simulated ? Math.floor(Date.now() / 1000) + 120 : null })
|
|
130
|
+
updateSession(sessionId, { ...base, status: 'limit', limit: { reason: 'rate_limit', detail: String(p.last_assistant_message ?? p.error_details ?? '').slice(0, 300), resets_at: u.limited_until, at: new Date().toISOString(), simulated } }, { event: { type: 'limit', summary: `claude usage limit${simulated ? ' (simulated)' : ''}: ${String(p.last_assistant_message ?? p.error_details ?? '').slice(0, 160)}` } })
|
|
131
|
+
return 'LIMIT'
|
|
132
|
+
}
|
|
133
|
+
appendEvent(sessionId, { type: 'error', summary: `claude ${p.error}: ${String(p.last_assistant_message ?? p.error_details ?? '').slice(0, 160)}` })
|
|
134
|
+
updateSession(sessionId, base)
|
|
135
|
+
return `error ${p.error}`
|
|
136
|
+
}
|
|
137
|
+
case 'SessionEnd':
|
|
138
|
+
updateSession(sessionId, { ...base, agent_exit_reason: p.reason ?? null }, { event: { type: 'agent_exit', summary: `claude session ended (${p.reason ?? 'unknown'})` } })
|
|
139
|
+
return 'session end'
|
|
140
|
+
default:
|
|
141
|
+
updateSession(sessionId, base)
|
|
142
|
+
return p.hook_event_name ?? 'unknown'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function relTo(root, file) {
|
|
147
|
+
const a = String(file).replace(/\\/g, '/')
|
|
148
|
+
const r = String(root ?? '').replace(/\\/g, '/').replace(/\/$/, '')
|
|
149
|
+
return r && a.toLowerCase().startsWith(r.toLowerCase() + '/') ? a.slice(r.length + 1) : a
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Status line: record the limits and print. Returns the text to print.
|
|
153
|
+
export function handleStatusline(sessionId, p) {
|
|
154
|
+
const s = readSession(sessionId)
|
|
155
|
+
if (!s) return { text: '', limits: null }
|
|
156
|
+
const limits = limitsFrom(p.rate_limits)
|
|
157
|
+
if (limits) recordUsage('claude', s.account, limits, 'claude statusline')
|
|
158
|
+
const hot = limits ? [['5h', limits.five_hour], ['7d', limits.seven_day]].filter(([, w]) => w).sort((a, b) => b[1].pct - a[1].pct)[0] : null
|
|
159
|
+
const warn = hot && hot[1].pct >= WARN_PCT
|
|
160
|
+
// reducer: the status-line hook is its own process; only nudge running↔warning
|
|
161
|
+
// from `cur`, so it never overwrites a 'limit' a StopFailure set at the same time
|
|
162
|
+
updateSession(sessionId, (cur) => {
|
|
163
|
+
const patch = { last_activity: new Date().toISOString() }
|
|
164
|
+
if (limits) patch.limits = limits
|
|
165
|
+
if (p.session_id && !cur.agent_session_id) patch.agent_session_id = p.session_id
|
|
166
|
+
if (p.transcript_path && !cur.transcript_path) patch.transcript_path = p.transcript_path
|
|
167
|
+
if (warn && !cur.warning) patch.warning = { window: hot[0], pct: hot[1].pct, resets_at: hot[1].resets_at, at: new Date().toISOString() }
|
|
168
|
+
if (!warn && cur.warning) patch.warning = null
|
|
169
|
+
if (warn && cur.status === 'running') patch.status = 'warning'
|
|
170
|
+
if (!warn && cur.status === 'warning') patch.status = 'running'
|
|
171
|
+
return patch
|
|
172
|
+
}, warn && !s.warning ? { event: { type: 'warning', summary: `claude ${hot[0]} window at ${Math.round(hot[1].pct)}%` } } : {})
|
|
173
|
+
const next = s.chain?.[0] ? `${s.chain[0].agent}${s.chain[0].account !== 'default' ? '/' + s.chain[0].account : ''}` : 'nothing'
|
|
174
|
+
const pct = limits ? ` 5h ${limits.five_hour ? Math.round(limits.five_hour.pct) + '%' : '-'} · 7d ${limits.seven_day ? Math.round(limits.seven_day.pct) + '%' : '-'}` : ''
|
|
175
|
+
const text = warn ? `⚠ leg: ${hot[0]} at ${Math.round(hot[1].pct)}% → next ${next}${pct}` : `leg ·${pct || ' limits pending'} · next ${next} · board ${s.board_url ?? ''}`
|
|
176
|
+
return { text, limits, warn }
|
|
177
|
+
}
|