@phnx-labs/agents-cli 1.20.15 → 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/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/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 +17 -1
- 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
|
|
@@ -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 ''.
|
package/dist/lib/versions.js
CHANGED
|
@@ -25,7 +25,7 @@ import { checkbox, select } from '@inquirer/prompts';
|
|
|
25
25
|
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
26
26
|
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
27
27
|
import { listResources } from './resources.js';
|
|
28
|
-
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError } from './agents.js';
|
|
28
|
+
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath } from './agents.js';
|
|
29
29
|
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
30
30
|
import { parseMcpServerConfig } from './mcp.js';
|
|
31
31
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
@@ -818,6 +818,14 @@ export function getBinaryPath(agent, version) {
|
|
|
818
818
|
catch { }
|
|
819
819
|
return path.join(grokDownloads, `grok-${version}`);
|
|
820
820
|
}
|
|
821
|
+
if (agent === 'droid') {
|
|
822
|
+
// Factory's installer drops a standalone native binary at ~/.local/bin/droid
|
|
823
|
+
// (no npm package, nothing in node_modules/.bin). The binary is global, not
|
|
824
|
+
// per-version — config isolation rides the ~/.factory symlink switch, not a
|
|
825
|
+
// separate binary per version. Mirror the shim's `droid` branch so
|
|
826
|
+
// isVersionInstalled/`agents view` agree with what actually executes.
|
|
827
|
+
return path.join(os.homedir(), '.local', 'bin', 'droid');
|
|
828
|
+
}
|
|
821
829
|
const versionDir = getVersionDir(agent, version);
|
|
822
830
|
return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
|
|
823
831
|
}
|
|
@@ -1005,19 +1013,28 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1005
1013
|
// but `agents view` shows the agent under "Not Managed" because
|
|
1006
1014
|
// listInstalledVersions returns [] — the installer drops the binary in
|
|
1007
1015
|
// ~/.local/bin (or similar) rather than the version's node_modules/.bin.
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1016
|
+
//
|
|
1017
|
+
// Agents whose binary is special-cased in getBinaryPath (grok ->
|
|
1018
|
+
// ~/.grok/downloads, droid -> ~/.local/bin/droid) need no symlink — and
|
|
1019
|
+
// creating one is actively harmful: `which <cli>` can resolve to OUR OWN
|
|
1020
|
+
// dispatcher shim, because ~/.agents/.cache/shims sits ahead of ~/.local/bin
|
|
1021
|
+
// on PATH. Symlinking node_modules/.bin/<cli> at the shim makes the shim
|
|
1022
|
+
// exec itself forever. So we skip the resolver-backed agents here AND, for
|
|
1023
|
+
// everyone else, filter the shims dir out of the `which` candidates so the
|
|
1024
|
+
// same race can't bite a non-special-cased installScript agent.
|
|
1025
|
+
if (agent !== 'grok' && agent !== 'droid') {
|
|
1026
|
+
// findInPath is a pure-Node PATH scan that already skips our own shims
|
|
1027
|
+
// dir — so it returns the genuine install, never our dispatcher shim
|
|
1028
|
+
// (which sits ahead of ~/.local/bin on PATH and would otherwise be
|
|
1029
|
+
// captured, producing a self-referential node_modules/.bin/<cli> link
|
|
1030
|
+
// that exec-loops forever).
|
|
1031
|
+
const installedBinary = findInPath(agentConfig.cliCommand);
|
|
1032
|
+
if (installedBinary) {
|
|
1033
|
+
importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
|
|
1020
1034
|
}
|
|
1035
|
+
/* If null: binary missing from PATH (install script failed silently) or
|
|
1036
|
+
only our shim is present. Leave the version dir empty so getBinaryPath
|
|
1037
|
+
correctly reports it uninstalled. */
|
|
1021
1038
|
}
|
|
1022
1039
|
createVersionedAlias(agent, installedVersion);
|
|
1023
1040
|
emit('version.install', { agent, version: installedVersion });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.16",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"@types/proper-lockfile": "4.1.4",
|
|
83
83
|
"@xterm/headless": "6.0.0",
|
|
84
84
|
"@zed-industries/agent-client-protocol": "0.4.5",
|
|
85
|
+
"aws4fetch": "1.0.20",
|
|
85
86
|
"chalk": "5.6.2",
|
|
86
87
|
"commander": "15.0.0",
|
|
87
88
|
"croner": "10.0.1",
|