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/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/adapters.d.ts +67 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +1085 -0
- package/dist/adapters.js.map +1 -0
- package/dist/api.d.ts +162 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +287 -0
- package/dist/api.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -0
- package/dist/daemon.d.ts +47 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +522 -0
- package/dist/daemon.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/dist/join.d.ts +10 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +51 -0
- package/dist/join.js.map +1 -0
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +135 -0
- package/dist/prompt.js.map +1 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +19 -0
- package/dist/ui.js.map +1 -0
- package/package.json +54 -0
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
|