@lumoai/cli 1.4.0 → 1.5.1
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/assets/skill.md +228 -17
- package/dist/cli/src/commands/auth-login.js +4 -3
- package/dist/cli/src/commands/auth-logout.js +2 -1
- package/dist/cli/src/commands/doc-bind.js +3 -3
- package/dist/cli/src/commands/doc-create.js +5 -5
- package/dist/cli/src/commands/doc-delete.js +4 -4
- package/dist/cli/src/commands/doc-import-gdoc.js +82 -0
- package/dist/cli/src/commands/doc-list.js +7 -7
- package/dist/cli/src/commands/doc-move.js +8 -8
- package/dist/cli/src/commands/doc-share-list.js +11 -8
- package/dist/cli/src/commands/doc-share.js +7 -5
- package/dist/cli/src/commands/doc-show.js +6 -6
- package/dist/cli/src/commands/doc-sync.js +44 -0
- package/dist/cli/src/commands/doc-unbind.js +4 -4
- package/dist/cli/src/commands/doc-unshare.js +9 -7
- package/dist/cli/src/commands/doc-update.js +5 -5
- package/dist/cli/src/commands/hook.js +2 -2
- package/dist/cli/src/commands/memory-project-add.js +86 -0
- package/dist/cli/src/commands/memory-project-list.js +58 -0
- package/dist/cli/src/commands/memory-promote.js +52 -0
- package/dist/cli/src/commands/memory-rm.js +42 -0
- package/dist/cli/src/commands/memory-task-add.js +99 -0
- package/dist/cli/src/commands/memory-task-list.js +61 -0
- package/dist/cli/src/commands/milestone-create.js +4 -4
- package/dist/cli/src/commands/milestone-delete.js +5 -5
- package/dist/cli/src/commands/milestone-list.js +3 -3
- package/dist/cli/src/commands/milestone-show.js +5 -5
- package/dist/cli/src/commands/milestone-update.js +6 -5
- package/dist/cli/src/commands/project-list.js +3 -3
- package/dist/cli/src/commands/session-attach.js +5 -5
- package/dist/cli/src/commands/session-detach.js +3 -3
- package/dist/cli/src/commands/session-status.js +3 -3
- package/dist/cli/src/commands/setup.js +33 -7
- package/dist/cli/src/commands/sprint-add.js +3 -3
- package/dist/cli/src/commands/sprint-close.js +5 -5
- package/dist/cli/src/commands/sprint-create.js +4 -4
- package/dist/cli/src/commands/sprint-delete.js +5 -5
- package/dist/cli/src/commands/sprint-list.js +3 -3
- package/dist/cli/src/commands/sprint-remove.js +3 -3
- package/dist/cli/src/commands/sprint-show.js +4 -4
- package/dist/cli/src/commands/sprint-start.js +4 -4
- package/dist/cli/src/commands/sprint-summary.js +7 -7
- package/dist/cli/src/commands/sprint-update.js +6 -5
- package/dist/cli/src/commands/task-artifact-add.js +35 -6
- package/dist/cli/src/commands/task-artifact-list.js +5 -3
- package/dist/cli/src/commands/task-artifact-rm.js +4 -4
- package/dist/cli/src/commands/task-artifact-show.js +9 -7
- package/dist/cli/src/commands/task-artifact-update.js +15 -6
- package/dist/cli/src/commands/task-comment-list.js +111 -0
- package/dist/cli/src/commands/task-comment.js +3 -3
- package/dist/cli/src/commands/task-context.js +24 -12
- package/dist/cli/src/commands/task-create.js +7 -7
- package/dist/cli/src/commands/task-figma-add.js +3 -2
- package/dist/cli/src/commands/task-figma-context.js +61 -0
- package/dist/cli/src/commands/task-figma-list.js +3 -2
- package/dist/cli/src/commands/task-figma-refresh.js +4 -3
- package/dist/cli/src/commands/task-figma-rm.js +3 -2
- package/dist/cli/src/commands/task-list.js +1 -2
- package/dist/cli/src/commands/task-pr-show.js +66 -0
- package/dist/cli/src/commands/task-show.js +8 -7
- package/dist/cli/src/commands/task-slack-show.js +59 -0
- package/dist/cli/src/commands/task-update.js +7 -7
- package/dist/cli/src/commands/task-web-show.js +64 -0
- package/dist/cli/src/commands/whoami.js +4 -3
- package/dist/cli/src/index.js +239 -102
- package/dist/cli/src/lib/agent.js +58 -0
- package/dist/cli/src/lib/api.js +81 -1
- package/dist/cli/src/lib/config.js +2 -1
- package/dist/cli/src/lib/doc-input.js +12 -1
- package/dist/cli/src/lib/figma-api.js +1 -1
- package/dist/cli/src/lib/format.js +3 -2
- package/dist/cli/src/lib/hook-runner.js +26 -10
- package/dist/cli/src/lib/hooks-template.js +52 -7
- package/dist/cli/src/lib/memory-content.js +88 -0
- package/dist/cli/src/lib/path-guard.js +125 -0
- package/dist/cli/src/lib/resolve-bound-task.js +31 -0
- package/dist/cli/src/lib/resolve-doc-id.js +2 -1
- package/dist/cli/src/lib/resolve-member.js +2 -1
- package/dist/cli/src/lib/resolve-project.js +24 -0
- package/dist/cli/src/lib/sanitize.js +17 -0
- package/dist/cli/src/lib/tag-resolver.js +2 -1
- package/dist/cli/src/lib/update-check.js +2 -2
- package/package.json +1 -1
package/dist/cli/src/index.js
CHANGED
|
@@ -55,11 +55,22 @@ const task_figma_add_1 = require("./commands/task-figma-add");
|
|
|
55
55
|
const task_figma_list_1 = require("./commands/task-figma-list");
|
|
56
56
|
const task_figma_rm_1 = require("./commands/task-figma-rm");
|
|
57
57
|
const task_figma_refresh_1 = require("./commands/task-figma-refresh");
|
|
58
|
+
const memory_task_list_1 = require("./commands/memory-task-list");
|
|
59
|
+
const memory_task_add_1 = require("./commands/memory-task-add");
|
|
60
|
+
const memory_project_list_1 = require("./commands/memory-project-list");
|
|
61
|
+
const memory_project_add_1 = require("./commands/memory-project-add");
|
|
62
|
+
const memory_promote_1 = require("./commands/memory-promote");
|
|
63
|
+
const memory_rm_1 = require("./commands/memory-rm");
|
|
58
64
|
const task_artifact_add_1 = require("./commands/task-artifact-add");
|
|
59
65
|
const task_artifact_list_1 = require("./commands/task-artifact-list");
|
|
60
66
|
const task_artifact_show_1 = require("./commands/task-artifact-show");
|
|
61
67
|
const task_artifact_rm_1 = require("./commands/task-artifact-rm");
|
|
62
68
|
const task_artifact_update_1 = require("./commands/task-artifact-update");
|
|
69
|
+
const task_slack_show_1 = require("./commands/task-slack-show");
|
|
70
|
+
const task_web_show_1 = require("./commands/task-web-show");
|
|
71
|
+
const task_figma_context_1 = require("./commands/task-figma-context");
|
|
72
|
+
const task_comment_list_1 = require("./commands/task-comment-list");
|
|
73
|
+
const task_pr_show_1 = require("./commands/task-pr-show");
|
|
63
74
|
const project_list_1 = require("./commands/project-list");
|
|
64
75
|
const milestone_list_1 = require("./commands/milestone-list");
|
|
65
76
|
const milestone_create_1 = require("./commands/milestone-create");
|
|
@@ -77,6 +88,8 @@ const sprint_summary_1 = require("./commands/sprint-summary");
|
|
|
77
88
|
const sprint_add_1 = require("./commands/sprint-add");
|
|
78
89
|
const sprint_remove_1 = require("./commands/sprint-remove");
|
|
79
90
|
const doc_create_1 = require("./commands/doc-create");
|
|
91
|
+
const doc_import_gdoc_1 = require("./commands/doc-import-gdoc");
|
|
92
|
+
const doc_sync_1 = require("./commands/doc-sync");
|
|
80
93
|
const doc_update_1 = require("./commands/doc-update");
|
|
81
94
|
const doc_show_1 = require("./commands/doc-show");
|
|
82
95
|
const doc_list_1 = require("./commands/doc-list");
|
|
@@ -90,6 +103,7 @@ const doc_move_1 = require("./commands/doc-move");
|
|
|
90
103
|
const update_1 = require("./commands/update");
|
|
91
104
|
const setup_1 = require("./commands/setup");
|
|
92
105
|
const update_check_1 = require("./lib/update-check");
|
|
106
|
+
const sanitize_1 = require("./lib/sanitize");
|
|
93
107
|
// Resolve package.json relative to __dirname so this works regardless of how
|
|
94
108
|
// deep the compiled output ends up (flat dist/ or nested dist/cli/src/).
|
|
95
109
|
const pkg = require(path.resolve(__dirname, '../../..', 'package.json'));
|
|
@@ -132,7 +146,7 @@ function wrap(fn) {
|
|
|
132
146
|
}
|
|
133
147
|
catch (err) {
|
|
134
148
|
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
-
console.error(`Error: ${msg}`);
|
|
149
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
|
|
136
150
|
process.exit(1);
|
|
137
151
|
}
|
|
138
152
|
};
|
|
@@ -164,6 +178,7 @@ program
|
|
|
164
178
|
.option('--user', 'Install into ~/.claude (applies across all projects for this user)')
|
|
165
179
|
.option('--project', 'Install into ./.claude (applies to the current project only)')
|
|
166
180
|
.option('--force', 'Overwrite an existing SKILL.md when its contents differ from the bundled version')
|
|
181
|
+
.option('--agent <token>', 'Coding agent these hooks run under (claude-code, codex, cursor, gemini-cli, github-copilot, windsurf). Baked into every hook command. Defaults to claude-code.')
|
|
167
182
|
.action(wrap(options => (0, setup_1.setup)(options)));
|
|
168
183
|
const session = program
|
|
169
184
|
.command('session')
|
|
@@ -237,6 +252,66 @@ taskFigma
|
|
|
237
252
|
.command('refresh <task>')
|
|
238
253
|
.description('Re-fetch Figma metadata + thumbnail for every link on this task. Per-link failures isolated.')
|
|
239
254
|
.action(wrap((taskId) => (0, task_figma_refresh_1.taskFigmaRefresh)({ identifier: taskId })));
|
|
255
|
+
// Tier-2 retrieval commands (LUM-122). `task context` prints the matching
|
|
256
|
+
// command at the end of each cheap inline card; the agent runs it to pull the
|
|
257
|
+
// full content on demand.
|
|
258
|
+
const taskSlack = task.command('slack').description('Inspect Slack context');
|
|
259
|
+
taskSlack
|
|
260
|
+
.command('show <identifier> <contextId>')
|
|
261
|
+
.description('Show the full stored Slack thread snapshot')
|
|
262
|
+
.action(wrap((id, ctx) => (0, task_slack_show_1.taskSlackShow)(id, ctx)));
|
|
263
|
+
const taskWeb = task.command('web').description('Inspect web link content');
|
|
264
|
+
taskWeb
|
|
265
|
+
.command('show <identifier> <linkId>')
|
|
266
|
+
.description('Show the fetched web link body as plain text')
|
|
267
|
+
.action(wrap((id, link) => (0, task_web_show_1.taskWebShow)(id, link)));
|
|
268
|
+
// taskFigma already exists above → attach the context leaf to it.
|
|
269
|
+
taskFigma
|
|
270
|
+
.command('context <identifier> <linkId>')
|
|
271
|
+
.description('Show the cached Figma design context (metadata)')
|
|
272
|
+
.action(wrap((id, link) => (0, task_figma_context_1.taskFigmaContext)(id, link)));
|
|
273
|
+
// Plural `comments` parent: `task comment <id> <body>` already exists for
|
|
274
|
+
// adding a comment, and commander disallows a duplicate name.
|
|
275
|
+
const taskComments = task
|
|
276
|
+
.command('comments')
|
|
277
|
+
.description('Inspect the full task comment thread');
|
|
278
|
+
taskComments
|
|
279
|
+
.command('list <identifier>')
|
|
280
|
+
.description('List the full task comment thread')
|
|
281
|
+
.action(wrap(id => (0, task_comment_list_1.taskCommentList)(id)));
|
|
282
|
+
const taskPr = task.command('pr').description('Inspect linked PRs');
|
|
283
|
+
taskPr
|
|
284
|
+
.command('show <identifier> <number>')
|
|
285
|
+
.description('Show the synced PR record (diff/review comments when available)')
|
|
286
|
+
.action(wrap((id, num) => (0, task_pr_show_1.taskPrShow)(id, num)));
|
|
287
|
+
const taskMemory = task
|
|
288
|
+
.command('memory')
|
|
289
|
+
.description('View and record memories scoped to a task');
|
|
290
|
+
taskMemory
|
|
291
|
+
.command('list [task]')
|
|
292
|
+
.description("List a task's memories. <task> defaults to the session-bound task.")
|
|
293
|
+
.option('--category <cat>', 'Filter by trap|decision|convention|procedural')
|
|
294
|
+
.option('-n, --limit <count>', 'Cap output to the first N rows')
|
|
295
|
+
.action(wrap((taskArg, opts) => (0, memory_task_list_1.memoryTaskList)(taskArg, opts)));
|
|
296
|
+
taskMemory
|
|
297
|
+
.command('add [task]')
|
|
298
|
+
.description('Record a memory on a task (<task> defaults to the bound session). ' +
|
|
299
|
+
'Record only knowledge invisible in the codebase (the why, a runtime gotcha, a non-obvious failure, a non-trivial workflow); skip routine work. ' +
|
|
300
|
+
'Pick --category then its fields.')
|
|
301
|
+
.requiredOption('--category <cat>', 'trap | decision | convention | procedural')
|
|
302
|
+
.option('--trigger <text>', 'trap/procedural: the situation that triggers it')
|
|
303
|
+
.option('--outcome <text>', 'trap: what goes wrong')
|
|
304
|
+
.option('--workaround <text>', 'trap: optional fix')
|
|
305
|
+
.option('--what <text>', 'decision: the decision')
|
|
306
|
+
.option('--why <text>', 'decision: rationale')
|
|
307
|
+
.option('--alternatives <text>', 'decision: optional alternatives considered')
|
|
308
|
+
.option('--implications <text>', 'decision: optional implications')
|
|
309
|
+
.option('--rule <text>', 'convention: the rule')
|
|
310
|
+
.option('--applies <text>', 'convention: where the rule applies')
|
|
311
|
+
.option('--workflow <text>', 'procedural: the workflow name')
|
|
312
|
+
.option('--step <text>', 'procedural: a step (repeatable)', collect, [])
|
|
313
|
+
.option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
|
|
314
|
+
.action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
|
|
240
315
|
const taskArtifact = task
|
|
241
316
|
.command('artifact')
|
|
242
317
|
.description('Record spec-engineering artifacts (spec / plan / design …) on a task');
|
|
@@ -247,6 +322,7 @@ taskArtifact
|
|
|
247
322
|
.requiredOption('--title <title>', 'Artifact title')
|
|
248
323
|
.requiredOption('--file <path>', 'File whose contents become the artifact body')
|
|
249
324
|
.requiredOption('--source <source>', 'Spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (opaque; quote names with spaces)')
|
|
325
|
+
.requiredOption('--agent <agent>', 'Coding tool that produced the artifact: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
|
|
250
326
|
.action(wrap((taskId, options) => (0, task_artifact_add_1.taskArtifactAdd)(taskId, options)));
|
|
251
327
|
taskArtifact
|
|
252
328
|
.command('update <task> <artifact-id>')
|
|
@@ -254,6 +330,7 @@ taskArtifact
|
|
|
254
330
|
.option('--kind <kind>', 'New artifact kind (opaque)')
|
|
255
331
|
.option('--title <title>', 'New artifact title')
|
|
256
332
|
.option('--source <source>', 'New spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (quote names with spaces)')
|
|
333
|
+
.option('--agent <agent>', 'New coding tool: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
|
|
257
334
|
.action(wrap((taskId, artifactId, options) => (0, task_artifact_update_1.taskArtifactUpdate)(taskId, artifactId, options)));
|
|
258
335
|
taskArtifact
|
|
259
336
|
.command('list <task>')
|
|
@@ -275,6 +352,44 @@ projectCmd
|
|
|
275
352
|
.command('list')
|
|
276
353
|
.description('List projects in the current workspace. Slug column matches `task create --project <ref>`.')
|
|
277
354
|
.action(wrap(() => (0, project_list_1.projectList)()));
|
|
355
|
+
const projectMemory = projectCmd
|
|
356
|
+
.command('memory')
|
|
357
|
+
.description('View and record PROJECT-scope memories');
|
|
358
|
+
projectMemory
|
|
359
|
+
.command('list [project]')
|
|
360
|
+
.description("List a project's PROJECT-scope memories. <project> defaults to the bound task's project.")
|
|
361
|
+
.option('--category <cat>', 'Filter by trap|decision|convention|procedural')
|
|
362
|
+
.option('-n, --limit <count>', 'Cap output to the first N rows')
|
|
363
|
+
.action(wrap((p, opts) => (0, memory_project_list_1.memoryProjectList)(p, opts)));
|
|
364
|
+
projectMemory
|
|
365
|
+
.command('add [project]')
|
|
366
|
+
.description("Record a PROJECT-scope memory. Use PROJECT scope only when the lesson helps any task in the project. <project> defaults to the bound task's project.")
|
|
367
|
+
.requiredOption('--category <cat>', 'trap | decision | convention | procedural')
|
|
368
|
+
.option('--trigger <text>', 'trap/procedural trigger')
|
|
369
|
+
.option('--outcome <text>', 'trap outcome')
|
|
370
|
+
.option('--workaround <text>', 'trap optional workaround')
|
|
371
|
+
.option('--what <text>', 'decision')
|
|
372
|
+
.option('--why <text>', 'decision rationale')
|
|
373
|
+
.option('--alternatives <text>', 'decision optional alternatives')
|
|
374
|
+
.option('--implications <text>', 'decision optional implications')
|
|
375
|
+
.option('--rule <text>', 'convention rule')
|
|
376
|
+
.option('--applies <text>', 'convention: where it applies')
|
|
377
|
+
.option('--workflow <text>', 'procedural workflow')
|
|
378
|
+
.option('--step <text>', 'procedural step (repeatable)', collect, [])
|
|
379
|
+
.option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
|
|
380
|
+
.action(wrap((p, opts) => (0, memory_project_add_1.memoryProjectAdd)(p, opts)));
|
|
381
|
+
const memoryCmd = program
|
|
382
|
+
.command('memory')
|
|
383
|
+
.description('Operate on a single memory by id (see `lumo task memory` / `lumo project memory` to list/add)');
|
|
384
|
+
memoryCmd
|
|
385
|
+
.command('promote <memoryId>')
|
|
386
|
+
.description('Promote a TASK memory to PROJECT scope. Only when the lesson recurs across 2+ tasks.')
|
|
387
|
+
.action(wrap((id) => (0, memory_promote_1.memoryPromote)(id)));
|
|
388
|
+
memoryCmd
|
|
389
|
+
.command('rm <memoryId>')
|
|
390
|
+
.description('Delete a memory (hard delete). Requires --yes.')
|
|
391
|
+
.option('--yes', 'Confirm deletion')
|
|
392
|
+
.action(wrap((id, opts) => (0, memory_rm_1.memoryRm)(id, opts)));
|
|
278
393
|
const milestoneCmd = program
|
|
279
394
|
.command('milestone')
|
|
280
395
|
.description('Inspect milestones from the terminal');
|
|
@@ -391,6 +506,16 @@ doc
|
|
|
391
506
|
.option('--tag <name>', 'Attach tag by name (repeatable)', collect, [])
|
|
392
507
|
.option('--tag-id <cuid>', 'Attach tag by id (repeatable)', collect, [])
|
|
393
508
|
.action(wrap((title, opts) => (0, doc_create_1.docCreate)(title, opts)));
|
|
509
|
+
doc
|
|
510
|
+
.command('import-gdoc <url>')
|
|
511
|
+
.description('Import a Google Doc as a Lumo doc (one-way Google → Lumo)')
|
|
512
|
+
.option('--scope <scope>', 'personal | workspace (default: personal)')
|
|
513
|
+
.option('--task <LUM-N>', 'Bind the imported doc to this task')
|
|
514
|
+
.action(wrap((url, opts) => (0, doc_import_gdoc_1.docImportGdoc)(url, opts)));
|
|
515
|
+
doc
|
|
516
|
+
.command('sync <doc>')
|
|
517
|
+
.description('Re-sync a Google-imported doc (overwrites the Lumo body)')
|
|
518
|
+
.action(wrap(ref => (0, doc_sync_1.docSync)(ref)));
|
|
394
519
|
doc
|
|
395
520
|
.command('update <doc>')
|
|
396
521
|
.description('Update an existing document. <doc> accepts a cuid or a case-insensitive title (ambiguous titles fail with candidates). Replacement body comes from --content, --file, or piped stdin (pick one). --tag/--tag-id (bulk replace) cannot be combined with --add-tag/--add-tag-id/--remove-tag/--remove-tag-id.')
|
|
@@ -479,110 +604,122 @@ task
|
|
|
479
604
|
const hook = program
|
|
480
605
|
.command('hook')
|
|
481
606
|
.description('Claude Code hook ingestion (invoked by settings.json, not humans)');
|
|
482
|
-
hook
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
607
|
+
// Slug → help-text for every `lumo hook <slug>` subcommand. Each one accepts
|
|
608
|
+
// an optional `--agent <token>` flag (baked into settings.json by `lumo setup
|
|
609
|
+
// --agent <token>`); the value rides to the server as the X-Lumo-Agent header
|
|
610
|
+
// so auto-sedimented memories are attributed to the agent that produced them.
|
|
611
|
+
// Keep this list in sync with LUMO_HOOK_EVENTS (cli/src/lib/hooks-template.ts)
|
|
612
|
+
// and the server-side HookEventType slugs.
|
|
613
|
+
const HOOK_SUBCOMMANDS = [
|
|
614
|
+
[
|
|
615
|
+
'pre-tool-use',
|
|
616
|
+
'Forward a PreToolUse hook event to Lumo (reads JSON from stdin)',
|
|
617
|
+
],
|
|
618
|
+
[
|
|
619
|
+
'post-tool-use',
|
|
620
|
+
'Forward a PostToolUse hook event to Lumo (reads JSON from stdin)',
|
|
621
|
+
],
|
|
622
|
+
[
|
|
623
|
+
'post-tool-use-failure',
|
|
624
|
+
'Forward a PostToolUseFailure hook event to Lumo (reads JSON from stdin)',
|
|
625
|
+
],
|
|
626
|
+
[
|
|
627
|
+
'user-prompt-submit',
|
|
628
|
+
'Forward a UserPromptSubmit hook event to Lumo (reads JSON from stdin)',
|
|
629
|
+
],
|
|
630
|
+
['stop', 'Forward a Stop hook event to Lumo (reads JSON from stdin)'],
|
|
631
|
+
[
|
|
632
|
+
'stop-failure',
|
|
633
|
+
'Forward a StopFailure hook event to Lumo (reads JSON from stdin)',
|
|
634
|
+
],
|
|
635
|
+
[
|
|
636
|
+
'permission-request',
|
|
637
|
+
'Forward a PermissionRequest hook event to Lumo (reads JSON from stdin)',
|
|
638
|
+
],
|
|
639
|
+
[
|
|
640
|
+
'permission-denied',
|
|
641
|
+
'Forward a PermissionDenied hook event to Lumo (reads JSON from stdin)',
|
|
642
|
+
],
|
|
643
|
+
[
|
|
644
|
+
'session-start',
|
|
645
|
+
'Forward a SessionStart hook event to Lumo (reads JSON from stdin)',
|
|
646
|
+
],
|
|
647
|
+
[
|
|
648
|
+
'session-end',
|
|
649
|
+
'Forward a SessionEnd hook event to Lumo (reads JSON from stdin)',
|
|
650
|
+
],
|
|
651
|
+
[
|
|
652
|
+
'subagent-start',
|
|
653
|
+
'Forward a SubagentStart hook event to Lumo (reads JSON from stdin)',
|
|
654
|
+
],
|
|
655
|
+
[
|
|
656
|
+
'subagent-stop',
|
|
657
|
+
'Forward a SubagentStop hook event to Lumo (reads JSON from stdin)',
|
|
658
|
+
],
|
|
659
|
+
[
|
|
660
|
+
'worktree-create',
|
|
661
|
+
'Forward a WorktreeCreate hook event to Lumo (reads JSON from stdin)',
|
|
662
|
+
],
|
|
663
|
+
[
|
|
664
|
+
'worktree-remove',
|
|
665
|
+
'Forward a WorktreeRemove hook event to Lumo (reads JSON from stdin)',
|
|
666
|
+
],
|
|
667
|
+
[
|
|
668
|
+
'file-changed',
|
|
669
|
+
'Forward a FileChanged hook event to Lumo (reads JSON from stdin)',
|
|
670
|
+
],
|
|
671
|
+
[
|
|
672
|
+
'config-change',
|
|
673
|
+
'Forward a ConfigChange hook event to Lumo (reads JSON from stdin)',
|
|
674
|
+
],
|
|
675
|
+
[
|
|
676
|
+
'task-created',
|
|
677
|
+
'Forward a TaskCreated hook event to Lumo (reads JSON from stdin)',
|
|
678
|
+
],
|
|
679
|
+
[
|
|
680
|
+
'task-completed',
|
|
681
|
+
'Forward a TaskCompleted hook event to Lumo (reads JSON from stdin)',
|
|
682
|
+
],
|
|
683
|
+
[
|
|
684
|
+
'post-tool-batch',
|
|
685
|
+
'Forward a PostToolBatch hook event to Lumo (reads JSON from stdin)',
|
|
686
|
+
],
|
|
687
|
+
[
|
|
688
|
+
'user-prompt-expansion',
|
|
689
|
+
'Forward a UserPromptExpansion hook event to Lumo (reads JSON from stdin)',
|
|
690
|
+
],
|
|
691
|
+
[
|
|
692
|
+
'notification',
|
|
693
|
+
'Forward a Notification hook event to Lumo (reads JSON from stdin)',
|
|
694
|
+
],
|
|
695
|
+
[
|
|
696
|
+
'elicitation',
|
|
697
|
+
'Forward an Elicitation hook event to Lumo (reads JSON from stdin)',
|
|
698
|
+
],
|
|
699
|
+
[
|
|
700
|
+
'elicitation-result',
|
|
701
|
+
'Forward an ElicitationResult hook event to Lumo (reads JSON from stdin)',
|
|
702
|
+
],
|
|
703
|
+
[
|
|
704
|
+
'cwd-changed',
|
|
705
|
+
'Forward a CwdChanged hook event to Lumo (reads JSON from stdin)',
|
|
706
|
+
],
|
|
707
|
+
[
|
|
708
|
+
'instructions-loaded',
|
|
709
|
+
'Forward an InstructionsLoaded hook event to Lumo (reads JSON from stdin)',
|
|
710
|
+
],
|
|
711
|
+
];
|
|
712
|
+
for (const [slug, description] of HOOK_SUBCOMMANDS) {
|
|
713
|
+
hook
|
|
714
|
+
.command(slug)
|
|
715
|
+
.description(description)
|
|
716
|
+
.option('--agent <token>', 'Coding agent that owns this session (e.g. claude-code, codex). Baked in by `lumo setup --agent`.')
|
|
717
|
+
.action(wrap((opts) => (0, hook_1.hookCommand)(slug, opts.agent)));
|
|
718
|
+
}
|
|
582
719
|
if (!isUpdateCheckWorker) {
|
|
583
720
|
program.parseAsync(process.argv).catch(err => {
|
|
584
721
|
const msg = err instanceof Error ? err.message : String(err);
|
|
585
|
-
console.error(`Error: ${msg}`);
|
|
722
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
|
|
586
723
|
process.exit(1);
|
|
587
724
|
});
|
|
588
725
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI-local agent enum support. The CLI talks to the API over HTTP only and
|
|
4
|
+
* does not depend on @prisma/client, so the enum values + accepted tokens are
|
|
5
|
+
* mirrored here. Keep in sync with prisma `enum TaskArtifactAgent`.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.VALID_AGENT_TOKENS = exports.ENUM_TO_TOKEN = exports.AGENT_LABELS = void 0;
|
|
9
|
+
exports.normalizeAgent = normalizeAgent;
|
|
10
|
+
/** enum value → display label (matches lib/i18n en.json taskArtifact.agent.*) */
|
|
11
|
+
exports.AGENT_LABELS = {
|
|
12
|
+
CLAUDE_CODE: 'Claude Code',
|
|
13
|
+
CODEX: 'Codex',
|
|
14
|
+
CURSOR: 'Cursor',
|
|
15
|
+
GEMINI_CLI: 'Gemini CLI',
|
|
16
|
+
GITHUB_COPILOT: 'GitHub Copilot',
|
|
17
|
+
WINDSURF: 'Windsurf',
|
|
18
|
+
};
|
|
19
|
+
/** enum value → canonical CLI token (inverse of the non-alias TOKEN_TO_ENUM rows) */
|
|
20
|
+
exports.ENUM_TO_TOKEN = {
|
|
21
|
+
CLAUDE_CODE: 'claude-code',
|
|
22
|
+
CODEX: 'codex',
|
|
23
|
+
CURSOR: 'cursor',
|
|
24
|
+
GEMINI_CLI: 'gemini-cli',
|
|
25
|
+
GITHUB_COPILOT: 'github-copilot',
|
|
26
|
+
WINDSURF: 'windsurf',
|
|
27
|
+
};
|
|
28
|
+
/** accepted CLI token (normalized) → enum value */
|
|
29
|
+
const TOKEN_TO_ENUM = {
|
|
30
|
+
'claude-code': 'CLAUDE_CODE',
|
|
31
|
+
codex: 'CODEX',
|
|
32
|
+
cursor: 'CURSOR',
|
|
33
|
+
'gemini-cli': 'GEMINI_CLI',
|
|
34
|
+
gemini: 'GEMINI_CLI',
|
|
35
|
+
'github-copilot': 'GITHUB_COPILOT',
|
|
36
|
+
copilot: 'GITHUB_COPILOT',
|
|
37
|
+
windsurf: 'WINDSURF',
|
|
38
|
+
};
|
|
39
|
+
/** Canonical tokens shown in error/usage hints (aliases omitted). */
|
|
40
|
+
exports.VALID_AGENT_TOKENS = [
|
|
41
|
+
'claude-code',
|
|
42
|
+
'codex',
|
|
43
|
+
'cursor',
|
|
44
|
+
'gemini-cli',
|
|
45
|
+
'github-copilot',
|
|
46
|
+
'windsurf',
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a user-supplied agent string to its enum value, or null if
|
|
50
|
+
* unknown. Case-insensitive; spaces and underscores are treated as hyphens.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeAgent(input) {
|
|
53
|
+
const key = input
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[\s_]+/g, '-');
|
|
57
|
+
return TOKEN_TO_ENUM[key] ?? null;
|
|
58
|
+
}
|
package/dist/cli/src/lib/api.js
CHANGED
|
@@ -1,12 +1,92 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertSafeApiUrl = assertSafeApiUrl;
|
|
4
|
+
exports.hostMismatchWarning = hostMismatchWarning;
|
|
5
|
+
exports.resolveAuthedApiUrl = resolveAuthedApiUrl;
|
|
3
6
|
exports.resolveApiUrl = resolveApiUrl;
|
|
4
7
|
exports.trimTrailingSlash = trimTrailingSlash;
|
|
5
8
|
exports.verifyToken = verifyToken;
|
|
6
9
|
const DEFAULT_API_URL = 'https://www.uselumo.ai';
|
|
10
|
+
// Hostnames allowed to use plaintext http:// — local dev only. Everything
|
|
11
|
+
// else MUST be https:// so the Bearer token is never sent in the clear.
|
|
12
|
+
// `URL.hostname` returns IPv6 literals wrapped in brackets, so `::1` appears
|
|
13
|
+
// as `[::1]`.
|
|
14
|
+
const LOCALHOST_HOSTNAMES = new Set([
|
|
15
|
+
'localhost',
|
|
16
|
+
'127.0.0.1',
|
|
17
|
+
'::1',
|
|
18
|
+
'[::1]',
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Throw if `url` is not a safe target for sending the API token.
|
|
22
|
+
*
|
|
23
|
+
* The CLI attaches a `Bearer` token (and the hook runner POSTs full session
|
|
24
|
+
* content) to whatever `LUMO_API_URL` resolves to. An attacker who can set
|
|
25
|
+
* that env var could otherwise exfiltrate the token / session stream by
|
|
26
|
+
* pointing it at their own host, or downgrade to http:// to sniff it in
|
|
27
|
+
* plaintext. We therefore require https://, with an http:// exception for
|
|
28
|
+
* localhost so local dev still works.
|
|
29
|
+
*/
|
|
30
|
+
function assertSafeApiUrl(url) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(url);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Invalid LUMO_API_URL: "${url}" is not a valid URL`);
|
|
37
|
+
}
|
|
38
|
+
if (parsed.protocol === 'https:')
|
|
39
|
+
return;
|
|
40
|
+
if (parsed.protocol === 'http:' && LOCALHOST_HOSTNAMES.has(parsed.hostname)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Refusing to use insecure API URL "${url}": only https:// is allowed ` +
|
|
44
|
+
`(http:// permitted for localhost only). This protects your API token ` +
|
|
45
|
+
`and session data from being sent in plaintext or to an untrusted host.`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return a prominent warning when the resolved API host differs from the host
|
|
49
|
+
* the credentials were issued for, otherwise null. Compares hostname only
|
|
50
|
+
* (case-insensitive, courtesy of URL parsing). Used to warn-then-send: a host
|
|
51
|
+
* change is suspicious (possible token exfiltration) but intentional dev/env
|
|
52
|
+
* redirects are legitimate, so we surface it rather than block.
|
|
53
|
+
*/
|
|
54
|
+
function hostMismatchWarning(resolvedUrl, credsApiUrl) {
|
|
55
|
+
let resolvedHost;
|
|
56
|
+
let credsHost;
|
|
57
|
+
try {
|
|
58
|
+
resolvedHost = new URL(resolvedUrl).hostname;
|
|
59
|
+
credsHost = new URL(credsApiUrl).hostname;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (resolvedHost === credsHost)
|
|
65
|
+
return null;
|
|
66
|
+
return (`⚠ LUMO_API_URL points at "${resolvedHost}" but your credentials were ` +
|
|
67
|
+
`issued for "${credsHost}". Your API token will be sent there. If you did ` +
|
|
68
|
+
`not set this, your token may be exfiltrated — unset LUMO_API_URL.`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the API URL for an authenticated command: prefer a non-empty
|
|
72
|
+
* `LUMO_API_URL` override, else the baked-in `credsApiUrl`. The resolved URL
|
|
73
|
+
* is validated (throws on an insecure target) and any host-mismatch warning is
|
|
74
|
+
* routed to `opts.warn` (defaults to stderr via console.error).
|
|
75
|
+
*/
|
|
76
|
+
function resolveAuthedApiUrl(credsApiUrl, opts) {
|
|
77
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
78
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : credsApiUrl;
|
|
79
|
+
assertSafeApiUrl(apiUrl);
|
|
80
|
+
const warning = hostMismatchWarning(apiUrl, credsApiUrl);
|
|
81
|
+
if (warning)
|
|
82
|
+
(opts?.warn ?? ((m) => console.error(m)))(warning);
|
|
83
|
+
return apiUrl;
|
|
84
|
+
}
|
|
7
85
|
function resolveApiUrl() {
|
|
8
86
|
const url = process.env.LUMO_API_URL?.trim();
|
|
9
|
-
|
|
87
|
+
const resolved = url && url.length > 0 ? url : DEFAULT_API_URL;
|
|
88
|
+
assertSafeApiUrl(resolved);
|
|
89
|
+
return resolved;
|
|
10
90
|
}
|
|
11
91
|
function trimTrailingSlash(url) {
|
|
12
92
|
return url.replace(/\/+$/, '');
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.configDir = configDir;
|
|
36
37
|
exports.credentialsPath = credentialsPath;
|
|
37
38
|
exports.readCredentials = readCredentials;
|
|
38
39
|
exports.writeCredentials = writeCredentials;
|
|
@@ -41,7 +42,7 @@ const fs = __importStar(require("fs"));
|
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const os = __importStar(require("os"));
|
|
43
44
|
function configDir() {
|
|
44
|
-
return path.join(os.homedir(), '.lumo');
|
|
45
|
+
return process.env.LUMO_CONFIG_DIR || path.join(os.homedir(), '.lumo');
|
|
45
46
|
}
|
|
46
47
|
function credentialsPath() {
|
|
47
48
|
return path.join(configDir(), 'credentials.json');
|
|
@@ -37,6 +37,7 @@ exports.resolveDocContent = resolveDocContent;
|
|
|
37
37
|
exports.readStdinToString = readStdinToString;
|
|
38
38
|
exports.readFileUtf8 = readFileUtf8;
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
|
+
const path_guard_1 = require("./path-guard");
|
|
40
41
|
/**
|
|
41
42
|
* Pick one of --content / --file / stdin as the markdown source.
|
|
42
43
|
* Explicit flags win: if --content or --file is set, stdin is ignored
|
|
@@ -56,7 +57,17 @@ async function resolveDocContent(args) {
|
|
|
56
57
|
if (hasContent)
|
|
57
58
|
return { kind: 'ok', markdown: args.content };
|
|
58
59
|
if (hasFile) {
|
|
59
|
-
const
|
|
60
|
+
const check = (args.checkFilePath ?? path_guard_1.checkArtifactFilePath)(args.file);
|
|
61
|
+
if (!check.ok) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'error',
|
|
64
|
+
message: check.reason === 'unreadable'
|
|
65
|
+
? `could not read file ${args.file}`
|
|
66
|
+
: `refusing to read ${args.file} — ${check.detail}. ` +
|
|
67
|
+
`--file must be a non-sensitive path inside the project directory.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const text = await args.readFile(check.resolved);
|
|
60
71
|
return { kind: 'ok', markdown: text };
|
|
61
72
|
}
|
|
62
73
|
if (!args.stdinIsTTY) {
|
|
@@ -18,7 +18,7 @@ async function call(path, init) {
|
|
|
18
18
|
const creds = (0, config_1.readCredentials)();
|
|
19
19
|
if (!creds)
|
|
20
20
|
throw new Error('Not logged in. Run: lumo auth login');
|
|
21
|
-
const apiUrl = (0, api_1.
|
|
21
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
22
22
|
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}${path}`, {
|
|
23
23
|
...init,
|
|
24
24
|
headers: {
|