@phnx-labs/agents-cli 1.20.14 → 1.20.16
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 +5 -0
- package/dist/commands/repo.js +22 -1
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/sessions-sync.d.ts +13 -0
- package/dist/commands/sessions-sync.js +73 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/view.js +11 -3
- package/dist/index.js +1 -1
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +11 -9
- package/dist/lib/browser/service.js +28 -18
- package/dist/lib/daemon.d.ts +19 -0
- package/dist/lib/daemon.js +97 -2
- package/dist/lib/migrate.d.ts +22 -0
- package/dist/lib/migrate.js +99 -1
- package/dist/lib/plugin-marketplace.d.ts +15 -0
- package/dist/lib/plugin-marketplace.js +44 -0
- package/dist/lib/secrets/index.js +20 -0
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +168 -2
- package/dist/lib/session/sync/agents.d.ts +46 -0
- package/dist/lib/session/sync/agents.js +94 -0
- package/dist/lib/session/sync/config.d.ts +30 -0
- package/dist/lib/session/sync/config.js +58 -0
- package/dist/lib/session/sync/crdt.d.ts +44 -0
- package/dist/lib/session/sync/crdt.js +119 -0
- package/dist/lib/session/sync/manifest.d.ts +51 -0
- package/dist/lib/session/sync/manifest.js +96 -0
- package/dist/lib/session/sync/r2.d.ts +32 -0
- package/dist/lib/session/sync/r2.js +121 -0
- package/dist/lib/session/sync/sync.d.ts +82 -0
- package/dist/lib/session/sync/sync.js +251 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +29 -3
- package/dist/lib/state.d.ts +18 -0
- package/dist/lib/state.js +73 -0
- package/dist/lib/teams/parsers.js +159 -1
- package/dist/lib/usage.d.ts +18 -0
- package/dist/lib/usage.js +25 -0
- package/dist/lib/versions.js +30 -13
- package/package.json +2 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-machine session sync orchestration.
|
|
3
|
+
*
|
|
4
|
+
* PUSH this machine's transcripts (changed since last tick) to its own R2
|
|
5
|
+
* prefix, then publish a manifest. Single-writer prefixes mean no two
|
|
6
|
+
* machines ever write the same object — zero remote contention.
|
|
7
|
+
*
|
|
8
|
+
* PULL every other machine's manifest, fetch changed transcripts, CRDT-union
|
|
9
|
+
* copies of the same session, and write the result into the local mirror
|
|
10
|
+
* (a scan root). The existing scanner indexes it; sessions present in the
|
|
11
|
+
* live home always win, so the mirror only fills in remote-origin sessions.
|
|
12
|
+
*
|
|
13
|
+
* Invariants:
|
|
14
|
+
* - Live home transcripts are only ever READ + uploaded, never rewritten.
|
|
15
|
+
* - Union is idempotent, so once sets match nothing is transferred (quiescence).
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { R2Client } from './r2.js';
|
|
20
|
+
import { loadR2Config, machineId } from './config.js';
|
|
21
|
+
import { SYNC_AGENTS, listLocalTranscripts, localSessionIds, mirrorPath, objectKey, manifestKey, SESSIONS_PREFIX, } from './agents.js';
|
|
22
|
+
import { mergeTranscripts, transcriptStats } from './crdt.js';
|
|
23
|
+
import { emptyManifest, parseManifest, hashContent, loadLedger, saveLedger, ledgerUnchanged, ledgerRecord, loadLocalManifest, saveLocalManifest, loadPullState, savePullState, sourceSignature, } from './manifest.js';
|
|
24
|
+
const nowIso = () => new Date().toISOString();
|
|
25
|
+
function specById(id) {
|
|
26
|
+
return SYNC_AGENTS.find(s => s.id === id);
|
|
27
|
+
}
|
|
28
|
+
/** Upload this machine's changed transcripts and publish its manifest. */
|
|
29
|
+
async function pushOwn(r2, me, opts, result) {
|
|
30
|
+
const ledger = loadLedger();
|
|
31
|
+
const prev = loadLocalManifest();
|
|
32
|
+
const manifest = emptyManifest(me, nowIso());
|
|
33
|
+
for (const spec of SYNC_AGENTS) {
|
|
34
|
+
const agentManifest = {};
|
|
35
|
+
for (const t of listLocalTranscripts(spec)) {
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = fs.statSync(t.absPath);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const prevEntry = prev?.agents?.[spec.id]?.[t.sessionId];
|
|
44
|
+
if (prevEntry && ledgerUnchanged(ledger, t.absPath, stat.size, stat.mtimeMs)) {
|
|
45
|
+
agentManifest[t.sessionId] = prevEntry; // unchanged: reuse, no read, no upload
|
|
46
|
+
result.pushSkipped++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let content;
|
|
50
|
+
try {
|
|
51
|
+
content = fs.readFileSync(t.absPath, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const hash = hashContent(content);
|
|
57
|
+
const { lastTs } = transcriptStats(content);
|
|
58
|
+
const entry = { relKey: t.relKey, size: stat.size, hash, lastTs };
|
|
59
|
+
try {
|
|
60
|
+
await r2.put(objectKey(me, spec.id, t.sessionId), content, 'application/x-ndjson');
|
|
61
|
+
ledgerRecord(ledger, t.absPath, stat.size, stat.mtimeMs, hash);
|
|
62
|
+
agentManifest[t.sessionId] = entry;
|
|
63
|
+
result.pushed++;
|
|
64
|
+
if (opts.verbose)
|
|
65
|
+
opts.log?.(` push ${spec.id}/${t.sessionId.slice(0, 8)} (${stat.size}B)`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
result.errors.push(`push ${spec.id}/${t.sessionId}: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(agentManifest).length > 0)
|
|
72
|
+
manifest.agents[spec.id] = agentManifest;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await r2.put(manifestKey(me), JSON.stringify(manifest), 'application/json');
|
|
76
|
+
saveLocalManifest(manifest);
|
|
77
|
+
saveLedger(ledger);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
result.errors.push(`manifest publish: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decide which remote sessions need fetching this tick. Pure: no I/O.
|
|
85
|
+
* Skips sessions we hold locally (home wins) and sessions whose source set is
|
|
86
|
+
* unchanged since we last materialized them (pull-state hit → quiescence).
|
|
87
|
+
*/
|
|
88
|
+
export function selectSessionsToFetch(copies, localIdsByAgent, pullState) {
|
|
89
|
+
const pending = [];
|
|
90
|
+
for (const [agentId, byAgent] of copies) {
|
|
91
|
+
const localIds = localIdsByAgent.get(agentId) ?? new Set();
|
|
92
|
+
for (const [sessionId, list] of byAgent) {
|
|
93
|
+
if (localIds.has(sessionId))
|
|
94
|
+
continue;
|
|
95
|
+
const sig = sourceSignature(list.map(c => c.entry.hash));
|
|
96
|
+
if (pullState[`${agentId}/${sessionId}`] === sig)
|
|
97
|
+
continue;
|
|
98
|
+
pending.push({ agentId, sessionId, copies: list, sig });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return pending;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the mirror destination + merged content for one session. Pure.
|
|
105
|
+
* The canonical path comes from the lexicographically-smallest machine so every
|
|
106
|
+
* puller derives an identical location; the content is the CRDT union of copies.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveMirrorWrite(spec, copies, contents) {
|
|
109
|
+
const canonical = [...copies].sort((a, b) => (a.machine < b.machine ? -1 : a.machine > b.machine ? 1 : 0))[0];
|
|
110
|
+
const content = contents.length === 1 ? contents[0] : mergeTranscripts(contents);
|
|
111
|
+
return {
|
|
112
|
+
dest: mirrorPath(spec, canonical.machine, canonical.entry.relKey),
|
|
113
|
+
content,
|
|
114
|
+
merged: contents.length > 1,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decide how to reconcile one session's fetched copies. Pure: no I/O.
|
|
119
|
+
*
|
|
120
|
+
* `fetched` is positionally aligned to `copies` — a `null` slot means that
|
|
121
|
+
* copy's object wasn't retrievable this tick (R2 404 / LIST→GET consistency
|
|
122
|
+
* lag / a transient get error). If ANY listed copy is missing, returns `null`:
|
|
123
|
+
* the caller must then skip the mirror write AND skip stamping pull-state, so
|
|
124
|
+
* the session is retried next tick. Writing a partial set instead would
|
|
125
|
+
* materialize a non-converged union and — because pull-state would record the
|
|
126
|
+
* full source signature — abandon the missing branch forever (the bug this
|
|
127
|
+
* guards: a signature match in selectSessionsToFetch never re-fetches it).
|
|
128
|
+
*/
|
|
129
|
+
export function reconcileCopies(spec, copies, fetched) {
|
|
130
|
+
const contents = [];
|
|
131
|
+
for (const text of fetched) {
|
|
132
|
+
if (text === null)
|
|
133
|
+
return null; // incomplete fetch — retry next tick
|
|
134
|
+
contents.push(text);
|
|
135
|
+
}
|
|
136
|
+
if (contents.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
return resolveMirrorWrite(spec, copies, contents);
|
|
139
|
+
}
|
|
140
|
+
/** Fetch other machines' manifests, union changed sessions into the mirror. */
|
|
141
|
+
async function pullAndReconcile(r2, me, opts, result) {
|
|
142
|
+
const prefixes = await r2.listPrefixes(SESSIONS_PREFIX); // sessions/<machine>/
|
|
143
|
+
const machines = prefixes
|
|
144
|
+
.map(p => p.slice(SESSIONS_PREFIX.length).replace(/\/$/, ''))
|
|
145
|
+
.filter(m => m && m !== me);
|
|
146
|
+
// agentId -> sessionId -> copies across machines
|
|
147
|
+
const copies = new Map();
|
|
148
|
+
for (const m of machines) {
|
|
149
|
+
let manifest = null;
|
|
150
|
+
try {
|
|
151
|
+
const text = await r2.get(manifestKey(m));
|
|
152
|
+
manifest = text ? parseManifest(text) : null;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
result.errors.push(`fetch manifest ${m}: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
if (!manifest)
|
|
158
|
+
continue;
|
|
159
|
+
for (const [agentId, sessions] of Object.entries(manifest.agents)) {
|
|
160
|
+
if (!specById(agentId))
|
|
161
|
+
continue;
|
|
162
|
+
let byAgent = copies.get(agentId);
|
|
163
|
+
if (!byAgent)
|
|
164
|
+
copies.set(agentId, (byAgent = new Map()));
|
|
165
|
+
for (const [sessionId, entry] of Object.entries(sessions)) {
|
|
166
|
+
const list = byAgent.get(sessionId) ?? [];
|
|
167
|
+
list.push({ machine: m, entry });
|
|
168
|
+
byAgent.set(sessionId, list);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const pullState = loadPullState();
|
|
173
|
+
const localIdsByAgent = new Map();
|
|
174
|
+
for (const agentId of copies.keys()) {
|
|
175
|
+
const spec = specById(agentId);
|
|
176
|
+
if (spec)
|
|
177
|
+
localIdsByAgent.set(agentId, localSessionIds(spec));
|
|
178
|
+
}
|
|
179
|
+
const pending = selectSessionsToFetch(copies, localIdsByAgent, pullState);
|
|
180
|
+
let candidates = 0;
|
|
181
|
+
for (const byAgent of copies.values())
|
|
182
|
+
candidates += byAgent.size;
|
|
183
|
+
result.pullSkipped += candidates - pending.length; // local-owned or unchanged
|
|
184
|
+
for (const { agentId, sessionId, copies: list, sig } of pending) {
|
|
185
|
+
const spec = specById(agentId);
|
|
186
|
+
// Download each copy (could be a fork across >1 machine), keeping the
|
|
187
|
+
// result positionally aligned to `list` — null marks a copy we couldn't
|
|
188
|
+
// fetch this tick (404 / consistency lag / error).
|
|
189
|
+
const fetched = [];
|
|
190
|
+
for (const c of list) {
|
|
191
|
+
try {
|
|
192
|
+
fetched.push(await r2.get(objectKey(c.machine, agentId, sessionId)));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
result.errors.push(`get ${c.machine}/${sessionId}: ${err.message}`);
|
|
196
|
+
fetched.push(null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// null ⇒ an incomplete fetch: skip the write AND the pull-state stamp so we
|
|
200
|
+
// retry next tick instead of persisting a partial union / abandoning a branch.
|
|
201
|
+
const resolved = reconcileCopies(spec, list, fetched);
|
|
202
|
+
if (!resolved)
|
|
203
|
+
continue;
|
|
204
|
+
const { dest, content, merged } = resolved;
|
|
205
|
+
try {
|
|
206
|
+
let existing = null;
|
|
207
|
+
try {
|
|
208
|
+
existing = fs.readFileSync(dest, 'utf-8');
|
|
209
|
+
}
|
|
210
|
+
catch { /* not present yet */ }
|
|
211
|
+
if (existing !== content) {
|
|
212
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
213
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
214
|
+
if (merged)
|
|
215
|
+
result.merged++;
|
|
216
|
+
result.pulled++;
|
|
217
|
+
if (opts.verbose) {
|
|
218
|
+
opts.log?.(` pull ${agentId}/${sessionId.slice(0, 8)} <- ${list.map(c => c.machine).join('+')}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
result.pullSkipped++;
|
|
223
|
+
}
|
|
224
|
+
pullState[`${agentId}/${sessionId}`] = sig;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
result.errors.push(`write mirror ${sessionId}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
savePullState(pullState);
|
|
231
|
+
}
|
|
232
|
+
/** Run one full sync cycle: push this machine's changes, pull everyone else's. */
|
|
233
|
+
export async function syncSessions(opts = {}) {
|
|
234
|
+
const cfg = loadR2Config();
|
|
235
|
+
const me = machineId();
|
|
236
|
+
const r2 = new R2Client(cfg);
|
|
237
|
+
const result = {
|
|
238
|
+
machine: me,
|
|
239
|
+
pushed: 0,
|
|
240
|
+
pushSkipped: 0,
|
|
241
|
+
pulled: 0,
|
|
242
|
+
merged: 0,
|
|
243
|
+
pullSkipped: 0,
|
|
244
|
+
errors: [],
|
|
245
|
+
};
|
|
246
|
+
if (opts.push !== false)
|
|
247
|
+
await pushOwn(r2, me, opts, result);
|
|
248
|
+
if (opts.pull !== false)
|
|
249
|
+
await pullAndReconcile(r2, me, opts, result);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 19;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -202,7 +202,7 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
202
202
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
203
203
|
* trigger auto-resync, run `agents sync` for that.
|
|
204
204
|
*/
|
|
205
|
-
export const SHIM_SCHEMA_VERSION =
|
|
205
|
+
export const SHIM_SCHEMA_VERSION = 19;
|
|
206
206
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
207
207
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
208
208
|
function shellQuote(value) {
|
|
@@ -414,6 +414,22 @@ elif [ "$AGENT" = "kimi" ]; then
|
|
|
414
414
|
# Last resort: whatever is on PATH
|
|
415
415
|
BINARY=$(command -v kimi 2>/dev/null || echo "")
|
|
416
416
|
fi
|
|
417
|
+
# Droid (Factory AI) special case: the official installer drops a standalone
|
|
418
|
+
# native binary at ~/.local/bin/droid — there is no npm package and nothing
|
|
419
|
+
# lands in node_modules/.bin. Resolve the fixed install path directly. The
|
|
420
|
+
# PATH fallback explicitly refuses anything under our own shims dir: that path
|
|
421
|
+
# IS this dispatcher, so exec'ing it would re-enter and spin in an infinite
|
|
422
|
+
# re-exec loop (the bug this branch fixes).
|
|
423
|
+
elif [ "$AGENT" = "droid" ]; then
|
|
424
|
+
DROID_BINARY="$HOME/.local/bin/droid"
|
|
425
|
+
if [ -x "$DROID_BINARY" ]; then
|
|
426
|
+
BINARY="$DROID_BINARY"
|
|
427
|
+
else
|
|
428
|
+
BINARY=$(command -v droid 2>/dev/null || echo "")
|
|
429
|
+
case "$BINARY" in
|
|
430
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
431
|
+
esac
|
|
432
|
+
fi
|
|
417
433
|
else
|
|
418
434
|
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
419
435
|
fi
|
|
@@ -685,7 +701,10 @@ export function ensureVersionedAliasCurrent(agent, version) {
|
|
|
685
701
|
createVersionedAlias(agent, version);
|
|
686
702
|
return 'created';
|
|
687
703
|
}
|
|
688
|
-
|
|
704
|
+
// Upgrade-only (newest-wins), same rationale as ensureShimCurrent: never
|
|
705
|
+
// downgrade an alias stamped by a newer install sharing the shims dir.
|
|
706
|
+
const onDisk = readVersionedAliasSchemaVersion(agent, version);
|
|
707
|
+
if (onDisk === null || onDisk < VERSIONED_ALIAS_SCHEMA_VERSION) {
|
|
689
708
|
createVersionedAlias(agent, version);
|
|
690
709
|
return 'updated';
|
|
691
710
|
}
|
|
@@ -1278,7 +1297,14 @@ export function ensureShimCurrent(agent) {
|
|
|
1278
1297
|
createShim(agent);
|
|
1279
1298
|
return 'created';
|
|
1280
1299
|
}
|
|
1281
|
-
|
|
1300
|
+
// Upgrade-only (newest-wins): regenerate only when the on-disk shim is
|
|
1301
|
+
// unversioned/unreadable (null) or OLDER than this binary. Never downgrade a
|
|
1302
|
+
// shim stamped by a NEWER agents-cli install. Two installs at different
|
|
1303
|
+
// SHIM_SCHEMA_VERSION sharing ~/.agents/.cache/shims/ (e.g. a dev build on
|
|
1304
|
+
// PATH alongside a Hermes-bundled published copy) otherwise ping-pong —
|
|
1305
|
+
// rewriting every shim on each alternating launch and adding boot latency.
|
|
1306
|
+
const onDisk = readShimSchemaVersion(agent);
|
|
1307
|
+
if (onDisk === null || onDisk < SHIM_SCHEMA_VERSION) {
|
|
1282
1308
|
createShim(agent);
|
|
1283
1309
|
return 'updated';
|
|
1284
1310
|
}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -247,6 +247,24 @@ export declare function recordVersionResources(_agent: AgentId, _version: string
|
|
|
247
247
|
* Pass all resource types you want to initialize in one call to batch the write.
|
|
248
248
|
*/
|
|
249
249
|
export declare function ensureVersionResourcePatterns(agent: AgentId, version: string, updates: Partial<Record<Exclude<keyof VersionResources, 'rulesPreset'>, ResourcePattern[]>>): void;
|
|
250
|
+
/**
|
|
251
|
+
* Insert `<alias>:*` at the canonical position (after the system/user/other-extra
|
|
252
|
+
* includes, before `project:*`), unless the alias is already referenced — as an
|
|
253
|
+
* include (`alias:...`) or an exclude (`!alias:...`). Returns a new array when it
|
|
254
|
+
* changes, otherwise the same reference (so callers can detect no-ops cheaply).
|
|
255
|
+
*/
|
|
256
|
+
export declare function withAlias(list: ResourcePattern[], alias: string): ResourcePattern[];
|
|
257
|
+
/** Strip every reference to `<alias>:...` / `!<alias>:...` from a selector list. */
|
|
258
|
+
export declare function withoutAlias(list: ResourcePattern[], alias: string): ResourcePattern[];
|
|
259
|
+
/**
|
|
260
|
+
* Backfill (add=true) or strip (add=false) an extra-repo alias across every
|
|
261
|
+
* already-installed version's selectors. New versions get the alias via
|
|
262
|
+
* `defaultPatterns()` at scaffold time; this keeps existing versions in sync
|
|
263
|
+
* when an extra repo is registered/enabled or removed. Only touches selector
|
|
264
|
+
* lists that are already set — an unset list is left for `defaultPatterns()`.
|
|
265
|
+
* Returns the number of (agent, version) pairs changed.
|
|
266
|
+
*/
|
|
267
|
+
export declare function applyExtraAliasToVersions(alias: string, add: boolean): number;
|
|
250
268
|
export declare function getVersionResources(agent: AgentId, version: string): VersionResources | null;
|
|
251
269
|
/** Active rules preset for an agent@version. Defaults to "default" when unset. */
|
|
252
270
|
export declare function getActiveRulesPreset(agent: AgentId, version: string): string;
|
package/dist/lib/state.js
CHANGED
|
@@ -711,6 +711,79 @@ export function ensureVersionResourcePatterns(agent, version, updates) {
|
|
|
711
711
|
if (changed)
|
|
712
712
|
writeMeta(meta);
|
|
713
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Resource types that resolve across the extra-repo layer. Mirrors
|
|
716
|
+
* `defaultPatterns()`: extras feed commands/skills/hooks/subagents/plugins/
|
|
717
|
+
* workflows, but never permissions (`system:*`) or mcp (`user:*`).
|
|
718
|
+
*/
|
|
719
|
+
const EXTRA_ELIGIBLE_TYPES = [
|
|
720
|
+
'commands', 'skills', 'hooks', 'subagents', 'plugins', 'workflows',
|
|
721
|
+
];
|
|
722
|
+
/**
|
|
723
|
+
* Insert `<alias>:*` at the canonical position (after the system/user/other-extra
|
|
724
|
+
* includes, before `project:*`), unless the alias is already referenced — as an
|
|
725
|
+
* include (`alias:...`) or an exclude (`!alias:...`). Returns a new array when it
|
|
726
|
+
* changes, otherwise the same reference (so callers can detect no-ops cheaply).
|
|
727
|
+
*/
|
|
728
|
+
export function withAlias(list, alias) {
|
|
729
|
+
const prefix = `${alias}:`;
|
|
730
|
+
if (list.some(p => p === `${alias}:*` || p.startsWith(prefix) || p.startsWith(`!${prefix}`))) {
|
|
731
|
+
return list;
|
|
732
|
+
}
|
|
733
|
+
const next = [...list];
|
|
734
|
+
const projIdx = next.findIndex(p => p === 'project:*' || p.startsWith('project:'));
|
|
735
|
+
if (projIdx >= 0)
|
|
736
|
+
next.splice(projIdx, 0, `${alias}:*`);
|
|
737
|
+
else
|
|
738
|
+
next.push(`${alias}:*`);
|
|
739
|
+
return next;
|
|
740
|
+
}
|
|
741
|
+
/** Strip every reference to `<alias>:...` / `!<alias>:...` from a selector list. */
|
|
742
|
+
export function withoutAlias(list, alias) {
|
|
743
|
+
const prefix = `${alias}:`;
|
|
744
|
+
const next = list.filter(p => !(p.startsWith(prefix) || p.startsWith(`!${prefix}`)));
|
|
745
|
+
return next.length === list.length ? list : next;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Backfill (add=true) or strip (add=false) an extra-repo alias across every
|
|
749
|
+
* already-installed version's selectors. New versions get the alias via
|
|
750
|
+
* `defaultPatterns()` at scaffold time; this keeps existing versions in sync
|
|
751
|
+
* when an extra repo is registered/enabled or removed. Only touches selector
|
|
752
|
+
* lists that are already set — an unset list is left for `defaultPatterns()`.
|
|
753
|
+
* Returns the number of (agent, version) pairs changed.
|
|
754
|
+
*/
|
|
755
|
+
export function applyExtraAliasToVersions(alias, add) {
|
|
756
|
+
const meta = readMeta();
|
|
757
|
+
if (!meta.versions)
|
|
758
|
+
return 0;
|
|
759
|
+
let changed = false;
|
|
760
|
+
let count = 0;
|
|
761
|
+
for (const versions of Object.values(meta.versions)) {
|
|
762
|
+
if (!versions)
|
|
763
|
+
continue;
|
|
764
|
+
for (const vr of Object.values(versions)) {
|
|
765
|
+
if (!vr)
|
|
766
|
+
continue;
|
|
767
|
+
let touched = false;
|
|
768
|
+
for (const type of EXTRA_ELIGIBLE_TYPES) {
|
|
769
|
+
const cur = vr[type];
|
|
770
|
+
if (!Array.isArray(cur) || cur.length === 0)
|
|
771
|
+
continue;
|
|
772
|
+
const next = add ? withAlias(cur, alias) : withoutAlias(cur, alias);
|
|
773
|
+
if (next !== cur) {
|
|
774
|
+
vr[type] = next;
|
|
775
|
+
touched = true;
|
|
776
|
+
changed = true;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (touched)
|
|
780
|
+
count++;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (changed)
|
|
784
|
+
writeMeta(meta);
|
|
785
|
+
return count;
|
|
786
|
+
}
|
|
714
787
|
export function getVersionResources(agent, version) {
|
|
715
788
|
const meta = readMeta();
|
|
716
789
|
return meta.versions?.[agent]?.[version] || null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Agent event stream parsers.
|
|
3
3
|
*
|
|
4
4
|
* Normalizes the heterogeneous JSON event formats emitted by each agent CLI
|
|
5
|
-
* (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity) into a unified
|
|
5
|
+
* (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity, Kimi) into a unified
|
|
6
6
|
* event schema with consistent types: init, message, tool_use, bash,
|
|
7
7
|
* file_read, file_write, file_create, file_delete, result, error, and others.
|
|
8
8
|
*/
|
|
@@ -31,6 +31,9 @@ export function normalizeEvents(agentType, raw) {
|
|
|
31
31
|
else if (agentType === 'antigravity') {
|
|
32
32
|
return normalizeAntigravity(raw);
|
|
33
33
|
}
|
|
34
|
+
else if (agentType === 'kimi') {
|
|
35
|
+
return normalizeKimi(raw);
|
|
36
|
+
}
|
|
34
37
|
// droid (Factory AI) intentionally falls through to the generic normalizer
|
|
35
38
|
// below: its `-o stream-json` JSONL event schema is not yet verified against
|
|
36
39
|
// a live run (the documented `debug` format differs from stream-json). Events
|
|
@@ -908,6 +911,161 @@ function normalizeGrok(raw) {
|
|
|
908
911
|
timestamp: timestamp,
|
|
909
912
|
}];
|
|
910
913
|
}
|
|
914
|
+
// --- Kimi parsing ---
|
|
915
|
+
// Kimi's `--output-format stream-json` emits one JSON object per line with a
|
|
916
|
+
// simple `role`-based schema:
|
|
917
|
+
// - {"role":"assistant","content":"..."} → final message
|
|
918
|
+
// - {"role":"assistant","tool_calls":[{"function":{"name":"Bash","arguments":"<json>"}}]} → tool use
|
|
919
|
+
// - {"role":"tool","tool_call_id":"...","content":"..."} → tool result
|
|
920
|
+
// - {"role":"meta","type":"session.resume_hint","session_id":"..."} → init / session id
|
|
921
|
+
// Tool arguments are JSON-stringified inside `function.arguments` and must be
|
|
922
|
+
// parsed before extracting paths/commands. Verified against live `kimi` runs.
|
|
923
|
+
function normalizeKimi(raw) {
|
|
924
|
+
const timestamp = new Date().toISOString();
|
|
925
|
+
if (!raw || typeof raw !== 'object') {
|
|
926
|
+
return [{
|
|
927
|
+
type: 'unknown',
|
|
928
|
+
agent: 'kimi',
|
|
929
|
+
raw: raw,
|
|
930
|
+
timestamp: timestamp,
|
|
931
|
+
}];
|
|
932
|
+
}
|
|
933
|
+
const role = typeof raw.role === 'string' ? raw.role : '';
|
|
934
|
+
// Assistant message (final answer or tool-call request).
|
|
935
|
+
if (role === 'assistant') {
|
|
936
|
+
const events = [];
|
|
937
|
+
if (typeof raw.content === 'string' && raw.content) {
|
|
938
|
+
events.push({
|
|
939
|
+
type: 'message',
|
|
940
|
+
agent: 'kimi',
|
|
941
|
+
content: raw.content,
|
|
942
|
+
complete: true,
|
|
943
|
+
timestamp: timestamp,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const toolCalls = Array.isArray(raw.tool_calls) ? raw.tool_calls : [];
|
|
947
|
+
for (const toolCall of toolCalls) {
|
|
948
|
+
const fn = toolCall?.function || {};
|
|
949
|
+
const toolName = typeof fn.name === 'string' ? fn.name : 'unknown';
|
|
950
|
+
let toolArgs = {};
|
|
951
|
+
if (typeof fn.arguments === 'string') {
|
|
952
|
+
try {
|
|
953
|
+
toolArgs = JSON.parse(fn.arguments);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
toolArgs = { _raw: fn.arguments };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else if (fn.arguments && typeof fn.arguments === 'object') {
|
|
960
|
+
toolArgs = fn.arguments;
|
|
961
|
+
}
|
|
962
|
+
const filePath = toolArgs?.path || toolArgs?.file_path || '';
|
|
963
|
+
const command = toolArgs?.command || '';
|
|
964
|
+
// Map known tools to structured events. If a known tool is missing the
|
|
965
|
+
// fields we need (e.g. unparseable arguments), fall back to tool_use so
|
|
966
|
+
// the event is still visible in summaries rather than dropped.
|
|
967
|
+
let normalized = null;
|
|
968
|
+
if (toolName === 'Bash' && command) {
|
|
969
|
+
const bashEvents = [{
|
|
970
|
+
type: 'bash',
|
|
971
|
+
agent: 'kimi',
|
|
972
|
+
tool: toolName,
|
|
973
|
+
command: command,
|
|
974
|
+
timestamp: timestamp,
|
|
975
|
+
}];
|
|
976
|
+
const [filesRead, filesWritten, filesDeleted] = extractFileOpsFromBash(command);
|
|
977
|
+
for (const p of filesRead) {
|
|
978
|
+
bashEvents.push({ type: 'file_read', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
|
|
979
|
+
}
|
|
980
|
+
for (const p of filesWritten) {
|
|
981
|
+
bashEvents.push({ type: 'file_write', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
|
|
982
|
+
}
|
|
983
|
+
for (const p of filesDeleted) {
|
|
984
|
+
bashEvents.push({ type: 'file_delete', agent: 'kimi', tool: 'bash', path: p, command, timestamp });
|
|
985
|
+
}
|
|
986
|
+
normalized = bashEvents;
|
|
987
|
+
}
|
|
988
|
+
else if (toolName === 'Read' && filePath) {
|
|
989
|
+
normalized = [{
|
|
990
|
+
type: 'file_read',
|
|
991
|
+
agent: 'kimi',
|
|
992
|
+
tool: toolName,
|
|
993
|
+
path: filePath,
|
|
994
|
+
timestamp: timestamp,
|
|
995
|
+
}];
|
|
996
|
+
}
|
|
997
|
+
else if (toolName === 'Edit' && filePath) {
|
|
998
|
+
normalized = [{
|
|
999
|
+
type: 'file_write',
|
|
1000
|
+
agent: 'kimi',
|
|
1001
|
+
tool: toolName,
|
|
1002
|
+
path: filePath,
|
|
1003
|
+
timestamp: timestamp,
|
|
1004
|
+
}];
|
|
1005
|
+
}
|
|
1006
|
+
else if (toolName === 'Write' && filePath) {
|
|
1007
|
+
normalized = [{
|
|
1008
|
+
type: 'file_create',
|
|
1009
|
+
agent: 'kimi',
|
|
1010
|
+
tool: toolName,
|
|
1011
|
+
path: filePath,
|
|
1012
|
+
timestamp: timestamp,
|
|
1013
|
+
}];
|
|
1014
|
+
}
|
|
1015
|
+
if (normalized) {
|
|
1016
|
+
events.push(...normalized);
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
events.push({
|
|
1020
|
+
type: 'tool_use',
|
|
1021
|
+
agent: 'kimi',
|
|
1022
|
+
tool: toolName,
|
|
1023
|
+
args: toolArgs,
|
|
1024
|
+
timestamp: timestamp,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return events.length > 0 ? events : [];
|
|
1029
|
+
}
|
|
1030
|
+
// Tool result (response to an assistant tool_call).
|
|
1031
|
+
if (role === 'tool') {
|
|
1032
|
+
const content = typeof raw.content === 'string' ? raw.content : '';
|
|
1033
|
+
const success = raw.isError !== true && !(content && content.startsWith('Error:'));
|
|
1034
|
+
return [{
|
|
1035
|
+
type: 'tool_result',
|
|
1036
|
+
agent: 'kimi',
|
|
1037
|
+
tool_call_id: typeof raw.tool_call_id === 'string' ? raw.tool_call_id : null,
|
|
1038
|
+
success: success,
|
|
1039
|
+
content: content,
|
|
1040
|
+
timestamp: timestamp,
|
|
1041
|
+
}];
|
|
1042
|
+
}
|
|
1043
|
+
// Meta events (session lifecycle).
|
|
1044
|
+
if (role === 'meta') {
|
|
1045
|
+
const metaType = typeof raw.type === 'string' ? raw.type : '';
|
|
1046
|
+
if (metaType === 'session.resume_hint') {
|
|
1047
|
+
return [{
|
|
1048
|
+
type: 'init',
|
|
1049
|
+
agent: 'kimi',
|
|
1050
|
+
session_id: typeof raw.session_id === 'string' ? raw.session_id : null,
|
|
1051
|
+
timestamp: timestamp,
|
|
1052
|
+
}];
|
|
1053
|
+
}
|
|
1054
|
+
return [{
|
|
1055
|
+
type: 'meta',
|
|
1056
|
+
agent: 'kimi',
|
|
1057
|
+
meta_type: metaType,
|
|
1058
|
+
raw: raw,
|
|
1059
|
+
timestamp: timestamp,
|
|
1060
|
+
}];
|
|
1061
|
+
}
|
|
1062
|
+
return [{
|
|
1063
|
+
type: raw.type || 'unknown',
|
|
1064
|
+
agent: 'kimi',
|
|
1065
|
+
raw: raw,
|
|
1066
|
+
timestamp: timestamp,
|
|
1067
|
+
}];
|
|
1068
|
+
}
|
|
911
1069
|
// --- Antigravity parsing ---
|
|
912
1070
|
// Intentionally conservative. Antigravity's `agy` binary advertises an
|
|
913
1071
|
// `--output-format json` flag in its docs, but the released binary errors with
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -85,6 +85,24 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
|
|
|
85
85
|
export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Promise<UsageInfo>;
|
|
86
86
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
87
87
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
|
|
88
|
+
/**
|
|
89
|
+
* Derive an account's real throttle state from its live usage windows — the
|
|
90
|
+
* same signal `agents usage` shows and balanced rotation trusts
|
|
91
|
+
* (`getRoutingUsedPercent` in rotate.ts). A window at 100% utilization means
|
|
92
|
+
* the account is throttled until that window resets.
|
|
93
|
+
*
|
|
94
|
+
* Returns `null` when there is no snapshot, so callers render no badge rather
|
|
95
|
+
* than a misleading one. This deliberately never consults
|
|
96
|
+
* `cachedExtraUsageDisabledReason`: that field describes why pay-as-you-go
|
|
97
|
+
* overage is disabled (`out_of_credits` = no overage credits purchased,
|
|
98
|
+
* `org_level_disabled` = an admin turned overage off), NOT whether the account
|
|
99
|
+
* can do work right now. A Pro account at 5% weekly usage with overage disabled
|
|
100
|
+
* is fully usable, yet that flag would mislabel it "out of credits".
|
|
101
|
+
*
|
|
102
|
+
* The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
|
|
103
|
+
* one model, not the account, so it shouldn't flip the whole row to throttled.
|
|
104
|
+
*/
|
|
105
|
+
export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined): 'available' | 'rate_limited' | null;
|
|
88
106
|
/**
|
|
89
107
|
* Compact colored badge for the account's overall usage status. Renders only
|
|
90
108
|
* when the account is throttled — `available` and `null` return ''.
|
package/dist/lib/usage.js
CHANGED
|
@@ -201,6 +201,31 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
201
201
|
}
|
|
202
202
|
return parts.join(' ');
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Derive an account's real throttle state from its live usage windows — the
|
|
206
|
+
* same signal `agents usage` shows and balanced rotation trusts
|
|
207
|
+
* (`getRoutingUsedPercent` in rotate.ts). A window at 100% utilization means
|
|
208
|
+
* the account is throttled until that window resets.
|
|
209
|
+
*
|
|
210
|
+
* Returns `null` when there is no snapshot, so callers render no badge rather
|
|
211
|
+
* than a misleading one. This deliberately never consults
|
|
212
|
+
* `cachedExtraUsageDisabledReason`: that field describes why pay-as-you-go
|
|
213
|
+
* overage is disabled (`out_of_credits` = no overage credits purchased,
|
|
214
|
+
* `org_level_disabled` = an admin turned overage off), NOT whether the account
|
|
215
|
+
* can do work right now. A Pro account at 5% weekly usage with overage disabled
|
|
216
|
+
* is fully usable, yet that flag would mislabel it "out of credits".
|
|
217
|
+
*
|
|
218
|
+
* The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
|
|
219
|
+
* one model, not the account, so it shouldn't flip the whole row to throttled.
|
|
220
|
+
*/
|
|
221
|
+
export function deriveUsageStatusFromSnapshot(snapshot) {
|
|
222
|
+
if (!snapshot || snapshot.windows.length === 0)
|
|
223
|
+
return null;
|
|
224
|
+
const blocking = snapshot.windows.filter((window) => window.key !== 'sonnet_week');
|
|
225
|
+
const windows = blocking.length > 0 ? blocking : snapshot.windows;
|
|
226
|
+
const maxUsed = Math.max(...windows.map((window) => window.usedPercent));
|
|
227
|
+
return maxUsed >= 100 ? 'rate_limited' : 'available';
|
|
228
|
+
}
|
|
204
229
|
/**
|
|
205
230
|
* Compact colored badge for the account's overall usage status. Renders only
|
|
206
231
|
* when the account is throttled — `available` and `null` return ''.
|