sprag-cli 3.58.1 → 3.60.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/README.ko.md +17 -0
- package/README.md +23 -0
- package/bin/cli.js +59 -4
- package/package.json +1 -1
- package/src/caps-cache.js +3 -3
- package/src/codex-brief.js +4 -5
- package/src/codex-cache-policy.js +3 -3
- package/src/codex-delegation.js +3 -3
- package/src/codex-doc2md-ledger.js +9 -4
- package/src/codex-hooks.js +3 -3
- package/src/codex-installer.js +3 -3
- package/src/codex-ledger.js +13 -8
- package/src/codex-panel-session.js +3 -3
- package/src/codex-panel-state.js +51 -5
- package/src/codex-route-scan.js +3 -3
- package/src/commands/codex.js +2 -0
- package/src/commands/compact-window.js +6 -2
- package/src/commands/install.js +4 -1
- package/src/commands/korean.js +69 -3
- package/src/commands/route-scan.js +25 -4
- package/src/commands/saved.js +154 -0
- package/src/ctx-window-cache.js +14 -0
- package/src/doc2md-ledger.cjs +7 -1
- package/src/formatters/statusline.js +167 -18
- package/src/harness-analyzer.cjs +8 -2
- package/src/harness.js +32 -23
- package/src/installer.js +52 -0
- package/src/model-alias.js +3 -3
- package/src/model-rules.js +3 -1
- package/src/preset-ratchet.js +3 -3
- package/src/reply-language.cjs +167 -0
- package/src/route-inject.js +43 -3
- package/src/route-scan.js +51 -11
- package/src/saved-reset.cjs +118 -0
- package/src/savings-ledger.js +12 -4
- package/src/session-cache.js +12 -4
- package/src/state-file.js +99 -2
- package/src/statusline-data.js +97 -0
- package/src/update-check.js +46 -6
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statusline data helpers that need more than a number from the payload.
|
|
3
|
+
*
|
|
4
|
+
* bin/cli.js assembles the report data inline and cannot be imported (it runs
|
|
5
|
+
* on load), so the two derivations below live here where a test can reach them.
|
|
6
|
+
* Both read only what the render has already loaded or one small JSON file the
|
|
7
|
+
* statusline reads anyway; neither scans a transcript.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { modelFamily } from './savings-ledger.js';
|
|
12
|
+
import { loadModelRules } from './model-rules.js';
|
|
13
|
+
|
|
14
|
+
/** Longest family name shown before it is cut. Keeps an opaque gateway id from taking the line. */
|
|
15
|
+
const FAMILY_MAX = 14;
|
|
16
|
+
|
|
17
|
+
const shortFamily = (name) => (name.length > FAMILY_MAX ? `${name.slice(0, FAMILY_MAX - 1)}…` : name);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The subagent runs of the session the statusline is drawing for.
|
|
21
|
+
*
|
|
22
|
+
* The parser already attaches each session's subagent runs (`subagentRuns`,
|
|
23
|
+
* one entry per run with its own model), so this only finds the right session
|
|
24
|
+
* among those loaded and tallies them by model family. The session is matched
|
|
25
|
+
* by transcript path first, then by session id, because Claude Code reports
|
|
26
|
+
* both and either can be absent or spelled differently (a symlinked home).
|
|
27
|
+
*
|
|
28
|
+
* The runs are those the analysis window kept, so a session older than the
|
|
29
|
+
* window (`sprag mode` default is one day) reports only its recent runs.
|
|
30
|
+
*
|
|
31
|
+
* Families come from `modelFamily`, which returns an unmapped model id as it
|
|
32
|
+
* is rather than folding it into a neighbour; the name is shortened here so an
|
|
33
|
+
* ARN cannot flood the line.
|
|
34
|
+
*
|
|
35
|
+
* @param {Array} sessions parseAllSessions result
|
|
36
|
+
* @param {{ transcriptPath?: string|null, sessionId?: string|null }} [ids]
|
|
37
|
+
* @returns {{ total: number, families: Array<{ family: string, runs: number }> }|null}
|
|
38
|
+
* null when the session cannot be found or ran no subagents
|
|
39
|
+
*/
|
|
40
|
+
export function sessionSubagentSummary(sessions, { transcriptPath = null, sessionId = null } = {}) {
|
|
41
|
+
if (!Array.isArray(sessions) || (!transcriptPath && !sessionId)) return null;
|
|
42
|
+
let hit = null;
|
|
43
|
+
if (transcriptPath) {
|
|
44
|
+
const want = resolve(transcriptPath);
|
|
45
|
+
hit = sessions.find((s) => s && s.filePath && resolve(s.filePath) === want) || null;
|
|
46
|
+
}
|
|
47
|
+
if (!hit && sessionId) {
|
|
48
|
+
// A resumed conversation can leave more than one file under one id; the
|
|
49
|
+
// most recently active is the one still being written.
|
|
50
|
+
const end = (s) => (s.endTime ? new Date(s.endTime).getTime() || 0 : 0);
|
|
51
|
+
hit = sessions
|
|
52
|
+
.filter((s) => s && s.sessionId === sessionId)
|
|
53
|
+
.sort((a, b) => end(b) - end(a))[0] || null;
|
|
54
|
+
}
|
|
55
|
+
const runs = hit && Array.isArray(hit.subagentRuns) ? hit.subagentRuns : [];
|
|
56
|
+
if (runs.length === 0) return null;
|
|
57
|
+
const byFamily = new Map();
|
|
58
|
+
for (const r of runs) {
|
|
59
|
+
const name = shortFamily(modelFamily(r && r.model));
|
|
60
|
+
byFamily.set(name, (byFamily.get(name) || 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
const families = [...byFamily.entries()]
|
|
63
|
+
.map(([family, n]) => ({ family, runs: n }))
|
|
64
|
+
.sort((a, b) => b.runs - a.runs || a.family.localeCompare(b.family));
|
|
65
|
+
return { total: runs.length, families };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How many delegation rules apply in this project, and how many of those are
|
|
70
|
+
* flagged for review.
|
|
71
|
+
*
|
|
72
|
+
* "Apply" is the same test the `rule-health` warning uses: a global rule, or a
|
|
73
|
+
* project rule whose target is this root. A rule switched `off` is not counted.
|
|
74
|
+
* Review is `status === 'review'`, which is what model-rules.js renders as its
|
|
75
|
+
* `⚠ rule-health` marker (the marker itself is a local closure there, so the
|
|
76
|
+
* condition is restated rather than imported).
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
* @returns {{ total: number, review: number }}
|
|
80
|
+
*/
|
|
81
|
+
export function delegationRuleStats(projectRoot) {
|
|
82
|
+
let rules = [];
|
|
83
|
+
try {
|
|
84
|
+
rules = loadModelRules().rules;
|
|
85
|
+
} catch {
|
|
86
|
+
return { total: 0, review: 0 };
|
|
87
|
+
}
|
|
88
|
+
let total = 0;
|
|
89
|
+
let review = 0;
|
|
90
|
+
for (const r of rules) {
|
|
91
|
+
if (!r || r.status === 'off') continue;
|
|
92
|
+
if (r.scope !== 'global' && r.targetRoot !== projectRoot) continue;
|
|
93
|
+
total += 1;
|
|
94
|
+
if (r.status === 'review') review += 1;
|
|
95
|
+
}
|
|
96
|
+
return { total, review };
|
|
97
|
+
}
|
package/src/update-check.js
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* answer is older than the check interval it spawns a detached, unref'd child
|
|
9
9
|
* that refreshes the cache for the *next* render. Nothing awaits the network.
|
|
10
10
|
*
|
|
11
|
+
* The cached answer is also treated as stale when the running version moved
|
|
12
|
+
* past the `latest` it holds: installing 3.59 proves a cache that says 3.57 is
|
|
13
|
+
* behind the registry, and waiting out the interval left a fresh install
|
|
14
|
+
* unaware of releases that shipped in the meantime. Only that case counts. A
|
|
15
|
+
* downgrade, or a dev checkout rendering next to the global install, changes
|
|
16
|
+
* the running version without telling us anything about the registry, and
|
|
17
|
+
* reading it as stale made two copies re-check on every alternation.
|
|
18
|
+
*
|
|
11
19
|
* State lives next to the other user-data files:
|
|
12
20
|
* { checkedAt: <ms>, latest: "3.25.0", current: "3.24.0",
|
|
13
21
|
* dismissedVersion: "3.25.0"|undefined,
|
|
@@ -42,6 +50,12 @@ const CANONICAL_NAME = 'sprag-cli';
|
|
|
42
50
|
const LEGACY_NAME = 'claude-token-saver';
|
|
43
51
|
const IS_LEGACY = PKG_NAME === LEGACY_NAME;
|
|
44
52
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h — the registry is not a health endpoint
|
|
53
|
+
/**
|
|
54
|
+
* Interval for the SessionStart caller. Session start happens a few times a day
|
|
55
|
+
* rather than several times a second, so it can afford to look more often than
|
|
56
|
+
* the statusline's 24h default and notice a release the same afternoon.
|
|
57
|
+
*/
|
|
58
|
+
export const SESSION_START_CHECK_INTERVAL_MS = 3 * 60 * 60 * 1000;
|
|
45
59
|
const FETCH_TIMEOUT_MS = 5000;
|
|
46
60
|
|
|
47
61
|
export function updateStatePath() {
|
|
@@ -89,12 +103,25 @@ export function isNewer(a, b) {
|
|
|
89
103
|
return false;
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
/** `opts.intervalMs` when it is a usable duration, else the 24h default. */
|
|
107
|
+
function checkInterval(opts) {
|
|
108
|
+
const ms = opts && opts.intervalMs;
|
|
109
|
+
return typeof ms === 'number' && Number.isFinite(ms) && ms >= 0 ? ms : CHECK_INTERVAL_MS;
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
/**
|
|
93
113
|
* The read-only accessor every render path uses.
|
|
94
114
|
*
|
|
115
|
+
* `stale` means "worth asking the registry again": the cached answer is older
|
|
116
|
+
* than the interval, or this copy was installed after the answer was recorded
|
|
117
|
+
* and is already newer than the `latest` it names. A cache that never recorded
|
|
118
|
+
* a version (written by an older release) is judged by age alone.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} currentVersion
|
|
121
|
+
* @param {{intervalMs?: number}} [opts] - how old a cached answer may get; defaults to 24h
|
|
95
122
|
* @returns {{current: string, latest: string|null, available: boolean, dismissed: boolean, stale: boolean}}
|
|
96
123
|
*/
|
|
97
|
-
export function updateStatus(currentVersion) {
|
|
124
|
+
export function updateStatus(currentVersion, opts) {
|
|
98
125
|
if (updateCheckDisabled()) {
|
|
99
126
|
return { current: currentVersion, latest: null, available: false, dismissed: false, stale: false };
|
|
100
127
|
}
|
|
@@ -102,6 +129,12 @@ export function updateStatus(currentVersion) {
|
|
|
102
129
|
const latest = typeof s.latest === 'string' ? s.latest : null;
|
|
103
130
|
const available = !!latest && isNewer(latest, currentVersion);
|
|
104
131
|
const age = Date.now() - (Number(s.checkedAt) || 0);
|
|
132
|
+
// Both halves are needed. The version test alone would stay true while the
|
|
133
|
+
// registry is unreachable, since the stamp below refreshes `current` but not
|
|
134
|
+
// `latest`; requiring a change since the stamp stops that from re-spawning
|
|
135
|
+
// on every render.
|
|
136
|
+
const versionChanged = typeof s.current === 'string' && s.current !== currentVersion;
|
|
137
|
+
const outrunCache = versionChanged && (!latest || isNewer(currentVersion, latest));
|
|
105
138
|
return {
|
|
106
139
|
current: currentVersion,
|
|
107
140
|
latest,
|
|
@@ -110,7 +143,7 @@ export function updateStatus(currentVersion) {
|
|
|
110
143
|
// of the session briefing until a newer one ships — otherwise "no thanks"
|
|
111
144
|
// means "ask me again in five minutes", forever.
|
|
112
145
|
dismissed: available && s.dismissedVersion === latest,
|
|
113
|
-
stale: age >=
|
|
146
|
+
stale: outrunCache || age >= checkInterval(opts),
|
|
114
147
|
// Only when they describe THIS version. A cache written for an earlier
|
|
115
148
|
// release would otherwise sell the wrong upgrade. Language is not checked
|
|
116
149
|
// here: a mismatch is corrected by the next refresh, and showing the other
|
|
@@ -123,14 +156,21 @@ export function updateStatus(currentVersion) {
|
|
|
123
156
|
* Fire the background refresh when the cached answer has aged out. Returns
|
|
124
157
|
* immediately in every case; the child is detached and unref'd so it cannot
|
|
125
158
|
* hold the statusline process open.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} currentVersion
|
|
161
|
+
* @param {{intervalMs?: number, spawnImpl?: Function}} [opts] - `intervalMs` as for
|
|
162
|
+
* updateStatus; `spawnImpl` replaces child_process.spawn (tests only)
|
|
126
163
|
*/
|
|
127
|
-
export function maybeSpawnUpdateCheck(currentVersion) {
|
|
164
|
+
export function maybeSpawnUpdateCheck(currentVersion, opts) {
|
|
128
165
|
if (updateCheckDisabled()) return false;
|
|
129
|
-
const { stale } = updateStatus(currentVersion);
|
|
166
|
+
const { stale } = updateStatus(currentVersion, opts);
|
|
130
167
|
if (!stale) return false;
|
|
168
|
+
const spawnChild = (opts && opts.spawnImpl) || spawn;
|
|
131
169
|
// Stamp the attempt before spawning. Without this, an offline machine
|
|
132
170
|
// re-spawns a doomed child on every single statusline render — several per
|
|
133
|
-
// second — because the cache never gets a fresh timestamp.
|
|
171
|
+
// second — because the cache never gets a fresh timestamp. The stamp carries
|
|
172
|
+
// `current` too, which is what stops an upgrade from reading as stale again
|
|
173
|
+
// on the very next render.
|
|
134
174
|
try {
|
|
135
175
|
writeUpdateState({ ...readUpdateState(), checkedAt: Date.now(), current: currentVersion });
|
|
136
176
|
} catch (e) {
|
|
@@ -138,7 +178,7 @@ export function maybeSpawnUpdateCheck(currentVersion) {
|
|
|
138
178
|
return false;
|
|
139
179
|
}
|
|
140
180
|
try {
|
|
141
|
-
|
|
181
|
+
spawnChild(process.execPath, [cliEntryPath(), 'update-check', '--refresh', '--quiet'], {
|
|
142
182
|
detached: true,
|
|
143
183
|
stdio: 'ignore',
|
|
144
184
|
// Without this Windows flashes a console window, and this one
|