@pulsarinteractive/geniustasker 0.1.0-beta.1 → 0.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CONTRIBUTING.md +72 -0
  3. package/LICENSE +21 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +47 -370
  6. package/SECURITY.md +14 -0
  7. package/THIRD_PARTY_NOTICES.md +15 -0
  8. package/docs/ARCHITECTURE.md +144 -0
  9. package/docs/CHATGPT.md +86 -0
  10. package/docs/CLI.md +444 -0
  11. package/docs/DOCUMENTATION_SYNC.md +144 -0
  12. package/docs/INTEGRATIONS.md +62 -0
  13. package/docs/LIMITS.md +62 -0
  14. package/docs/MEMORY.md +38 -0
  15. package/docs/OPENAI.md +21 -0
  16. package/docs/OPENAI_PUBLICATION.md +22 -0
  17. package/docs/PLANNING.md +79 -0
  18. package/docs/PROJECT_FILTERS.md +77 -0
  19. package/docs/RELEASING.md +23 -0
  20. package/docs/TESTING.md +52 -0
  21. package/docs/WORKFLOWS.md +37 -0
  22. package/examples/codex.toml +3 -0
  23. package/examples/mcp.json +8 -0
  24. package/mcp.json +10 -0
  25. package/package.json +45 -9
  26. package/plugin.json +24 -0
  27. package/remote/tool-catalog.json +1374 -0
  28. package/remote/workflows.json +67 -0
  29. package/skills/geniustasker/SKILL.md +49 -116
  30. package/skills/geniustasker/agents/openai.yaml +4 -0
  31. package/skills/geniustasker/references/collaboration.md +70 -0
  32. package/skills/geniustasker/references/incremental-reads.md +16 -0
  33. package/skills/geniustasker/references/profiles.md +22 -0
  34. package/skills/geniustasker/references/project-reads.md +43 -0
  35. package/skills/geniustasker-documentation/SKILL.md +39 -0
  36. package/skills/geniustasker-documentation/agents/openai.yaml +4 -0
  37. package/skills/geniustasker-documentation/references/structure.md +29 -0
  38. package/skills/geniustasker-graph/SKILL.md +31 -0
  39. package/skills/geniustasker-graph/agents/openai.yaml +4 -0
  40. package/skills/geniustasker-media/SKILL.md +33 -0
  41. package/skills/geniustasker-media/agents/openai.yaml +4 -0
  42. package/skills/geniustasker-memory/SKILL.md +120 -0
  43. package/skills/geniustasker-memory/agents/openai.yaml +4 -0
  44. package/skills/geniustasker-memory/references/knowledge-workflows.md +57 -0
  45. package/skills/geniustasker-planning/SKILL.md +28 -0
  46. package/skills/geniustasker-planning/agents/openai.yaml +4 -0
  47. package/src/changes.mjs +104 -28
  48. package/src/cli.mjs +453 -77
  49. package/src/client.mjs +203 -34
  50. package/src/command-catalog.mjs +545 -263
  51. package/src/commands.mjs +215 -52
  52. package/src/documentation-graph.mjs +251 -0
  53. package/src/documentation-store.mjs +589 -0
  54. package/src/documentation-sync.mjs +517 -0
  55. package/src/documentation.mjs +136 -0
  56. package/src/errors.mjs +55 -12
  57. package/src/knowledge.mjs +109 -0
  58. package/src/login.mjs +190 -47
  59. package/src/mcp.mjs +310 -43
  60. package/src/media.mjs +307 -0
  61. package/src/presentation.d.mts +1 -0
  62. package/src/presentation.mjs +12 -0
  63. package/src/profiles.mjs +123 -32
  64. package/src/read-cache.mjs +111 -0
  65. package/src/store.mjs +101 -26
  66. package/src/transport.mjs +165 -32
  67. package/src/workflows.mjs +9 -0
package/src/cli.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ import { workflow } from './workflows.mjs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import {
4
+ syncDocumentation,
5
+ documentationStatus,
6
+ unlockDocumentation,
7
+ } from './documentation-sync.mjs';
1
8
  import open, { apps } from 'open';
2
9
  import catalog from './command-catalog.mjs';
3
10
  import { CredentialStore } from './store.mjs';
@@ -7,126 +14,495 @@ import { deviceLogin, browserLogin } from './login.mjs';
7
14
  import { CommandJournal, describeCommands, readCommandFile } from './commands.mjs';
