@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -1,5 +1,4 @@
1
1
  import { SessionProjection } from '../session/projection.js';
2
- import { randomUUID } from 'node:crypto';
3
2
  import * as fs from 'node:fs';
4
3
  import * as path from 'node:path';
5
4
  import { loadDevices, isDialableDevice } from '../devices/registry.js';
@@ -12,21 +11,26 @@ import { watchLocalSessions } from '../session/watch.js';
12
11
  import { readBlock, readResolution, blockIdForSession } from './feed.js';
13
12
  import { reconcileAttention } from './attention.js';
14
13
  import { ActivityStream } from './activity-stream.js';
14
+ import { collectToolRows, watchToolActivity } from './tool-activity.js';
15
+ import { FeedWatchState } from './envelope.js';
16
+ import { FeedHub } from './hub.js';
17
+ import { getCachedToolSetup, subscribeToolSetup } from '../setup-tool-status.js';
15
18
  import { PR_STATUS_TTL_MS, readPullRequestStatus, withPullRequestStatus } from './pr-status.js';
16
- export class FeedWatchState {
17
- streamId;
18
- sequence = 0;
19
- constructor(streamId = randomUUID()) { this.streamId = streamId; }
20
- emit(event) {
21
- return { v: 1, streamId: this.streamId, sequence: ++this.sequence, ...event };
22
- }
23
- }
19
+ export { FeedWatchState } from './envelope.js';
24
20
  /** Reuse session ownership reconciliation for the combined operator stream. */
25
21
  export class FeedSessionProjection {
26
22
  state;
27
23
  sessions = new SessionProjection();
28
24
  observations = new Map();
29
25
  projectedAttention = new Map();
26
+ /**
27
+ * Tool rows per observing scope. A reset replaces one scope's rows and must
28
+ * not touch another's — the same per-scope rule the agent rows follow, and
29
+ * the reason a peer reconnecting cannot erase the local machine's tasks.
30
+ */
31
+ toolsByScope = new Map();
32
+ /** Whole-set per scope; see the `setup.snapshot` docblock in `envelope.ts`. */
33
+ setupByScope = new Map();
30
34
  constructor(state = new FeedWatchState()) {
31
35
  this.state = state;
32
36
  }
@@ -43,6 +47,8 @@ export class FeedSessionProjection {
43
47
  attention.set(row.rowKey, item);
44
48
  }
45
49
  this.observations.set(scope, attention);
50
+ this.toolsByScope.set(scope, new Map(event.tools.map((tool) => [tool.rowKey, tool])));
51
+ this.setupByScope.set(scope, event.setup);
46
52
  }
