@shardflux/cli 0.15.2 → 0.17.0

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 (77) hide show
  1. package/CHANGELOG.md +144 -14
  2. package/README.md +92 -7
  3. package/dist/acp-models.js +5 -1
  4. package/dist/agent-commands.d.ts +15 -1
  5. package/dist/agent-commands.js +84 -23
  6. package/dist/agent-models.d.ts +61 -0
  7. package/dist/agent-models.js +79 -0
  8. package/dist/agent-state.d.ts +10 -0
  9. package/dist/agent-state.js +13 -0
  10. package/dist/agent-target-commands.d.ts +13 -3
  11. package/dist/agent-target-commands.js +129 -23
  12. package/dist/agent-targets.d.ts +5 -0
  13. package/dist/agent-targets.js +14 -5
  14. package/dist/agent-terminal.d.ts +8 -0
  15. package/dist/agent-terminal.js +35 -2
  16. package/dist/args.d.ts +5 -1
  17. package/dist/args.js +74 -11
  18. package/dist/cli-proxy-forward.d.ts +45 -0
  19. package/dist/cli-proxy-forward.js +432 -0
  20. package/dist/cli-proxy-hub.d.ts +273 -0
  21. package/dist/cli-proxy-hub.js +749 -0
  22. package/dist/cli-proxy.d.ts +325 -0
  23. package/dist/cli-proxy.js +1016 -0
  24. package/dist/codex-cloud.d.ts +3 -0
  25. package/dist/codex-cloud.js +17 -5
  26. package/dist/command-center-actions.js +25 -1
  27. package/dist/commands.d.ts +22 -0
  28. package/dist/commands.js +54 -14
  29. package/dist/conversation-commands.js +6 -0
  30. package/dist/conversation-local.js +2 -2
  31. package/dist/conversation-stash.js +4 -3
  32. package/dist/conversation-terminal.d.ts +5 -0
  33. package/dist/conversation-terminal.js +54 -6
  34. package/dist/conversations.d.ts +31 -3
  35. package/dist/conversations.js +53 -10
  36. package/dist/device-approvals.d.ts +12 -0
  37. package/dist/device-approvals.js +73 -0
  38. package/dist/device-helper.d.ts +3 -1
  39. package/dist/device-helper.js +14 -4
  40. package/dist/errors.d.ts +7 -0
  41. package/dist/errors.js +12 -2
  42. package/dist/files-mirror.d.ts +164 -0
  43. package/dist/files-mirror.js +761 -0
  44. package/dist/handlers.js +5 -1
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.js +3 -0
  47. package/dist/linux-helper.d.ts +12 -0
  48. package/dist/linux-helper.js +88 -0
  49. package/dist/linux-terminal.d.ts +11 -0
  50. package/dist/linux-terminal.js +42 -0
  51. package/dist/main.js +38 -8
  52. package/dist/onboarding.d.ts +2 -0
  53. package/dist/onboarding.js +53 -5
  54. package/dist/registry.d.ts +2 -42
  55. package/dist/registry.js +6 -90
  56. package/dist/t3-launch.d.ts +37 -0
  57. package/dist/t3-launch.js +133 -0
  58. package/dist/t3-mode.d.ts +121 -0
  59. package/dist/t3-mode.js +654 -0
  60. package/dist/t3-phone.d.ts +17 -0
  61. package/dist/t3-phone.js +15 -0
  62. package/dist/t3-thread-script.d.ts +18 -0
  63. package/dist/t3-thread-script.js +245 -0
  64. package/dist/terminal-onboarding.d.ts +11 -0
  65. package/dist/terminal-onboarding.js +75 -0
  66. package/dist/thread-on-computer.d.ts +35 -0
  67. package/dist/thread-on-computer.js +206 -0
  68. package/dist/update.d.ts +1 -1
  69. package/dist/update.js +12 -1
  70. package/dist/wait-status.d.ts +22 -0
  71. package/dist/wait-status.js +59 -0
  72. package/dist/worktree-twin-engine.d.ts +125 -0
  73. package/dist/worktree-twin-engine.js +615 -0
  74. package/dist/worktree-twin-land.py +331 -0
  75. package/dist/worktree-twin.d.ts +313 -0
  76. package/dist/worktree-twin.js +843 -0
  77. package/package.json +3 -3