8
15
  import { AgentProfiles } from './profiles.mjs';
9
16
  import { readChanges, watchChanges } from './changes.mjs';
17
+ import { readMemoryTree, readMemoryMarkdown } from './documentation.mjs';
18
+ import { searchMemory, exploreGraph, resolveMemory } from './knowledge.mjs';
19
+ import { MediaJournal, mediaAction, downloadMedia, compareMedia } from './media.mjs';
10
20
  import { fail, safeError, exitCode } from './errors.mjs';
11
21
 
12
22
  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: [] },
23
+ 'workflow show': { summary: 'Read a reviewed workflow guide; no network.', options: ['topic'] },
24
+ 'auth login': {
25
+ summary: 'Open browser sign-in and choose access. No tutorial or token to copy.',
26
+ options: ['flow', 'browser', 'no-browser', 'scope', 'interactive'],
27
+ },
28
+ 'auth logout': {
29
+ summary: 'Revoke this terminal session on the server, then erase local credentials.',
30
+ options: [],
31
+ },
15
32
  'auth status': { summary: 'Read current server identity and permission ceiling.', options: [] },
16
33
  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: [] },
34
+ capabilities: {
35
+ summary: 'Discover collections, fields, limits and write availability.',
36
+ options: [],
37
+ },
38
+ 'profiles list': {
39
+ summary: 'List selectable profiles in this connection within the current grant.',
40
+ options: [],
41
+ },
42
+ 'profiles create': {
43
+ summary:
44
+ 'Create a narrower profile from JSON with a UUID chosen once; never switches automatically.',
45
+ options: ['file'],
46
+ },
47
+ 'profiles restrict': {
48
+ summary:
49
+ 'Rename or narrow the current profile from JSON with its expectedRevision. Cannot broaden access.',
50
+ options: ['file'],
51
+ },
52
+ 'profiles select': {
53
+ summary:
54
+ 'Rotate this context into a selectable profile. Cannot return to broader access; use separate contexts for independent terminals.',
55
+ options: ['profile'],
56
+ },
57
+ 'memories list': {
58
+ summary: 'List independent account memories; follow nextAfter even for an empty page.',
59
+ options: ['after'],
60
+ },
61
+ 'media compare': {
62
+ summary: 'Compare a local file with an existing resource before uploading a replacement.',
63
+ options: ['scope', 'id', 'file'],
64
+ },
65
+ 'media prepare': {
66
+ summary: 'Prepare a local private attachment; retain its operation ID.',
67
+ options: ['scope', 'file', 'content-type', 'epoch'],
68
+ },
69
+ 'media upload': {
70
+ summary: 'Upload or safely retry the prepared operation.',
71
+ options: ['operation'],
72
+ },
73
+ 'media action': {
74
+ summary: 'Inspect, publish, cancel or remove a private attachment.',
75
+ options: ['scope', 'owner', 'id', 'action'],
76
+ },
77
+ 'media download': {
78
+ summary: 'Download an authorized attachment to a new local file.',
79
+ options: ['scope', 'owner', 'id', 'file'],
80
+ },
81
+ 'docs sync': {
82
+ summary: 'Refresh selected Memory/Graph sources in a managed local documentation directory.',
83
+ options: ['directory', 'file', 'max-files', 'max-bytes'],
84
+ },
85
+ 'docs status': {
86
+ summary: 'Inspect the local snapshot date and selections without a network request.',
87
+ options: ['directory'],
88
+ },
89
+ 'docs unlock': {
90
+ summary: 'Remove a sync lock only when its process on this host has stopped.',
91
+ options: ['directory'],
92
+ },
93
+ 'memory tree': {
94
+ summary: 'Read one folder and descendant versions; unchanged validators avoid page bodies.',
95
+ options: ['scope', 'parent', 'after', 'epoch', 'checkpoint', 'if-none-match'],
96
+ },
97
+ 'memory markdown': {
98
+ summary: 'Fetch one portable Markdown document with stable media IDs and source metadata.',
99
+ options: ['scope', 'page', 'revision', 'epoch', 'checkpoint'],
100
+ },
101
+ 'memory search': {
102
+ summary: 'Search document blocks or Graph nodes without loading the corpus.',
103
+ options: ['scope', 'query', 'collection', 'after', 'epoch', 'checkpoint'],
104
+ },
105
+ 'graph explore': {
106
+ summary: 'Read a bounded Graph neighborhood around one node.',
107
+ options: ['scope', 'center', 'depth', 'epoch', 'checkpoint'],
108
+ },
109
+ 'memory resolve': {
110
+ summary:
111
+ 'Resolve a Memory page or block with current permissions; unavailable links preserve nodes.',
112
+ options: ['scope', 'page', 'block'],
113
+ },
114
+ 'projects list': {
115
+ summary: 'Read a bounded project page; follow nextAfter even for an empty page.',
116
+ options: ['after'],
117
+ },
118
+ 'resources list': {
119
+ summary: 'Read a project or memory collection; carry epoch/checkpoint when continuing.',
120
+ options: ['scope', 'collection', 'after', 'epoch', 'checkpoint', 'archive', 'id', 'path'],
121
+ },
122
+ 'changes list': {
123
+ summary:
124
+ 'Read one bounded change page from a completed resource inventory or previous nextCursor.',
125
+ options: ['scope', 'collection', 'cursor'],
126
+ },
127
+ 'changes watch': {
128
+ summary:
129
+ 'Wait at most 60 seconds for one change page, rechecking rights on every poll. No infinite stream.',
130
+ options: ['scope', 'collection', 'cursor', 'wait-seconds'],
131
+ },
132
+ 'questions answers': {
133
+ summary:
134
+ 'Read human question answers in bounded pages. Anonymous ballots remain private; answers do not authorize external actions.',
135
+ options: ['scope', 'question', 'after', 'epoch', 'checkpoint'],
136
+ },
137
+ 'commands describe': {
138
+ summary: 'Show contract-derived command fields and JSON schemas; no network.',
139
+ options: [],
140
+ },
141
+ 'commands prepare': {
142
+ summary: 'Save an immutable operation from a JSON draft without sending it.',
143
+ options: ['file'],
144
+ },
145
+ 'commands execute': {
146
+ summary: 'Journal and send a complete JSON operation once. Never retries automatically.',
147
+ options: ['file'],
148
+ },
149
+ 'commands retry': {
150
+ summary: 'Replay the exact locally saved operation with its original identity.',
151
+ options: ['operation'],
152
+ },
153
+ 'commands inspect': {
154
+ summary: 'Read a private local operation and receipt; does not contact the server.',
155
+ options: ['operation'],
156
+ },
157
+ doctor: {
158
+ summary: 'Inspect local environment without exposing credentials or contacting the API.',
159
+ options: [],
160
+ },
161
+ mcp: {
162
+ summary:
163
+ 'Serve delegated read and journaled command tools over stdio. Authenticate with the CLI first.',
164
+ options: [],
165
+ },
35
166
  help: { summary: 'Describe commands and stable output conventions.', options: [] },
