@phnx-labs/agents-cli 1.20.28 → 1.20.29
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/dist/commands/exec.js +22 -10
- package/dist/commands/secrets.js +93 -6
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/ssh.d.ts +14 -0
- package/dist/commands/ssh.js +263 -0
- package/dist/index.js +2 -1
- package/dist/lib/devices/connect.d.ts +34 -0
- package/dist/lib/devices/connect.js +101 -0
- package/dist/lib/devices/registry.d.ts +78 -0
- package/dist/lib/devices/registry.js +168 -0
- package/dist/lib/devices/ssh-config.d.ts +21 -0
- package/dist/lib/devices/ssh-config.js +33 -0
- package/dist/lib/devices/tailscale.d.ts +31 -0
- package/dist/lib/devices/tailscale.js +126 -0
- package/dist/lib/secrets/remote.d.ts +67 -0
- package/dist/lib/secrets/remote.js +133 -0
- package/dist/lib/session/db.d.ts +1 -0
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +228 -0
- package/dist/lib/session/parse.d.ts +7 -0
- package/dist/lib/session/parse.js +110 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/package.json +1 -1
package/dist/lib/session/db.js
CHANGED
|
@@ -290,9 +290,9 @@ export function getDBPath() {
|
|
|
290
290
|
export function getScanStampByPath(filePath) {
|
|
291
291
|
const db = getDB();
|
|
292
292
|
const row = db
|
|
293
|
-
.prepare(`SELECT file_mtime_ms, file_size FROM scan_ledger WHERE file_path = ? LIMIT 1`)
|
|
293
|
+
.prepare(`SELECT file_mtime_ms, file_size, scanned_at FROM scan_ledger WHERE file_path = ? LIMIT 1`)
|
|
294
294
|
.get(canonicalLedgerKey(filePath));
|
|
295
|
-
return row ? { fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size } : null;
|
|
295
|
+
return row ? { fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size, scannedAt: row.scanned_at } : null;
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
298
|
* Bulk-load the stamp ledger for a set of file paths in a single SQL query.
|
|
@@ -324,13 +324,13 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
324
324
|
const placeholders = chunk.map(() => '?').join(',');
|
|
325
325
|
const rows = db
|
|
326
326
|
.prepare(`
|
|
327
|
-
SELECT file_path, file_mtime_ms, file_size
|
|
327
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at
|
|
328
328
|
FROM scan_ledger
|
|
329
329
|
WHERE file_path IN (${placeholders})
|
|
330
330
|
`)
|
|
331
331
|
.all(...chunk);
|
|
332
332
|
for (const row of rows) {
|
|
333
|
-
const stamp = { fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size };
|
|
333
|
+
const stamp = { fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size, scannedAt: row.scanned_at };
|
|
334
334
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
335
335
|
result.set(original, stamp);
|
|
336
336
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* subsequent queries are served entirely from the cache.
|
|
8
8
|
*/
|
|
9
9
|
import type { SessionAgentId, SessionMeta } from './types.js';
|
|
10
|
+
import { type ScanStamp } from './db.js';
|
|
10
11
|
/** Options controlling which sessions to discover and how to report progress. */
|
|
11
12
|
export interface DiscoverOptions {
|
|
12
13
|
agent?: SessionAgentId;
|
|
@@ -85,6 +86,7 @@ export declare function resolveSessionById(sessions: SessionMeta[], idQuery: str
|
|
|
85
86
|
* preserving the existing SessionMeta[] contract so sessions.ts is unchanged.
|
|
86
87
|
*/
|
|
87
88
|
export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
|
|
89
|
+
export declare function shouldDeferRecentAppend(prev: ScanStamp, current: ScanStamp, nowMs: number, debounceMs?: number): boolean;
|
|
88
90
|
/**
|
|
89
91
|
* Collect all directories to scan for an agent's sessions. Deduplicates by
|
|
90
92
|
* realpath to avoid double-counting symlinked version homes.
|
|
@@ -34,6 +34,7 @@ const RUSH_SESSIONS_DIR = path.join(HOME, '.rush', 'sessions');
|
|
|
34
34
|
const HERMES_SESSIONS_DIR = path.join(HOME, '.hermes', 'sessions');
|
|
35
35
|
/** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
|
|
36
36
|
const OPENCLAW_TTL_MS = 60_000;
|
|
37
|
+
const ACTIVE_APPEND_RESCAN_DEBOUNCE_MS = 5_000;
|
|
37
38
|
let cachedOpenClawWorkspaces = null;
|
|
38
39
|
const cachedAgentVersions = new Map();
|
|
39
40
|
/**
|
|
@@ -63,6 +64,7 @@ export async function discoverSessions(options) {
|
|
|
63
64
|
case 'rush': return scanRushIncremental(onProgress);
|
|
64
65
|
case 'hermes': return scanHermesIncremental(onProgress);
|
|
65
66
|
case 'kimi': return scanKimiIncremental(onProgress);
|
|
67
|
+
case 'droid': return scanDroidIncremental(onProgress);
|
|
66
68
|
}
|
|
67
69
|
}));
|
|
68
70
|
}
|
|
@@ -164,10 +166,16 @@ export function searchContentIndex(sessions, query) {
|
|
|
164
166
|
/**
|
|
165
167
|
* For a list of files, stat each, compare to the DB ledger, and return only
|
|
166
168
|
* the ones that need rescanning. One bulk DB query for the whole list.
|
|
169
|
+
*
|
|
170
|
+
* Actively running agents append to their JSONL every few seconds. Without a
|
|
171
|
+
* small debounce, repeated `agents sessions` invocations stream-parse the same
|
|
172
|
+
* growing transcript over and over. The cached row is good enough for a few
|
|
173
|
+
* seconds; once writes settle or the debounce expires, the file is parsed once.
|
|
167
174
|
*/
|
|
168
175
|
function filterChangedFiles(filePaths) {
|
|
169
176
|
const ledger = getScanStampsForPaths(filePaths);
|
|
170
177
|
const out = [];
|
|
178
|
+
const now = Date.now();
|
|
171
179
|
for (const filePath of filePaths) {
|
|
172
180
|
const stat = safeStatSync(filePath);
|
|
173
181
|
if (!stat)
|
|
@@ -180,10 +188,22 @@ function filterChangedFiles(filePaths) {
|
|
|
180
188
|
if (prev && prev.fileMtimeMs === scan.fileMtimeMs && prev.fileSize === scan.fileSize) {
|
|
181
189
|
continue;
|
|
182
190
|
}
|
|
191
|
+
if (prev && shouldDeferRecentAppend(prev, scan, now)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
183
194
|
out.push({ filePath, scan });
|
|
184
195
|
}
|
|
185
196
|
return out;
|
|
186
197
|
}
|
|
198
|
+
export function shouldDeferRecentAppend(prev, current, nowMs, debounceMs = ACTIVE_APPEND_RESCAN_DEBOUNCE_MS) {
|
|
199
|
+
if (prev.scannedAt === undefined)
|
|
200
|
+
return false;
|
|
201
|
+
if (current.fileSize <= prev.fileSize)
|
|
202
|
+
return false;
|
|
203
|
+
if (current.fileMtimeMs < prev.fileMtimeMs)
|
|
204
|
+
return false;
|
|
205
|
+
return nowMs - prev.scannedAt < debounceMs;
|
|
206
|
+
}
|
|
187
207
|
// ---------------------------------------------------------------------------
|
|
188
208
|
// Multi-version directory scanning
|
|
189
209
|
// ---------------------------------------------------------------------------
|
|
@@ -1291,6 +1311,214 @@ function extractHermesMessageText(content) {
|
|
|
1291
1311
|
.join('\n')
|
|
1292
1312
|
.trim();
|
|
1293
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Incrementally re-scan changed Droid (Factory) session files and upsert into
|
|
1316
|
+
* the DB. Droid writes one `<uuid>.jsonl` transcript plus a sibling
|
|
1317
|
+
* `<uuid>.settings.json` (model + token usage) under
|
|
1318
|
+
* `~/.factory/sessions/<encoded-cwd>/`.
|
|
1319
|
+
*/
|
|
1320
|
+
async function scanDroidIncremental(onProgress) {
|
|
1321
|
+
const currentVersion = await getCurrentAgentVersion('droid');
|
|
1322
|
+
const filePaths = [];
|
|
1323
|
+
for (const sessionsDir of getAgentSessionDirs('droid', 'sessions')) {
|
|
1324
|
+
// High limit: we only stat files here, parsing is gated by ledger match.
|
|
1325
|
+
for (const fp of walkForFiles(sessionsDir, '.jsonl', 100_000)) {
|
|
1326
|
+
filePaths.push(fp);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const changed = filterChangedFiles(filePaths);
|
|
1330
|
+
if (changed.length === 0)
|
|
1331
|
+
return;
|
|
1332
|
+
onProgress?.({ agent: 'droid', parsed: 0, total: changed.length });
|
|
1333
|
+
const entries = [];
|
|
1334
|
+
const touched = [];
|
|
1335
|
+
const seen = new Set();
|
|
1336
|
+
let parsed = 0;
|
|
1337
|
+
for (const { filePath, scan } of changed) {
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await readDroidMeta(filePath, currentVersion);
|
|
1340
|
+
if (result && !seen.has(result.meta.id)) {
|
|
1341
|
+
seen.add(result.meta.id);
|
|
1342
|
+
entries.push({ meta: result.meta, content: result.content, scan });
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
touched.push({ filePath, scan });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
touched.push({ filePath, scan });
|
|
1350
|
+
}
|
|
1351
|
+
parsed++;
|
|
1352
|
+
onProgress?.({ agent: 'droid', parsed, total: changed.length });
|
|
1353
|
+
}
|
|
1354
|
+
upsertSessionsBatch(entries);
|
|
1355
|
+
recordScans(touched);
|
|
1356
|
+
}
|
|
1357
|
+
/** Stream-parse a single Droid JSONL file (+ sibling settings) into session metadata. */
|
|
1358
|
+
async function readDroidMeta(filePath, currentVersion) {
|
|
1359
|
+
const scan = await scanDroidSession(filePath);
|
|
1360
|
+
// The filename is the canonical session id; fall back to the session_start id.
|
|
1361
|
+
const sessionId = path.basename(filePath).replace(/\.jsonl$/, '') || scan.sessionId || '';
|
|
1362
|
+
if (!sessionId)
|
|
1363
|
+
return null;
|
|
1364
|
+
// Token usage and cost live only in the sibling `<uuid>.settings.json`.
|
|
1365
|
+
const settings = readDroidSettings(filePath.replace(/\.jsonl$/, '.settings.json'));
|
|
1366
|
+
const model = settings.model || scan.model;
|
|
1367
|
+
const tokenCount = settings.tokenCount;
|
|
1368
|
+
const costUsd = model && settings.usage
|
|
1369
|
+
? costOfUsage({
|
|
1370
|
+
model,
|
|
1371
|
+
inputTokens: settings.usage.inputTokens,
|
|
1372
|
+
outputTokens: settings.usage.outputTokens,
|
|
1373
|
+
cacheReadTokens: settings.usage.cacheReadTokens,
|
|
1374
|
+
cacheCreationTokens: settings.usage.cacheCreationTokens,
|
|
1375
|
+
})
|
|
1376
|
+
: 0;
|
|
1377
|
+
const stat = safeStatSync(filePath);
|
|
1378
|
+
const cwd = normalizeCwd(scan.cwd || '');
|
|
1379
|
+
const meta = {
|
|
1380
|
+
id: sessionId,
|
|
1381
|
+
shortId: sessionId.slice(0, 8),
|
|
1382
|
+
agent: 'droid',
|
|
1383
|
+
timestamp: scan.timestamp || (stat ? stat.mtime.toISOString() : new Date().toISOString()),
|
|
1384
|
+
project: cwd ? path.basename(cwd) : undefined,
|
|
1385
|
+
cwd,
|
|
1386
|
+
filePath,
|
|
1387
|
+
version: resolveSessionVersion('droid', filePath, undefined, currentVersion),
|
|
1388
|
+
topic: scan.topic,
|
|
1389
|
+
messageCount: scan.messageCount,
|
|
1390
|
+
tokenCount,
|
|
1391
|
+
costUsd: costUsd > 0 ? costUsd : undefined,
|
|
1392
|
+
durationMs: scan.durationMs,
|
|
1393
|
+
};
|
|
1394
|
+
return { meta, content: scan.contentText || '' };
|
|
1395
|
+
}
|
|
1396
|
+
/** Read model + token usage from a Droid `<uuid>.settings.json` sidecar. */
|
|
1397
|
+
function readDroidSettings(settingsPath) {
|
|
1398
|
+
try {
|
|
1399
|
+
const data = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1400
|
+
const model = typeof data.model === 'string' ? data.model : undefined;
|
|
1401
|
+
const u = data.tokenUsage;
|
|
1402
|
+
if (!u || typeof u !== 'object')
|
|
1403
|
+
return { model };
|
|
1404
|
+
const usage = {
|
|
1405
|
+
inputTokens: u.inputTokens,
|
|
1406
|
+
outputTokens: u.outputTokens,
|
|
1407
|
+
cacheReadTokens: u.cacheReadTokens,
|
|
1408
|
+
cacheCreationTokens: u.cacheCreationTokens,
|
|
1409
|
+
};
|
|
1410
|
+
const tokenCount = sumKnownNumbers([
|
|
1411
|
+
u.inputTokens,
|
|
1412
|
+
u.outputTokens,
|
|
1413
|
+
u.cacheCreationTokens,
|
|
1414
|
+
u.cacheReadTokens,
|
|
1415
|
+
]) ?? undefined;
|
|
1416
|
+
return { model, tokenCount, usage };
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
return {};
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
/** Stream a Droid JSONL file and extract scan-level metadata (id, cwd, topic, model, duration). */
|
|
1423
|
+
async function scanDroidSession(filePath) {
|
|
1424
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
1425
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1426
|
+
let sessionId;
|
|
1427
|
+
let timestamp;
|
|
1428
|
+
let cwd;
|
|
1429
|
+
let title;
|
|
1430
|
+
let sessionTitle;
|
|
1431
|
+
let firstUserTopic;
|
|
1432
|
+
let model;
|
|
1433
|
+
let messageCount = 0;
|
|
1434
|
+
let firstTsMs;
|
|
1435
|
+
let lastTsMs;
|
|
1436
|
+
const userTexts = [];
|
|
1437
|
+
try {
|
|
1438
|
+
for await (const line of rl) {
|
|
1439
|
+
if (!line.trim())
|
|
1440
|
+
continue;
|
|
1441
|
+
let parsed;
|
|
1442
|
+
try {
|
|
1443
|
+
parsed = JSON.parse(line);
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (parsed.type === 'session_start') {
|
|
1449
|
+
sessionId = typeof parsed.id === 'string' ? parsed.id : sessionId;
|
|
1450
|
+
cwd = typeof parsed.cwd === 'string' ? parsed.cwd : cwd;
|
|
1451
|
+
// Droid auto-generates `sessionTitle`; `title` is the raw first prompt.
|
|
1452
|
+
if (typeof parsed.sessionTitle === 'string' && parsed.sessionTitle.trim()) {
|
|
1453
|
+
sessionTitle = parsed.sessionTitle.trim();
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof parsed.title === 'string' && parsed.title.trim()) {
|
|
1456
|
+
title = parsed.title.trim();
|
|
1457
|
+
}
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (parsed.type !== 'message')
|
|
1461
|
+
continue;
|
|
1462
|
+
// Track duration across every timestamped message.
|
|
1463
|
+
if (typeof parsed.timestamp === 'string') {
|
|
1464
|
+
const ms = new Date(parsed.timestamp).getTime();
|
|
1465
|
+
if (!Number.isNaN(ms)) {
|
|
1466
|
+
if (firstTsMs === undefined || ms < firstTsMs)
|
|
1467
|
+
firstTsMs = ms;
|
|
1468
|
+
if (lastTsMs === undefined || ms > lastTsMs)
|
|
1469
|
+
lastTsMs = ms;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (!timestamp && typeof parsed.timestamp === 'string')
|
|
1473
|
+
timestamp = parsed.timestamp;
|
|
1474
|
+
const msg = parsed.message || {};
|
|
1475
|
+
if (typeof msg.modelId === 'string')
|
|
1476
|
+
model = msg.modelId;
|
|
1477
|
+
const text = extractDroidMessageText(msg.content);
|
|
1478
|
+
if (!text)
|
|
1479
|
+
continue;
|
|
1480
|
+
messageCount++;
|
|
1481
|
+
if (msg.role === 'user') {
|
|
1482
|
+
userTexts.push(text);
|
|
1483
|
+
if (!firstUserTopic)
|
|
1484
|
+
firstUserTopic = extractSessionTopic(text);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
finally {
|
|
1489
|
+
rl.close();
|
|
1490
|
+
stream.destroy();
|
|
1491
|
+
}
|
|
1492
|
+
const durationMs = firstTsMs !== undefined && lastTsMs !== undefined && lastTsMs > firstTsMs
|
|
1493
|
+
? lastTsMs - firstTsMs
|
|
1494
|
+
: undefined;
|
|
1495
|
+
return {
|
|
1496
|
+
sessionId,
|
|
1497
|
+
timestamp,
|
|
1498
|
+
cwd,
|
|
1499
|
+
// Prefer Droid's auto-title, then the raw first-prompt title, then the
|
|
1500
|
+
// derived first-user-message topic.
|
|
1501
|
+
topic: sessionTitle || title || firstUserTopic,
|
|
1502
|
+
model,
|
|
1503
|
+
messageCount,
|
|
1504
|
+
durationMs,
|
|
1505
|
+
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
/** Extract plain text from a Droid message content field (Anthropic-shaped blocks). */
|
|
1509
|
+
function extractDroidMessageText(content) {
|
|
1510
|
+
if (typeof content === 'string')
|
|
1511
|
+
return content.trim();
|
|
1512
|
+
if (!Array.isArray(content))
|
|
1513
|
+
return '';
|
|
1514
|
+
return content
|
|
1515
|
+
.map((part) => (typeof part?.text === 'string' && part.type === 'text' ? part.text : ''))
|
|
1516
|
+
// Droid front-loads injected context (date, skills list) as <system-reminder>
|
|
1517
|
+
// text blocks on the first user turn — drop them so topic/content stay clean.
|
|
1518
|
+
.filter((text) => text.trim() && !text.trim().startsWith('<system-reminder>'))
|
|
1519
|
+
.join('\n')
|
|
1520
|
+
.trim();
|
|
1521
|
+
}
|
|
1294
1522
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
1295
1523
|
export async function scanClaudeSession(filePath) {
|
|
1296
1524
|
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
@@ -51,3 +51,10 @@ export declare function parseRush(filePath: string): SessionEvent[];
|
|
|
51
51
|
export declare function parseHermes(filePath: string): SessionEvent[];
|
|
52
52
|
/** Parse a Kimi session state.json file by reading its agents/main/wire.jsonl. */
|
|
53
53
|
export declare function parseKimi(filePath: string): SessionEvent[];
|
|
54
|
+
/**
|
|
55
|
+
* Parse a Droid (Factory) JSONL session file into normalized events. Droid
|
|
56
|
+
* wraps each turn in a `{type:'message', message:{role, content, modelId}}`
|
|
57
|
+
* envelope; the content blocks are Anthropic-shaped (text/thinking/tool_use/
|
|
58
|
+
* tool_result), so block handling mirrors the Claude parser.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseDroid(filePath: string): SessionEvent[];
|
|
@@ -109,6 +109,9 @@ export function parseSession(filePath, agent) {
|
|
|
109
109
|
case 'kimi':
|
|
110
110
|
events = parseKimi(filePath);
|
|
111
111
|
break;
|
|
112
|
+
case 'droid':
|
|
113
|
+
events = parseDroid(filePath);
|
|
114
|
+
break;
|
|
112
115
|
}
|
|
113
116
|
// Chokepoint: every string field that originated in an untrusted session
|
|
114
117
|
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
@@ -133,6 +136,8 @@ export function detectAgent(filePath) {
|
|
|
133
136
|
return 'hermes';
|
|
134
137
|
if (filePath.includes('/.kimi-code/') || filePath.includes('\\.kimi-code\\'))
|
|
135
138
|
return 'kimi';
|
|
139
|
+
if (filePath.includes('/.factory/') || filePath.includes('\\.factory\\'))
|
|
140
|
+
return 'droid';
|
|
136
141
|
// Cloud convention: cloud-sessions/<id>/session.<format>.jsonl
|
|
137
142
|
const cloudMatch = filePath.match(/session\.(claude|codex|rush)\.jsonl(?:$|[?#])/);
|
|
138
143
|
if (cloudMatch)
|
|
@@ -1124,3 +1129,108 @@ export function parseKimi(filePath) {
|
|
|
1124
1129
|
}
|
|
1125
1130
|
return events;
|
|
1126
1131
|
}
|
|
1132
|
+
// ---------------------------------------------------------------------------
|
|
1133
|
+
// Droid (Factory) parser
|
|
1134
|
+
// ---------------------------------------------------------------------------
|
|
1135
|
+
/**
|
|
1136
|
+
* Parse a Droid (Factory) JSONL session file into normalized events. Droid
|
|
1137
|
+
* wraps each turn in a `{type:'message', message:{role, content, modelId}}`
|
|
1138
|
+
* envelope; the content blocks are Anthropic-shaped (text/thinking/tool_use/
|
|
1139
|
+
* tool_result), so block handling mirrors the Claude parser.
|
|
1140
|
+
*/
|
|
1141
|
+
export function parseDroid(filePath) {
|
|
1142
|
+
const content = safeReadSessionFile(filePath);
|
|
1143
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
1144
|
+
const events = [];
|
|
1145
|
+
// Map tool_use id -> {tool, args} for correlating with tool_result.
|
|
1146
|
+
const toolUseMap = new Map();
|
|
1147
|
+
for (const line of lines) {
|
|
1148
|
+
let raw;
|
|
1149
|
+
try {
|
|
1150
|
+
raw = JSON.parse(line);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
if (raw.type !== 'message')
|
|
1156
|
+
continue;
|
|
1157
|
+
const message = raw.message || {};
|
|
1158
|
+
const role = message.role === 'user' ? 'user' : 'assistant';
|
|
1159
|
+
const timestamp = raw.timestamp || new Date().toISOString();
|
|
1160
|
+
const blocks = message.content;
|
|
1161
|
+
// Plain-string content (rare) renders as a single message.
|
|
1162
|
+
if (typeof blocks === 'string') {
|
|
1163
|
+
const text = blocks.trim();
|
|
1164
|
+
if (text)
|
|
1165
|
+
events.push({ type: 'message', agent: 'droid', timestamp, role, content: text });
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (!Array.isArray(blocks))
|
|
1169
|
+
continue;
|
|
1170
|
+
for (const block of blocks) {
|
|
1171
|
+
if (block.type === 'text') {
|
|
1172
|
+
const text = (block.text || '').trim();
|
|
1173
|
+
// Skip injected context blocks (date, skills list) on the first user turn.
|
|
1174
|
+
if (text && !(role === 'user' && text.startsWith('<system-reminder>'))) {
|
|
1175
|
+
events.push({ type: 'message', agent: 'droid', timestamp, role, content: text });
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
else if (block.type === 'thinking') {
|
|
1179
|
+
const thinkingText = (block.thinking || '').trim();
|
|
1180
|
+
if (thinkingText)
|
|
1181
|
+
events.push({ type: 'thinking', agent: 'droid', timestamp, content: thinkingText });
|
|
1182
|
+
}
|
|
1183
|
+
else if (block.type === 'tool_use') {
|
|
1184
|
+
const toolName = block.name || 'unknown';
|
|
1185
|
+
const toolInput = block.input || {};
|
|
1186
|
+
if (block.id)
|
|
1187
|
+
toolUseMap.set(block.id, { tool: toolName, args: toolInput });
|
|
1188
|
+
events.push({
|
|
1189
|
+
type: 'tool_use',
|
|
1190
|
+
agent: 'droid',
|
|
1191
|
+
timestamp,
|
|
1192
|
+
tool: toolName,
|
|
1193
|
+
args: toolInput,
|
|
1194
|
+
path: toolInput.file_path || toolInput.path || undefined,
|
|
1195
|
+
command: toolName === 'Bash' ? toolInput.command : undefined,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
else if (block.type === 'tool_result') {
|
|
1199
|
+
const toolId = block.tool_use_id;
|
|
1200
|
+
const toolInfo = toolId ? toolUseMap.get(toolId) : undefined;
|
|
1201
|
+
const isError = block.is_error === true;
|
|
1202
|
+
let output = '';
|
|
1203
|
+
if (typeof block.content === 'string') {
|
|
1204
|
+
output = block.content;
|
|
1205
|
+
}
|
|
1206
|
+
else if (Array.isArray(block.content)) {
|
|
1207
|
+
output = block.content
|
|
1208
|
+
.filter((c) => c.type === 'text')
|
|
1209
|
+
.map((c) => c.text || '')
|
|
1210
|
+
.join('\n');
|
|
1211
|
+
}
|
|
1212
|
+
if (isError) {
|
|
1213
|
+
events.push({ type: 'error', agent: 'droid', timestamp, tool: toolInfo?.tool, content: output || 'Tool execution failed' });
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
events.push({
|
|
1217
|
+
type: 'tool_result',
|
|
1218
|
+
agent: 'droid',
|
|
1219
|
+
timestamp,
|
|
1220
|
+
tool: toolInfo?.tool,
|
|
1221
|
+
success: true,
|
|
1222
|
+
output: output.length > 500 ? output.slice(0, 497) + '...' : output,
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
if (toolId)
|
|
1226
|
+
toolUseMap.delete(toolId);
|
|
1227
|
+
}
|
|
1228
|
+
else if (block.type === 'image') {
|
|
1229
|
+
const source = block.source || {};
|
|
1230
|
+
const sizeBytes = source.type === 'base64' ? Math.ceil((source.data?.length || 0) * 0.75) : 0;
|
|
1231
|
+
events.push({ type: 'attachment', agent: 'droid', timestamp, mediaType: source.media_type || 'image/png', sizeBytes });
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return events;
|
|
1236
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
|
@@ -68,6 +68,7 @@ export declare const loadTmux: ModuleLoader;
|
|
|
68
68
|
export declare const loadBrowser: ModuleLoader;
|
|
69
69
|
export declare const loadComputer: ModuleLoader;
|
|
70
70
|
export declare const loadHosts: ModuleLoader;
|
|
71
|
+
export declare const loadSsh: ModuleLoader;
|
|
71
72
|
export declare const loadPull: ModuleLoader;
|
|
72
73
|
export declare const loadPush: ModuleLoader;
|
|
73
74
|
export declare const loadRepo: ModuleLoader;
|
|
@@ -46,6 +46,7 @@ export const loadTmux = async () => (await import('../../commands/tmux.js')).reg
|
|
|
46
46
|
export const loadBrowser = async () => (await import('../../commands/browser.js')).registerBrowserCommand;
|
|
47
47
|
export const loadComputer = async () => (await import('../../commands/computer.js')).registerComputerCommand;
|
|
48
48
|
export const loadHosts = async () => (await import('../../commands/hosts.js')).registerHostsCommand;
|
|
49
|
+
export const loadSsh = async () => (await import('../../commands/ssh.js')).registerSshCommands;
|
|
49
50
|
export const loadPull = async () => (await import('../../commands/pull.js')).registerPullCommand;
|
|
50
51
|
export const loadPush = async () => (await import('../../commands/push.js')).registerPushCommand;
|
|
51
52
|
export const loadRepo = async () => (await import('../../commands/repo.js')).registerRepoCommands;
|
|
@@ -128,6 +129,8 @@ export const COMMAND_LOADERS = {
|
|
|
128
129
|
browser: [loadBrowser],
|
|
129
130
|
computer: [loadComputer],
|
|
130
131
|
hosts: [loadHosts],
|
|
132
|
+
ssh: [loadSsh],
|
|
133
|
+
devices: [loadSsh],
|
|
131
134
|
pull: [loadPull],
|
|
132
135
|
push: [loadPush],
|
|
133
136
|
repo: [loadRepo],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -152,6 +152,8 @@ export declare function getTeamsDir(): string;
|
|
|
152
152
|
export declare function getTeamsAgentsDir(): string;
|
|
153
153
|
/** Path to the team registry — list of named teams with timestamps. Durable runtime, per-machine. */
|
|
154
154
|
export declare function getTeamsRegistryPath(): string;
|
|
155
|
+
/** Path to the device registry — SSH device profiles with platform/auth metadata. Durable runtime, per-machine (host list + addresses are NOT pulled by `agents repo push`). */
|
|
156
|
+
export declare function getDevicesRegistryPath(): string;
|
|
155
157
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
|
156
158
|
export declare function getCloudDir(): string;
|
|
157
159
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -334,6 +334,8 @@ export function getTeamsDir() { return TEAMS_DIR; }
|
|
|
334
334
|
export function getTeamsAgentsDir() { return TEAMS_AGENTS_DIR; }
|
|
335
335
|
/** Path to the team registry — list of named teams with timestamps. Durable runtime, per-machine. */
|
|
336
336
|
export function getTeamsRegistryPath() { return path.join(HISTORY_DIR, 'teams', 'registry.json'); }
|
|
337
|
+
/** Path to the device registry — SSH device profiles with platform/auth metadata. Durable runtime, per-machine (host list + addresses are NOT pulled by `agents repo push`). */
|
|
338
|
+
export function getDevicesRegistryPath() { return path.join(HISTORY_DIR, 'devices', 'registry.json'); }
|
|
337
339
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
|
338
340
|
export function getCloudDir() { return CLOUD_DIR; }
|
|
339
341
|
/** Path to terminal session metadata (~/.agents/.cache/terminals/). */
|
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.29",
|
|
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",
|