@shardflux/cli 0.13.5 → 0.14.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 (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +40 -4
  3. package/dist/acp-catalog.d.ts +152 -0
  4. package/dist/acp-catalog.js +287 -0
  5. package/dist/acp-journal.d.ts +18 -0
  6. package/dist/acp-journal.js +232 -0
  7. package/dist/acp-rpc.d.ts +41 -0
  8. package/dist/acp-rpc.js +115 -0
  9. package/dist/acp.d.ts +27 -0
  10. package/dist/acp.js +290 -0
  11. package/dist/agent-commands.d.ts +10 -2
  12. package/dist/agent-commands.js +37 -18
  13. package/dist/agent-machine.d.ts +6 -0
  14. package/dist/agent-machine.js +93 -0
  15. package/dist/agent-repository.js +1 -1
  16. package/dist/agent-state.d.ts +3 -0
  17. package/dist/agent-state.js +33 -12
  18. package/dist/agent-target-commands.d.ts +4 -1
  19. package/dist/agent-target-commands.js +64 -22
  20. package/dist/agent-targets.d.ts +6 -4
  21. package/dist/agent-targets.js +72 -31
  22. package/dist/args.d.ts +1 -1
  23. package/dist/args.js +53 -14
  24. package/dist/command-center-actions.js +26 -3
  25. package/dist/command-center.js +20 -4
  26. package/dist/conversation-commands.d.ts +3 -0
  27. package/dist/conversation-commands.js +36 -0
  28. package/dist/conversation-local-resume.d.ts +8 -0
  29. package/dist/conversation-local-resume.js +56 -0
  30. package/dist/conversation-local.d.ts +7 -0
  31. package/dist/conversation-local.js +61 -0
  32. package/dist/conversation-router.d.ts +12 -0
  33. package/dist/conversation-router.js +89 -0
  34. package/dist/conversation-stash.d.ts +11 -0
  35. package/dist/conversation-stash.js +158 -0
  36. package/dist/conversation-terminal.d.ts +3 -0
  37. package/dist/conversation-terminal.js +74 -0
  38. package/dist/conversation-worktree.d.ts +4 -0
  39. package/dist/conversation-worktree.js +22 -0
  40. package/dist/conversations.d.ts +53 -0
  41. package/dist/conversations.js +118 -0
  42. package/dist/device-bridge.d.ts +80 -0
  43. package/dist/device-bridge.js +398 -0
  44. package/dist/device-helper.d.ts +3 -0
  45. package/dist/device-helper.js +104 -0
  46. package/dist/errors.js +5 -0
  47. package/dist/format.js +3 -2
  48. package/dist/handlers.js +3 -1
  49. package/dist/main.js +15 -3
  50. package/dist/menubar.d.ts +5 -0
  51. package/dist/menubar.js +22 -1
  52. package/dist/project-sync.d.ts +5 -1
  53. package/dist/project-sync.js +7 -6
  54. package/dist/t3-provider.d.ts +6 -0
  55. package/dist/t3-provider.js +99 -0
  56. package/dist/t3-runtime.d.ts +6 -0
  57. package/dist/t3-runtime.js +63 -0
  58. package/dist/target-lock.d.ts +3 -0
  59. package/dist/target-lock.js +143 -0
  60. package/package.json +3 -3
@@ -0,0 +1,232 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { StringDecoder } from 'node:string_decoder';
3
+ import { AcpRpc } from "./acp-rpc.js";
4
+ // InputACP's private, same-uid socket receipt protocol. Stdin avoids Linux's 128 KiB per-argument limit.
5
+ export const ACP_INPUT_SCRIPT = `import json, os, socket, sys
6
+ payload = sys.stdin.buffer.readline((2 << 20) + 1024)
7
+ if not payload.endswith(b'\\n') or len(json.dumps(json.loads(payload)['message'], separators=(',', ':'), ensure_ascii=False).encode()) > 2 << 20:
8
+ raise ValueError('ACP input exceeds 2 MiB')
9
+ folder = os.open(sys.argv[1], os.O_RDONLY)
10
+ try:
11
+ with socket.socket(socket.AF_UNIX) as peer:
12
+ peer.settimeout(10)
13
+ address = os.path.join(sys.argv[1], 'control.sock')
14
+ if len(address.encode()) >= 108: address = '/proc/self/fd/%d/control.sock' % folder
15
+ peer.connect(address)
16
+ peer.sendall(payload)
17
+ response = peer.makefile('rb').readline(65536)
18
+ result = json.loads(response)
19
+ if result.get('accepted') is not True: raise ValueError(result.get('error', 'ACP input was not acknowledged'))
20
+ sys.stdout.buffer.write(response)
21
+ finally:
22
+ os.close(folder)
23
+ `;
24
+ /** Replay is finite; a live follower exists only while a turn/RPC needs output. Native supervision is CVB's. */
25
+ export class AcpJournal {
26
+ rpc;
27
+ #cell;
28
+ #conversation;
29
+ #cwd;
30
+ #cursor;
31
+ #save;
32
+ #controller = new AbortController();
33
+ #signal;
34
+ #initial;
35
+ #ready = false;
36
+ #reading;
37
+ #reader;
38
+ #follower;
39
+ #writing = Promise.resolve();
40
+ #paused = 0;
41
+ constructor(cell, conversation, _harness, cwd, cursor, save, signal) {
42
+ if (!/^[a-zA-Z0-9_-]{1,96}$/.test(conversation))
43
+ throw new Error('Invalid ACP conversation identity');
44
+ this.#cell = cell;
45
+ this.#conversation = conversation;
46
+ this.#cwd = cwd;
47
+ this.#cursor = cursor;
48
+ this.#save = save;
49
+ this.#signal = AbortSignal.any([signal, this.#controller.signal]);
50
+ this.rpc = new AcpRpc(message => {
51
+ const encoded = JSON.stringify(message);
52
+ if (Buffer.byteLength(encoded) > 2 << 20)
53
+ return Promise.reject(new Error('ACP input exceeds 2 MiB'));
54
+ const inputId = randomUUID();
55
+ if (message.method === 'session/prompt' && message.id != null) {
56
+ this.#cursor.prompt_id = message.id;
57
+ this.#save();
58
+ }
59
+ const run = this.#writing.then(async () => {
60
+ await this.#deliver(inputId, encoded);
61
+ this.#follow();
62
+ });
63
+ this.#writing = run.catch(error => { this.rpc.close(error instanceof Error ? error : new Error(String(error))); });
64
+ return run;
65
+ });
66
+ this.rpc.onIdle = () => { if (!this.#wanted())
67
+ this.#reader?.abort(); };
68
+ }
69
+ #wanted() { return !this.#signal.aborted && !this.rpc.closed && this.#paused === 0 && (this.rpc.busy || this.#cursor.prompt_id !== undefined); }
70
+ async #deliver(inputId, message) {
71
+ const dir = `/home/user/.local/share/shardflux/acp/${this.#conversation}`;
72
+ const payload = Buffer.from(`{"mode":"input","input_id":${JSON.stringify(inputId)},"message":${message}}\n`);
73
+ const session = await this.#cell.exec.start({ argv: ['python3', '-c', ACP_INPUT_SCRIPT, dir], cwd: this.#cwd, stdin_open: true, timeout_ms: 15_000 }, this.#signal);
74
+ let offset = 0, stdout = '';
75
+ try {
76
+ while (offset < payload.length) {
77
+ const bytes = payload.subarray(offset, offset + 65536);
78
+ const ack = await this.#cell.exec.input(session.session_id, bytes, { offset, close: offset + bytes.length === payload.length, signal: this.#signal });
79
+ const accepted = ack.offset - offset;
80
+ if (!Number.isSafeInteger(accepted) || accepted <= 0 || accepted > bytes.length || (ack.closed && ack.offset !== payload.length))
81
+ throw new Error('Invalid ACP input acknowledgement');
82
+ offset = ack.offset;
83
+ }
84
+ let exit;
85
+ for await (const event of await this.#cell.exec.output(session.session_id, { follow: true, signal: this.#signal })) {
86
+ if (event.type === 'error')
87
+ throw new Error(event.error?.error.message ?? 'ACP input failed');
88
+ if (event.type === 'exit')
89
+ exit = event.session?.exit_code;
90
+ if (event.type === 'output' && event.stream === 'stdout' && event.data)
91
+ stdout += Buffer.from(event.data, 'base64').toString();
92
+ if (stdout.length > 65536)
93
+ throw new Error('Invalid ACP input receipt');
94
+ }
95
+ if (exit !== 0 || JSON.parse(stdout).accepted !== true)
96
+ throw new Error('ACP delivery is unverified; load this session before continuing.');
97
+ }
98
+ finally {
99
+ await this.#cell.exec.cancel(session.session_id, 0).catch(() => undefined);
100
+ }
101
+ // Never resend an uncertain mutation. Its stable input identity is fenced by CVB before provider stdin.
102
+ }
103
+ start() {
104
+ this.#initial ??= this.#replay().then(() => { this.#ready = true; this.#follow(); }).catch(error => { this.rpc.close(error instanceof Error ? error : new Error(String(error))); throw error; });
105
+ return this.#initial;
106
+ }
107
+ pause() { this.#paused++; this.#reader?.abort(); void this.#detach(); }
108
+ resume() { this.#paused = Math.max(0, this.#paused - 1); this.#follow(); }
109
+ async #entry(line) {
110
+ if (!line.trim())
111
+ return;
112
+ const entry = JSON.parse(line);
113
+ if (entry.error)
114
+ throw new Error(entry.error);
115
+ if (!Number.isSafeInteger(entry.seq) || entry.seq < 1)
116
+ throw new Error('Invalid ACP journal entry');
117
+ if (entry.seq <= this.#cursor.from_seq)
118
+ return;
119
+ if (entry.seq !== this.#cursor.from_seq + 1)
120
+ throw new Error('ACP journal has a sequence gap');
121
+ if (entry.message) {
122
+ if (entry.message.method === undefined && entry.message.id === this.#cursor.prompt_id) {
123
+ delete this.#cursor.prompt_id;
124
+ }
125
+ this.rpc.feed(Buffer.from(JSON.stringify(entry.message) + '\n'));
126
+ await this.rpc.drain();
127
+ }
128
+ this.#cursor.from_seq = entry.seq;
129
+ this.#save();
130
+ if (entry.ended)
131
+ this.rpc.close(new Error('Native ACP session ended'));
132
+ }
133
+ async #replay() {
134
+ const path = `/home/user/.local/share/shardflux/acp/${this.#conversation}/journal.ndjson`;
135
+ this.#signal.throwIfAborted();
136
+ const snapshot = await this.#cell.files.stat(path);
137
+ const decoder = new StringDecoder('utf8');
138
+ let tail = '';
139
+ for (let offset = 0; offset < snapshot.size;) {
140
+ this.#signal.throwIfAborted();
141
+ const bytes = await this.#cell.files.read(path, { offset, length: Math.min(1 << 20, snapshot.size - offset) });
142
+ if (!bytes.length)
143
+ throw new Error('ACP journal changed during replay');
144
+ offset += bytes.length;
145
+ tail += decoder.write(bytes);
146
+ let newline;
147
+ while ((newline = tail.indexOf('\n')) >= 0) {
148
+ const line = tail.slice(0, newline);
149
+ tail = tail.slice(newline + 1);
150
+ await this.#entry(line);
151
+ }
152
+ if (Buffer.byteLength(tail) > 4 << 20)
153
+ throw new Error('ACP journal frame exceeds 4 MiB');
154
+ }
155
+ // An append can straddle the snapshot cut. The follower replays that whole frame from the saved sequence.
156
+ decoder.end();
157
+ }
158
+ #follow() {
159
+ if (!this.#ready || this.#reading || !this.#wanted())
160
+ return;
161
+ const reader = new AbortController();
162
+ this.#reader = reader;
163
+ this.#reading = this.#read(reader).finally(() => { this.#reading = undefined; if (this.#reader === reader)
164
+ this.#reader = undefined; this.#follow(); });
165
+ }
166
+ async #read(reader) {
167
+ const signal = AbortSignal.any([this.#signal, reader.signal]);
168
+ try {
169
+ let failures = 0;
170
+ while (this.#wanted() && !signal.aborted) {
171
+ const decoder = new StringDecoder('utf8');
172
+ let tail = '', offset = 0;
173
+ try {
174
+ const session = await this.#cell.exec.start({ argv: ['sf-agent', 'acp', 'attach', '--conversation', this.#conversation, '--from-seq', String(this.#cursor.from_seq), '--follow'], cwd: this.#cwd, timeout_ms: 0 }, signal);
175
+ this.#follower = session.session_id;
176
+ for await (const event of await this.#cell.exec.output(session.session_id, { follow: true, signal })) {
177
+ if (event.type === 'error')
178
+ throw new Error(event.error?.error.message ?? 'ACP journal failed');
179
+ if (event.type === 'exit')
180
+ throw new Error('ACP journal detached');
181
+ if (event.type !== 'output' || event.stream !== 'stdout' || !event.data)
182
+ continue;
183
+ const bytes = Buffer.from(event.data, 'base64'), at = event.offset ?? offset;
184
+ if (at > offset)
185
+ throw new Error('ACP follower output has a byte gap');
186
+ const fresh = bytes.subarray(Math.max(0, offset - at));
187
+ offset = Math.max(offset, at + bytes.length);
188
+ tail += decoder.write(fresh);
189
+ for (let newline; (newline = tail.indexOf('\n')) >= 0;) {
190
+ const line = tail.slice(0, newline);
191
+ tail = tail.slice(newline + 1);
192
+ await this.#entry(line);
193
+ failures = 0;
194
+ }
195
+ if (Buffer.byteLength(tail) > 4 << 20)
196
+ throw new Error('ACP journal frame exceeds 4 MiB');
197
+ if (!this.#wanted())
198
+ return;
199
+ }
200
+ if (!signal.aborted)
201
+ throw new Error('ACP journal connection ended');
202
+ }
203
+ catch (error) {
204
+ if (signal.aborted)
205
+ break;
206
+ if (++failures > 3)
207
+ throw error;
208
+ }
209
+ finally {
210
+ await this.#detach();
211
+ }
212
+ }
213
+ }
214
+ catch (error) {
215
+ this.rpc.close(error instanceof Error ? error : new Error(String(error)));
216
+ }
217
+ }
218
+ async #detach() {
219
+ const follower = this.#follower;
220
+ this.#follower = undefined;
221
+ if (follower)
222
+ await this.#cell.exec.cancel(follower, 0).catch(() => undefined);
223
+ }
224
+ async close() {
225
+ this.#controller.abort();
226
+ this.rpc.close();
227
+ await this.#writing;
228
+ await this.#initial?.catch(() => undefined);
229
+ await this.#reading;
230
+ await this.#detach();
231
+ }
232
+ }
@@ -0,0 +1,41 @@
1
+ export type RpcId = string | number;
2
+ export type ObjectValue = Record<string, unknown>;
3
+ export interface RpcMessage extends ObjectValue {
4
+ jsonrpc: '2.0';
5
+ id?: RpcId | null;
6
+ method?: string;
7
+ params?: ObjectValue;
8
+ result?: unknown;
9
+ error?: {
10
+ code: number;
11
+ message: string;
12
+ data?: unknown;
13
+ };
14
+ }
15
+ export declare class RpcError extends Error {
16
+ readonly code: number;
17
+ readonly data?: unknown;
18
+ constructor(code: number, message: string, data?: unknown);
19
+ }
20
+ export declare const objectValue: (value: unknown) => ObjectValue;
21
+ /** Each peer owns its request namespace; adapter IDs never escape to another conversation. */
22
+ export declare class AcpRpc {
23
+ #private;
24
+ readonly pending: Map<RpcId, {
25
+ resolve(value: unknown): void;
26
+ reject(error: unknown): void;
27
+ timer?: ReturnType<typeof setTimeout>;
28
+ cleanup?(): void;
29
+ }>;
30
+ handler: (method: string, params: ObjectValue, request: boolean) => Promise<unknown>;
31
+ onIdle: () => void;
32
+ onResponse: (message: RpcMessage) => void;
33
+ constructor(write: (message: RpcMessage) => Promise<void>);
34
+ get busy(): boolean;
35
+ get closed(): boolean;
36
+ feed(bytes: Uint8Array): void;
37
+ request(method: string, params: ObjectValue, timeoutMs?: number, signal?: AbortSignal): Promise<unknown>;
38
+ notify(method: string, params: ObjectValue): Promise<void>;
39
+ close(error?: Error): void;
40
+ drain(): Promise<void>;
41
+ }
@@ -0,0 +1,115 @@
1
+ import { StringDecoder } from 'node:string_decoder';
2
+ import { randomUUID } from 'node:crypto';
3
+ export class RpcError extends Error {
4
+ code;
5
+ data;
6
+ constructor(code, message, data) { super(message); this.code = code; this.data = data; }
7
+ }
8
+ export const objectValue = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
9
+ /** Each peer owns its request namespace; adapter IDs never escape to another conversation. */
10
+ export class AcpRpc {
11
+ pending = new Map();
12
+ handler = () => Promise.reject(new RpcError(-32601, 'Method not found'));
13
+ onIdle = () => { };
14
+ onResponse = () => { };
15
+ #write;
16
+ #decoder = new StringDecoder('utf8');
17
+ #buffer = '';
18
+ #next = 0;
19
+ #namespace = randomUUID();
20
+ #closed;
21
+ #inbound = 0;
22
+ #receiving = new Set();
23
+ constructor(write) { this.#write = write; }
24
+ get busy() { return this.pending.size > 0 || this.#inbound > 0; }
25
+ get closed() { return this.#closed !== undefined; }
26
+ feed(bytes) {
27
+ this.#buffer += this.#decoder.write(Buffer.from(bytes));
28
+ let newline;
29
+ while ((newline = this.#buffer.indexOf('\n')) >= 0) {
30
+ const line = this.#buffer.slice(0, newline);
31
+ this.#buffer = this.#buffer.slice(newline + 1);
32
+ if (Buffer.byteLength(line) > 16 << 20)
33
+ throw new RpcError(-32700, 'Message exceeds 16 MiB');
34
+ if (!line.trim())
35
+ continue;
36
+ let value;
37
+ try {
38
+ value = JSON.parse(line);
39
+ }
40
+ catch {
41
+ throw new RpcError(-32700, 'Invalid JSON');
42
+ }
43
+ const message = objectValue(value);
44
+ if (message.jsonrpc !== '2.0')
45
+ throw new RpcError(-32600, 'Invalid JSON-RPC message');
46
+ const receiving = this.#receive(message).catch(error => this.close(error instanceof Error ? error : new Error(String(error))));
47
+ this.#receiving.add(receiving);
48
+ void receiving.finally(() => this.#receiving.delete(receiving));
49
+ }
50
+ if (Buffer.byteLength(this.#buffer) > 16 << 20)
51
+ throw new RpcError(-32700, 'Message exceeds 16 MiB');
52
+ }
53
+ async #receive(message) {
54
+ if (message.method !== undefined) {
55
+ const request = message.id !== undefined;
56
+ this.#inbound++;
57
+ try {
58
+ const result = await this.handler(message.method, message.params ?? {}, request);
59
+ if (request)
60
+ await this.#write({ jsonrpc: '2.0', id: message.id, result: result ?? {} });
61
+ }
62
+ catch (error) {
63
+ if (request) {
64
+ const e = error instanceof RpcError ? error : new RpcError(-32603, error instanceof Error ? error.message : String(error));
65
+ await this.#write({ jsonrpc: '2.0', id: message.id, error: { code: e.code, message: e.message, ...(e.data === undefined ? {} : { data: e.data }) } });
66
+ }
67
+ }
68
+ finally {
69
+ this.#inbound--;
70
+ this.onIdle();
71
+ }
72
+ return;
73
+ }
74
+ if (message.id === undefined || message.id === null)
75
+ return;
76
+ this.onResponse(message);
77
+ const pending = this.pending.get(message.id);
78
+ if (!pending)
79
+ return;
80
+ this.pending.delete(message.id);
81
+ clearTimeout(pending.timer);
82
+ pending.cleanup?.();
83
+ if (message.error)
84
+ pending.reject(new RpcError(message.error.code, message.error.message, message.error.data));
85
+ else
86
+ pending.resolve(message.result);
87
+ this.onIdle();
88
+ }
89
+ request(method, params, timeoutMs = 120_000, signal) {
90
+ if (this.#closed)
91
+ return Promise.reject(this.#closed);
92
+ if (signal?.aborted)
93
+ return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error('Request interrupted'));
94
+ const id = `shard-${this.#namespace}-${++this.#next}`;
95
+ return new Promise((resolve, reject) => {
96
+ const cleanup = () => signal?.removeEventListener('abort', abort);
97
+ const timer = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(id); cleanup(); reject(new RpcError(-32000, 'Request timed out', { code: 'timeout', fix: 'Try the message again.' })); this.onIdle(); }, timeoutMs) : undefined;
98
+ const abort = () => { this.pending.delete(id); clearTimeout(timer); cleanup(); reject(signal?.reason instanceof Error ? signal.reason : new Error('Request interrupted')); this.onIdle(); };
99
+ this.pending.set(id, { resolve, reject, timer, cleanup });
100
+ signal?.addEventListener('abort', abort, { once: true });
101
+ void this.#write({ jsonrpc: '2.0', id, method, params }).catch((error) => { this.pending.delete(id); clearTimeout(timer); cleanup(); reject(error instanceof Error ? error : new Error(String(error))); this.onIdle(); });
102
+ });
103
+ }
104
+ notify(method, params) { return this.#write({ jsonrpc: '2.0', method, params }); }
105
+ close(error = new Error('Connection closed')) {
106
+ this.#closed = error;
107
+ for (const pending of this.pending.values()) {
108
+ clearTimeout(pending.timer);
109
+ pending.cleanup?.();
110
+ pending.reject(error);
111
+ }
112
+ this.pending.clear();
113
+ }
114
+ async drain() { await Promise.all([...this.#receiving]); }
115
+ }
package/dist/acp.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Ctx } from './commands.js';
2
+ import { AcpRpc, RpcError } from './acp-rpc.js';
3
+ import type { ObjectValue } from './acp-rpc.js';
4
+ type App = 'claude' | 'codex';
5
+ type ErrorMapper = (error: unknown) => RpcError;
6
+ export declare function acpModel(app: App, id: string): string;
7
+ export declare function parseAcpModel(value: unknown): {
8
+ app: App;
9
+ id: string;
10
+ };
11
+ export declare function acpModelOptions(catalog: Record<App, ObjectValue>): ObjectValue[];
12
+ export declare function acpError(error: unknown): RpcError;
13
+ /** Rewrite structural session/file references, never the agent's prose or command output. */
14
+ export declare function acpReferences(value: unknown, record: {
15
+ id: string;
16
+ nativeId?: string;
17
+ folder: string;
18
+ }, root: string, inbound?: boolean): unknown;
19
+ /** One provider, two harness catalogs. The model selection determines the VM's native harness. */
20
+ export declare class ShardfluxAcp {
21
+ #private;
22
+ constructor(ctx: Ctx, client: AcpRpc, errors?: ErrorMapper);
23
+ handle(method: string, params: ObjectValue, request: boolean): Promise<unknown>;
24
+ close(): Promise<void>;
25
+ }
26
+ export declare function runAcp(ctx: Ctx, errors?: ErrorMapper): Promise<number>;
27
+ export {};