@pulsarinteractive/geniustasker 0.1.0-beta.2 → 0.1.0-beta.4

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/src/cli.mjs CHANGED
@@ -7,126 +7,421 @@ import { deviceLogin, browserLogin } from './login.mjs';
7
7
  import { CommandJournal, describeCommands, readCommandFile } from './commands.mjs';
8
8
  import { AgentProfiles } from './profiles.mjs';
9
9
  import { readChanges, watchChanges } from './changes.mjs';
10
+ import { searchMemory, exploreGraph, resolveMemory } from './knowledge.mjs';
11
+ import { MediaJournal, mediaAction, downloadMedia } from './media.mjs';
10
12
  import { fail, safeError, exitCode } from './errors.mjs';
11
13
 
12
14
  export const commands = {
13
- 'auth login': { summary: 'Open browser sign-in and choose access. No tutorial or token to copy.', options: ['flow', 'browser', 'no-browser', 'scope', 'interactive'] },
14
- 'auth logout': { summary: 'Revoke this terminal session on the server, then erase local credentials.', options: [] },
15
+ 'auth login': {
16
+ summary: 'Open browser sign-in and choose access. No tutorial or token to copy.',
17
+ options: ['flow', 'browser', 'no-browser', 'scope', 'interactive'],
18
+ },
19
+ 'auth logout': {
20
+ summary: 'Revoke this terminal session on the server, then erase local credentials.',
21
+ options: [],
22
+ },
15
23
  'auth status': { summary: 'Read current server identity and permission ceiling.', options: [] },
16
24
  whoami: { summary: 'Read current server identity.', options: [] },
17
- capabilities: { summary: 'Discover collections, fields, limits and write availability.', options: [] },
18
- 'profiles list': { summary: 'List selectable profiles in this connection within the current grant.', options: [] },
19
- 'profiles create': { summary: 'Create a narrower profile from JSON with a UUID chosen once; never switches automatically.', options: ['file'] },
20
- 'profiles restrict': { summary: 'Rename or narrow the current profile from JSON with its expectedRevision. Cannot broaden access.', options: ['file'] },
21
- 'profiles select': { summary: 'Rotate this context into a selectable profile. Cannot return to broader access; use separate contexts for independent terminals.', options: ['profile'] },
22
- 'memories list': { summary: 'List independent account memories; follow nextAfter even for an empty page.', options: ['after'] },
23
- 'projects list': { summary: 'Read a bounded project page; follow nextAfter even for an empty page.', options: ['after'] },
24
- 'resources list': { summary: 'Read a project or memory collection; carry epoch/checkpoint when continuing.', options: ['scope', 'collection', 'after', 'epoch', 'checkpoint', 'archive', 'id', 'path'] },
25
- 'changes list': { summary: 'Read one bounded change page from a completed resource inventory or previous nextCursor.', options: ['scope', 'collection', 'cursor'] },
26
- 'changes watch': { summary: 'Wait at most 60 seconds for one change page, rechecking rights on every poll. No infinite stream.', options: ['scope', 'collection', 'cursor', 'wait-seconds'] },
27
- 'questions answers': { summary: 'Read human question answers in bounded pages. Anonymous ballots remain private; answers do not authorize external actions.', options: ['scope', 'question', 'after', 'epoch', 'checkpoint'] },
28
- 'commands describe': { summary: 'Show contract-derived command fields and JSON schemas; no network.', options: [] },
29
- 'commands prepare': { summary: 'Save an immutable operation from a JSON draft without sending it.', options: ['file'] },
30
- 'commands execute': { summary: 'Journal and send a complete JSON operation once. Never retries automatically.', options: ['file'] },
31
- 'commands retry': { summary: 'Replay the exact locally saved operation with its original identity.', options: ['operation'] },
32
- 'commands inspect': { summary: 'Read a private local operation and receipt; does not contact the server.', options: ['operation'] },
33
- doctor: { summary: 'Inspect local environment without exposing credentials or contacting the API.', options: [] },
34
- mcp: { summary: 'Serve delegated read and journaled command tools over stdio. Authenticate with the CLI first.', options: [] },
25
+ capabilities: {
26
+ summary: 'Discover collections, fields, limits and write availability.',
27
+ options: [],
28
+ },
29
+ 'profiles list': {
30
+ summary: 'List selectable profiles in this connection within the current grant.',
31
+ options: [],
32
+ },
33
+ 'profiles create': {
34
+ summary:
35
+ 'Create a narrower profile from JSON with a UUID chosen once; never switches automatically.',
36
+ options: ['file'],
37
+ },
38
+ 'profiles restrict': {
39
+ summary:
40
+ 'Rename or narrow the current profile from JSON with its expectedRevision. Cannot broaden access.',
41
+ options: ['file'],
42
+ },
43
+ 'profiles select': {
44
+ summary:
45
+ 'Rotate this context into a selectable profile. Cannot return to broader access; use separate contexts for independent terminals.',
46
+ options: ['profile'],
47
+ },
48
+ 'memories list': {
49
+ summary: 'List independent account memories; follow nextAfter even for an empty page.',
50
+ options: ['after'],
51
+ },
52
+ 'media prepare': {
53
+ summary: 'Prepare a local private attachment; retain its operation ID.',
54
+ options: ['scope', 'file', 'content-type', 'epoch'],
55
+ },
56
+ 'media upload': {
57
+ summary: 'Upload or safely retry the prepared operation.',
58
+ options: ['operation'],
59
+ },
60
+ 'media action': {
61
+ summary: 'Inspect, publish, cancel or remove a private attachment.',
62
+ options: ['scope', 'owner', 'id', 'action'],
63
+ },
64
+ 'media download': {
65
+ summary: 'Download an authorized attachment to a new local file.',
66
+ options: ['scope', 'owner', 'id', 'file'],
67
+ },
68
+ 'memory search': {
69
+ summary: 'Search document blocks or Graph nodes without loading the corpus.',
70
+ options: ['scope', 'query', 'collection', 'after', 'epoch', 'checkpoint'],
71
+ },
72
+ 'graph explore': {
73
+ summary: 'Read a bounded Graph neighborhood around one node.',
74
+ options: ['scope', 'center', 'depth', 'epoch', 'checkpoint'],
75
+ },
76
+ 'memory resolve': {
77
+ summary:
78
+ 'Resolve a Memory page or block with current permissions; unavailable links preserve nodes.',
79
+ options: ['scope', 'page', 'block'],
80
+ },
81
+ 'projects list': {
82
+ summary: 'Read a bounded project page; follow nextAfter even for an empty page.',
83
+ options: ['after'],
84
+ },
85
+ 'resources list': {
86
+ summary: 'Read a project or memory collection; carry epoch/checkpoint when continuing.',
87
+ options: ['scope', 'collection', 'after', 'epoch', 'checkpoint', 'archive', 'id', 'path'],
88
+ },
89
+ 'changes list': {
90
+ summary:
91
+ 'Read one bounded change page from a completed resource inventory or previous nextCursor.',
92
+ options: ['scope', 'collection', 'cursor'],
93
+ },
94
+ 'changes watch': {
95
+ summary:
96
+ 'Wait at most 60 seconds for one change page, rechecking rights on every poll. No infinite stream.',
97
+ options: ['scope', 'collection', 'cursor', 'wait-seconds'],
98
+ },
99
+ 'questions answers': {
100
+ summary:
101
+ 'Read human question answers in bounded pages. Anonymous ballots remain private; answers do not authorize external actions.',
102
+ options: ['scope', 'question', 'after', 'epoch', 'checkpoint'],
103
+ },
104
+ 'commands describe': {
105
+ summary: 'Show contract-derived command fields and JSON schemas; no network.',
106
+ options: [],
107
+ },
108
+ 'commands prepare': {
109
+ summary: 'Save an immutable operation from a JSON draft without sending it.',
110
+ options: ['file'],
111
+ },
112
+ 'commands execute': {
113
+ summary: 'Journal and send a complete JSON operation once. Never retries automatically.',
114
+ options: ['file'],
115
+ },
116
+ 'commands retry': {
117
+ summary: 'Replay the exact locally saved operation with its original identity.',
118
+ options: ['operation'],
119
+ },
120
+ 'commands inspect': {
121
+ summary: 'Read a private local operation and receipt; does not contact the server.',
122
+ options: ['operation'],
123
+ },
124
+ doctor: {
125
+ summary: 'Inspect local environment without exposing credentials or contacting the API.',
126
+ options: [],
127
+ },
128
+ mcp: {
129
+ summary:
130
+ 'Serve delegated read and journaled command tools over stdio. Authenticate with the CLI first.',
131
+ options: [],
132
+ },
35
133
  help: { summary: 'Describe commands and stable output conventions.', options: [] },
36
134
  };
