crewx-agent-cli 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,618 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { stdin, stdout } from 'node:process';
4
+ import { resolve } from 'node:path';
5
+ import * as tls from 'node:tls';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { Command, InvalidArgumentError } from 'commander';
8
+ import { parseAdapter, probeAdapter, runAdapter } from './adapters.js';
9
+ import { CrewXApi } from './api.js';
10
+ import { resolveConfig, saveConfig, readStoredConfig } from './config.js';
11
+ import { CLI_NAME, CLI_VERSION, DEFAULT_ADAPTER, DEFAULT_POLL_INTERVAL_MS } from './constants.js';
12
+ import { runDaemon } from './daemon.js';
13
+ import { CliError, errorMessage, redactSecrets } from './errors.js';
14
+ import { decodeJoinCode } from './join.js';
15
+ import { ui } from './ui.js';
16
+ function trustSystemCertificateAuthorities() {
17
+ if (typeof tls.getCACertificates !== 'function' || typeof tls.setDefaultCACertificates !== 'function')
18
+ return;
19
+ try {
20
+ tls.setDefaultCACertificates([
21
+ ...new Set([...tls.getCACertificates('default'), ...tls.getCACertificates('system')]),
22
+ ]);
23
+ }
24
+ catch {
25
+ // Older Node 22 releases may not expose every certificate-store mode.
26
+ }
27
+ }
28
+ trustSystemCertificateAuthorities();
29
+ function positiveInteger(value) {
30
+ const parsed = Number(value);
31
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
32
+ throw new InvalidArgumentError('must be a positive integer');
33
+ }
34
+ return parsed;
35
+ }
36
+ function memoryImportance(value) {
37
+ const parsed = positiveInteger(value);
38
+ if (parsed > 5)
39
+ throw new InvalidArgumentError('must be between 1 and 5');
40
+ return parsed;
41
+ }
42
+ async function readStdin() {
43
+ if (stdin.isTTY)
44
+ return '';
45
+ stdin.setEncoding('utf8');
46
+ let value = '';
47
+ for await (const chunk of stdin)
48
+ value += chunk;
49
+ return value;
50
+ }
51
+ function printJson(value) {
52
+ console.log(JSON.stringify(value, null, 2));
53
+ }
54
+ function commaSeparated(value) {
55
+ return [...new Set(value.split(',').map((item) => item.trim()).filter(Boolean))];
56
+ }
57
+ async function listenForWork(options) {
58
+ if (!options.codingCommand) {
59
+ const runtime = await probeAdapter(options.adapter);
60
+ if (!runtime.available) {
61
+ throw new CliError(`Cannot start the ${options.adapter} runner: ${runtime.error ?? 'the local runtime is unavailable'}. Run \`crewx doctor\` for installation details.`);
62
+ }
63
+ console.log(ui.muted(` Runtime ${options.adapter}${runtime.version ? ` (${runtime.version})` : ''}`));
64
+ }
65
+ if (options.adapter === 'openclaw') {
66
+ console.log(ui.warning('OpenClaw uses its own configured workspace, sandbox, tools, and approvals; CrewX does not rewrite them.'));
67
+ }
68
+ const controller = new AbortController();
69
+ let interrupts = 0;
70
+ const stop = () => {
71
+ interrupts += 1;
72
+ if (interrupts === 1) {
73
+ console.log('\nStopping CrewX agent…');
74
+ controller.abort();
75
+ }
76
+ else {
77
+ process.exitCode = 130;
78
+ }
79
+ };
80
+ process.on('SIGINT', stop);
81
+ process.on('SIGTERM', stop);
82
+ try {
83
+ await runDaemon({
84
+ api: options.api,
85
+ adapter: options.adapter,
86
+ ...(options.name ? { name: options.name } : {}),
87
+ cwd: resolve(options.cwd),
88
+ pollIntervalMs: options.pollIntervalMs,
89
+ once: options.once,
90
+ ...(options.codingCommand ? { codingCommand: options.codingCommand } : {}),
91
+ ...(options.environment ? { environment: options.environment } : {}),
92
+ signal: controller.signal,
93
+ log: (message) => console.log(ui.muted(`[crewx] ${message}`)),
94
+ });
95
+ }
96
+ finally {
97
+ process.removeListener('SIGINT', stop);
98
+ process.removeListener('SIGTERM', stop);
99
+ }
100
+ }
101
+ export function createProgram() {
102
+ const program = new Command();
103
+ program
104
+ .name(CLI_NAME)
105
+ .description('Bring your local coding agents into a CrewX workspace.')
106
+ .version(CLI_VERSION)
107
+ .option('--join <code>', 'join an agent profile using the command copied from CrewX')
108
+ .option('-a, --adapter <adapter>', 'override the runtime encoded in a join command')
109
+ .option('--coding-cmd <command>', 'advanced: run a local argv command and append the prompt (use {prompt} to position it)')
110
+ .option('--once', 'poll once, process available work, then exit')
111
+ .option('--poll-interval <milliseconds>', 'delay between polls', positiveInteger, DEFAULT_POLL_INTERVAL_MS)
112
+ .option('-C, --cwd <directory>', 'local working directory', process.cwd())
113
+ .showHelpAfterError()
114
+ .configureHelp({ sortSubcommands: true, sortOptions: true })
115
+ .action(async (options) => {
116
+ if (!options.join) {
117
+ program.outputHelp();
118
+ return;
119
+ }
120
+ const joined = decodeJoinCode(options.join);
121
+ const adapter = parseAdapter(options.adapter ?? joined.adapter);
122
+ const api = new CrewXApi(joined);
123
+ await api.status(AbortSignal.timeout(10_000));
124
+ console.log(ui.success(`✓ Joined CrewX with ${adapter}`));
125
+ console.log(` Server ${api.baseUrl}`);
126
+ console.log(` Folder ${resolve(options.cwd)}`);
127
+ console.log(ui.muted(' Runs in this terminal. Press Ctrl+C to disconnect.'));
128
+ if (options.codingCmd) {
129
+ console.log(ui.warning(' Custom command enabled; CrewX will execute it directly without a shell.'));
130
+ }
131
+ await listenForWork({
132
+ api,
133
+ adapter,
134
+ cwd: options.cwd,
135
+ pollIntervalMs: options.pollInterval,
136
+ once: options.once ?? false,
137
+ ...(options.codingCmd ? { codingCommand: options.codingCmd } : {}),
138
+ environment: { CREWX_URL: joined.url, CREWX_TOKEN: joined.token },
139
+ });
140
+ });
141
+ program
142
+ .command('connect')
143
+ .description('Authenticate this machine with a CrewX workspace')
144
+ .requiredOption('--url <url>', 'CrewX server URL')
145
+ .requiredOption('--token <token>', 'agent connection token')
146
+ .option('--name <name>', 'friendly name for this machine')
147
+ .action(async (options) => {
148
+ const api = new CrewXApi({ url: options.url, token: options.token });
149
+ await api.status(AbortSignal.timeout(10_000));
150
+ const path = await saveConfig(options);
151
+ console.log(ui.success('✓ Connected to CrewX'));
152
+ console.log(` Server ${api.baseUrl}`);
153
+ if (options.name)
154
+ console.log(` Name ${options.name}`);
155
+ console.log(` Config ${path} ${ui.muted('(mode 0600)')}`);
156
+ console.log(`\nStart listening with ${ui.heading('crewx daemon')}.`);
157
+ });
158
+ program
159
+ .command('status')
160
+ .description('Show the current CrewX connection')
161
+ .option('--json', 'print machine-readable JSON')
162
+ .action(async (options) => {
163
+ const config = await resolveConfig();
164
+ const api = new CrewXApi(config);
165
+ const response = await api.status(AbortSignal.timeout(10_000));
166
+ if (options.json) {
167
+ printJson({ connected: true, url: api.baseUrl, name: config.name, response });
168
+ return;
169
+ }
170
+ console.log(ui.success('● CrewX is reachable'));
171
+ console.log(` Server ${api.baseUrl}`);
172
+ if (config.name)
173
+ console.log(` Name ${config.name}`);
174
+ console.log(` Auth ${config.tokenSource === 'environment' ? 'CREWX_TOKEN' : config.path}`);
175
+ });
176
+ program
177
+ .command('doctor')
178
+ .description('Check local agent binaries and CrewX connectivity')
179
+ .option('--json', 'print machine-readable JSON')
180
+ .action(async (options) => {
181
+ const adapters = await Promise.all(['codex', 'claude', 'pi', 'hermes', 'openclaw'].map(async (adapter) => [adapter, await probeAdapter(adapter)]));
182
+ let connection = {
183
+ configured: false,
184
+ reachable: false,
185
+ };
186
+ try {
187
+ const config = await resolveConfig();
188
+ const api = new CrewXApi(config);
189
+ connection = { configured: true, reachable: true, url: api.baseUrl };
190
+ try {
191
+ await api.status(AbortSignal.timeout(10_000));
192
+ }
193
+ catch (error) {
194
+ connection.reachable = false;
195
+ connection.error = redactSecrets(errorMessage(error), [config.token]);
196
+ }
197
+ }
198
+ catch {
199
+ // An absent connection is a normal doctor result.
200
+ }
201
+ const result = { node: process.version, adapters: Object.fromEntries(adapters), connection };
202
+ if (options.json) {
203
+ printJson(result);
204
+ return;
205
+ }
206
+ console.log(ui.heading('CrewX doctor'));
207
+ console.log(` Node ${process.version} ${Number(process.versions.node.split('.')[0]) >= 22 ? ui.success('✓') : ui.warning('requires 22+')}`);
208
+ for (const [adapter, probe] of adapters) {
209
+ console.log(` ${adapter.padEnd(8)} ${probe.available ? ui.success('✓') : ui.warning('–')} ${probe.version ?? probe.error ?? 'not found'}`);
210
+ }
211
+ console.log(` API ${connection.reachable ? ui.success('✓ reachable') : connection.configured ? ui.warning(`unreachable — ${connection.error}`) : ui.warning('not configured')}`);
212
+ });
213
+ const task = program.command('task').description('List, create, and update CrewX tasks');
214
+ task
215
+ .command('list')
216
+ .description('List tasks visible to this connected agent')
217
+ .option('--status <status>', 'filter by task status')
218
+ .option('--assigned-to-me', 'show only tasks assigned to this agent')
219
+ .option('--include-epics', 'include epics in the result')
220
+ .option('--search <text>', 'search titles and descriptions')
221
+ .option('--json', 'print machine-readable JSON')
222
+ .action(async (options) => {
223
+ const api = new CrewXApi(await resolveConfig());
224
+ const tasks = await api.listTasks({
225
+ ...(options.status ? { status: options.status } : {}),
226
+ ...(options.assignedToMe ? { assignedToMe: true } : {}),
227
+ ...(options.includeEpics ? { includeEpics: true } : {}),
228
+ ...(options.search ? { search: options.search } : {}),
229
+ });
230
+ if (options.json) {
231
+ printJson({ tasks });
232
+ return;
233
+ }
234
+ if (tasks.length === 0) {
235
+ console.log(ui.muted('No matching tasks.'));
236
+ return;
237
+ }
238
+ for (const item of tasks) {
239
+ console.log(`#${item.id} [${item.status}] ${item.title}${item.assigned_to_me ? ' (assigned to me)' : ''}`);
240
+ }
241
+ });
242
+ task
243
+ .command('create')
244
+ .description('Create a workspace task')
245
+ .requiredOption('--title <title>', 'task title')
246
+ .option('--description <markdown>', 'task description')
247
+ .option('--status <status>', 'backlog, todo, in_progress, review, done, or cancelled')
248
+ .option('--priority <priority>', 'low, medium, high, or urgent')
249
+ .option('--labels <labels>', 'comma-separated labels')
250
+ .option('--due <date>', 'due date (YYYY-MM-DD)')
251
+ .option('--project <channel-id>', 'project channel ID', positiveInteger)
252
+ .option('--epic <task-id>', 'epic task ID', positiveInteger)
253
+ .option('--assign-to-me', 'assign the new task to this connected agent')
254
+ .option('--json', 'print machine-readable JSON')
255
+ .action(async (options) => {
256
+ const api = new CrewXApi(await resolveConfig());
257
+ const created = await api.createTask({
258
+ title: options.title,
259
+ ...(options.description !== undefined ? { description: options.description } : {}),
260
+ ...(options.status ? { status: options.status } : {}),
261
+ ...(options.priority ? { priority: options.priority } : {}),
262
+ ...(options.labels !== undefined ? { labels: commaSeparated(options.labels) } : {}),
263
+ ...(options.due ? { due_at: options.due } : {}),
264
+ ...(options.project !== undefined ? { project_channel_id: options.project } : {}),
265
+ ...(options.epic !== undefined ? { epic_id: options.epic } : {}),
266
+ ...(options.assignToMe ? { assign_to_me: true } : {}),
267
+ });
268
+ if (options.json) {
269
+ printJson({ task: created });
270
+ return;
271
+ }
272
+ console.log(ui.success(`✓ Created task #${created.id}: ${created.title}`));
273
+ });
274
+ task
275
+ .command('update')
276
+ .description('Update a task assigned to this connected agent')
277
+ .argument('<id>', 'task ID', positiveInteger)
278
+ .option('--title <title>', 'replace the task title')
279
+ .option('--description <markdown>', 'replace the task description')
280
+ .option('--status <status>', 'set task status')
281
+ .option('--priority <priority>', 'set task priority')
282
+ .option('--labels <labels>', 'replace labels with a comma-separated list')
283
+ .option('--due <date>', 'set due date (YYYY-MM-DD)')
284
+ .option('--result <markdown>', 'record the work result')
285
+ .option('--pull-request <url>', 'record a pull request URL')
286
+ .option('--json', 'print machine-readable JSON')
287
+ .action(async (id, options) => {
288
+ const attributes = {
289
+ ...(options.title !== undefined ? { title: options.title } : {}),
290
+ ...(options.description !== undefined ? { description: options.description } : {}),
291
+ ...(options.status ? { status: options.status } : {}),
292
+ ...(options.priority ? { priority: options.priority } : {}),
293
+ ...(options.labels !== undefined ? { labels: commaSeparated(options.labels) } : {}),
294
+ ...(options.due ? { due_at: options.due } : {}),
295
+ ...(options.result !== undefined ? { result: options.result } : {}),
296
+ ...(options.pullRequest !== undefined ? { pull_request_url: options.pullRequest } : {}),
297
+ };
298
+ if (Object.keys(attributes).length === 0)
299
+ throw new CliError('Provide at least one task field to update.');
300
+ const api = new CrewXApi(await resolveConfig());
301
+ const updated = await api.updateTask(id, attributes);
302
+ if (options.json) {
303
+ printJson({ task: updated });
304
+ return;
305
+ }
306
+ console.log(ui.success(`✓ Updated task #${updated.id}: ${updated.title} [${updated.status}]`));
307
+ });
308
+ const doc = program.command('doc').description('List, create, and update CrewX documents');
309
+ doc
310
+ .command('list')
311
+ .description('List documents readable by connected agents')
312
+ .option('--search <text>', 'search document titles and contents')
313
+ .option('--json', 'print machine-readable JSON')
314
+ .action(async (options) => {
315
+ const api = new CrewXApi(await resolveConfig());
316
+ const documents = await api.listDocuments(options.search ? { search: options.search } : {});
317
+ if (options.json) {
318
+ printJson({ documents });
319
+ return;
320
+ }
321
+ if (documents.length === 0) {
322
+ console.log(ui.muted('No matching documents.'));
323
+ return;
324
+ }
325
+ for (const document of documents) {
326
+ console.log(`#${document.id} v${document.current_version} ${document.title}${document.protected ? ' (protected)' : ''}`);
327
+ }
328
+ });
329
+ doc
330
+ .command('create')
331
+ .description('Create an agent-readable workspace document')
332
+ .requiredOption('--title <title>', 'document title')
333
+ .option('--content <markdown>', 'document content')
334
+ .option('--folder <folder-id>', 'folder ID', positiveInteger)
335
+ .option('--summary <summary>', 'version summary')
336
+ .option('--json', 'print machine-readable JSON')
337
+ .action(async (options) => {
338
+ const api = new CrewXApi(await resolveConfig());
339
+ const created = await api.createDocument({
340
+ title: options.title,
341
+ ...(options.content !== undefined ? { content: options.content } : {}),
342
+ ...(options.folder !== undefined ? { folder_id: options.folder } : {}),
343
+ ...(options.summary ? { summary: options.summary } : {}),
344
+ });
345
+ if (options.json) {
346
+ printJson({ document: created });
347
+ return;
348
+ }
349
+ console.log(ui.success(`✓ Created document #${created.id} v${created.current_version}: ${created.title}`));
350
+ });
351
+ doc
352
+ .command('update')
353
+ .description('Update an agent-readable, unprotected document')
354
+ .argument('<id>', 'document ID', positiveInteger)
355
+ .requiredOption('--expected-version <version>', 'current version from doc list', positiveInteger)
356
+ .option('--title <title>', 'replace the document title')
357
+ .option('--content <markdown>', 'replace the document content')
358
+ .option('--folder <folder-id>', 'move to a folder', positiveInteger)
359
+ .option('--summary <summary>', 'version summary')
360
+ .option('--json', 'print machine-readable JSON')
361
+ .action(async (id, options) => {
362
+ const attributes = {
363
+ expected_version: options.expectedVersion,
364
+ ...(options.title !== undefined ? { title: options.title } : {}),
365
+ ...(options.content !== undefined ? { content: options.content } : {}),
366
+ ...(options.folder !== undefined ? { folder_id: options.folder } : {}),
367
+ ...(options.summary ? { summary: options.summary } : {}),
368
+ };
369
+ if (Object.keys(attributes).length === 1)
370
+ throw new CliError('Provide at least one document field to update.');
371
+ const api = new CrewXApi(await resolveConfig());
372
+ const updated = await api.updateDocument(id, attributes);
373
+ if (options.json) {
374
+ printJson({ document: updated });
375
+ return;
376
+ }
377
+ console.log(ui.success(`✓ Updated document #${updated.id} to v${updated.current_version}: ${updated.title}`));
378
+ });
379
+ const memory = program.command('memory').description('List, create, and update durable CrewX memories');
380
+ memory
381
+ .command('list')
382
+ .description('List memories visible to this connected agent')
383
+ .option('--search <text>', 'search memory titles, contents, and sources')
384
+ .option('--category <category>', 'filter by category')
385
+ .option('--scope <scope>', 'workspace or channel')
386
+ .option('--channel <channel-id>', 'filter by channel ID', positiveInteger)
387
+ .option('--json', 'print machine-readable JSON')
388
+ .action(async (options) => {
389
+ if (options.scope && !['workspace', 'channel'].includes(options.scope)) {
390
+ throw new CliError('Memory scope must be workspace or channel.');
391
+ }
392
+ const api = new CrewXApi(await resolveConfig());
393
+ const memories = await api.listMemories({
394
+ ...(options.search ? { search: options.search } : {}),
395
+ ...(options.category ? { category: options.category } : {}),
396
+ ...(options.scope ? { scope: options.scope } : {}),
397
+ ...(options.channel !== undefined ? { channelId: options.channel } : {}),
398
+ });
399
+ if (options.json) {
400
+ printJson({ memories });
401
+ return;
402
+ }
403
+ if (memories.length === 0) {
404
+ console.log(ui.muted('No matching memories.'));
405
+ return;
406
+ }
407
+ for (const item of memories) {
408
+ const scope = item.scope === 'channel' ? `channel ${item.channel?.name ?? ''}`.trim() : 'workspace';
409
+ console.log(`#${item.id} [${item.category}] ${item.title} (${scope})`);
410
+ }
411
+ });
412
+ memory
413
+ .command('create')
414
+ .description('Record a durable learning for this workspace')
415
+ .requiredOption('--content <text>', 'memory content')
416
+ .option('--title <title>', 'memory title; generated from content when omitted')
417
+ .option('--category <category>', 'memory category', 'general')
418
+ .option('--importance <level>', 'importance from 1 to 5', memoryImportance, 3)
419
+ .option('--source <source>', 'memory source', 'agent')
420
+ .option('--channel <channel-id>', 'scope the memory to a visible channel', positiveInteger)
421
+ .option('--json', 'print machine-readable JSON')
422
+ .action(async (options) => {
423
+ const api = new CrewXApi(await resolveConfig());
424
+ const created = await api.createMemory({
425
+ content: options.content,
426
+ ...(options.title ? { title: options.title } : {}),
427
+ category: options.category,
428
+ importance: options.importance,
429
+ source: options.source,
430
+ ...(options.channel !== undefined ? { channel_id: options.channel } : {}),
431
+ });
432
+ if (options.json) {
433
+ printJson({ memory: created });
434
+ return;
435
+ }
436
+ console.log(ui.success(`✓ Saved memory #${created.id}: ${created.title}`));
437
+ });
438
+ memory
439
+ .command('update')
440
+ .description('Update a memory recorded by this connected agent')
441
+ .argument('<id>', 'memory ID', positiveInteger)
442
+ .option('--title <title>', 'replace the memory title')
443
+ .option('--content <text>', 'replace the memory content')
444
+ .option('--category <category>', 'replace the category')
445
+ .option('--importance <level>', 'importance from 1 to 5', memoryImportance)
446
+ .option('--source <source>', 'replace the source')
447
+ .option('--channel <channel-id>', 'move the memory to a visible channel', positiveInteger)
448
+ .option('--workspace-scope', 'move the memory to workspace scope')
449
+ .option('--json', 'print machine-readable JSON')
450
+ .action(async (id, options) => {
451
+ if (options.channel !== undefined && options.workspaceScope) {
452
+ throw new CliError('Choose either --channel or --workspace-scope, not both.');
453
+ }
454
+ const attributes = {
455
+ ...(options.title !== undefined ? { title: options.title } : {}),
456
+ ...(options.content !== undefined ? { content: options.content } : {}),
457
+ ...(options.category !== undefined ? { category: options.category } : {}),
458
+ ...(options.importance !== undefined ? { importance: options.importance } : {}),
459
+ ...(options.source !== undefined ? { source: options.source } : {}),
460
+ ...(options.channel !== undefined ? { channel_id: options.channel } : {}),
461
+ ...(options.workspaceScope ? { channel_id: null } : {}),
462
+ };
463
+ if (Object.keys(attributes).length === 0)
464
+ throw new CliError('Provide at least one memory field to update.');
465
+ const api = new CrewXApi(await resolveConfig());
466
+ const updated = await api.updateMemory(id, attributes);
467
+ if (options.json) {
468
+ printJson({ memory: updated });
469
+ return;
470
+ }
471
+ console.log(ui.success(`✓ Updated memory #${updated.id}: ${updated.title}`));
472
+ });
473
+ const integration = program
474
+ .command('integration')
475
+ .description('Search connected Slack, Notion, and Google Drive knowledge');
476
+ integration
477
+ .command('list')
478
+ .description('List knowledge providers connected to this workspace')
479
+ .option('--json', 'print machine-readable JSON')
480
+ .action(async (options) => {
481
+ const api = new CrewXApi(await resolveConfig());
482
+ const integrations = await api.listIntegrations();
483
+ if (options.json) {
484
+ printJson({ integrations });
485
+ return;
486
+ }
487
+ if (integrations.length === 0) {
488
+ console.log(ui.muted('No knowledge integrations are connected.'));
489
+ return;
490
+ }
491
+ for (const item of integrations) {
492
+ console.log(`${item.provider}${item.account_name ? ` — ${item.account_name}` : ''}`);
493
+ }
494
+ });
495
+ integration
496
+ .command('search')
497
+ .description('Search one connected knowledge provider')
498
+ .argument('<provider>', 'slack, notion, or google_drive')
499
+ .argument('<query...>', 'search query')
500
+ .option('--json', 'print machine-readable JSON')
501
+ .action(async (provider, queryParts, options) => {
502
+ if (!['slack', 'notion', 'google_drive'].includes(provider)) {
503
+ throw new CliError('Provider must be slack, notion, or google_drive.');
504
+ }
505
+ const query = queryParts.join(' ').trim();
506
+ if (!query)
507
+ throw new CliError('Provide a search query.');
508
+ const api = new CrewXApi(await resolveConfig());
509
+ const results = await api.searchIntegration(provider, query);
510
+ if (options.json) {
511
+ printJson({ results });
512
+ return;
513
+ }
514
+ if (results.length === 0) {
515
+ console.log(ui.muted('No matching provider results.'));
516
+ return;
517
+ }
518
+ for (const item of results) {
519
+ console.log(`${item.title ?? 'Untitled'}${item.url ? `\n ${item.url}` : ''}`);
520
+ }
521
+ });
522
+ program
523
+ .command('daemon')
524
+ .description('Listen for CrewX work and execute it with a local agent')
525
+ .option('-a, --adapter <adapter>', 'codex, claude, pi, hermes, or openclaw', process.env.CREWX_ADAPTER || DEFAULT_ADAPTER)
526
+ .option('--coding-cmd <command>', 'advanced: run a local argv command and append the prompt (use {prompt} to position it)')
527
+ .option('--once', 'poll once, process available work, then exit')
528
+ .option('--poll-interval <milliseconds>', 'delay between polls', positiveInteger, DEFAULT_POLL_INTERVAL_MS)
529
+ .option('-C, --cwd <directory>', 'local working directory', process.cwd())
530
+ .action(async (options) => {
531
+ // Commander treats the identically named root options as global even
532
+ // when they appear after `daemon`. Read those values explicitly so
533
+ // `crewx daemon --adapter claude --once` does not silently use the
534
+ // subcommand defaults instead.
535
+ const globalOptions = program.opts();
536
+ const adapter = parseAdapter(globalOptions.adapter ?? options.adapter);
537
+ const config = await resolveConfig();
538
+ const api = new CrewXApi(config);
539
+ await listenForWork({
540
+ api,
541
+ adapter,
542
+ ...(config.name ? { name: config.name } : {}),
543
+ cwd: globalOptions.cwd ?? options.cwd,
544
+ pollIntervalMs: globalOptions.pollInterval ?? options.pollInterval,
545
+ once: globalOptions.once ?? options.once ?? false,
546
+ ...((globalOptions.codingCmd ?? options.codingCmd)
547
+ ? { codingCommand: globalOptions.codingCmd ?? options.codingCmd }
548
+ : {}),
549
+ });
550
+ });
551
+ program
552
+ .command('run')
553
+ .description('Run one local agent without connecting to CrewX')
554
+ .argument('<adapter>', 'codex, claude, pi, hermes, or openclaw')
555
+ .argument('[prompt...]', 'prompt (reads stdin when omitted)')
556
+ .option('-C, --cwd <directory>', 'local working directory', process.cwd())
557
+ .option('--coding-cmd <command>', 'advanced: run a local argv command and append the prompt (use {prompt} to position it)')
558
+ .action(async (adapterName, promptParts, options) => {
559
+ const globalOptions = program.opts();
560
+ const adapter = parseAdapter(adapterName);
561
+ const argumentPrompt = promptParts.join(' ').trim();
562
+ const prompt = argumentPrompt || (await readStdin()).trim();
563
+ if (!prompt)
564
+ throw new CliError('Provide a prompt argument or pipe a prompt on stdin.');
565
+ let displayedMessages = 0;
566
+ const result = await runAdapter({
567
+ adapter,
568
+ prompt,
569
+ cwd: resolve(globalOptions.cwd ?? options.cwd),
570
+ ...((globalOptions.codingCmd ?? options.codingCmd)
571
+ ? { codingCommand: globalOptions.codingCmd ?? options.codingCmd }
572
+ : {}),
573
+ onMessage(message) {
574
+ stdout.write(message.endsWith('\n') ? message : `${message}\n`);
575
+ displayedMessages += 1;
576
+ },
577
+ onStderr(line) {
578
+ process.stderr.write(`${line}\n`);
579
+ },
580
+ });
581
+ if (displayedMessages === 0 && result.stdout.length > 0) {
582
+ stdout.write(`${result.stdout.join('\n')}\n`);
583
+ }
584
+ if (result.exitCode !== 0) {
585
+ throw new CliError(`${adapter} exited with code ${String(result.exitCode)}.`, result.exitCode ?? 1);
586
+ }
587
+ });
588
+ return program;
589
+ }
590
+ export async function main(argv = process.argv) {
591
+ try {
592
+ await createProgram().parseAsync(argv);
593
+ }
594
+ catch (error) {
595
+ const stored = await readStoredConfig().catch(() => undefined);
596
+ console.error(ui.warning(`Error: ${redactSecrets(errorMessage(error), stored ? [stored.token] : [])}`));
597
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
598
+ }
599
+ }
600
+ export function isMainModule(moduleUrl, invokedPath = process.argv[1]) {
601
+ if (!invokedPath)
602
+ return false;
603
+ try {
604
+ return realpathSync.native(fileURLToPath(moduleUrl)) === realpathSync.native(resolve(invokedPath));
605
+ }
606
+ catch {
607
+ return false;
608
+ }
609
+ }
610
+ if (isMainModule(import.meta.url)) {
611
+ await main();
612
+ }
613
+ export { CrewXApi } from './api.js';
614
+ export { configPath, readStoredConfig, resolveConfig, saveConfig } from './config.js';
615
+ export { decodeJoinCode, encodeJoinCode } from './join.js';
616
+ export { assemblePrompt, assignmentFromEvent } from './prompt.js';
617
+ export { buildAdapterInvocation, parseAdapterLine, parseCodingCommand, runAdapter } from './adapters.js';
618
+ //# sourceMappingURL=index.js.map