package/dist/update.js CHANGED
@@ -286,12 +286,23 @@ async function refreshMenubar(ctx, menubar, runtime, run, env) {
286
286
  return after?.version && after.version !== before.version ? { status: 'restarted', version: after.version } : { status: 'current' };
287
287
  }
288
288
  /** Records where `shard` runs, for the menu bar's update button. Written on macOS, only when it changed. */
289
- export function recordInstall(dir, runtime, distribution) {
289
+ export function recordInstall(dir, runtime, distribution, env = {}) {
290
290
  let script = runtime.script;
291
291
  try {
292
292
  script = realpathSync(script);
293
293
  }
294
294
  catch { /* keep the path */ }
295
+ // A global install (npm, pnpm, bun, Volta) is what the menu bar runs and updates. A run from a source checkout, a
296
+ // project's node_modules or npx never replaces a recorded install that still exists, so a one-off run cannot point
297
+ // the menu bar (and its update button) at it. SHARDFLUX_RECORD_INSTALL=1 records any run (menu bar development).
298
+ if (env.SHARDFLUX_RECORD_INSTALL !== '1' && !['npm', 'pnpm', 'bun', 'volta'].includes(detectInstall(script, distribution.package).manager)) {
299
+ try {
300
+ const recorded = JSON.parse(readFileSync(join(dir, INSTALL_FILE), 'utf8')).script;
301
+ if (typeof recorded === 'string' && recorded !== script && existsSync(recorded))
302
+ return;
303
+ }
304
+ catch { /* nothing recorded yet: this run is the best there is */ }
305
+ }
295
306
  // The app this CLI's npm package carries (0.15.2+): the menu bar compares itself with it and swaps itself in.
296
307
  let menubarApp = packagedAppPath();
297
308
  try {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Where a wait stands (0.16.0+): while a command waits on an operation whose start is queued for capacity, or a call
3
+ * waits for room (the workspace's host making room to wake it, or a free working slot of the plan), one line on stderr
4
+ * says so: the queue position, what the pool is doing (a new host starting, when it is expected) and until when the
5
+ * start holds its place. A line is printed only when it changes; dim on a terminal, plain when piped; never with
6
+ * --json, where stdout stays one document and stderr is left to the error.
7
+ */
8
+ import type { ProgressEvent, ProgressListener, RetryRecord } from '@shardflux/sdk';
9
+ import type { Writer } from './commands.js';
10
+ /** `2026-10-09T21:40:12Z` as `21:40Z` (UTC); the input when it is not a timestamp. */
11
+ export declare function clockZ(iso: string): string;
12
+ /** A queued start in words: `queued for capacity (position 3): a new host is starting, ready by 21:40Z; it holds its place until 21:51Z`. */
13
+ export declare function queueLine(e: Pick<Extract<ProgressEvent, {
14
+ type: 'phase';
15
+ }>, 'queuePosition' | 'scaleOut' | 'expectedReadyBy' | 'deadlineAt'>): string;
16
+ /** A call waiting to be admitted, in words. */
17
+ export declare function admissionLine(r: Pick<RetryRecord, 'reason' | 'queuePosition'>): string;
18
+ /**
19
+ * The client listener that prints where a wait stands. Silent with `json`. A phase other than capacity_pending ends
20
+ * the queue, so a later queue prints again.
21
+ */
22
+ export declare function waitStatusListener(stderr: Writer, json: boolean): ProgressListener;
@@ -0,0 +1,59 @@
1
+ /** `2026-10-09T21:40:12Z` as `21:40Z` (UTC); the input when it is not a timestamp. */
2
+ export function clockZ(iso) {
3
+ const t = Date.parse(iso);
4
+ return Number.isFinite(t) ? `${new Date(t).toISOString().slice(11, 16)}Z` : iso;
5
+ }
6
+ /** A queued start in words: `queued for capacity (position 3): a new host is starting, ready by 21:40Z; it holds its place until 21:51Z`. */
7
+ export function queueLine(e) {
8
+ const position = e.queuePosition ? ` (position ${e.queuePosition})` : '';
9
+ const pool = e.scaleOut === 'requested'
10
+ ? `: a new host is starting${e.expectedReadyBy ? `, ready by ${clockZ(e.expectedReadyBy)}` : ''}`
11
+ : e.scaleOut === 'clamped'
12
+ ? ': it starts as soon as a host has room'
13
+ : '';
14
+ const until = e.deadlineAt ? `; it holds its place until ${clockZ(e.deadlineAt)}` : '';
15
+ return `queued for capacity${position}${pool}${until}`;
16
+ }
17
+ /** A call waiting to be admitted, in words. */
18
+ export function admissionLine(r) {
19
+ const position = r.queuePosition ? ` (position ${r.queuePosition})` : '';
20
+ if (r.reason === 'host_capacity')
21
+ return `waiting for room on the workspace's host${position}`;
22
+ if (r.reason === 'concurrent_workspaces')
23
+ return 'waiting for a free working slot: every workspace the plan lets work at once is busy';
24
+ return `waiting to be admitted${r.reason ? ` (${r.reason})` : ''}${position}`;
25
+ }
26
+ /**
27
+ * The client listener that prints where a wait stands. Silent with `json`. A phase other than capacity_pending ends
28
+ * the queue, so a later queue prints again.
29
+ */
30
+ export function waitStatusListener(stderr, json) {
31
+ let last = null;
32
+ return (e) => {
33
+ if (json)
34
+ return;
35
+ let line;
36
+ if (e.type === 'retry') {
37
+ if (!e.retry.admission)
38
+ return;
39
+ line = admissionLine(e.retry);
40
+ }
41
+ else if (e.type === 'phase') {
42
+ if (e.phase !== 'capacity_pending') {
43
+ if (e.action !== 'tool')
44
+ last = null;
45
+ return;
46
+ }
47
+ line = queueLine(e);
48
+ }
49
+ else {
50
+ if (e.action !== 'token' && e.action !== 'tool')
51
+ last = null;
52
+ return;
53
+ }
54
+ if (line === last)
55
+ return;
56
+ last = line;
57
+ stderr.write(stderr.isTTY ? `\x1b[2m· ${line}\x1b[0m\n` : `${line}\n`);
58
+ };
59
+ }
@@ -0,0 +1,125 @@
1
+ import type { Ctx, Handler } from './commands.js';
2
+ import type { Conversation, LocalConversation } from './conversations.js';
3
+ import { TwinLedger } from './worktree-twin.js';
4
+ import type { LandingMode, TwinCell } from './worktree-twin.js';
5
+ export interface TwinAgent {
6
+ id: string;
7
+ workspace_id: string;
8
+ state: 'working' | 'waiting' | 'exited';
9
+ since: string;
10
+ }
11
+ export type TwinConversation = Conversation & {
12
+ changes: {
13
+ files: number;
14
+ insertions: number;
15
+ deletions: number;
16
+ commits: number;
17
+ uncommitted: boolean;
18
+ };
19
+ landed: {
20
+ how: string;
21
+ ref: string;
22
+ at: string;
23
+ } | null;
24
+ summary: string;
25
+ name?: string;
26
+ join_mode?: string | null;
27
+ base_branch?: string | null;
28
+ last_activity_at?: string;
29
+ repo_remote?: string;
30
+ };
31
+ /** Its VM is gone: the conversation ended (archived after the retention window, or done). */
32
+ export declare const ended: (c: TwinConversation) => boolean;
33
+ export interface EngineDeps {
34
+ records(): LocalConversation[];
35
+ conversations(projectId: string): Promise<TwinConversation[]>;
36
+ agents(projectId: string): Promise<TwinAgent[]>;
37
+ /** The conversation's VM (woken when suspended), and its release; null when the VM is gone. */
38
+ cell(c: TwinConversation): Promise<{
39
+ cell: TwinCell;
40
+ close(): void;
41
+ } | null>;
42
+ /** A VM of the project that reads the project hub (an awake one, else the project's space reader). */
43
+ hubCell(projectId: string): Promise<{
44
+ cell: TwinCell;
45
+ close(): void;
46
+ }>;
47
+ /** Restores an ended conversation into a fresh VM (so its agent can resolve a conflict). */
48
+ resume(c: TwinConversation): Promise<void>;
49
+ land(c: TwinConversation, action: 'merge' | 'pr', into: string): Promise<void>;
50
+ message(c: TwinConversation, text: string): Promise<void>;
51
+ ledger: TwinLedger;
52
+ stateDir: string;
53
+ device: string;
54
+ log(line: string): void;
55
+ now(): number;
56
+ hub?: string;
57
+ vmTmp?: string;
58
+ quietMs?: number;
59
+ /**
60
+ * Long-polls the conversation's next finished turn after `after` (RFC 3339): the turn's conversation and report time,
61
+ * null when the wait timed out. Throws TurnWaitUnsupported when the API has no turn waits.
62
+ */
63
+ waitTurn?(c: TwinConversation, after: string, signal: AbortSignal): Promise<{
64
+ conversation: TwinConversation;
65
+ reported_at: string;
66
+ } | null | 'ended'>;
67
+ }
68
+ /** A turn ended with committed work only; the agent is not asking anything. */
69
+ export declare function finished(c: TwinConversation, agents: TwinAgent[]): TwinAgent | 'state' | null;
70
+ export declare function eligible(c: TwinConversation, ledger: TwinLedger, now: number): boolean;
71
+ export declare function conflictMessage(into: string, files: string[]): string;
72
+ type Ready = {
73
+ c: TwinConversation;
74
+ record: LocalConversation;
75
+ mode: Exclude<LandingMode, 'off'>;
76
+ agent: TwinAgent | null;
77
+ at: number;
78
+ };
79
+ /** Refused by an API without turn waits (POST /wait with turn_after, 422): the twin polls instead. */
80
+ export declare class TurnWaitUnsupported extends Error {
81
+ }
82
+ export declare class TwinEngine {
83
+ #private;
84
+ constructor(deps: EngineDeps);
85
+ /** Resolves when a pushed turn is waiting to land (the loop's sleep ends at once). */
86
+ pushed(): Promise<void>;
87
+ /** Stops every turn wait. */
88
+ stop(): void;
89
+ /**
90
+ * One pass; returns the suggested wait before the next. Turns pushed by the waits land first, without a listing;
91
+ * otherwise (and at least every 15 s, or every second when the API has no turn waits) every project is listed.
92
+ */
93
+ tick(signal?: AbortSignal): Promise<number>;
94
+ /** Back online or awake: every conversation's retry backoff is cleared, so the next pass tries all of them. */
95
+ resetBackoff(): void;
96
+ landOne(c: TwinConversation, record: LocalConversation, mode: Exclude<LandingMode, 'off'>, agent: TwinAgent | null, signal?: AbortSignal): Promise<number | undefined>;
97
+ /** Several finished conversations for one branch: merged in their VMs at once, the files written once. */
98
+ landBatch(items: Ready[], signal?: AbortSignal): Promise<number | undefined>;
99
+ }
100
+ export interface LoopDeps {
101
+ tick(signal: AbortSignal): Promise<number>;
102
+ resetBackoff(): void;
103
+ sleep(ms: number, signal: AbortSignal): Promise<void>;
104
+ /** Wall-clock and monotonic milliseconds: the monotonic clock stands still while the computer sleeps. */
105
+ wall(): number;
106
+ mono(): number;
107
+ /** The computer's network addresses (any change is a network change). */
108
+ network(): string;
109
+ /** Whether the API host answers now (a TCP connect, no request). */
110
+ reachable(): Promise<boolean>;
111
+ /** Resolves when a pushed turn waits to land: the loop's sleep ends at once. */
112
+ kicked?(): Promise<void>;
113
+ log(line: string): void;
114
+ }
115
+ /**
116
+ * Runs the twin's passes. A failing pass (offline) backs off up to 5 minutes, but the wait ends at once when the
117
+ * computer wakes from sleep (the wall clock jumps past the monotonic one), when its network changes, or when the API
118
+ * host answers again (probed every 5 s while failing); every conversation's retry backoff is then cleared and a pass
119
+ * runs immediately. The first pass that succeeds after failures clears them too and runs again at once.
120
+ */
121
+ export declare function runTwinLoop(deps: LoopDeps, signal: AbortSignal): Promise<void>;
122
+ /** The device helper's twin. Nothing here asks the user anything. */
123
+ export declare function runWorktreeTwin(ctx: Ctx, device: string, signal: AbortSignal): Promise<void>;
124
+ export declare const TWIN_HANDLERS: Record<string, Handler>;
125
+ export {};