37
135
  const globalOptions = ['json', 'context', 'environment', 'origin', 'allow-local', 'help'];
38
136
  const flags = new Set(['json', 'allow-local', 'help', 'interactive', 'no-browser']);
39
137
  export function parse(argv) {
40
- const options = {}, words = [];
138
+ const options = {},
139
+ words = [];
41
140
  for (let i = 0; i < argv.length; i++) {
42
141
  const arg = argv[i];
43
- if (!arg.startsWith('--')) { if (arg.startsWith('-')) fail('invalid_arguments', 'Use long options from help.'); words.push(arg); continue; }
142
+ if (!arg.startsWith('--')) {
143
+ if (arg.startsWith('-')) {
144
+ fail('invalid_arguments', 'Use long options from help.');
145
+ }
146
+ words.push(arg);
147
+ continue;
148
+ }
44
149
  const key = arg.slice(2);
45
- if (!key || key.includes('=') || Object.hasOwn(options, key)) fail('invalid_arguments', 'Options must be unique and use --name value syntax.');
46
- if (flags.has(key)) options[key] = true;
47
- else {
150
+ if (!key || key.includes('=') || Object.hasOwn(options, key)) {
151
+ fail('invalid_arguments', 'Options must be unique and use --name value syntax.');
152
+ }
153
+ if (flags.has(key)) {
154
+ options[key] = true;
155
+ } else {
48
156
  const value = argv[++i];
49
- if (!value || value.startsWith('--') || value.length > 4096) fail('invalid_arguments', 'An option value is missing or too long.');
157
+ if (!value || value.startsWith('--') || value.length > 4096) {
158
+ fail('invalid_arguments', 'An option value is missing or too long.');
159
+ }
50
160
  options[key] = value;
51
161
  }
52
162
  }
53
163
  const command = words.join(' ') || 'help';
54
164
  const definition = commands[command];
55
- if (!definition || Object.keys(options).some(key => !globalOptions.includes(key) && !definition.options.includes(key)))
165
+ if (
166
+ !definition ||
167
+ Object.keys(options).some(
168
+ (key) => !globalOptions.includes(key) && !definition.options.includes(key),
169
+ )
170
+ ) {
56
171
  fail('invalid_arguments', 'Unknown command or option. Run help.');
172
+ }
57
173
  return { command, options };
58
174
  }
59
175
  export function help(command = 'help') {
60
- return { formatVersion: 1, command, commands: command === 'help' ? commands : { [command]: commands[command] },
61
- globalOptions: { context: 'default; use separate names for separate accounts or agent sessions', environment: 'development (beta default) | production', origin: 'explicit loopback origin with --allow-local, for local tests only', json: 'one versioned JSON result on stdout; progress/errors use stderr' },
62
- loginOptions: { flow: 'browser (default, PKCE loopback) | device (SSH/headless)', browser: 'default (automatic) | chrome; --no-browser prints the link without opening it', scope: 'space-separated permission ceiling; browser defaults to read-only and requires explicit write selection', interactive: 'required when starting login without a terminal; never use to bypass human consent' },
63
- exitCodes: { 0: 'success', 1: 'failure', 2: 'usage', 3: 'authentication', 4: 'permission', 5: 'conflict', 6: 'rate limit', 7: 'transient failure' },
64
- examples: ['geniustasker auth login --environment development --context work --browser chrome', 'geniustasker projects list --environment development --context work --json', 'geniustasker resources list --scope project:ID --collection tasks --json'],
65
- privacy: 'Do not put tokens in arguments, environment variables, tickets, logs or MCP configuration. This beta uses the development environment by default; access still requires an admitted account.' };
176
+ return {
177
+ formatVersion: 1,
178
+ command,
179
+ commands: command === 'help' ? commands : { [command]: commands[command] },
180
+ globalOptions: {
181
+ context: 'default; use separate names for separate accounts or agent sessions',
182
+ environment: 'development (beta default) | production',
183
+ origin: 'explicit loopback origin with --allow-local, for local tests only',
184
+ json: 'one versioned JSON result on stdout; progress/errors use stderr',
185
+ },
186
+ loginOptions: {
187
+ flow: 'browser (default, PKCE loopback) | device (SSH/headless)',
188
+ browser: 'default (automatic) | chrome; --no-browser prints the link without opening it',
189
+ scope:
190
+ 'space-separated permission ceiling; browser defaults to read-only and requires explicit write selection',
191
+ interactive:
192
+ 'required when starting login without a terminal; never use to bypass human consent',
193
+ },
194
+ exitCodes: {
195
+ 0: 'success',
196
+ 1: 'failure',
197
+ 2: 'usage',
198
+ 3: 'authentication',
199
+ 4: 'permission',
200
+ 5: 'conflict',
201
+ 6: 'rate limit',
202
+ 7: 'transient failure',
203
+ },
204
+ examples: [
205
+ 'geniustasker auth login --environment development --context work --browser chrome',
206
+ 'geniustasker projects list --environment development --context work --json',
207
+ 'geniustasker resources list --scope project:ID --collection tasks --json',
208
+ ],
209
+ privacy:
210
+ 'Do not put tokens in arguments, environment variables, tickets, logs or MCP configuration. This beta uses the development environment by default; access still requires an admitted account.',
211
+ };
66
212
  }
67
213
  async function openConsent(url, browser) {
68
214
  await open(url, { wait: false, ...(browser === 'chrome' ? { app: { name: apps.chrome } } : {}) });
69
215
  }
70
- export async function main(argv, { stdout = process.stdout, stderr = process.stderr, isTTY = process.stdin.isTTY, directory, fetchImpl, signal, openBrowserImpl = openConsent } = {}) {
216
+ export async function main(
217
+ argv,
218
+ {
219
+ stdout = process.stdout,
220
+ stderr = process.stderr,
221
+ isTTY = process.stdin.isTTY,
222
+ directory,
223
+ fetchImpl,
224
+ signal,
225
+ openBrowserImpl = openConsent,
226
+ } = {},
227
+ ) {
71
228
  try {
72
229
  const { command, options } = parse(argv);
73
- const emit = data => stdout.write(JSON.stringify({ formatVersion: 1, ok: true, data }, null, options.json ? 0 : 2) + '\n');
74
- if (command === 'help' || options.help) { emit(help(command)); return 0; }
75
- if (options.environment && !Object.hasOwn(ORIGINS, options.environment)) fail('invalid_arguments', 'Environment must be production or development.');
76
- if (options.origin && options.environment) fail('invalid_arguments', 'Choose --environment or --origin, not both.');
77
- const origin = originOf(options.origin ?? ORIGINS[options.environment ?? 'development'], options['allow-local']);
230
+ const emit = (data) =>
231
+ stdout.write(
232
+ JSON.stringify({ formatVersion: 1, ok: true, data }, null, options.json ? 0 : 2) + '\n',
233
+ );
234
+ if (command === 'help' || options.help) {
235
+ emit(help(command));
236
+ return 0;
237
+ }
238
+ if (options.environment && !Object.hasOwn(ORIGINS, options.environment)) {
239
+ fail('invalid_arguments', 'Environment must be production or development.');
240
+ }
241
+ if (options.origin && options.environment) {
242
+ fail('invalid_arguments', 'Choose --environment or --origin, not both.');
243
+ }
244
+ const origin = originOf(
245
+ options.origin ?? ORIGINS[options.environment ?? 'development'],
246
+ options['allow-local'],
247
+ );
78
248
  const store = new CredentialStore({ context: options.context, directory });
79
249
  const client = new AgentClient(new Transport(origin, { fetchImpl, signal }), store);
80
250
  let data;
81
251
  if (command === 'auth login') {
82
- if (!isTTY && !options.interactive) fail('invalid_arguments', 'Login needs an interactive human.', { remediation: 'Run auth login in a terminal. Automated clients must use an existing connection.' });
83
- if (options.flow && !['device', 'browser'].includes(options.flow) || options.browser && !['chrome', 'default'].includes(options.browser) || options.browser && options['no-browser']) fail('invalid_arguments', 'Use --flow browser|device, with --browser default|chrome or --no-browser.');
84
- const notify = event => {
85
- if (options.json) stderr.write(JSON.stringify({ formatVersion: 1, ...event }) + '\n');
86
- else if (event.event === 'consent_required') stderr.write(`\nGeniusTasker · Connect your agent\n\n${options['no-browser'] ? 'Open this link' : 'Opening your browser. If it does not open, use this link'}:\n\n${event.url}\n${event.userCode ? `\nDevice code: ${event.userCode}\n` : ''}\nSign in, choose access, and approve. This terminal will finish automatically.\nWaiting for your decision…\n`);
87
- else if (event.event === 'browser_unavailable') stderr.write('Could not open the browser. Use the link above; login is still waiting.\n');
252
+ if (!isTTY && !options.interactive) {
253
+ fail('invalid_arguments', 'Login needs an interactive human.', {
254
+ remediation:
255
+ 'Run auth login in a terminal. Automated clients must use an existing connection.',
256
+ });
257
+ }
258
+ if (
259
+ (options.flow && !['device', 'browser'].includes(options.flow)) ||
260
+ (options.browser && !['chrome', 'default'].includes(options.browser)) ||
261
+ (options.browser && options['no-browser'])
262
+ ) {
263
+ fail(
264
+ 'invalid_arguments',
265
+ 'Use --flow browser|device, with --browser default|chrome or --no-browser.',
266
+ );
267
+ }
268
+ const notify = (event) => {
269
+ if (options.json) {
270
+ stderr.write(JSON.stringify({ formatVersion: 1, ...event }) + '\n');
271
+ } else if (event.event === 'consent_required') {
272
+ stderr.write(
273
+ `\nGeniusTasker · Connect your agent\n\n${options['no-browser'] ? 'Open this link' : 'Opening your browser. If it does not open, use this link'}:\n\n${event.url}\n${event.userCode ? `\nDevice code: ${event.userCode}\n` : ''}\nSign in, choose access, and approve. This terminal will finish automatically.\nWaiting for your decision…\n`,
274
+ );
275
+ } else if (event.event === 'browser_unavailable') {
276
+ stderr.write('Could not open the browser. Use the link above; login is still waiting.\n');
277
+ }
88
278
  };
89
- data = await (options.flow === 'device' ? deviceLogin : browserLogin)(client, { scope: options.scope ?? catalog.grants.permissions.join(' '), openBrowser: options['no-browser'] ? undefined : async url => {
90
- try { await openBrowserImpl(url, options.browser); }
91
- catch { notify({ event: 'browser_unavailable', message: 'Open the printed consent URL in your browser. Login is still waiting.' }); }
92
- },
93
- notify });
279
+ data = await (options.flow === 'device' ? deviceLogin : browserLogin)(client, {
280
+ scope: options.scope ?? catalog.grants.permissions.join(' '),
281
+ openBrowser: options['no-browser']
282
+ ? undefined
283
+ : async (url) => {
284
+ try {
285
+ await openBrowserImpl(url, options.browser);
286
+ } catch {
287
+ notify({
288
+ event: 'browser_unavailable',
289
+ message: 'Open the printed consent URL in your browser. Login is still waiting.',
290
+ });
291
+ }
292
+ },
293
+ notify,
294
+ });
94
295
  if (!options.json) {
95
- stdout.write(`\nConnected to GeniusTasker.\nYour chosen access is ready for the CLI and MCP.\nRun geniustasker auth status${options.context ? ' --context ' + options.context : ''} to check the connection.\n`);
296
+ stdout.write(
297
+ `\nConnected to GeniusTasker.\nYour chosen access is ready for the CLI and MCP.\nRun geniustasker auth status${options.context ? ' --context ' + options.context : ''} to check the connection.\n`,
298
+ );
96
299
  return 0;
97
300
  }
98
- } else if (command === 'auth logout') data = await client.logout();
99
- else if (command === 'profiles list') data = await new AgentProfiles(client).list();
100
- else if (command === 'profiles create') data = await new AgentProfiles(client).create(await readCommandFile(options.file));
101
- else if (command === 'profiles restrict') data = await new AgentProfiles(client).restrict(await readCommandFile(options.file));
102
- else if (command === 'profiles select') data = await new AgentProfiles(client).select({ profileId: options.profile });
103
- else if (command === 'commands describe') data = describeCommands();
104
- else if (command === 'commands prepare') data = await new CommandJournal(client).prepare(await readCommandFile(options.file));
105
- else if (command === 'commands execute') data = await new CommandJournal(client).execute(await readCommandFile(options.file));
106
- else if (command === 'commands retry') data = await new CommandJournal(client).retry(options.operation);
107
- else if (command === 'commands inspect') data = await new CommandJournal(client).inspect(options.operation);
108
- else if (command === 'doctor') data = await client.doctor();
109
- else if (command === 'whoami' || command === 'auth status') data = await client.get('me');
110
- else if (command === 'capabilities') data = await client.get('capabilities');
111
- else if (command === 'projects list' || command === 'memories list') data = await client.get(command === 'projects list' ? 'projects' : 'memories', { after: options.after });
112
- else if (command === 'changes list' || command === 'changes watch') {
113
- const input = { scope: options.scope, collection: options.collection, cursor: options.cursor };
114
- data = command === 'changes list' ? await readChanges(client, input) :
115
- await watchChanges(client, { ...input, ...(options['wait-seconds'] === undefined ? {} : { waitSeconds: Number(options['wait-seconds']) }) });
116
- }
117
- else if (command === 'resources list' || command === 'questions answers') {
118
- if (!(command === 'questions answers' ? options.scope?.startsWith('project:') : /^(project|memory):[^\u0000-\u001f]+$/.test(options.scope ?? '')) || !(command === 'resources list' ? options.collection : options.question) || options.after && (!options.epoch || !options.checkpoint))
119
- fail('invalid_arguments', 'Use --scope project:ID or memory:ID and --collection for resources or --question for answers; continuation needs --epoch and --checkpoint.');
120
- data = await client.get(command === 'resources list' ? 'resources' : 'answers', Object.fromEntries(commands[command].options.filter(key => options[key] !== undefined).map(key => [key, options[key]])));
301
+ } else if (command === 'auth logout') {
302
+ data = await client.logout();
303
+ } else if (command === 'profiles list') {
304
+ data = await new AgentProfiles(client).list();
305
+ } else if (command === 'profiles create') {
306
+ data = await new AgentProfiles(client).create(await readCommandFile(options.file));
307
+ } else if (command === 'profiles restrict') {
308
+ data = await new AgentProfiles(client).restrict(await readCommandFile(options.file));
309
+ } else if (command === 'profiles select') {
310
+ data = await new AgentProfiles(client).select({ profileId: options.profile });
311
+ } else if (command === 'commands describe') {
312
+ data = describeCommands();
313
+ } else if (command === 'commands prepare') {
314
+ data = await new CommandJournal(client).prepare(await readCommandFile(options.file));
315
+ } else if (command === 'commands execute') {
316
+ data = await new CommandJournal(client).execute(await readCommandFile(options.file));
317
+ } else if (command === 'commands retry') {
318
+ data = await new CommandJournal(client).retry(options.operation);
319
+ } else if (command === 'commands inspect') {
320
+ data = await new CommandJournal(client).inspect(options.operation);
321
+ } else if (command === 'doctor') {
322
+ data = await client.doctor();
323
+ } else if (command === 'whoami' || command === 'auth status') {
324
+ data = await client.get('me');
325
+ } else if (command === 'capabilities') {
326
+ data = await client.get('capabilities');
327
+ } else if (command === 'projects list' || command === 'memories list') {
328
+ data = await client.get(command === 'projects list' ? 'projects' : 'memories', {
329
+ after: options.after,
330
+ });
331
+ } else if (command === 'changes list' || command === 'changes watch') {
332
+ const input = {
333
+ scope: options.scope,
334
+ collection: options.collection,
335
+ cursor: options.cursor,
336
+ };
337
+ data =
338
+ command === 'changes list'
339
+ ? await readChanges(client, input)
340
+ : await watchChanges(client, {
341
+ ...input,
342
+ ...(options['wait-seconds'] === undefined
343
+ ? {}
344
+ : { waitSeconds: Number(options['wait-seconds']) }),
345
+ });
346
+ } else if (command === 'media prepare') {
347
+ data = await new MediaJournal(client).prepare({
348
+ scope: options.scope,
349
+ file: options.file,
350
+ contentType: options['content-type'],
351
+ epoch: options.epoch,
352
+ });
353
+ } else if (command === 'media upload') {
354
+ data = await new MediaJournal(client).upload(options.operation);
355
+ } else if (command === 'media action') {
356
+ data = await mediaAction(client, {
357
+ scope: options.scope,
358
+ ownerId: options.owner,
359
+ id: options.id,
360
+ action: options.action,
361
+ });
362
+ } else if (command === 'media download') {
363
+ data = await downloadMedia(client, {
364
+ scope: options.scope,
365
+ ownerId: options.owner,
366
+ id: options.id,
367
+ file: options.file,
368
+ });
369
+ } else if (['memory search', 'graph explore', 'memory resolve'].includes(command)) {
370
+ const input = Object.fromEntries(
371
+ commands[command].options
372
+ .filter((key) => options[key] !== undefined)
373
+ .map((key) => [key, options[key]]),
374
+ );
375
+ if (command === 'memory search') {
376
+ data = await searchMemory(client, input);
377
+ } else if (command === 'graph explore') {
378
+ data = await exploreGraph(client, {
379
+ ...input,
380
+ ...(input.depth === undefined ? {} : { depth: Number(input.depth) }),
381
+ });
382
+ } else {
383
+ data = await resolveMemory(client, {
384
+ scope: input.scope,
385
+ pageId: input.page,
386
+ ...(input.block === undefined ? {} : { blockId: input.block }),
387
+ });
388
+ }
389
+ } else if (command === 'resources list' || command === 'questions answers') {
390
+ if (
391
+ !(command === 'questions answers'
392
+ ? options.scope?.startsWith('project:')
393
+ : // eslint-disable-next-line no-control-regex -- Reject control characters in untrusted input.
394
+ /^(project|memory):[^\u0000-\u001f]+$/.test(options.scope ?? '')) ||
395
+ !(command === 'resources list' ? options.collection : options.question) ||
396
+ (options.after && (!options.epoch || !options.checkpoint))
397
+ ) {
398
+ fail(
399
+ 'invalid_arguments',
400
+ 'Use --scope project:ID or memory:ID and --collection for resources or --question for answers; continuation needs --epoch and --checkpoint.',
401
+ );
402
+ }
403
+ data = await client.get(
404
+ command === 'resources list' ? 'resources' : 'answers',
405
+ Object.fromEntries(
406
+ commands[command].options
407
+ .filter((key) => options[key] !== undefined)
408
+ .map((key) => [key, options[key]]),
409
+ ),
410
+ );
121
411
  } else if (command === 'mcp') {
122
412
  const { runMcp } = await import('./mcp.mjs');
123
413
  const server = await runMcp(client);
124
- const close = () => { void server.close(); };
414
+ const close = () => {
415
+ void server.close();
416
+ };
125
417
  signal?.addEventListener('abort', close, { once: true });
126
- if (signal?.aborted) close();
418
+ if (signal?.aborted) {
419
+ close();
420
+ }
127
421
  return 0; // stdout belongs exclusively to JSON-RPC.
128
422
  }
129
- emit(data); return 0;
423
+ emit(data);
424
+ return 0;
130
425
  } catch (error) {
131
426
  const safe = safeError(error);
132
427
  stderr.write(JSON.stringify({ formatVersion: 1, ok: false, error: safe.toJSON() }) + '\n');