workbite 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -1,324 +1,334 @@
1
- 'use strict';
2
- const path = require('node:path');
3
- const { fail, readSmall } = require('./common');
4
- const { locations, releaseSpec, ensureInstalled, stageReporter, packageVersion } = require('./install');
5
- const { openSuite, FRONTENDS } = require('./runtime');
6
- const { bindAgent, executeAgent, agentCapabilities, WRITE_COMMANDS, MAX_PACKET_BYTES } = require('./agent');
7
- const transfer = require('./transfer');
8
-
9
- const HELP = `WorkBite ${packageVersion} - Work Stack organizes the work. StackBite gets it moving.
10
-
11
- Usage:
12
- workbite install download, verify and install the pinned native suite
13
- workbite check verify the installed suite (no download, no processes)
14
- workbite status [--json] inspect startup records without starting anything
15
- workbite doctor [--json] explain startup evidence and its limits (read-only)
16
- workbite recover --dry-run [--json] print a recovery plan only; never changes state
17
- workbite open --workspace ABS [--init] [--no-ui] [--frontend typescript|rust]
18
- start or reuse this workspace's Work Stack owner and the
19
- StackBite backend, then run the Work Stack terminal UI
20
- workbite [open] [--no-ui] [--frontend typescript|rust]
21
- reopen the last workspace WorkBite opened successfully
22
- workbite agent [--workspace ABS --workspace-uid UUID] COMMAND [--input-file FILE | --stdin] [flags]
23
- curated Work Stack agent commands against the running,
24
- WorkBite-owned owner (never starts one): capabilities, status,
25
- tasks list|create|update, context, checkpoint, operation-status,
26
- export, import, import-status, import-rollback
27
- workbite mcp [--workspace ABS --workspace-uid UUID]
28
- WorkBite MCP server on stdio for the same agent commands
29
- workbite --version | --help
30
-
31
- agent:
32
- Reads use the last workspace WorkBite opened unless --workspace and --workspace-uid name one.
33
- Writes (tasks create, tasks update, checkpoint) always need --workspace ABS --workspace-uid UUID
34
- and a packet from --input-file FILE or --stdin (UTF-8, at most 32768 bytes). An uncertain write
35
- is never replayed; check it with agent operation-status --intent-id ID.
36
-
37
- Task transfer (archive at most 1 MiB; uses the running owner, never starts one):
38
- workbite agent export --output tasks.json [--include-history] [--redact]
39
- workbite agent import --input-file tasks.json --mode new --dry-run
40
- workbite agent --workspace ABS --workspace-uid UUID import --input-file tasks.json --mode new
41
- --intent-id ID --plan-digest sha256:HEX --expected-snapshot-digest sha256:HEX [--id-map map.json]
42
- workbite agent import-status --intent-id ID
43
- workbite agent --workspace ABS --workspace-uid UUID import-rollback --intent-id ID --import-id ID
44
- --expected-snapshot-digest sha256:HEX
45
- merge is also supported. --preserve-ids is explicit. Output files must be new local files.
46
- Import unknowns require import-status with the SAME intent; no automatic replay or Task status changes.
47
- Full workspace relocation uses official backup/restore.
48
-
49
- open:
50
- --workspace ABS absolute workspace folder; always wins over the remembered one
51
- --init create a new Work Stack workspace in a new or empty folder (needs --workspace)
52
- --no-ui start or reuse the backends, print one JSON summary and exit; does not prove UI works
53
- --frontend NAME typescript (default): the familiar Work Stack UI, with same-window 9 Execution
54
- on execution bundles; rust: the explicit alternative unified UI (execution
55
- bundles only). Chosen per open, never saved.
56
-
57
- First time, name the workspace once:
58
- workbite open --workspace /abs/path/new-folder --init
59
- workbite open --workspace /abs/path/existing-workspace
60
- Afterwards just run:
61
- workbite
62
-
63
- The Work Stack owner and StackBite backend keep running after the UI exits, so
64
- agents continue. WorkBite never auto-updates, installs dependencies, approves
65
- provider trust or stops processes it cannot prove are its own.
66
-
67
- Locations (override with absolute WORKBITE_HOME / WORKBITE_STATE_DIR):
68
- Windows %LOCALAPPDATA%\\WorkBite\\app\\<version> %LOCALAPPDATA%\\WorkBite\\state
69
- Linux $XDG_DATA_HOME/workbite/app/<version> $XDG_STATE_HOME/workbite
70
- `;
71
-
72
- // Native flags each curated agent command accepts (value-taking unless listed in FLAGS_WITHOUT_VALUE).
73
- const AGENT_FLAGS = Object.freeze({
74
- capabilities: ['--command', '--section'],
75
- status: ['--workspace-revision'],
76
- 'tasks list': ['--status', '--limit', '--offset'],
77
- 'tasks create': ['--intent-id'],
78
- 'tasks update': ['--intent-id', '--task', '--expected-revision'],
79
- context: ['--task', '--view', '--format', '--source', '--fingerprint', '--if-fingerprint'],
80
- checkpoint: ['--intent-id'],
81
- 'operation-status': ['--intent-id'],
82
- export: ['--include-history', '--redact'],
83
- import: ['--mode', '--dry-run', '--preserve-ids', '--intent-id', '--plan-digest', '--expected-snapshot-digest'],
84
- 'import-status': ['--intent-id'],
85
- 'import-rollback': ['--intent-id', '--import-id', '--expected-snapshot-digest'],
86
- });
87
- const FLAGS_WITHOUT_VALUE = new Set(['--workspace-revision', '--fingerprint', '--include-history', '--redact', '--dry-run', '--preserve-ids']);
88
- const REPEATABLE = new Set(['--source']);
89
- const PACKET_COMMANDS = new Set(['tasks create', 'tasks update', 'checkpoint', 'import']);
90
-
91
- function parseBinding(options, argument, next) {
92
- if (argument === '--workspace' && options.workspace === undefined && next !== undefined && !next.startsWith('--')) {
93
- if (!path.isAbsolute(next)) fail('--workspace must be an absolute path');
94
- options.workspace = next; return true;
95
- }
96
- if (argument === '--workspace-uid' && options.workspaceUid === undefined && next !== undefined && !next.startsWith('--')) {
97
- options.workspaceUid = next; return true;
98
- }
99
- return false;
100
- }
101
- function pairedBinding(options) {
102
- if ((options.workspace === undefined) !== (options.workspaceUid === undefined)) {
103
- fail('--workspace and --workspace-uid must be given together');
104
- }
105
- }
106
-
107
- function parseAgent(rest) {
108
- // The binding pair may come before the command words, after them, or split around them;
109
- // each flag is still accepted once, with its value, and nothing else may precede the words.
110
- const options = { command: 'agent' };
111
- while (rest.length && parseBinding(options, rest[0], rest[1])) rest.splice(0, 2);
112
- if (rest.length && rest[0].startsWith('-')) {
113
- fail(`Unknown or repeated agent option before the command: ${String(rest[0]).slice(0, 64)}. See workbite --help.`);
114
- }
115
- const words = [];
116
- while (rest.length && !rest[0].startsWith('-')) words.push(rest.shift());
117
- const name = words.join(' ');
118
- if (!Object.hasOwn(AGENT_FLAGS, name)) {
119
- fail(`Unknown agent command: ${name.slice(0, 64) || '(none)'}. Supported: ${Object.keys(AGENT_FLAGS).join(', ')}.`);
120
- }
121
- Object.assign(options, { name, native: ['agent', ...words] });
122
- for (let index = 0; index < rest.length; index++) {
123
- const argument = rest[index], next = rest[index + 1];
124
- if (parseBinding(options, argument, next)) { index++; continue; }
125
- if (((argument === '--output' && name === 'export' && options.output === undefined)
126
- || (argument === '--id-map' && name === 'import' && options.idMap === undefined))
127
- && next !== undefined && !next.startsWith('--')) {
128
- options[argument === '--output' ? 'output' : 'idMap'] = next; index++; continue;
129
- }
130
- if (argument === '--input-file' && options.inputFile === undefined && !options.stdin && next !== undefined
131
- && !next.startsWith('--')) { options.inputFile = next; index++; continue; }
132
- if (argument === '--stdin' && !options.stdin && options.inputFile === undefined) { options.stdin = true; continue; }
133
- if (!AGENT_FLAGS[name].includes(argument) || (!REPEATABLE.has(argument) && options.native.includes(argument))) {
134
- fail(`Unknown or repeated agent ${name} option: ${String(argument).slice(0, 64)}. See workbite --help.`);
135
- }
136
- options.native.push(argument);
137
- if (FLAGS_WITHOUT_VALUE.has(argument)) continue;
138
- if (next === undefined || next.startsWith('--')) fail(`${argument} needs a value`);
139
- options.native.push(next); index++;
140
- }
141
- pairedBinding(options);
142
- const packet = options.stdin || options.inputFile !== undefined;
143
- if (PACKET_COMMANDS.has(name) !== packet) {
144
- fail(PACKET_COMMANDS.has(name) ? `agent ${name} needs a packet from --input-file FILE or --stdin`
145
- : `agent ${name} does not take --input-file or --stdin`);
146
- }
147
- const value = flag => options.native[options.native.indexOf(flag) + 1];
148
- const has = flag => options.native.includes(flag);
149
- const dryRun = name === 'import' && has('--dry-run');
150
- if (name === 'import') {
151
- if (options.inputFile === undefined) fail('agent import requires --input-file FILE');
152
- if (!has('--mode') || !['new', 'merge'].includes(value('--mode'))) fail('--mode must be new or merge');
153
- const proofs = ['--intent-id', '--plan-digest', '--expected-snapshot-digest'];
154
- if (dryRun && (proofs.some(has) || options.idMap !== undefined)) fail('Import preview does not accept write identity, proofs or --id-map');
155
- if (!dryRun && proofs.some(flag => !has(flag))) fail('Import apply requires --intent-id, --plan-digest and --expected-snapshot-digest from preview');
156
- }
157
- if (['import-status', 'import-rollback'].includes(name) && !has('--intent-id')) fail(`agent ${name} requires --intent-id`);
158
- if (name === 'import-rollback' && (!has('--import-id') || !has('--expected-snapshot-digest'))) fail('Import rollback requires --import-id and --expected-snapshot-digest');
159
- for (const proof of ['--plan-digest', '--expected-snapshot-digest']) {
160
- if (has(proof) && !transfer.DIGEST.test(value(proof))) fail(`${proof} must be sha256: followed by 64 lowercase hex digits`);
161
- }
162
- if (has('--intent-id') && name.startsWith('import') && !/^[A-Za-z0-9._:-]{8,128}$/.test(value('--intent-id'))) fail('Invalid transfer --intent-id');
163
- if (WRITE_COMMANDS.includes(name) && !dryRun && options.workspace === undefined) {
164
- fail(`agent ${name} changes Work Stack; name the workspace with --workspace ABS --workspace-uid UUID`);
165
- }
166
- if (packet) options.native.push('--stdin');
167
- return options;
168
- }
169
- function parseMcp(rest) {
170
- const options = { command: 'mcp' };
171
- for (let index = 0; index < rest.length; index++) {
172
- if (!parseBinding(options, rest[index], rest[index + 1])) {
173
- fail(`Unknown or repeated mcp option: ${String(rest[index]).slice(0, 64)}. See workbite --help.`);
174
- }
175
- index++;
176
- }
177
- pairedBinding(options);
178
- return options;
179
- }
180
-
181
- function parseArguments(argv) {
182
- if (argv.length === 1 && ['--help', '-h', 'help'].includes(argv[0])) return { command: 'help' };
183
- if (argv.length === 1 && ['--version', '-v', 'version'].includes(argv[0])) return { command: 'version' };
184
- // Bare `workbite` (or only open options) means `workbite open`.
185
- const [command, ...rest] = argv.length && !argv[0].startsWith('--') ? argv : ['open', ...argv];
186
- if ((command === 'install' || command === 'check') && !rest.length) return { command };
187
- if (['status', 'doctor', 'recover'].includes(command)) {
188
- const options = { command, json: false, dryRun: false };
189
- for (const argument of rest) {
190
- if (argument === '--json' && !options.json) options.json = true;
191
- else if (command === 'recover' && argument === '--dry-run' && !options.dryRun) options.dryRun = true;
192
- else fail(`Unsupported or repeated ${command} option. See workbite --help.`);
193
- }
194
- if (command === 'recover' && !options.dryRun) fail('recover requires --dry-run; automatic repair is not supported.');
195
- return options;
196
- }
197
- if (command === 'agent') return parseAgent([...rest]);
198
- if (command === 'mcp') return parseMcp(rest);
199
- if (command === 'open') {
200
- const options = { command, init: false, ui: true };
201
- for (let index = 0; index < rest.length; index++) {
202
- const argument = rest[index];
203
- if (argument === '--init' && !options.init) options.init = true;
204
- else if (argument === '--no-ui' && options.ui) options.ui = false;
205
- else if (argument === '--workspace' && options.workspace === undefined && rest[index + 1] !== undefined
206
- && !rest[index + 1].startsWith('--')) options.workspace = rest[++index];
207
- else if (argument === '--frontend' && options.frontend === undefined) {
208
- const name = rest[++index];
209
- if (!FRONTENDS.includes(name)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
210
- options.frontend = name;
211
- }
212
- else fail(`Unknown or repeated open option: ${String(argument).slice(0, 64)}. See workbite --help.`);
213
- }
214
- // Without --workspace the remembered workspace is reopened; --init never picks a folder by itself.
215
- if (options.workspace === undefined) {
216
- if (options.init) fail('--init needs --workspace ABS naming the new or empty folder');
217
- return options;
218
- }
219
- if (!path.isAbsolute(options.workspace)) fail('--workspace must be an absolute path');
220
- return options;
221
- }
222
- fail(`Unknown command: ${String(command).slice(0, 64)}. See workbite --help.`);
223
- }
224
-
225
- // A packet is bounded UTF-8 text handed to the native client as stdin; it never enters argv.
226
- function packetText(bytes) {
227
- if (bytes.length > MAX_PACKET_BYTES) fail(`The agent packet exceeds ${MAX_PACKET_BYTES} bytes`);
228
- try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail('The agent packet is not valid UTF-8'); }
229
- }
230
- async function readPacket(parsed, stdin) {
231
- if (parsed.inputFile !== undefined) {
232
- let bytes;
233
- try { bytes = await readSmall(path.resolve(parsed.inputFile), MAX_PACKET_BYTES); } catch (error) {
234
- fail(/oversized/.test(error.message) ? `The agent packet file is not a regular file of at most ${MAX_PACKET_BYTES} bytes`
235
- : 'The agent packet file could not be read');
236
- }
237
- return packetText(bytes);
238
- }
239
- if (!parsed.stdin) return null;
240
- const chunks = [];
241
- let size = 0;
242
- for await (const chunk of stdin) {
243
- size += chunk.length;
244
- if (size > MAX_PACKET_BYTES) fail(`The agent packet exceeds ${MAX_PACKET_BYTES} bytes`);
245
- chunks.push(Buffer.from(chunk));
246
- }
247
- return packetText(Buffer.concat(chunks));
248
- }
249
-
250
- async function runAgent(parsed, { installed, stateRoot, environment, commands, timeouts, stdout, stderr, stdin }) {
251
- let result, binding = null;
252
- if (parsed.name === 'capabilities' && parsed.workspace === undefined) {
253
- result = await agentCapabilities({ installed, commands, environment, argv: parsed.native, timeouts });
254
- } else {
255
- binding = await bindAgent({ installed, stateRoot, environment, workspace: parsed.workspace,
256
- workspaceUid: parsed.workspaceUid, commands, timeouts });
257
- if (parsed.output !== undefined) await transfer.outputPath(parsed.output);
258
- if (parsed.idMap !== undefined) await transfer.outputPath(parsed.idMap);
259
- const packet = parsed.name === 'import' ? await transfer.readArchive(parsed.inputFile) : await readPacket(parsed, stdin || process.stdin);
260
- result = await executeAgent(binding, parsed.native, { stdin: packet });
261
- }
262
- let output = result.stdout;
263
- if (result.code === 0 && (parsed.output !== undefined || parsed.idMap !== undefined)) {
264
- try {
265
- const intentIndex = parsed.native.indexOf('--intent-id');
266
- const data = transfer.resultData(result, parsed.name, binding.workspaceUid,
267
- intentIndex === -1 ? undefined : parsed.native[intentIndex + 1]);
268
- if (parsed.output !== undefined) await transfer.writePrivate(parsed.output, transfer.archive(data, binding.workspaceUid));
269
- if (parsed.idMap !== undefined) await transfer.writePrivate(parsed.idMap, transfer.idMap(data.id_map));
270
- } catch (error) {
271
- // Apply may already be committed: preserve its receipt and never replay for a local file failure.
272
- if (output) stdout.write(output);
273
- stderr.write(`error: transfer file output failed: ${error.message}. Native operation was not replayed; retain this response and use import-status for any write.\n`);
274
- return { code: 1, signal: null };
275
- }
276
- }
277
- if (parsed.name === 'status' && result.code === 0) {
278
- try {
279
- const document = JSON.parse(output);
280
- if (document !== null && typeof document === 'object' && !Array.isArray(document)) {
281
- document.workbite = { schema: 'workbite.agent.binding.v1', workspace: binding.workspace,
282
- workspace_uid: binding.workspaceUid, endpoint: binding.endpoint, state: binding.stateRoot };
283
- output = JSON.stringify(document) + '\n';
284
- }
285
- } catch { /* not JSON: passed through unchanged */ }
286
- }
287
- if (output) stdout.write(output);
288
- if (result.stderr) stderr.write(result.stderr);
289
- return { code: result.code, signal: null };
290
- }
291
-
292
- async function main(argv = process.argv.slice(2), options = {}) {
293
- const stdout = options.stdout || process.stdout, stderr = options.stderr || process.stderr;
294
- const parsed = parseArguments(argv);
295
- if (parsed.command === 'help') { stdout.write(HELP); return { code: 0, signal: null }; }
296
- if (parsed.command === 'version') { stdout.write(`workbite ${packageVersion}\n`); return { code: 0, signal: null }; }
297
- const environment = options.environment || process.env;
298
- const platform = options.platform || process.platform;
299
- if (['status', 'doctor', 'recover'].includes(parsed.command)) {
300
- return require('./diagnostics').runDiagnostics(parsed, { ...options, environment, platform, stdout });
301
- }
302
- const spec = releaseSpec(options.manifest || require('../release.json'), platform, options.arch || process.arch, options);
303
- const where = locations(environment, platform);
304
- // agent and mcp only use an already installed, verified suite: nothing is downloaded or started.
305
- const installed = await ensureInstalled(spec, { ...options, apps: options.apps || where.apps,
306
- checkOnly: ['check', 'agent', 'mcp'].includes(parsed.command), progress: options.progress || stageReporter(stderr) });
307
- const stateRoot = options.stateRoot || where.state;
308
- if (parsed.command === 'agent') return runAgent(parsed, { ...options, installed, stateRoot, environment, stdout, stderr });
309
- if (parsed.command === 'mcp') {
310
- return require('./mcp').runMcp({ installed, stateRoot, environment, workspace: parsed.workspace,
311
- workspaceUid: parsed.workspaceUid, commands: options.commands, timeouts: options.timeouts,
312
- stdin: options.stdin || process.stdin, stdout, stderr });
313
- }
314
- if (parsed.command === 'install' || parsed.command === 'check') {
315
- stdout.write(JSON.stringify({ schema: 'workbite.install.v1', version: spec.version, target: spec.target,
316
- home: installed.home, state: stateRoot }) + '\n');
317
- return { code: 0, signal: null };
318
- }
319
- return openSuite({ installed, commands: options.commands, workspace: parsed.workspace, init: parsed.init, ui: parsed.ui,
320
- frontend: parsed.frontend, stateRoot, environment, spawnImpl: options.spawnImpl, execImpl: options.execImpl, stdout, stderr, timeouts: options.timeouts,
321
- mobileBootstrap: options.mobileBootstrap });
322
- }
323
-
324
- module.exports = { main, parseArguments, HELP };
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const { fail, readSmall } = require('./common');
4
+ const { locations, releaseSpec, ensureInstalled, stageReporter, packageVersion } = require('./install');
5
+ const { openSuite, FRONTENDS } = require('./runtime');
6
+ const { bindAgent, executeAgent, agentCapabilities, WRITE_COMMANDS, MAX_PACKET_BYTES } = require('./agent');
7
+ const transfer = require('./transfer');
8
+
9
+ const HELP = `WorkBite ${packageVersion} - Work Stack organizes the work. StackBite gets it moving.
10
+
11
+ Usage:
12
+ workbite install download, verify and install the pinned native suite
13
+ workbite update update the global npm launcher and stage its verified suite
14
+ workbite check verify the installed suite (no download, no processes)
15
+ workbite status [--json] inspect startup records without starting anything
16
+ workbite doctor [--json] explain startup evidence and its limits (read-only)
17
+ workbite recover --dry-run [--json] print a recovery plan only; never changes state
18
+ workbite open --workspace ABS [--init] [--no-ui] [--frontend typescript|rust]
19
+ start or reuse this workspace's Work Stack owner and the
20
+ StackBite backend, then run the Work Stack terminal UI
21
+ workbite [open] [--no-ui] [--frontend typescript|rust]
22
+ reopen the last workspace WorkBite opened successfully
23
+ workbite agent [--workspace ABS --workspace-uid UUID] COMMAND [--input-file FILE | --stdin] [flags]
24
+ curated Work Stack agent commands against the running,
25
+ WorkBite-owned owner (never starts one): capabilities, status,
26
+ tasks list|create|update, context, checkpoint, operation-status,
27
+ export, import, import-status, import-rollback
28
+ workbite mcp [--workspace ABS --workspace-uid UUID]
29
+ WorkBite MCP server on stdio for the same agent commands
30
+ workbite --version | --help
31
+
32
+ agent:
33
+ Reads use the last workspace WorkBite opened unless --workspace and --workspace-uid name one.
34
+ Writes (tasks create, tasks update, checkpoint) always need --workspace ABS --workspace-uid UUID
35
+ and a packet from --input-file FILE or --stdin (UTF-8, at most 32768 bytes). An uncertain write
36
+ is never replayed; check it with agent operation-status --intent-id ID.
37
+
38
+ Task transfer (archive at most 1 MiB; uses the running owner, never starts one):
39
+ workbite agent export --output tasks.json [--include-history] [--redact]
40
+ workbite agent import --input-file tasks.json --mode new --dry-run
41
+ workbite agent --workspace ABS --workspace-uid UUID import --input-file tasks.json --mode new
42
+ --intent-id ID --plan-digest sha256:HEX --expected-snapshot-digest sha256:HEX [--id-map map.json]
43
+ workbite agent import-status --intent-id ID
44
+ workbite agent --workspace ABS --workspace-uid UUID import-rollback --intent-id ID --import-id ID
45
+ --expected-snapshot-digest sha256:HEX
46
+ merge is also supported. --preserve-ids is explicit. Output files must be new local files.
47
+ Import unknowns require import-status with the SAME intent; no automatic replay or Task status changes.
48
+ Full workspace relocation uses official backup/restore.
49
+
50
+ open:
51
+ --workspace ABS absolute workspace folder; always wins over the remembered one
52
+ --init create a new Work Stack workspace in a new or empty folder (needs --workspace)
53
+ --no-ui start or reuse the backends, print one JSON summary and exit; does not prove UI works
54
+ --frontend NAME typescript (default): the familiar Work Stack UI, with same-window 9 Execution
55
+ on execution bundles; rust: the explicit alternative unified UI (execution
56
+ bundles only). Chosen per open, never saved.
57
+
58
+ update:
59
+ Uses official npm latest; global installs only (local/npx copies are never replaced).
60
+ Does not switch or stop running owners, Seats or Phone. Inspect workbite status
61
+ --json and workbite doctor --json; ask your operator for handover before opening
62
+ the new runtime. Closing the UI leaves backends and agents running.
63
+
64
+ First time, name the workspace once:
65
+ workbite open --workspace /abs/path/new-folder --init
66
+ workbite open --workspace /abs/path/existing-workspace
67
+ Afterwards just run:
68
+ workbite
69
+
70
+ The Work Stack owner and StackBite backend keep running after the UI exits, so
71
+ agents continue. WorkBite never auto-updates, installs dependencies, approves
72
+ provider trust or stops processes it cannot prove are its own.
73
+
74
+ Locations (override with absolute WORKBITE_HOME / WORKBITE_STATE_DIR):
75
+ Windows %LOCALAPPDATA%\\WorkBite\\app\\<version> %LOCALAPPDATA%\\WorkBite\\state
76
+ Linux $XDG_DATA_HOME/workbite/app/<version> $XDG_STATE_HOME/workbite
77
+ `;
78
+
79
+ // Native flags each curated agent command accepts (value-taking unless listed in FLAGS_WITHOUT_VALUE).
80
+ const AGENT_FLAGS = Object.freeze({
81
+ capabilities: ['--command', '--section'],
82
+ status: ['--workspace-revision'],
83
+ 'tasks list': ['--status', '--limit', '--offset'],
84
+ 'tasks create': ['--intent-id'],
85
+ 'tasks update': ['--intent-id', '--task', '--expected-revision'],
86
+ context: ['--task', '--view', '--format', '--source', '--fingerprint', '--if-fingerprint'],
87
+ checkpoint: ['--intent-id'],
88
+ 'operation-status': ['--intent-id'],
89
+ export: ['--include-history', '--redact'],
90
+ import: ['--mode', '--dry-run', '--preserve-ids', '--intent-id', '--plan-digest', '--expected-snapshot-digest'],
91
+ 'import-status': ['--intent-id'],
92
+ 'import-rollback': ['--intent-id', '--import-id', '--expected-snapshot-digest'],
93
+ });
94
+ const FLAGS_WITHOUT_VALUE = new Set(['--workspace-revision', '--fingerprint', '--include-history', '--redact', '--dry-run', '--preserve-ids']);
95
+ const REPEATABLE = new Set(['--source']);
96
+ const PACKET_COMMANDS = new Set(['tasks create', 'tasks update', 'checkpoint', 'import']);
97
+
98
+ function parseBinding(options, argument, next) {
99
+ if (argument === '--workspace' && options.workspace === undefined && next !== undefined && !next.startsWith('--')) {
100
+ if (!path.isAbsolute(next)) fail('--workspace must be an absolute path');
101
+ options.workspace = next; return true;
102
+ }
103
+ if (argument === '--workspace-uid' && options.workspaceUid === undefined && next !== undefined && !next.startsWith('--')) {
104
+ options.workspaceUid = next; return true;
105
+ }
106
+ return false;
107
+ }
108
+ function pairedBinding(options) {
109
+ if ((options.workspace === undefined) !== (options.workspaceUid === undefined)) {
110
+ fail('--workspace and --workspace-uid must be given together');
111
+ }
112
+ }
113
+
114
+ function parseAgent(rest) {
115
+ // The binding pair may come before the command words, after them, or split around them;
116
+ // each flag is still accepted once, with its value, and nothing else may precede the words.
117
+ const options = { command: 'agent' };
118
+ while (rest.length && parseBinding(options, rest[0], rest[1])) rest.splice(0, 2);
119
+ if (rest.length && rest[0].startsWith('-')) {
120
+ fail(`Unknown or repeated agent option before the command: ${String(rest[0]).slice(0, 64)}. See workbite --help.`);
121
+ }
122
+ const words = [];
123
+ while (rest.length && !rest[0].startsWith('-')) words.push(rest.shift());
124
+ const name = words.join(' ');
125
+ if (!Object.hasOwn(AGENT_FLAGS, name)) {
126
+ fail(`Unknown agent command: ${name.slice(0, 64) || '(none)'}. Supported: ${Object.keys(AGENT_FLAGS).join(', ')}.`);
127
+ }
128
+ Object.assign(options, { name, native: ['agent', ...words] });
129
+ for (let index = 0; index < rest.length; index++) {
130
+ const argument = rest[index], next = rest[index + 1];
131
+ if (parseBinding(options, argument, next)) { index++; continue; }
132
+ if (((argument === '--output' && name === 'export' && options.output === undefined)
133
+ || (argument === '--id-map' && name === 'import' && options.idMap === undefined))
134
+ && next !== undefined && !next.startsWith('--')) {
135
+ options[argument === '--output' ? 'output' : 'idMap'] = next; index++; continue;
136
+ }
137
+ if (argument === '--input-file' && options.inputFile === undefined && !options.stdin && next !== undefined
138
+ && !next.startsWith('--')) { options.inputFile = next; index++; continue; }
139
+ if (argument === '--stdin' && !options.stdin && options.inputFile === undefined) { options.stdin = true; continue; }
140
+ if (!AGENT_FLAGS[name].includes(argument) || (!REPEATABLE.has(argument) && options.native.includes(argument))) {
141
+ fail(`Unknown or repeated agent ${name} option: ${String(argument).slice(0, 64)}. See workbite --help.`);
142
+ }
143
+ options.native.push(argument);
144
+ if (FLAGS_WITHOUT_VALUE.has(argument)) continue;
145
+ if (next === undefined || next.startsWith('--')) fail(`${argument} needs a value`);
146
+ options.native.push(next); index++;
147
+ }
148
+ pairedBinding(options);
149
+ const packet = options.stdin || options.inputFile !== undefined;
150
+ if (PACKET_COMMANDS.has(name) !== packet) {
151
+ fail(PACKET_COMMANDS.has(name) ? `agent ${name} needs a packet from --input-file FILE or --stdin`
152
+ : `agent ${name} does not take --input-file or --stdin`);
153
+ }
154
+ const value = flag => options.native[options.native.indexOf(flag) + 1];
155
+ const has = flag => options.native.includes(flag);
156
+ const dryRun = name === 'import' && has('--dry-run');
157
+ if (name === 'import') {
158
+ if (options.inputFile === undefined) fail('agent import requires --input-file FILE');
159
+ if (!has('--mode') || !['new', 'merge'].includes(value('--mode'))) fail('--mode must be new or merge');
160
+ const proofs = ['--intent-id', '--plan-digest', '--expected-snapshot-digest'];
161
+ if (dryRun && (proofs.some(has) || options.idMap !== undefined)) fail('Import preview does not accept write identity, proofs or --id-map');
162
+ if (!dryRun && proofs.some(flag => !has(flag))) fail('Import apply requires --intent-id, --plan-digest and --expected-snapshot-digest from preview');
163
+ }
164
+ if (['import-status', 'import-rollback'].includes(name) && !has('--intent-id')) fail(`agent ${name} requires --intent-id`);
165
+ if (name === 'import-rollback' && (!has('--import-id') || !has('--expected-snapshot-digest'))) fail('Import rollback requires --import-id and --expected-snapshot-digest');
166
+ for (const proof of ['--plan-digest', '--expected-snapshot-digest']) {
167
+ if (has(proof) && !transfer.DIGEST.test(value(proof))) fail(`${proof} must be sha256: followed by 64 lowercase hex digits`);
168
+ }
169
+ if (has('--intent-id') && name.startsWith('import') && !/^[A-Za-z0-9._:-]{8,128}$/.test(value('--intent-id'))) fail('Invalid transfer --intent-id');
170
+ if (WRITE_COMMANDS.includes(name) && !dryRun && options.workspace === undefined) {
171
+ fail(`agent ${name} changes Work Stack; name the workspace with --workspace ABS --workspace-uid UUID`);
172
+ }
173
+ if (packet) options.native.push('--stdin');
174
+ return options;
175
+ }
176
+ function parseMcp(rest) {
177
+ const options = { command: 'mcp' };
178
+ for (let index = 0; index < rest.length; index++) {
179
+ if (!parseBinding(options, rest[index], rest[index + 1])) {
180
+ fail(`Unknown or repeated mcp option: ${String(rest[index]).slice(0, 64)}. See workbite --help.`);
181
+ }
182
+ index++;
183
+ }
184
+ pairedBinding(options);
185
+ return options;
186
+ }
187
+
188
+ function parseArguments(argv) {
189
+ if (argv.length === 1 && ['--help', '-h', 'help'].includes(argv[0])) return { command: 'help' };
190
+ if (argv.length === 1 && ['--version', '-v', 'version'].includes(argv[0])) return { command: 'version' };
191
+ // Bare `workbite` (or only open options) means `workbite open`.
192
+ const [command, ...rest] = argv.length && !argv[0].startsWith('--') ? argv : ['open', ...argv];
193
+ if (['install', 'check', 'update'].includes(command) && !rest.length) return { command };
194
+ if (['status', 'doctor', 'recover'].includes(command)) {
195
+ const options = { command, json: false, dryRun: false };
196
+ for (const argument of rest) {
197
+ if (argument === '--json' && !options.json) options.json = true;
198
+ else if (command === 'recover' && argument === '--dry-run' && !options.dryRun) options.dryRun = true;
199
+ else fail(`Unsupported or repeated ${command} option. See workbite --help.`);
200
+ }
201
+ if (command === 'recover' && !options.dryRun) fail('recover requires --dry-run; automatic repair is not supported.');
202
+ return options;
203
+ }
204
+ if (command === 'agent') return parseAgent([...rest]);
205
+ if (command === 'mcp') return parseMcp(rest);
206
+ if (command === 'open') {
207
+ const options = { command, init: false, ui: true };
208
+ for (let index = 0; index < rest.length; index++) {
209
+ const argument = rest[index];
210
+ if (argument === '--init' && !options.init) options.init = true;
211
+ else if (argument === '--no-ui' && options.ui) options.ui = false;
212
+ else if (argument === '--workspace' && options.workspace === undefined && rest[index + 1] !== undefined
213
+ && !rest[index + 1].startsWith('--')) options.workspace = rest[++index];
214
+ else if (argument === '--frontend' && options.frontend === undefined) {
215
+ const name = rest[++index];
216
+ if (!FRONTENDS.includes(name)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
217
+ options.frontend = name;
218
+ }
219
+ else fail(`Unknown or repeated open option: ${String(argument).slice(0, 64)}. See workbite --help.`);
220
+ }
221
+ // Without --workspace the remembered workspace is reopened; --init never picks a folder by itself.
222
+ if (options.workspace === undefined) {
223
+ if (options.init) fail('--init needs --workspace ABS naming the new or empty folder');
224
+ return options;
225
+ }
226
+ if (!path.isAbsolute(options.workspace)) fail('--workspace must be an absolute path');
227
+ return options;
228
+ }
229
+ fail(`Unknown command: ${String(command).slice(0, 64)}. See workbite --help.`);
230
+ }
231
+
232
+ // A packet is bounded UTF-8 text handed to the native client as stdin; it never enters argv.
233
+ function packetText(bytes) {
234
+ if (bytes.length > MAX_PACKET_BYTES) fail(`The agent packet exceeds ${MAX_PACKET_BYTES} bytes`);
235
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { fail('The agent packet is not valid UTF-8'); }
236
+ }
237
+ async function readPacket(parsed, stdin) {
238
+ if (parsed.inputFile !== undefined) {
239
+ let bytes;
240
+ try { bytes = await readSmall(path.resolve(parsed.inputFile), MAX_PACKET_BYTES); } catch (error) {
241
+ fail(/oversized/.test(error.message) ? `The agent packet file is not a regular file of at most ${MAX_PACKET_BYTES} bytes`
242
+ : 'The agent packet file could not be read');
243
+ }
244
+ return packetText(bytes);
245
+ }
246
+ if (!parsed.stdin) return null;
247
+ const chunks = [];
248
+ let size = 0;
249
+ for await (const chunk of stdin) {
250
+ size += chunk.length;
251
+ if (size > MAX_PACKET_BYTES) fail(`The agent packet exceeds ${MAX_PACKET_BYTES} bytes`);
252
+ chunks.push(Buffer.from(chunk));
253
+ }
254
+ return packetText(Buffer.concat(chunks));
255
+ }
256
+
257
+ async function runAgent(parsed, { installed, stateRoot, environment, commands, timeouts, stdout, stderr, stdin }) {
258
+ let result, binding = null;
259
+ if (parsed.name === 'capabilities' && parsed.workspace === undefined) {
260
+ result = await agentCapabilities({ installed, commands, environment, argv: parsed.native, timeouts });
261
+ } else {
262
+ binding = await bindAgent({ installed, stateRoot, environment, workspace: parsed.workspace,
263
+ workspaceUid: parsed.workspaceUid, commands, timeouts });
264
+ if (parsed.output !== undefined) await transfer.outputPath(parsed.output);
265
+ if (parsed.idMap !== undefined) await transfer.outputPath(parsed.idMap);
266
+ const packet = parsed.name === 'import' ? await transfer.readArchive(parsed.inputFile) : await readPacket(parsed, stdin || process.stdin);
267
+ result = await executeAgent(binding, parsed.native, { stdin: packet });
268
+ }
269
+ let output = result.stdout;
270
+ if (result.code === 0 && (parsed.output !== undefined || parsed.idMap !== undefined)) {
271
+ try {
272
+ const intentIndex = parsed.native.indexOf('--intent-id');
273
+ const data = transfer.resultData(result, parsed.name, binding.workspaceUid,
274
+ intentIndex === -1 ? undefined : parsed.native[intentIndex + 1]);
275
+ if (parsed.output !== undefined) await transfer.writePrivate(parsed.output, transfer.archive(data, binding.workspaceUid));
276
+ if (parsed.idMap !== undefined) await transfer.writePrivate(parsed.idMap, transfer.idMap(data.id_map));
277
+ } catch (error) {
278
+ // Apply may already be committed: preserve its receipt and never replay for a local file failure.
279
+ if (output) stdout.write(output);
280
+ stderr.write(`error: transfer file output failed: ${error.message}. Native operation was not replayed; retain this response and use import-status for any write.\n`);
281
+ return { code: 1, signal: null };
282
+ }
283
+ }
284
+ if (parsed.name === 'status' && result.code === 0) {
285
+ try {
286
+ const document = JSON.parse(output);
287
+ if (document !== null && typeof document === 'object' && !Array.isArray(document)) {
288
+ document.workbite = { schema: 'workbite.agent.binding.v1', workspace: binding.workspace,
289
+ workspace_uid: binding.workspaceUid, endpoint: binding.endpoint, state: binding.stateRoot };
290
+ output = JSON.stringify(document) + '\n';
291
+ }
292
+ } catch { /* not JSON: passed through unchanged */ }
293
+ }
294
+ if (output) stdout.write(output);
295
+ if (result.stderr) stderr.write(result.stderr);
296
+ return { code: result.code, signal: null };
297
+ }
298
+
299
+ async function main(argv = process.argv.slice(2), options = {}) {
300
+ const stdout = options.stdout || process.stdout, stderr = options.stderr || process.stderr;
301
+ const parsed = parseArguments(argv);
302
+ if (parsed.command === 'help') { stdout.write(HELP); return { code: 0, signal: null }; }
303
+ if (parsed.command === 'version') { stdout.write(`workbite ${packageVersion}\n`); return { code: 0, signal: null }; }
304
+ const environment = options.environment || process.env;
305
+ const platform = options.platform || process.platform;
306
+ if (parsed.command === 'update') {
307
+ return require('./update').runUpdate({ ...options, environment, platform, stdout, stderr });
308
+ }
309
+ if (['status', 'doctor', 'recover'].includes(parsed.command)) {
310
+ return require('./diagnostics').runDiagnostics(parsed, { ...options, environment, platform, stdout });
311
+ }
312
+ const spec = releaseSpec(options.manifest || require('../release.json'), platform, options.arch || process.arch, options);
313
+ const where = locations(environment, platform);
314
+ // agent and mcp only use an already installed, verified suite: nothing is downloaded or started.
315
+ const installed = await ensureInstalled(spec, { ...options, apps: options.apps || where.apps,
316
+ checkOnly: ['check', 'agent', 'mcp'].includes(parsed.command), progress: options.progress || stageReporter(stderr) });
317
+ const stateRoot = options.stateRoot || where.state;
318
+ if (parsed.command === 'agent') return runAgent(parsed, { ...options, installed, stateRoot, environment, stdout, stderr });
319
+ if (parsed.command === 'mcp') {
320
+ return require('./mcp').runMcp({ installed, stateRoot, environment, workspace: parsed.workspace,
321
+ workspaceUid: parsed.workspaceUid, commands: options.commands, timeouts: options.timeouts,
322
+ stdin: options.stdin || process.stdin, stdout, stderr });
323
+ }
324
+ if (parsed.command === 'install' || parsed.command === 'check') {
325
+ stdout.write(JSON.stringify({ schema: 'workbite.install.v1', version: spec.version, target: spec.target,
326
+ home: installed.home, state: stateRoot }) + '\n');
327
+ return { code: 0, signal: null };
328
+ }
329
+ return openSuite({ installed, commands: options.commands, workspace: parsed.workspace, init: parsed.init, ui: parsed.ui,
330
+ frontend: parsed.frontend, stateRoot, environment, spawnImpl: options.spawnImpl, execImpl: options.execImpl, stdout, stderr, timeouts: options.timeouts,
331
+ mobileBootstrap: options.mobileBootstrap });
332
+ }
333
+
334
+ module.exports = { main, parseArguments, HELP };