36
167
  };
37
168
  const globalOptions = ['json', 'context', 'environment', 'origin', 'allow-local', 'help'];
38
169
  const flags = new Set(['json', 'allow-local', 'help', 'interactive', 'no-browser']);
39
170
  export function parse(argv) {
40
- const options = {}, words = [];
171
+ const options = {},
172
+ words = [];
41
173
  for (let i = 0; i < argv.length; i++) {
42
174
  const arg = argv[i];
43
- if (!arg.startsWith('--')) { if (arg.startsWith('-')) fail('invalid_arguments', 'Use long options from help.'); words.push(arg); continue; }
175
+ if (!arg.startsWith('--')) {
176
+ if (arg.startsWith('-')) {
177
+ fail('invalid_arguments', 'Use long options from help.');
178
+ }
179
+ words.push(arg);
180
+ continue;
181
+ }
44
182
  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 {
183
+ if (!key || key.includes('=') || Object.hasOwn(options, key)) {
184
+ fail('invalid_arguments', 'Options must be unique and use --name value syntax.');
185
+ }
186
+ if (flags.has(key)) {
187
+ options[key] = true;
188
+ } else {
48
189
  const value = argv[++i];
49
- if (!value || value.startsWith('--') || value.length > 4096) fail('invalid_arguments', 'An option value is missing or too long.');
190
+ if (!value || value.startsWith('--') || value.length > 4096) {
191
+ fail('invalid_arguments', 'An option value is missing or too long.');
192
+ }
50
193
  options[key] = value;
51
194
  }
52
195
  }
53
196
  const command = words.join(' ') || 'help';
54
197
  const definition = commands[command];
55
- if (!definition || Object.keys(options).some(key => !globalOptions.includes(key) && !definition.options.includes(key)))
198
+ if (
199
+ !definition ||
200
+ Object.keys(options).some(
201
+ (key) => !globalOptions.includes(key) && !definition.options.includes(key),
202
+ )
203
+ ) {
56
204
  fail('invalid_arguments', 'Unknown command or option. Run help.');
205
+ }
57
206
  return { command, options };
58
207
  }
59
208
  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.' };
209
+ return {
210
+ formatVersion: 1,
211
+ command,
212
+ commands: command === 'help' ? commands : { [command]: commands[command] },
213
+ globalOptions: {
214
+ context: 'default; use separate names for separate accounts or agent sessions',
215
+ environment: 'development (beta default) | production',
216
+ origin: 'explicit loopback origin with --allow-local, for local tests only',
217
+ json: 'one versioned JSON result on stdout; progress/errors use stderr',
218
+ },
219
+ loginOptions: {
220
+ flow: 'browser (default, PKCE loopback) | device (SSH/headless)',
221
+ browser: 'default (automatic) | chrome; --no-browser prints the link without opening it',
222
+ scope:
223
+ 'space-separated permission ceiling; browser defaults to read-only and requires explicit write selection',
224
+ interactive:
225
+ 'required when starting login without a terminal; never use to bypass human consent',
226
+ },
227
+ exitCodes: {
228
+ 0: 'success',
229
+ 1: 'failure',
230
+ 2: 'usage',
231
+ 3: 'authentication',
232
+ 4: 'permission',
233
+ 5: 'conflict',
234
+ 6: 'rate limit',
235
+ 7: 'transient failure',
236
+ },
237
+ examples: [
238
+ 'geniustasker auth login --environment development --context work --browser chrome',
239
+ 'geniustasker projects list --environment development --context work --json',
240
+ 'geniustasker resources list --scope project:ID --collection tasks --json',
241
+ ],
242
+ privacy:
243
+ '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.',
244
+ };
66
245
  }
