@phnx-labs/agents-cli 1.22.50 → 1.22.52
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 +219 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/attach.js +7 -0
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +191 -54
- package/dist/commands/config.js +20 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +8 -4
- package/dist/commands/detach.js +1 -1
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/focus.d.ts +1 -10
- package/dist/commands/focus.js +14 -79
- package/dist/commands/go.d.ts +26 -0
- package/dist/commands/go.js +63 -5
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/repo.js +31 -3
- package/dist/commands/sessions-resume.d.ts +1 -0
- package/dist/commands/sessions-resume.js +13 -2
- package/dist/commands/sessions-stop.js +1 -1
- package/dist/commands/sessions.d.ts +23 -13
- package/dist/commands/sessions.js +40 -20
- package/dist/commands/setup-browser.d.ts +5 -2
- package/dist/commands/setup-browser.js +14 -29
- package/dist/commands/setup-preferences.d.ts +22 -3
- package/dist/commands/setup-preferences.js +25 -8
- package/dist/commands/share.js +12 -8
- package/dist/commands/status.js +5 -0
- package/dist/commands/sync.js +58 -2
- package/dist/commands/tmux.d.ts +8 -1
- package/dist/commands/tmux.js +167 -17
- package/dist/commands/traces.js +32 -4
- package/dist/lib/browser/ipc.d.ts +44 -0
- package/dist/lib/browser/ipc.js +120 -8
- package/dist/lib/browser/profiles.d.ts +39 -17
- package/dist/lib/browser/profiles.js +51 -52
- package/dist/lib/browser/runtime-state.d.ts +4 -2
- package/dist/lib/browser/runtime-state.js +4 -2
- package/dist/lib/browser/service.js +4 -3
- package/dist/lib/channels/owner-forward.d.ts +88 -0
- package/dist/lib/channels/owner-forward.js +116 -0
- package/dist/lib/channels/owner-sink.js +7 -0
- package/dist/lib/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +7 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/feed-broadcast.js +15 -1
- package/dist/lib/git.d.ts +93 -0
- package/dist/lib/git.js +232 -0
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- package/dist/lib/monitors/remote.d.ts +18 -1
- package/dist/lib/monitors/remote.js +15 -2
- package/dist/lib/notify.d.ts +7 -0
- package/dist/lib/notify.js +15 -1
- package/dist/lib/session/local-tmux-attach.d.ts +69 -0
- package/dist/lib/session/local-tmux-attach.js +164 -0
- package/dist/lib/session/remote-active.d.ts +8 -0
- package/dist/lib/session/remote-active.js +1 -0
- package/dist/lib/share/publish.d.ts +8 -11
- package/dist/lib/share/publish.js +16 -20
- package/dist/lib/share/worker-template.js +99 -12
- package/dist/lib/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- package/dist/lib/sync-status.d.ts +17 -0
- package/dist/lib/sync-status.js +21 -2
- package/dist/lib/tmux/index.d.ts +1 -1
- package/dist/lib/tmux/index.js +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +91 -10
- package/package.json +3 -3
package/dist/lib/tmux/session.js
CHANGED
|
@@ -315,6 +315,35 @@ export async function killSession(name, socket, opts = {}) {
|
|
|
315
315
|
removeSessionMeta(name);
|
|
316
316
|
return true;
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* After an attach client returns: destroy the session if every pane is dead
|
|
320
|
+
* (the agent exited); leave it if any pane is still alive (Ctrl-b d).
|
|
321
|
+
*
|
|
322
|
+
* `runInTmux` already does this via `resolveAfterAttach`. The attach verbs
|
|
323
|
+
* (`agents tmux attach`, `sessions focus`/`resume --attach-only`, `jumpTo`)
|
|
324
|
+
* used to `process.exit` the tmux client status and leave a `remain-on-exit`
|
|
325
|
+
* husk — the session "came back" in `tmux ls` after the user exited the agent.
|
|
326
|
+
*/
|
|
327
|
+
export async function teardownIfAgentExited(name, socket) {
|
|
328
|
+
assertValidSessionName(name);
|
|
329
|
+
const sock = socket ?? getDefaultSocketPath();
|
|
330
|
+
if (!(await hasSession(name, sock)))
|
|
331
|
+
return 'absent';
|
|
332
|
+
const res = await runTmux({
|
|
333
|
+
socket: sock,
|
|
334
|
+
args: ['list-panes', '-t', `=${name}`, '-F', '#{pane_dead}'],
|
|
335
|
+
throwOnError: false,
|
|
336
|
+
}).catch(() => null);
|
|
337
|
+
if (!res || res.code !== 0) {
|
|
338
|
+
await killSession(name, sock).catch(() => { });
|
|
339
|
+
return 'killed';
|
|
340
|
+
}
|
|
341
|
+
const flags = res.stdout.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
342
|
+
if (flags.some((d) => d === '0'))
|
|
343
|
+
return 'kept';
|
|
344
|
+
await killSession(name, sock).catch(() => { });
|
|
345
|
+
return 'killed';
|
|
346
|
+
}
|
|
318
347
|
/**
|
|
319
348
|
* Kill every session on the shared server AND the server itself, then prune
|
|
320
349
|
* meta files. Wipes the socket so the next `new` starts from a clean slate.
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* Sync gate: `sessions.db` `file_mtime_ms` is the source of truth. A ledger
|
|
10
10
|
* at `getRuntimeStateDir()/traces-sync.json` records the last sync timestamp
|
|
11
11
|
* per device so re-runs skip unchanged sessions.
|
|
12
|
+
*
|
|
13
|
+
* Retry ledger: the mtime watermark alone cannot retry failures — a later-mtime
|
|
14
|
+
* success advances it past an earlier failed row, which the next run's
|
|
15
|
+
* `file_mtime_ms > watermark` filter then skips forever. So the same ledger also
|
|
16
|
+
* records the identity + typed evidence of each failed session (`failures`), and
|
|
17
|
+
* the row query unions those retry-worthy ids back in regardless of the watermark
|
|
18
|
+
* (PHNX-3267). Failures are typed so a gone transcript (`transcript-unavailable`,
|
|
19
|
+
* expected history, aged out after a TTL and never re-read) is distinguished from a
|
|
20
|
+
* real parse/upload failure (retried until it resolves).
|
|
12
21
|
*/
|
|
13
22
|
import { type SessionTrajectory } from '../session/trajectory.js';
|
|
14
23
|
import { type BucketStats, type DriftSignal, type TraceFailureCause, type TraceTopicGroup } from './classify.js';
|
|
@@ -30,7 +39,14 @@ export interface SyncOpts {
|
|
|
30
39
|
export interface SyncResult {
|
|
31
40
|
uploaded: number;
|
|
32
41
|
skipped: number;
|
|
42
|
+
/** Total failures = transcriptUnavailable + parseFailed + uploadFailed. Kept for callers. */
|
|
33
43
|
errors: number;
|
|
44
|
+
/** The transcript file the row points at is gone/unreadable — expected history, not retried each run. */
|
|
45
|
+
transcriptUnavailable: number;
|
|
46
|
+
/** The transcript exists but could not be parsed into a trajectory — retried until it parses. */
|
|
47
|
+
parseFailed: number;
|
|
48
|
+
/** Parsed fine but the upload PUT failed (network/5xx) — retried on the next sync. */
|
|
49
|
+
uploadFailed: number;
|
|
34
50
|
}
|
|
35
51
|
/** Push derived, redacted trajectories for this device to the traces store. */
|
|
36
52
|
export declare function syncTraces(opts?: SyncOpts): Promise<SyncResult>;
|
|
@@ -107,8 +123,22 @@ export interface SessionDetail {
|
|
|
107
123
|
* written to R2. `repo` is the cwd basename only.
|
|
108
124
|
*/
|
|
109
125
|
export declare function buildSessionDetail(traj: SessionTrajectory): SessionDetail;
|
|
126
|
+
/** How a session failed to sync. Only parse/upload failures are re-queried every run. */
|
|
127
|
+
export type SyncFailureKind = 'transcript-unavailable' | 'parse-failed' | 'upload-failed';
|
|
128
|
+
export interface SyncFailure {
|
|
129
|
+
id: string;
|
|
130
|
+
mtimeMs: number;
|
|
131
|
+
kind: SyncFailureKind;
|
|
132
|
+
/** Bounded, redacted first line of the underlying error — actionable evidence. */
|
|
133
|
+
detail: string;
|
|
134
|
+
/** Epoch ms first recorded, so a permanently-unavailable transcript can be aged out. */
|
|
135
|
+
firstSeen: number;
|
|
136
|
+
attempts: number;
|
|
137
|
+
}
|
|
110
138
|
interface SyncLedger {
|
|
111
139
|
lastSyncMtime?: number;
|
|
140
|
+
/** Failed session identities, keyed by id, that survive across syncs. */
|
|
141
|
+
failures?: SyncFailure[];
|
|
112
142
|
}
|
|
113
143
|
export declare function readSyncLedger(): SyncLedger;
|
|
114
144
|
export declare function writeSyncLedger(ledger: SyncLedger): void;
|
package/dist/lib/traces/sync.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* Sync gate: `sessions.db` `file_mtime_ms` is the source of truth. A ledger
|
|
10
10
|
* at `getRuntimeStateDir()/traces-sync.json` records the last sync timestamp
|
|
11
11
|
* per device so re-runs skip unchanged sessions.
|
|
12
|
+
*
|
|
13
|
+
* Retry ledger: the mtime watermark alone cannot retry failures — a later-mtime
|
|
14
|
+
* success advances it past an earlier failed row, which the next run's
|
|
15
|
+
* `file_mtime_ms > watermark` filter then skips forever. So the same ledger also
|
|
16
|
+
* records the identity + typed evidence of each failed session (`failures`), and
|
|
17
|
+
* the row query unions those retry-worthy ids back in regardless of the watermark
|
|
18
|
+
* (PHNX-3267). Failures are typed so a gone transcript (`transcript-unavailable`,
|
|
19
|
+
* expected history, aged out after a TTL and never re-read) is distinguished from a
|
|
20
|
+
* real parse/upload failure (retried until it resolves).
|
|
12
21
|
*/
|
|
13
22
|
import fs from 'node:fs';
|
|
14
23
|
import os from 'node:os';
|
|
@@ -39,17 +48,60 @@ export async function syncTraces(opts = {}) {
|
|
|
39
48
|
// NULL machine rows are legacy local sessions (pre-machine-field). A dry-run
|
|
40
49
|
// ignores the incremental watermark so the export covers every session.
|
|
41
50
|
const sinceMtime = dryRun ? 0 : (ledger.lastSyncMtime ?? 0);
|
|
42
|
-
const
|
|
51
|
+
const watermarkRows = db
|
|
43
52
|
.prepare('SELECT * FROM sessions WHERE (machine = ? OR machine IS NULL) AND file_mtime_ms > ? ORDER BY file_mtime_ms ASC')
|
|
44
53
|
.all(device, sinceMtime);
|
|
54
|
+
// The watermark alone loses failures: a later-mtime success advances it past an
|
|
55
|
+
// earlier failed row, which the `file_mtime_ms > sinceMtime` filter then skips
|
|
56
|
+
// forever. So union in the sessions we explicitly recorded as retry-worthy
|
|
57
|
+
// failures (parse/upload — a `transcript-unavailable` file is gone, re-reading it
|
|
58
|
+
// wastes work) regardless of where the watermark sits. A dry-run ignores the
|
|
59
|
+
// ledger entirely and already selects every row.
|
|
60
|
+
const retryIds = dryRun
|
|
61
|
+
? []
|
|
62
|
+
: (ledger.failures ?? [])
|
|
63
|
+
.filter((f) => f.kind !== 'transcript-unavailable')
|
|
64
|
+
.map((f) => f.id);
|
|
65
|
+
let rows = watermarkRows;
|
|
66
|
+
if (retryIds.length) {
|
|
67
|
+
const seen = new Set(watermarkRows.map((r) => r.id));
|
|
68
|
+
const retryRows = [];
|
|
69
|
+
for (let i = 0; i < retryIds.length; i += 400) {
|
|
70
|
+
const chunk = retryIds.slice(i, i + 400);
|
|
71
|
+
retryRows.push(...db
|
|
72
|
+
.prepare(`SELECT * FROM sessions WHERE (machine = ? OR machine IS NULL) AND id IN (${chunk.map(() => '?').join(',')})`)
|
|
73
|
+
.all(device, ...chunk));
|
|
74
|
+
}
|
|
75
|
+
const stranded = retryRows.filter((r) => !seen.has(r.id));
|
|
76
|
+
rows = [...watermarkRows, ...stranded].sort((a, b) => (a.file_mtime_ms ?? 0) - (b.file_mtime_ms ?? 0));
|
|
77
|
+
}
|
|
45
78
|
const limited = opts.limit !== undefined ? rows.slice(0, opts.limit) : rows;
|
|
46
79
|
const knownSecrets = knownSecretValuesFromEnv();
|
|
47
80
|
let uploaded = 0;
|
|
48
81
|
let skipped = 0;
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
let transcriptUnavailable = 0;
|
|
83
|
+
let parseFailed = 0;
|
|
84
|
+
let uploadFailed = 0;
|
|
85
|
+
// Advance the watermark only to the max mtime of successfully uploaded sessions.
|
|
86
|
+
// On its own this does NOT retry failures (a later success strands earlier ones);
|
|
87
|
+
// the failure ledger below is what actually re-selects them next run.
|
|
52
88
|
let maxSuccessMtime = ledger.lastSyncMtime ?? 0;
|
|
89
|
+
// Carry prior failures forward by id so a stranded row's identity + evidence
|
|
90
|
+
// survives across syncs. A success removes the id; a repeat failure updates it.
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const failures = new Map((ledger.failures ?? []).map((f) => [f.id, f]));
|
|
93
|
+
const recordFailure = (row, kind, err) => {
|
|
94
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
95
|
+
const prev = failures.get(row.id);
|
|
96
|
+
failures.set(row.id, {
|
|
97
|
+
id: row.id,
|
|
98
|
+
mtimeMs: row.file_mtime_ms ?? 0,
|
|
99
|
+
kind,
|
|
100
|
+
detail: redactSecrets(raw.split('\n')[0] ?? '', knownSecrets).slice(0, 200),
|
|
101
|
+
firstSeen: prev?.firstSeen ?? now,
|
|
102
|
+
attempts: (prev?.attempts ?? 0) + 1,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
53
105
|
if (dryRun && outDir) {
|
|
54
106
|
fs.mkdirSync(path.join(outDir, 'sessions'), { recursive: true });
|
|
55
107
|
}
|
|
@@ -65,8 +117,17 @@ export async function syncTraces(opts = {}) {
|
|
|
65
117
|
const events = parseSession(row.file_path, row.agent);
|
|
66
118
|
traj = buildTrajectory(events, session, { redact: true, knownSecrets });
|
|
67
119
|
}
|
|
68
|
-
catch {
|
|
69
|
-
|
|
120
|
+
catch (err) {
|
|
121
|
+
// A gone/unreadable transcript is expected history, not a retry-worthy error;
|
|
122
|
+
// a file that IS present but failed to parse is a real problem to retry.
|
|
123
|
+
if (!fs.existsSync(row.file_path)) {
|
|
124
|
+
transcriptUnavailable++;
|
|
125
|
+
recordFailure(row, 'transcript-unavailable', err);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
parseFailed++;
|
|
129
|
+
recordFailure(row, 'parse-failed', err);
|
|
130
|
+
}
|
|
70
131
|
continue;
|
|
71
132
|
}
|
|
72
133
|
try {
|
|
@@ -78,9 +139,11 @@ export async function syncTraces(opts = {}) {
|
|
|
78
139
|
}
|
|
79
140
|
uploaded++;
|
|
80
141
|
maxSuccessMtime = Math.max(maxSuccessMtime, row.file_mtime_ms ?? 0);
|
|
142
|
+
failures.delete(row.id); // recovered — drop it from the retry set
|
|
81
143
|
}
|
|
82
|
-
catch {
|
|
83
|
-
|
|
144
|
+
catch (err) {
|
|
145
|
+
uploadFailed++;
|
|
146
|
+
recordFailure(row, 'upload-failed', err);
|
|
84
147
|
}
|
|
85
148
|
}
|
|
86
149
|
if (!opts.skipIndex) {
|
|
@@ -114,7 +177,11 @@ export async function syncTraces(opts = {}) {
|
|
|
114
177
|
}
|
|
115
178
|
// A dry-run never advances the incremental watermark: it is a read-only export.
|
|
116
179
|
if (!dryRun) {
|
|
117
|
-
|
|
180
|
+
// Age out long-unavailable transcripts so a corpus of deleted files cannot grow
|
|
181
|
+
// the retry set without bound; parse/upload failures persist until they resolve.
|
|
182
|
+
const persistedFailures = [...failures.values()].filter((f) => !(f.kind === 'transcript-unavailable' &&
|
|
183
|
+
now - f.firstSeen > TRANSCRIPT_UNAVAILABLE_TTL_MS));
|
|
184
|
+
writeSyncLedger({ lastSyncMtime: maxSuccessMtime, failures: persistedFailures });
|
|
118
185
|
// Register the Phoenix session with Prix so the console can fetch live data
|
|
119
186
|
// (PHNX-3257). Fire-and-forget — a link failure must not block sync.
|
|
120
187
|
// Managed backend only: the BYO path's token is a static write token that
|
|
@@ -128,7 +195,14 @@ export async function syncTraces(opts = {}) {
|
|
|
128
195
|
}).catch(() => { });
|
|
129
196
|
}
|
|
130
197
|
}
|
|
131
|
-
return {
|
|
198
|
+
return {
|
|
199
|
+
uploaded,
|
|
200
|
+
skipped,
|
|
201
|
+
errors: transcriptUnavailable + parseFailed + uploadFailed,
|
|
202
|
+
transcriptUnavailable,
|
|
203
|
+
parseFailed,
|
|
204
|
+
uploadFailed,
|
|
205
|
+
};
|
|
132
206
|
}
|
|
133
207
|
function percentile(values, ratio) {
|
|
134
208
|
if (values.length === 0)
|
|
@@ -397,6 +471,13 @@ async function putIndexShard(backend, device, owner, shard) {
|
|
|
397
471
|
throw new Error(`PUT ${url} → ${res.status}`);
|
|
398
472
|
}
|
|
399
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* A `transcript-unavailable` failure is kept (so its count stays reportable) but
|
|
476
|
+
* not retried, because re-reading a file that is gone wastes work. Age it out of
|
|
477
|
+
* the ledger after this window so a corpus of long-deleted transcripts cannot grow
|
|
478
|
+
* the failure set without bound.
|
|
479
|
+
*/
|
|
480
|
+
const TRANSCRIPT_UNAVAILABLE_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
400
481
|
function ledgerPath() {
|
|
401
482
|
return path.join(getRuntimeStateDir(), 'traces-sync.json');
|
|
402
483
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.52",
|
|
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",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
},
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/phnx-labs/
|
|
39
|
+
"url": "git+https://github.com/phnx-labs/agi-cli.git"
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://agents-cli.sh",
|
|
42
42
|
"bugs": {
|
|
43
|
-
"url": "https://github.com/phnx-labs/
|
|
43
|
+
"url": "https://github.com/phnx-labs/agi-cli/issues"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ -f 'bin/agents-macos' ] && mkdir -p dist/bin && cp -f 'bin/agents-macos' 'dist/bin/agents' && chmod 755 'dist/bin/agents' || true)",
|