@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/dist/bootstrap.js +1 -1
  3. package/dist/commands/attach.js +7 -0
  4. package/dist/commands/browser.d.ts +10 -0
  5. package/dist/commands/browser.js +191 -54
  6. package/dist/commands/config.js +20 -0
  7. package/dist/commands/daemon.d.ts +2 -0
  8. package/dist/commands/daemon.js +8 -4
  9. package/dist/commands/detach.js +1 -1
  10. package/dist/commands/feedback.js +1 -1
  11. package/dist/commands/focus.d.ts +1 -10
  12. package/dist/commands/focus.js +14 -79
  13. package/dist/commands/go.d.ts +26 -0
  14. package/dist/commands/go.js +63 -5
  15. package/dist/commands/menubar.js +6 -4
  16. package/dist/commands/monitors.js +1 -1
  17. package/dist/commands/repo.js +31 -3
  18. package/dist/commands/sessions-resume.d.ts +1 -0
  19. package/dist/commands/sessions-resume.js +13 -2
  20. package/dist/commands/sessions-stop.js +1 -1
  21. package/dist/commands/sessions.d.ts +23 -13
  22. package/dist/commands/sessions.js +40 -20
  23. package/dist/commands/setup-browser.d.ts +5 -2
  24. package/dist/commands/setup-browser.js +14 -29
  25. package/dist/commands/setup-preferences.d.ts +22 -3
  26. package/dist/commands/setup-preferences.js +25 -8
  27. package/dist/commands/share.js +12 -8
  28. package/dist/commands/status.js +5 -0
  29. package/dist/commands/sync.js +58 -2
  30. package/dist/commands/tmux.d.ts +8 -1
  31. package/dist/commands/tmux.js +167 -17
  32. package/dist/commands/traces.js +32 -4
  33. package/dist/lib/browser/ipc.d.ts +44 -0
  34. package/dist/lib/browser/ipc.js +120 -8
  35. package/dist/lib/browser/profiles.d.ts +39 -17
  36. package/dist/lib/browser/profiles.js +51 -52
  37. package/dist/lib/browser/runtime-state.d.ts +4 -2
  38. package/dist/lib/browser/runtime-state.js +4 -2
  39. package/dist/lib/browser/service.js +4 -3
  40. package/dist/lib/channels/owner-forward.d.ts +88 -0
  41. package/dist/lib/channels/owner-forward.js +116 -0
  42. package/dist/lib/channels/owner-sink.js +7 -0
  43. package/dist/lib/claude-statusline.d.ts +9 -0
  44. package/dist/lib/claude-statusline.js +45 -4
  45. package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
  46. package/dist/lib/computer/ssh-tunnel.js +13 -8
  47. package/dist/lib/config-keys.d.ts +4 -3
  48. package/dist/lib/config-keys.js +9 -2
  49. package/dist/lib/device-config.js +23 -0
  50. package/dist/lib/exec.d.ts +8 -0
  51. package/dist/lib/exec.js +7 -0
  52. package/dist/lib/factory/snapshot.d.ts +1 -1
  53. package/dist/lib/factory/snapshot.js +1 -1
  54. package/dist/lib/feed-broadcast.js +15 -1
  55. package/dist/lib/git.d.ts +93 -0
  56. package/dist/lib/git.js +232 -0
  57. package/dist/lib/helper-download.d.ts +12 -2
  58. package/dist/lib/helper-download.js +12 -2
  59. package/dist/lib/installations/migrate.js +4 -4
  60. package/dist/lib/menubar/install-menubar.d.ts +71 -8
  61. package/dist/lib/menubar/install-menubar.js +183 -24
  62. package/dist/lib/monitors/remote.d.ts +18 -1
  63. package/dist/lib/monitors/remote.js +15 -2
  64. package/dist/lib/notify.d.ts +7 -0
  65. package/dist/lib/notify.js +15 -1
  66. package/dist/lib/session/local-tmux-attach.d.ts +69 -0
  67. package/dist/lib/session/local-tmux-attach.js +164 -0
  68. package/dist/lib/session/remote-active.d.ts +8 -0
  69. package/dist/lib/session/remote-active.js +1 -0
  70. package/dist/lib/share/publish.d.ts +8 -11
  71. package/dist/lib/share/publish.js +16 -20
  72. package/dist/lib/share/worker-template.js +99 -12
  73. package/dist/lib/star-nudge.d.ts +2 -2
  74. package/dist/lib/star-nudge.js +2 -2
  75. package/dist/lib/state.d.ts +1 -1
  76. package/dist/lib/state.js +2 -2
  77. package/dist/lib/sync-status.d.ts +17 -0
  78. package/dist/lib/sync-status.js +21 -2
  79. package/dist/lib/tmux/index.d.ts +1 -1
  80. package/dist/lib/tmux/index.js +1 -1
  81. package/dist/lib/tmux/session.d.ts +10 -0
  82. package/dist/lib/tmux/session.js +29 -0
  83. package/dist/lib/traces/sync.d.ts +30 -0
  84. package/dist/lib/traces/sync.js +91 -10
  85. package/package.json +3 -3
@@ -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;
@@ -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 rows = db
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 errors = 0;
50
- // Advance the watermark only to the max mtime of successfully uploaded sessions
51
- // so failures are retried on the next run.
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
- errors++;
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
- errors++;
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
- writeSyncLedger({ lastSyncMtime: maxSuccessMtime });
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 { uploaded, skipped, errors };
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.50",
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/agents-cli.git"
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/agents-cli/issues"
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)",