67
246
  async function openConsent(url, browser) {
68
247
  await open(url, { wait: false, ...(browser === 'chrome' ? { app: { name: apps.chrome } } : {}) });
69
248
  }
70
- export async function main(argv, { stdout = process.stdout, stderr = process.stderr, isTTY = process.stdin.isTTY, directory, fetchImpl, signal, openBrowserImpl = openConsent } = {}) {
249
+ export async function main(
250
+ argv,
251
+ {
252
+ stdout = process.stdout,
253
+ stderr = process.stderr,
254
+ isTTY = process.stdin.isTTY,
255
+ directory,
256
+ fetchImpl,
257
+ signal,
258
+ openBrowserImpl = openConsent,
259
+ } = {},
260
+ ) {
71
261
  try {
72
262
  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']);
263
+ const emit = (data) =>
264
+ stdout.write(
265
+ JSON.stringify({ formatVersion: 1, ok: true, data }, null, options.json ? 0 : 2) + '\n',
266
+ );
267
+ if (command === 'help' || options.help) {
268
+ emit(help(command));
269
+ return 0;
270
+ }
271
+ if (options.environment && !Object.hasOwn(ORIGINS, options.environment)) {
272
+ fail('invalid_arguments', 'Environment must be production or development.');
273
+ }
274
+ if (options.origin && options.environment) {
275
+ fail('invalid_arguments', 'Choose --environment or --origin, not both.');
276
+ }
277
+ const origin = originOf(
278
+ options.origin ?? ORIGINS[options.environment ?? 'development'],
279
+ options['allow-local'],
280
+ );
78
281
  const store = new CredentialStore({ context: options.context, directory });
79
282
  const client = new AgentClient(new Transport(origin, { fetchImpl, signal }), store);
80
283
  let data;
81
284
  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');
285
+ if (!isTTY && !options.interactive) {
286
+ fail('invalid_arguments', 'Login needs an interactive human.', {
287
+ remediation:
288
+ 'Run auth login in a terminal. Automated clients must use an existing connection.',
289
+ });
290
+ }
291
+ if (
292
+ (options.flow && !['device', 'browser'].includes(options.flow)) ||
293
+ (options.browser && !['chrome', 'default'].includes(options.browser)) ||
294
+ (options.browser && options['no-browser'])
295
+ ) {
296
+ fail(
297
+ 'invalid_arguments',
298
+ 'Use --flow browser|device, with --browser default|chrome or --no-browser.',
299
+ );
300
+ }
301
+ const notify = (event) => {
302
+ if (options.json) {
303
+ stderr.write(JSON.stringify({ formatVersion: 1, ...event }) + '\n');
304
+ } else if (event.event === 'consent_required') {
305
+ stderr.write(
306
+ `\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`,
307
+ );
308
+ } else if (event.event === 'browser_unavailable') {
309
+ stderr.write('Could not open the browser. Use the link above; login is still waiting.\n');
310
+ }
88
311
  };
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 });
312
+ data = await (options.flow === 'device' ? deviceLogin : browserLogin)(client, {
313
+ scope: options.scope ?? catalog.grants.permissions.join(' '),
314
+ openBrowser: options['no-browser']
315
+ ? undefined
316
+ : async (url) => {
317
+ try {
318
+ await openBrowserImpl(url, options.browser);
319
+ } catch {
320
+ notify({
321
+ event: 'browser_unavailable',
322
+ message: 'Open the printed consent URL in your browser. Login is still waiting.',
323
+ });
324
+ }
325
+ },
326
+ notify,
327
+ });
94
328
  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`);
329
+ stdout.write(
330
+ `\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`,
331
+ );
96
332
  return 0;
97
333
  }
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]])));
334
+ } else if (command === 'auth logout') {
335
+ data = await client.logout();
336
+ } else if (command === 'profiles list') {
337
+ data = await new AgentProfiles(client).list();
338
+ } else if (command === 'profiles create') {
339
+ data = await new AgentProfiles(client).create(await readCommandFile(options.file));
340
+ } else if (command === 'profiles restrict') {
341
+ data = await new AgentProfiles(client).restrict(await readCommandFile(options.file));
342
+ } else if (command === 'profiles select') {
343
+ data = await new AgentProfiles(client).select({ profileId: options.profile });
344
+ } else if (command === 'workflow show') {
345
+ data = workflow({ topic: options.topic });
346
+ } else if (command === 'commands describe') {
347
+ data = describeCommands();
348
+ } else if (command === 'commands prepare') {
349
+ data = await new CommandJournal(client).prepare(await readCommandFile(options.file));
350
+ } else if (command === 'commands execute') {
351
+ data = await new CommandJournal(client).execute(await readCommandFile(options.file));
352
+ } else if (command === 'commands retry') {
353
+ data = await new CommandJournal(client).retry(options.operation);
354
+ } else if (command === 'commands inspect') {
355
+ data = await new CommandJournal(client).inspect(options.operation);
356
+ } else if (command === 'doctor') {
357
+ data = await client.doctor();
358
+ } else if (command === 'whoami' || command === 'auth status') {
359
+ data = await client.get('me');
360
+ } else if (command === 'capabilities') {
361
+ data = await client.get('capabilities');
362
+ } else if (command === 'projects list' || command === 'memories list') {
363
+ data = await client.get(command === 'projects list' ? 'projects' : 'memories', {
364
+ after: options.after,
365
+ });
366
+ } else if (command === 'changes list' || command === 'changes watch') {
367
+ const input = {
368
+ scope: options.scope,
369
+ collection: options.collection,
370
+ cursor: options.cursor,
371
+ };
372
+ data =
373
+ command === 'changes list'
374
+ ? await readChanges(client, input)
375
+ : await watchChanges(client, {
376
+ ...input,
377
+ ...(options['wait-seconds'] === undefined
378
+ ? {}
379
+ : { waitSeconds: Number(options['wait-seconds']) }),
380
+ });
381
+ } else if (command === 'docs sync') {
382
+ let configuration;
383
+ if (options.file) {
384
+ const raw = await readFile(options.file, 'utf8');
385
+ if (Buffer.byteLength(raw) > 32768) {
386
+ fail('invalid_arguments', 'The source configuration is too large.');
387
+ }
388
+ try {
389
+ configuration = JSON.parse(raw);
390
+ } catch {
391
+ fail('invalid_arguments', 'The source configuration must be JSON.');
392
+ }
393
+ }
394
+ data = await syncDocumentation(client, {
395
+ directory: options.directory,
396
+ configuration,
397
+ maxFiles: options['max-files'] === undefined ? undefined : Number(options['max-files']),
398
+ maxBytes: options['max-bytes'] === undefined ? undefined : Number(options['max-bytes']),
399
+ });
400
+ } else if (command === 'docs status' || command === 'docs unlock') {
401
+ data = await (command === 'docs status' ? documentationStatus : unlockDocumentation)({
402
+ directory: options.directory,
403
+ });
404
+ } else if (command === 'memory tree') {
405
+ data = await readMemoryTree(client, {
406
+ scope: options.scope,
407
+ parentId: options.parent,
408
+ after: options.after,
409
+ epoch: options.epoch,
410
+ checkpoint: options.checkpoint,
411
+ ifNoneMatch: options['if-none-match'],
412
+ });
413
+ } else if (command === 'memory markdown') {
414
+ data = await readMemoryMarkdown(client, {
415
+ scope: options.scope,
416
+ pageId: options.page,
417
+ revision: options.revision,
418
+ epoch: options.epoch,
419
+ checkpoint: options.checkpoint,
420
+ });
421
+ } else if (command === 'media compare') {
422
+ data = await compareMedia(client, {
423
+ scope: options.scope,
424
+ id: options.id,
425
+ file: options.file,
426
+ });
427
+ } else if (command === 'media prepare') {
428
+ data = await new MediaJournal(client).prepare({
429
+ scope: options.scope,
430
+ file: options.file,
431
+ contentType: options['content-type'],
432
+ epoch: options.epoch,
433
+ });
434
+ } else if (command === 'media upload') {
435
+ data = await new MediaJournal(client).upload(options.operation);
436
+ } else if (command === 'media action') {
437
+ data = await mediaAction(client, {
438
+ scope: options.scope,
439
+ ownerId: options.owner,
440
+ id: options.id,
441
+ action: options.action,
442
+ });
443
+ } else if (command === 'media download') {
444
+ data = await downloadMedia(client, {
445
+ scope: options.scope,
446
+ ownerId: options.owner,
447
+ id: options.id,
448
+ file: options.file,
449
+ });
450
+ } else if (['memory search', 'graph explore', 'memory resolve'].includes(command)) {
451
+ const input = Object.fromEntries(
452
+ commands[command].options
453
+ .filter((key) => options[key] !== undefined)
454
+ .map((key) => [key, options[key]]),
455
+ );
456
+ if (command === 'memory search') {
457
+ data = await searchMemory(client, input);
458
+ } else if (command === 'graph explore') {
459
+ data = await exploreGraph(client, {
460
+ ...input,
461
+ ...(input.depth === undefined ? {} : { depth: Number(input.depth) }),
462
+ });
463
+ } else {
464
+ data = await resolveMemory(client, {
465
+ scope: input.scope,
466
+ pageId: input.page,
467
+ ...(input.block === undefined ? {} : { blockId: input.block }),
468
+ });
469
+ }
470
+ } else if (command === 'resources list' || command === 'questions answers') {
471
+ if (
472
+ !(command === 'questions answers'
473
+ ? options.scope?.startsWith('project:')
474
+ : // eslint-disable-next-line no-control-regex -- Reject control characters in untrusted input.
475
+ /^(project|memory):[^\u0000-\u001f]+$/.test(options.scope ?? '')) ||
476
+ !(command === 'resources list' ? options.collection : options.question) ||
477
+ (options.after && (!options.epoch || !options.checkpoint))
478
+ ) {
479
+ fail(
480
+ 'invalid_arguments',
481
+ 'Use --scope project:ID or memory:ID and --collection for resources or --question for answers; continuation needs --epoch and --checkpoint.',
482
+ );
483
+ }
484
+ data = await client.get(
485
+ command === 'resources list' ? 'resources' : 'answers',
486
+ Object.fromEntries(
487
+ commands[command].options
488
+ .filter((key) => options[key] !== undefined)
489
+ .map((key) => [key, options[key]]),
490
+ ),
491
+ );
121
492
  } else if (command === 'mcp') {
122
493
  const { runMcp } = await import('./mcp.mjs');
123
494
  const server = await runMcp(client);
124
- const close = () => { void server.close(); };
495
+ const close = () => {
496
+ void server.close();
497
+ };
125
498
  signal?.addEventListener('abort', close, { once: true });
126
- if (signal?.aborted) close();
499
+ if (signal?.aborted) {
500
+ close();
501
+ }
127
502
  return 0; // stdout belongs exclusively to JSON-RPC.
128
503
  }
129
- emit(data); return 0;
504
+ emit(data);
505
+ return 0;
130
506
  } catch (error) {
131
507
  const safe = safeError(error);
132
508
  stderr.write(JSON.stringify({ formatVersion: 1, ok: false, error: safe.toJSON() }) + '\n');