47
53
  else if (event.type === 'agent.upsert') {
48
54
  sessionEvent = { ...base, type: 'upsert', rowKey: event.rowKey, row: event.agent };
@@ -59,6 +65,20 @@ export class FeedSessionProjection {
59
65
  else if (event.type === 'attention.remove') {
60
66
  this.observations.get(scope)?.delete(event.rowKey);
61
67
  }
68
+ else if (event.type === 'tool.upsert') {
69
+ if (!this.toolsByScope.has(scope))
70
+ this.toolsByScope.set(scope, new Map());
71
+ this.toolsByScope.get(scope).set(event.rowKey, event.tool);
72
+ return [this.state.emit({ type: 'tool.upsert', scope, rowKey: event.rowKey, tool: event.tool })];
73
+ }
74
+ else if (event.type === 'tool.remove') {
75
+ this.toolsByScope.get(scope)?.delete(event.rowKey);
76
+ return [this.state.emit({ type: 'tool.remove', scope, rowKey: event.rowKey })];
77
+ }
78
+ else if (event.type === 'setup.snapshot') {
79
+ this.setupByScope.set(scope, event.setup);
80
+ return [this.state.emit({ type: 'setup.snapshot', scope, capturedAt: event.capturedAt, setup: event.setup })];
81
+ }
62
82
  else {
63
83
  const { v: _v, streamId: _streamId, sequence: _sequence, ...payload } = event;
64
84
  return [this.state.emit(payload)];
@@ -77,7 +97,9 @@ export class FeedSessionProjection {
77
97
  if (rowEvent.type === 'reset') {
78
98
  resetScopes.add(rowEvent.scope);
79
99
  result.push(this.state.emit({ type: 'reset', scope: rowEvent.scope, capturedAt: rowEvent.capturedAt, agents: rowEvent.rows,
80
- attention: [...next.values()].filter(value => value.scope === rowEvent.scope).map(value => value.item) }));
100
+ attention: [...next.values()].filter(value => value.scope === rowEvent.scope).map(value => value.item),
101
+ tools: [...(this.toolsByScope.get(rowEvent.scope)?.values() ?? [])],
102
+ setup: this.setupByScope.get(rowEvent.scope) ?? [] }));
81
103
  }
82
104
  else if (rowEvent.type === 'upsert') {
83
105
  result.push(this.state.emit({ type: 'agent.upsert', scope: rowEvent.scope, rowKey: rowEvent.rowKey, agent: rowEvent.row }));
@@ -136,11 +158,11 @@ async function projectAgent(agent, gh) {
136
158
  const pullRequest = await pullRequestFor(agent, gh);
137
159
  return { agent: withPullRequestStatus(agent, pullRequest), attention: attentionFor(agent, pullRequest) };
138
160
  }
139
- export async function projectSessionEnvelope(event, state, gh) {
161
+ export async function projectSessionEnvelope(event, state, gh, tools = [], setup = []) {
140
162
  if (event.type === 'reset') {
141
163
  const projected = await Promise.all(event.rows.map((row) => projectAgent(row, gh)));
142
164
  const attention = projected.map((p) => p.attention).filter((item) => item !== undefined);
143
- return [state.emit({ type: 'reset', capturedAt: event.capturedAt, scope: event.scope, agents: projected.map((p) => p.agent), attention })];
165
+ return [state.emit({ type: 'reset', capturedAt: event.capturedAt, scope: event.scope, agents: projected.map((p) => p.agent), attention, tools, setup })];
144
166
  }
145
167
  if (event.type === 'upsert') {
146
168
  const { agent, attention } = await projectAgent(event.row, gh);
@@ -198,6 +220,23 @@ export async function watchLocalFeed(options) {
198
220
  // dropping it from a window the caller believes was covered.
199
221
  const activity = new ActivityStream();
200
222
  let activityCursor = Date.now();
223
+ // The tool rows as this machine last projected them. Held here rather than
224
+ // re-collected per reset: a session-watch reconnect emits a fresh reset, and
225
+ // re-reading the browser tree and the ledger for it would reintroduce exactly
226
+ // the per-render cost `tool-activity.ts` exists to remove.
227
+ let toolRows = collectToolRows(options.scope, options.tools?.sources).rows;
228
+ // Setup rows come from the setup teammate's cache, never from a probe here:
229
+ // `getCachedToolSetup` reads metadata plus the last EXPLICIT health check, so
230
+ // publishing it costs no subprocess and cannot unlock a secret store.
231
+ const readSetup = options.setup?.read ?? getCachedToolSetup;
232
+ let setupRows = (() => {
233
+ try {
234
+ return readSetup();
235
+ }
236
+ catch {
237
+ return [];
238
+ }
239
+ })();
201
240
  const agents = new Map();
202
241
  const attention = new Map();
203
242
  // The PR status last projected onto each row, so a merge or a check verdict
@@ -245,7 +284,30 @@ export async function watchLocalFeed(options) {
245
284
  await reconcileRows();
246
285
  });
247
286
  }, options.activityPollMs ?? 500);
248
- const stopActivity = () => { clearInterval(activityTimer); stopAttentionWatch(); activity.close(); };
287
+ const toolWatch = watchToolActivity({
288
+ ...options.tools, scope: options.scope, signal: options.signal, initial: toolRows,
289
+ onDiff: (diff) => {
290
+ for (const tool of diff.upserts) {
291
+ toolRows = [...toolRows.filter((row) => row.rowKey !== tool.rowKey), tool];
292
+ options.emit(state.emit({ type: 'tool.upsert', scope: options.scope, rowKey: tool.rowKey, tool }));
293
+ }
294
+ for (const rowKey of diff.removes) {
295
+ toolRows = toolRows.filter((row) => row.rowKey !== rowKey);
296
+ options.emit(state.emit({ type: 'tool.remove', scope: options.scope, rowKey }));
297
+ }
298
+ },
299
+ });
300
+ // The setup cache is file-backed and notifies on change, so a Settings pane
301
+ // costs no polling: `subscribeToolSetup` is the invalidation the setup side
302
+ // publishes for exactly this consumer.
303
+ const stopSetupWatch = (options.setup?.subscribe ?? subscribeToolSetup)((rows) => {
304
+ const next = JSON.stringify(rows);
305
+ if (next === JSON.stringify(setupRows))
306
+ return;
307
+ setupRows = rows;
308
+ options.emit(state.emit({ type: 'setup.snapshot', scope: options.scope, capturedAt: Date.now(), setup: rows }));
309
+ });
310
+ const stopActivity = () => { clearInterval(activityTimer); stopAttentionWatch(); activity.close(); toolWatch.stop(); stopSetupWatch(); };
249
311
  options.signal.addEventListener('abort', stopActivity, { once: true });
250
312
  try {
251
313
  await watchLocalSessions({ ...options.sessions, scope: options.scope, signal: options.signal, emit: (event) => {
@@ -261,7 +323,7 @@ export async function watchLocalFeed(options) {
261
323
  attention.delete(event.rowKey);
262
324
  prStatus.delete(event.rowKey);
263
325
  }
264
- pending = pending.then(() => projectSessionEnvelope(event, state, options.gh)).then((events) => {
326
+ pending = pending.then(() => projectSessionEnvelope(event, state, options.gh, toolRows, setupRows)).then((events) => {
265
327
  for (const projected of events) {
266
328
  if (projected.type === 'reset') {
267
329
  attention.clear();
@@ -301,6 +363,75 @@ function remoteFeedWatchCommand(os) {
301
363
  ? buildWindowsAgentsCommand({ args })
302
364
  : `bash -lc ${shellQuote(`agents ${args.map(shellQuote).join(' ')}`)}`;
303
365
  }
366
+ /**
367
+ * The one ingress for an envelope produced by ANOTHER agents-cli.
368
+ *
369
+ * `tools` was added to the `reset` payload within protocol v1, so a peer on an
370
+ * older CLI is a correct v1 producer that simply reports no tool rows — which is
371
+ * true of it, not a data defect to paper over. Normalizing HERE, at the single
372
+ * place a foreign envelope enters, is what keeps every consumer downstream able
373
+ * to treat the field as present instead of each re-checking it. A mixed-version
374
+ * fleet is the normal state during a rollout, so an absent field must not take
375
+ * the whole fan-out down.
376
+ */
377
+ export function normalizePeerEnvelope(event) {
378
+ if (event.type !== 'reset')
379
+ return event;
380
+ if (Array.isArray(event.tools) && Array.isArray(event.setup))
381
+ return event;
382
+ return { ...event, tools: event.tools ?? [], setup: event.setup ?? [] };
383
+ }
384
+ /**
385
+ * The process-wide local collector, shared by refcount.
386
+ *
387
+ * `watchLocalFeed` is cheap per tick but NOT free to duplicate: each call builds
388
+ * its own `ActivityStream` cursor set over the activity directory, its own
389
+ * recursive tool watchers, its own feed/resolution watchers and its own
390
+ * setup subscription. Two callers in one process paid all of that twice and
391
+ * produced identical envelopes — and there are genuinely two callers on a daemon
392
+ * box: the fleet fan-out (which watches the local scope alongside every peer)
393
+ * and anything else that wants this machine's rows.
394
+ *
395
+ * One {@link FeedHub} over `watchLocalFeed` collapses them: the watcher starts on
396
+ * the first subscriber, stops on the last, and a late subscriber is caught up
397
+ * from held state instead of constructing a second cursor set.
398
+ *
399
+ * This is deliberately PROCESS-wide and not machine-wide. Making `feed watch
400
+ * --json --local` attach to the daemon instead would be the machine-wide
401
+ * version, and it would put a daemon dependency on the one command the fleet
402
+ * fan-out runs over ssh on every peer — a peer with a sick daemon would drop off
403
+ * the fleet stream entirely rather than degrade. The cross-process sharing that
404
+ * matters is the FLEET stream, which is what the daemon hub serves.
405
+ */
406
+ let sharedLocal = null;
407
+ /** The shared local collector, constructed on first use. Exposed for tests. */
408
+ export function sharedLocalFeedHub() {
409
+ return sharedLocal ??= new FeedHub({
410
+ watch: (hubOptions) => watchLocalFeed({
411
+ scope: machineId(), signal: hubOptions.signal, emit: hubOptions.emit,
412
+ }),
413
+ });
414
+ }
415
+ /** Reset the shared collector. Tests only — it must not leak across cases. */
416
+ export async function resetSharedLocalFeedHub() {
417
+ const hub = sharedLocal;
418
+ sharedLocal = null;
419
+ await hub?.close();
420
+ }
421
+ /**
422
+ * Subscribe to this machine's rows through the shared collector, until abort.
423
+ */
424
+ export function subscribeSharedLocalFeed(options) {
425
+ const detach = sharedLocalFeedHub().subscribe(options.emit);
426
+ return new Promise((resolve) => {
427
+ if (options.signal.aborted) {
428
+ detach();
429
+ resolve();
430
+ return;
431
+ }
432
+ options.signal.addEventListener('abort', () => { detach(); resolve(); }, { once: true });
433
+ });
434
+ }
304
435
  export async function watchFleetFeed(options) {
305
436
  const coordinator = new FeedWatchState();
306
437
  const projection = new FeedSessionProjection(coordinator);
@@ -308,7 +439,7 @@ export async function watchFleetFeed(options) {
308
439
  for (const projected of projection.apply(event))
309
440
  options.emit(projected);
310
441
  };
311
- const local = watchLocalFeed({ scope: machineId(), signal: options.signal, emit: forward });
442
+ const local = subscribeSharedLocalFeed({ signal: options.signal, emit: forward });
312
443
  let devices;
313
444
  try {
314
445
  devices = await loadDevices();
@@ -331,7 +462,7 @@ export async function watchFleetFeed(options) {
331
462
  const event = JSON.parse(line);
332
463
  if (event.v !== 1)
333
464
  return false;
334
- forward(event);
465
+ forward(normalizePeerEnvelope(event));
335
466
  return true;
336
467
  }
337
468
  catch {
@@ -61,7 +61,7 @@ export declare function relTime(iso: string): string;
61
61
  export declare function humanDuration(ms: number): string;
62
62
  /**
63
63
  * Human-readable byte size. Previously copy-pasted into five files
64
- * (`commands/prune.ts`, `commands/share.ts`, `commands/inspect.ts`,
64
+ * (`commands/prune.ts`, `commands/inspect.ts`,
65
65
  * `commands/sessions.ts`, `lib/browser/sessions-list.ts`) — this is the
66
66
  * canonical home.
67
67
  */
@@ -115,7 +115,7 @@ export function humanDuration(ms) {
115
115
  }
116
116
  /**
117
117
  * Human-readable byte size. Previously copy-pasted into five files
118
- * (`commands/prune.ts`, `commands/share.ts`, `commands/inspect.ts`,
118
+ * (`commands/prune.ts`, `commands/inspect.ts`,
119
119
  * `commands/sessions.ts`, `lib/browser/sessions-list.ts`) — this is the
120
120
  * canonical home.
121
121
  */
package/dist/lib/git.d.ts CHANGED
@@ -106,22 +106,6 @@ export declare function readRepoState(repoPath: string): RepoStateSnapshot | nul
106
106
  export declare function resolveSnapshotSha(repoRoot: string): string | undefined;
107
107
  /** Test seam: clear the memoized snapshot-sha cache between test cases. */
108
108
  export declare function _resetSnapshotShaCacheForTest(): void;
109
- /**
110
- * Resolve the GitHub username synchronously for `agents artifacts share`. Order:
111
- * 1. `AGENTS_SHARE_GITHUB_USER` env override
112
- * 2. `git config --global github.user`
113
- * Falls back to null so callers can decide whether to require auth or proceed
114
- * without a namespace.
115
- */
116
- export declare function resolveGitHubUsernameSync(): string | null;
117
- /**
118
- * Resolve the GitHub username asynchronously. Order:
119
- * 1. `AGENTS_SHARE_GITHUB_USER` env override
120
- * 2. `gh api user --jq ".login"`
121
- * 3. `git config --global github.user`
122
- * Returns null if none succeed.
123
- */
124
- export declare function resolveGitHubUsername(): Promise<string | null>;
125
109
  /**
126
110
  * Get the remote URL for origin in a git repo.
127
111
  */
package/dist/lib/git.js CHANGED
@@ -389,64 +389,6 @@ export function resolveSnapshotSha(repoRoot) {
389
389
  export function _resetSnapshotShaCacheForTest() {
390
390
  _snapshotShaCache.clear();
391
391
  }
392
- /**
393
- * Get the current GitHub username using gh CLI.
394
- * Returns null if gh is not installed or user is not authenticated.
395
- */
396
- async function getGitHubUsername() {
397
- try {
398
- const { exec } = await import('child_process');
399
- const { promisify } = await import('util');
400
- const execAsync = promisify(exec);
401
- const { stdout } = await execAsync('gh api user --jq ".login"');
402
- return stdout.trim() || null;
403
- }
404
- catch {
405
- /* gh CLI not installed or not authenticated */
406
- return null;
407
- }
408
- }
409
- const GITHUB_USER_ENV = 'AGENTS_SHARE_GITHUB_USER';
410
- /** Best-effort sync read of `github.user` from git config. */
411
- function readGitConfigUser() {
412
- try {
413
- return execFileSync('git', ['config', '--global', 'github.user'], { stdio: ['ignore', 'pipe', 'ignore'] })
414
- .toString()
415
- .trim() || null;
416
- }
417
- catch {
418
- return null;
419
- }
420
- }
421
- /**
422
- * Resolve the GitHub username synchronously for `agents artifacts share`. Order:
423
- * 1. `AGENTS_SHARE_GITHUB_USER` env override
424
- * 2. `git config --global github.user`
425
- * Falls back to null so callers can decide whether to require auth or proceed
426
- * without a namespace.
427
- */
428
- export function resolveGitHubUsernameSync() {
429
- const env = process.env[GITHUB_USER_ENV]?.trim();
430
- if (env)
431
- return env;
432
- return readGitConfigUser();
433
- }
434
- /**
435
- * Resolve the GitHub username asynchronously. Order:
436
- * 1. `AGENTS_SHARE_GITHUB_USER` env override
437
- * 2. `gh api user --jq ".login"`
438
- * 3. `git config --global github.user`
439
- * Returns null if none succeed.
440
- */
441
- export async function resolveGitHubUsername() {
442
- const env = process.env[GITHUB_USER_ENV]?.trim();
443
- if (env)
444
- return env;
445
- const gh = await getGitHubUsername();
446
- if (gh)
447
- return gh;
448
- return readGitConfigUser();
449
- }
450
392
  /**
451
393
  * Get the remote URL for origin in a git repo.
452
394
  */
@@ -41,7 +41,7 @@
41
41
  * it would claim a distribution responsibility agents-cli no longer has.
42
42
  */
43
43
  export const HELPER_RELEASES = {
44
- menubar: { tagPrefix: 'menubar', floor: '1.4.0' },
44
+ menubar: { tagPrefix: 'menubar', floor: '1.5.0' },
45
45
  };
46
46
  /** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
47
47
  export function helperTag(helper, version) {
@@ -158,12 +158,62 @@ export declare const POWERSHELL_PROGRESS_SILENCE = "$ProgressPreference = 'Silen
158
158
  * every remote-JSON boundary regardless of the peer's OS.
159
159
  */
160
160
  export declare function stripClixml(stdout: string): string;
161
+ /**
162
+ * Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
163
+ * child's exit code in `$zq`.
164
+ *
165
+ * `& agents …` cannot be used, and the reason is not our quoting. On Windows
166
+ * `agents` is an npm-generated `agents.ps1`, whose body ends in
167
+ *
168
+ * & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
169
+ *
170
+ * — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
171
+ * serializer. So the loss happens INSIDE the user's shim, after our tokens were
172
+ * already correct: measured on a real peer, `no-such-"menu"-proof` reached the
173
+ * Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
174
+ * and rewriting a user's npm shim is not ours to do.
175
+ *
176
+ * Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
177
+ * resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
178
+ * The entry and runtime are resolved ON THE PEER from the launcher's own location —
179
+ * the identical two-step the shim itself performs — and executed through .NET with
180
+ * a `CommandLineToArgvW`-escaped argument string.
181
+ *
182
+ * A peer whose `agents` is already a native executable is used directly. A peer
183
+ * where neither resolves FAILS LOUD: falling back to `& agents` would silently
184
+ * restore the argument loss, which is worse than an error naming what is missing.
185
+ *
186
+ * The entry comes from the package's DECLARED `bin` rather than a literal
187
+ * `dist/index.js`, so it follows the package instead of needing to be kept in sync
188
+ * with it.
189
+ */
190
+ export declare function windowsAgentsInvocation(args: string[], binName?: 'agents' | 'ag'): string;
161
191
  export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
162
192
  /**
163
193
  * Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
164
- * Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
194
+ * Windows remote: a rendered `powershell -NoProfile` call. The
165
195
  * Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
166
196
  */
197
+ /**
198
+ * Render a PowerShell script as the single command string ssh sends to a Windows
199
+ * peer, compressing it when that is genuinely shorter.
200
+ *
201
+ * The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
202
+ * script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
203
+ * command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
204
+ * succeed and 3102 fail — so a longer script eats the headroom a caller's
205
+ * arguments, `--remote-cwd` and forwarded env need.
206
+ *
207
+ * The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
208
+ * that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
209
+ * which on scripts of this shape is a large net win. The variable-free bootstrap
210
+ * uses `-Command` with a base64 payload and leaves stdin available to the child.
211
+ *
212
+ * Applied ONLY when the result is actually shorter: for a small script the fixed
213
+ * bootstrap costs more than it saves, so picking the shorter of the two keeps the
214
+ * plain form in play for every small caller and is obviously correct either way.
215
+ */
216
+ export declare function renderPowershellCommand(script: string): string;
167
217
  export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): string;
168
218
  /**
169
219
  * Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
@@ -8,6 +8,9 @@
8
8
  * (`sshExec`/`sshStream`); orchestration lives in `passthrough.ts`.
9
9
  */
10
10
  import { shellQuote } from '../ssh-exec.js';
11
+ import { pwshLiteral, pwshNativeExecStatements } from '../pwsh.js';
12
+ import { quoteWin32ExecArg } from '../platform/exec.js';
13
+ import * as zlib from 'node:zlib';
11
14
  /**
12
15
  * Remove routing flags (and their values) from a command's args, leaving the
13
16
  * rest untouched and in order so they forward verbatim to the remote binary.
@@ -136,8 +139,6 @@ export const RUN_OPTION_FORWARDING = {
136
139
  // On a --device dispatch the remote box runs its own run-exit sync, so this
137
140
  // flag is never forwarded — it is a local-exit-behavior toggle, not remote.
138
141
  traceSync: 'local-only',
139
- // Deprecated alias for --device auto; resolved on the launching box before SSH.
140
- smart: 'local-only',
141
142
  // Broadcast mode (agents run --broadcast) is its own fan-out dispatch — mutually
142
143
  // exclusive with --host. All broadcast options are local-only; exec.ts handles them
143
144
  // before any SSH dispatch path is reached.
@@ -293,30 +294,146 @@ export function stripClixml(stdout) {
293
294
  .replace(/#< CLIXML[^\n]*\r?\n?(?:\s*<Objs\b[\s\S]*?<\/Objs>)*/g, '')
294
295
  .trim();
295
296
  }
297
+ /**
298
+ * Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
299
+ * child's exit code in `$zq`.
300
+ *
301
+ * `& agents …` cannot be used, and the reason is not our quoting. On Windows
302
+ * `agents` is an npm-generated `agents.ps1`, whose body ends in
303
+ *
304
+ * & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
305
+ *
306
+ * — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
307
+ * serializer. So the loss happens INSIDE the user's shim, after our tokens were
308
+ * already correct: measured on a real peer, `no-such-"menu"-proof` reached the
309
+ * Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
310
+ * and rewriting a user's npm shim is not ours to do.
311
+ *
312
+ * Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
313
+ * resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
314
+ * The entry and runtime are resolved ON THE PEER from the launcher's own location —
315
+ * the identical two-step the shim itself performs — and executed through .NET with
316
+ * a `CommandLineToArgvW`-escaped argument string.
317
+ *
318
+ * A peer whose `agents` is already a native executable is used directly. A peer
319
+ * where neither resolves FAILS LOUD: falling back to `& agents` would silently
320
+ * restore the argument loss, which is worse than an error naming what is missing.
321
+ *
322
+ * The entry comes from the package's DECLARED `bin` rather than a literal
323
+ * `dist/index.js`, so it follows the package instead of needing to be kept in sync
324
+ * with it.
325
+ */
326
+ export function windowsAgentsInvocation(args, binName = 'agents') {
327
+ const escaped = pwshLiteral(args.map(quoteWin32ExecArg).join(' '));
328
+ return [
329
+ // Set before any resolution: a non-terminating failure below would otherwise
330
+ // leave the exit code null, and `exit $null` reports 0.
331
+ `$ErrorActionPreference = 'Stop'`,
332
+ `$zc = Get-Command ${powershellQuote(binName)} -ErrorAction Stop`,
333
+ `$ze = $zc.Source`,
334
+ `$zr = ''`,
335
+ // A real native executable needs no interpreter prefix. `CommandType` alone is
336
+ // NOT that test: `Get-Command` reports `Application` for `agents.cmd` too, and
337
+ // a .cmd launcher re-parses through cmd.exe with the same argument loss as the
338
+ // .ps1 one — so a cmd-only install must not bypass this either.
339
+ `if ($zc.CommandType -ne 'Application' -or $ze -match '\\.(cmd|bat)$') {`,
340
+ ` $zb = Split-Path $ze`,
341
+ ` $zk = Join-Path $zb 'node_modules\\@phnx-labs\\agents-cli'`,
342
+ // The package's DECLARED bin, so the entry follows the package rather than a
343
+ // hand-synced `dist/index.js` literal that would rot on upgrade. One check
344
+ // covers a missing bin AND the single-string `bin` form (which yields $null).
345
+ ` $zl = (Get-Content -Raw (Join-Path $zk 'package.json') | ConvertFrom-Json).bin.${binName}`,
346
+ ` if (-not $zl) { throw "agents package at $zk declares no bin.${binName}" }`,
347
+ ` $zt = Join-Path $zk $zl`,
348
+ ` if (-not [IO.File]::Exists($zt)) { throw "agents CLI entry not found at $zt" }`,
349
+ // The npm shim prefers a node.exe beside itself before PATH; match that.
350
+ ` $zd = Join-Path $zb 'node.exe'`,
351
+ ` $ze = if ([IO.File]::Exists($zd)) { $zd } else { 'node' }`,
352
+ // A Windows path cannot contain `"`, so wrapping is sufficient escaping here.
353
+ ` $zr = '--no-warnings=ExperimentalWarning "' + $zt + '" '`,
354
+ `}`,
355
+ ...pwshNativeExecStatements('$ze', `$zr + ${escaped}`),
356
+ // Reported through a plain variable rather than by assigning the automatic
357
+ // $LASTEXITCODE, which nothing set here since no PowerShell command ran.
358
+ `$zq = $zp.ExitCode`,
359
+ // Never let an unknown outcome read as success.
360
+ `if ($null -eq $zq) { $zq = 1 }`,
361
+ // NEWLINE-joined, and that is required rather than stylistic: the caller joins
362
+ // its parts with `'; '`, which between a closing `}` and `else`/an indented block
363
+ // would produce invalid PowerShell.
364
+ ].join('\n');
365
+ }
296
366
  export function windowsAgentsScript(cmd) {
297
367
  const { args, env, cwd, propagateExit = true, remapExit255 = false } = cmd;
298
- const parts = [POWERSHELL_PROGRESS_SILENCE];
368
+ // `Stop` FIRST, before the env assignments and the Set-Location: a failing
369
+ // `Set-Location` (a cwd that does not exist on the peer) must abort rather than
370
+ // continue into the launcher and run the command in the wrong directory.
371
+ const parts = [POWERSHELL_PROGRESS_SILENCE, `$ErrorActionPreference = 'Stop'`];
299
372
  if (env)
300
373
  for (const [k, v] of Object.entries(env))
301
374
  parts.push(`$env:${k} = ${powershellQuote(v)}`);
302
375
  if (cwd)
303
376
  parts.push(`Set-Location -LiteralPath ${powershellQuote(cwd)}`);
304
- parts.push(`& ${['agents', ...args].map(powershellQuote).join(' ')}`);
377
+ parts.push(windowsAgentsInvocation(args));
305
378
  if (propagateExit) {
306
379
  if (remapExit255)
307
- parts.push('$agentsExit = $LASTEXITCODE', 'if ($agentsExit -eq 255) { exit 254 }', 'exit $agentsExit');
380
+ parts.push('if ($zq -eq 255) { exit 254 }', 'exit $zq');
308
381
  else
309
- parts.push('exit $LASTEXITCODE');
382
+ parts.push('exit $zq');
310
383
  }
311
384
  return parts.join('; ');
312
385
  }
313
386
  /**
314
387
  * Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
315
- * Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
388
+ * Windows remote: a rendered `powershell -NoProfile` call. The
316
389
  * Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
317
390
  */
391
+ /**
392
+ * Render a PowerShell script as the single command string ssh sends to a Windows
393
+ * peer, compressing it when that is genuinely shorter.
394
+ *
395
+ * The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
396
+ * script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
397
+ * command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
398
+ * succeed and 3102 fail — so a longer script eats the headroom a caller's
399
+ * arguments, `--remote-cwd` and forwarded env need.
400
+ *
401
+ * The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
402
+ * that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
403
+ * which on scripts of this shape is a large net win. The variable-free bootstrap
404
+ * uses `-Command` with a base64 payload and leaves stdin available to the child.
405
+ *
406
+ * Applied ONLY when the result is actually shorter: for a small script the fixed
407
+ * bootstrap costs more than it saves, so picking the shorter of the two keeps the
408
+ * plain form in play for every small caller and is obviously correct either way.
409
+ */
410
+ export function renderPowershellCommand(script) {
411
+ const plain = `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
412
+ // Raw DEFLATE (RFC 1951) — what .NET's `DeflateStream` reads. `deflateSync`
413
+ // would prepend a zlib header that `DeflateStream` rejects.
414
+ const packed = zlib.deflateRawSync(Buffer.from(script, 'utf-8'), { level: 9 }).toString('base64');
415
+ // The blob is embedded DIRECTLY in a `-Command` string rather than wrapped in a
416
+ // second `-EncodedCommand`: routing an already-base64 payload through the encoded
417
+ // form would inflate it by another 2.67x and cancel most of the compression.
418
+ //
419
+ // The bootstrap is deliberately VARIABLE-FREE — one nested expression, no `$`
420
+ // anywhere. A `$b = …` form is unsafe here: OpenSSH-for-Windows may have
421
+ // PowerShell as its `DefaultShell`, in which case the outer double-quoted string
422
+ // is parsed by PowerShell first and `$b` would be expanded before our script ever
423
+ // runs. With no `$`, no `%`, no backtick and no cmd metacharacter — base64's
424
+ // alphabet is `A-Za-z0-9+/=` and the rest is ASCII punctuation neither shell
425
+ // touches inside quotes — the same text survives either default shell. stdin is
426
+ // untouched by both routes.
427
+ const bootstrap = 'iex ([IO.StreamReader]::new([IO.Compression.DeflateStream]::new('
428
+ + `[IO.MemoryStream]::new([Convert]::FromBase64String('${packed}')),`
429
+ + '[IO.Compression.CompressionMode]::Decompress),[Text.Encoding]::UTF8).ReadToEnd())';
430
+ const compressed = `powershell -NoProfile -Command "${bootstrap}"`;
431
+ // Only when genuinely shorter: for a small script the fixed bootstrap costs more
432
+ // than it saves, so this keeps the plain form in play for every small caller.
433
+ return compressed.length < plain.length ? compressed : plain;
434
+ }
318
435
  export function buildWindowsAgentsCommand(cmd) {
319
- return `powershell -NoProfile -EncodedCommand ${encodePowershell(windowsAgentsScript(cmd))}`;
436
+ return renderPowershellCommand(windowsAgentsScript(cmd));
320
437
  }
321
438
  /**
322
439
  * Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
@@ -0,0 +1,2 @@
1
+ /** The script behind either render route. Throws if the command is neither. */
2
+ export declare function decodeRenderedPowershell(command: string): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Test-only decoder for a rendered remote PowerShell command.
3
+ *
4
+ * `renderPowershellCommand` emits whichever of two representations is shorter: the
5
+ * plain `-EncodedCommand <base64 UTF-16LE>`, or a `-Command` bootstrap carrying a
6
+ * deflated UTF-8 payload. Tests assert on the SCRIPT, not the representation, so
7
+ * every suite needs both — and four copies of that parsing is exactly how one of
8
+ * them silently stops decoding the form it was meant to check.
9
+ *
10
+ * Named `*.test-fixture.ts` after the existing `daemon.test-fixture.ts`.
11
+ */
12
+ import * as zlib from 'node:zlib';
13
+ /** The script behind either render route. Throws if the command is neither. */
14
+ export function decodeRenderedPowershell(command) {
15
+ const encoded = /-EncodedCommand (\S+)\s*$/.exec(command);
16
+ if (encoded)
17
+ return Buffer.from(encoded[1], 'base64').toString('utf16le');
18
+ const packed = /FromBase64String\('([A-Za-z0-9+/=]+)'\)/.exec(command);
19
+ if (!packed)
20
+ throw new Error(`not a rendered PowerShell command: ${command}`);
21
+ return zlib.inflateRawSync(Buffer.from(packed[1], 'base64')).toString('utf-8');
22
+ }