@lumoai/cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/assets/skill.md +189 -16
  2. package/dist/cli/src/commands/auth-login.js +4 -3
  3. package/dist/cli/src/commands/auth-logout.js +2 -1
  4. package/dist/cli/src/commands/doc-bind.js +3 -3
  5. package/dist/cli/src/commands/doc-create.js +5 -5
  6. package/dist/cli/src/commands/doc-delete.js +4 -4
  7. package/dist/cli/src/commands/doc-import-gdoc.js +82 -0
  8. package/dist/cli/src/commands/doc-list.js +7 -7
  9. package/dist/cli/src/commands/doc-move.js +8 -8
  10. package/dist/cli/src/commands/doc-share-list.js +11 -8
  11. package/dist/cli/src/commands/doc-share.js +7 -5
  12. package/dist/cli/src/commands/doc-show.js +6 -6
  13. package/dist/cli/src/commands/doc-sync.js +44 -0
  14. package/dist/cli/src/commands/doc-unbind.js +4 -4
  15. package/dist/cli/src/commands/doc-unshare.js +9 -7
  16. package/dist/cli/src/commands/doc-update.js +5 -5
  17. package/dist/cli/src/commands/hook.js +2 -2
  18. package/dist/cli/src/commands/memory-project-add.js +19 -4
  19. package/dist/cli/src/commands/memory-project-list.js +1 -2
  20. package/dist/cli/src/commands/memory-promote.js +3 -3
  21. package/dist/cli/src/commands/memory-rm.js +1 -2
  22. package/dist/cli/src/commands/memory-task-add.js +19 -4
  23. package/dist/cli/src/commands/memory-task-list.js +1 -2
  24. package/dist/cli/src/commands/milestone-create.js +4 -4
  25. package/dist/cli/src/commands/milestone-delete.js +5 -5
  26. package/dist/cli/src/commands/milestone-list.js +3 -3
  27. package/dist/cli/src/commands/milestone-show.js +5 -5
  28. package/dist/cli/src/commands/milestone-update.js +6 -5
  29. package/dist/cli/src/commands/project-list.js +3 -3
  30. package/dist/cli/src/commands/session-attach.js +5 -5
  31. package/dist/cli/src/commands/session-detach.js +3 -3
  32. package/dist/cli/src/commands/session-status.js +3 -3
  33. package/dist/cli/src/commands/session-wrap.js +32 -0
  34. package/dist/cli/src/commands/setup.js +33 -7
  35. package/dist/cli/src/commands/sprint-add.js +3 -3
  36. package/dist/cli/src/commands/sprint-close.js +5 -5
  37. package/dist/cli/src/commands/sprint-create.js +4 -4
  38. package/dist/cli/src/commands/sprint-delete.js +5 -5
  39. package/dist/cli/src/commands/sprint-list.js +3 -3
  40. package/dist/cli/src/commands/sprint-remove.js +3 -3
  41. package/dist/cli/src/commands/sprint-show.js +4 -4
  42. package/dist/cli/src/commands/sprint-start.js +4 -4
  43. package/dist/cli/src/commands/sprint-summary.js +7 -7
  44. package/dist/cli/src/commands/sprint-update.js +6 -5
  45. package/dist/cli/src/commands/task-artifact-add.js +17 -5
  46. package/dist/cli/src/commands/task-artifact-list.js +4 -4
  47. package/dist/cli/src/commands/task-artifact-rm.js +4 -4
  48. package/dist/cli/src/commands/task-artifact-show.js +8 -8
  49. package/dist/cli/src/commands/task-artifact-update.js +5 -5
  50. package/dist/cli/src/commands/task-comment-list.js +111 -0
  51. package/dist/cli/src/commands/task-comment.js +3 -3
  52. package/dist/cli/src/commands/task-context.js +29 -12
  53. package/dist/cli/src/commands/task-create.js +7 -7
  54. package/dist/cli/src/commands/task-figma-add.js +3 -2
  55. package/dist/cli/src/commands/task-figma-context.js +61 -0
  56. package/dist/cli/src/commands/task-figma-list.js +3 -2
  57. package/dist/cli/src/commands/task-figma-refresh.js +4 -3
  58. package/dist/cli/src/commands/task-figma-rm.js +3 -2
  59. package/dist/cli/src/commands/task-list.js +1 -2
  60. package/dist/cli/src/commands/task-pr-show.js +66 -0
  61. package/dist/cli/src/commands/task-show.js +8 -7
  62. package/dist/cli/src/commands/task-slack-show.js +59 -0
  63. package/dist/cli/src/commands/task-update.js +7 -7
  64. package/dist/cli/src/commands/task-web-show.js +64 -0
  65. package/dist/cli/src/commands/whoami.js +4 -3
  66. package/dist/cli/src/commands/wrap/progress-comment-section.js +81 -0
  67. package/dist/cli/src/index.js +174 -102
  68. package/dist/cli/src/lib/agent.js +10 -1
  69. package/dist/cli/src/lib/api.js +81 -1
  70. package/dist/cli/src/lib/config.js +2 -1
  71. package/dist/cli/src/lib/doc-input.js +12 -1
  72. package/dist/cli/src/lib/editor.js +66 -0
  73. package/dist/cli/src/lib/figma-api.js +1 -1
  74. package/dist/cli/src/lib/format.js +3 -2
  75. package/dist/cli/src/lib/hook-runner.js +64 -19
  76. package/dist/cli/src/lib/hooks-template.js +52 -7
  77. package/dist/cli/src/lib/memory-content.js +4 -3
  78. package/dist/cli/src/lib/path-guard.js +125 -0
  79. package/dist/cli/src/lib/progress-comment-api.js +47 -0
  80. package/dist/cli/src/lib/resolve-doc-id.js +2 -1
  81. package/dist/cli/src/lib/resolve-member.js +2 -1
  82. package/dist/cli/src/lib/sanitize.js +17 -0
  83. package/dist/cli/src/lib/tag-resolver.js +2 -1
  84. package/dist/cli/src/lib/update-check.js +2 -2
  85. package/dist/cli/src/lib/wrap-panel.js +15 -0
  86. package/package.json +1 -1
@@ -45,6 +45,7 @@ const hook_1 = require("./commands/hook");
45
45
  const session_attach_1 = require("./commands/session-attach");
46
46
  const session_detach_1 = require("./commands/session-detach");
47
47
  const session_status_1 = require("./commands/session-status");
48
+ const session_wrap_1 = require("./commands/session-wrap");
48
49
  const task_context_1 = require("./commands/task-context");
49
50
  const task_create_1 = require("./commands/task-create");
50
51
  const task_update_1 = require("./commands/task-update");
@@ -66,6 +67,11 @@ const task_artifact_list_1 = require("./commands/task-artifact-list");
66
67
  const task_artifact_show_1 = require("./commands/task-artifact-show");
67
68
  const task_artifact_rm_1 = require("./commands/task-artifact-rm");
68
69
  const task_artifact_update_1 = require("./commands/task-artifact-update");
70
+ const task_slack_show_1 = require("./commands/task-slack-show");
71
+ const task_web_show_1 = require("./commands/task-web-show");
72
+ const task_figma_context_1 = require("./commands/task-figma-context");
73
+ const task_comment_list_1 = require("./commands/task-comment-list");
74
+ const task_pr_show_1 = require("./commands/task-pr-show");
69
75
  const project_list_1 = require("./commands/project-list");
70
76
  const milestone_list_1 = require("./commands/milestone-list");
71
77
  const milestone_create_1 = require("./commands/milestone-create");
@@ -83,6 +89,8 @@ const sprint_summary_1 = require("./commands/sprint-summary");
83
89
  const sprint_add_1 = require("./commands/sprint-add");
84
90
  const sprint_remove_1 = require("./commands/sprint-remove");
85
91
  const doc_create_1 = require("./commands/doc-create");
92
+ const doc_import_gdoc_1 = require("./commands/doc-import-gdoc");
93
+ const doc_sync_1 = require("./commands/doc-sync");
86
94
  const doc_update_1 = require("./commands/doc-update");
87
95
  const doc_show_1 = require("./commands/doc-show");
88
96
  const doc_list_1 = require("./commands/doc-list");
@@ -96,6 +104,7 @@ const doc_move_1 = require("./commands/doc-move");
96
104
  const update_1 = require("./commands/update");
97
105
  const setup_1 = require("./commands/setup");
98
106
  const update_check_1 = require("./lib/update-check");
107
+ const sanitize_1 = require("./lib/sanitize");
99
108
  // Resolve package.json relative to __dirname so this works regardless of how
100
109
  // deep the compiled output ends up (flat dist/ or nested dist/cli/src/).
101
110
  const pkg = require(path.resolve(__dirname, '../../..', 'package.json'));
@@ -138,7 +147,7 @@ function wrap(fn) {
138
147
  }
139
148
  catch (err) {
140
149
  const msg = err instanceof Error ? err.message : String(err);
141
- console.error(`Error: ${msg}`);
150
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
142
151
  process.exit(1);
143
152
  }
144
153
  };
@@ -170,6 +179,7 @@ program
170
179
  .option('--user', 'Install into ~/.claude (applies across all projects for this user)')
171
180
  .option('--project', 'Install into ./.claude (applies to the current project only)')
172
181
  .option('--force', 'Overwrite an existing SKILL.md when its contents differ from the bundled version')
182
+ .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.')
173
183
  .action(wrap(options => (0, setup_1.setup)(options)));
174
184
  const session = program
175
185
  .command('session')
@@ -186,6 +196,12 @@ session
186
196
  .command('detach')
187
197
  .description('Clear the task binding on the current Claude Code session. Past hook events keep their taskId; only future events become untagged.')
188
198
  .action(wrap(() => (0, session_detach_1.sessionDetach)()));
199
+ session
200
+ .command('wrap')
201
+ .description("Session-end wrap-up: draft a progress comment from this session's turn summaries and post it to the bound task after confirmation.")
202
+ .option('-y, --yes', 'Post the drafted comment without prompting (agent-friendly)')
203
+ .option('--dry-run', 'Print the draft but do not post or advance the watermark')
204
+ .action(wrap(options => (0, session_wrap_1.sessionWrap)(options)));
189
205
  const task = program
190
206
  .command('task')
191
207
  .description('Inspect tasks from the terminal');
@@ -243,6 +259,38 @@ taskFigma
243
259
  .command('refresh <task>')
244
260
  .description('Re-fetch Figma metadata + thumbnail for every link on this task. Per-link failures isolated.')
245
261
  .action(wrap((taskId) => (0, task_figma_refresh_1.taskFigmaRefresh)({ identifier: taskId })));
262
+ // Tier-2 retrieval commands (LUM-122). `task context` prints the matching
263
+ // command at the end of each cheap inline card; the agent runs it to pull the
264
+ // full content on demand.
265
+ const taskSlack = task.command('slack').description('Inspect Slack context');
266
+ taskSlack
267
+ .command('show <identifier> <contextId>')
268
+ .description('Show the full stored Slack thread snapshot')
269
+ .action(wrap((id, ctx) => (0, task_slack_show_1.taskSlackShow)(id, ctx)));
270
+ const taskWeb = task.command('web').description('Inspect web link content');
271
+ taskWeb
272
+ .command('show <identifier> <linkId>')
273
+ .description('Show the fetched web link body as plain text')
274
+ .action(wrap((id, link) => (0, task_web_show_1.taskWebShow)(id, link)));
275
+ // taskFigma already exists above → attach the context leaf to it.
276
+ taskFigma
277
+ .command('context <identifier> <linkId>')
278
+ .description('Show the cached Figma design context (metadata)')
279
+ .action(wrap((id, link) => (0, task_figma_context_1.taskFigmaContext)(id, link)));
280
+ // Plural `comments` parent: `task comment <id> <body>` already exists for
281
+ // adding a comment, and commander disallows a duplicate name.
282
+ const taskComments = task
283
+ .command('comments')
284
+ .description('Inspect the full task comment thread');
285
+ taskComments
286
+ .command('list <identifier>')
287
+ .description('List the full task comment thread')
288
+ .action(wrap(id => (0, task_comment_list_1.taskCommentList)(id)));
289
+ const taskPr = task.command('pr').description('Inspect linked PRs');
290
+ taskPr
291
+ .command('show <identifier> <number>')
292
+ .description('Show the synced PR record (diff/review comments when available)')
293
+ .action(wrap((id, num) => (0, task_pr_show_1.taskPrShow)(id, num)));
246
294
  const taskMemory = task
247
295
  .command('memory')
248
296
  .description('View and record memories scoped to a task');
@@ -269,6 +317,7 @@ taskMemory
269
317
  .option('--applies <text>', 'convention: where the rule applies')
270
318
  .option('--workflow <text>', 'procedural: the workflow name')
271
319
  .option('--step <text>', 'procedural: a step (repeatable)', collect, [])
320
+ .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
272
321
  .action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
273
322
  const taskArtifact = task
274
323
  .command('artifact')
@@ -334,6 +383,7 @@ projectMemory
334
383
  .option('--applies <text>', 'convention: where it applies')
335
384
  .option('--workflow <text>', 'procedural workflow')
336
385
  .option('--step <text>', 'procedural step (repeatable)', collect, [])
386
+ .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
337
387
  .action(wrap((p, opts) => (0, memory_project_add_1.memoryProjectAdd)(p, opts)));
338
388
  const memoryCmd = program
339
389
  .command('memory')
@@ -463,6 +513,16 @@ doc
463
513
  .option('--tag <name>', 'Attach tag by name (repeatable)', collect, [])
464
514
  .option('--tag-id <cuid>', 'Attach tag by id (repeatable)', collect, [])
465
515
  .action(wrap((title, opts) => (0, doc_create_1.docCreate)(title, opts)));
516
+ doc
517
+ .command('import-gdoc <url>')
518
+ .description('Import a Google Doc as a Lumo doc (one-way Google → Lumo)')
519
+ .option('--scope <scope>', 'personal | workspace (default: personal)')
520
+ .option('--task <LUM-N>', 'Bind the imported doc to this task')
521
+ .action(wrap((url, opts) => (0, doc_import_gdoc_1.docImportGdoc)(url, opts)));
522
+ doc
523
+ .command('sync <doc>')
524
+ .description('Re-sync a Google-imported doc (overwrites the Lumo body)')
525
+ .action(wrap(ref => (0, doc_sync_1.docSync)(ref)));
466
526
  doc
467
527
  .command('update <doc>')
468
528
  .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.')
@@ -551,110 +611,122 @@ task
551
611
  const hook = program
552
612
  .command('hook')
553
613
  .description('Claude Code hook ingestion (invoked by settings.json, not humans)');
554
- hook
555
- .command('pre-tool-use')
556
- .description('Forward a PreToolUse hook event to Lumo (reads JSON from stdin)')
557
- .action(wrap(() => (0, hook_1.hookCommand)('pre-tool-use')));
558
- hook
559
- .command('post-tool-use')
560
- .description('Forward a PostToolUse hook event to Lumo (reads JSON from stdin)')
561
- .action(wrap(() => (0, hook_1.hookCommand)('post-tool-use')));
562
- hook
563
- .command('post-tool-use-failure')
564
- .description('Forward a PostToolUseFailure hook event to Lumo (reads JSON from stdin)')
565
- .action(wrap(() => (0, hook_1.hookCommand)('post-tool-use-failure')));
566
- hook
567
- .command('user-prompt-submit')
568
- .description('Forward a UserPromptSubmit hook event to Lumo (reads JSON from stdin)')
569
- .action(wrap(() => (0, hook_1.hookCommand)('user-prompt-submit')));
570
- hook
571
- .command('stop')
572
- .description('Forward a Stop hook event to Lumo (reads JSON from stdin)')
573
- .action(wrap(() => (0, hook_1.hookCommand)('stop')));
574
- hook
575
- .command('stop-failure')
576
- .description('Forward a StopFailure hook event to Lumo (reads JSON from stdin)')
577
- .action(wrap(() => (0, hook_1.hookCommand)('stop-failure')));
578
- hook
579
- .command('permission-request')
580
- .description('Forward a PermissionRequest hook event to Lumo (reads JSON from stdin)')
581
- .action(wrap(() => (0, hook_1.hookCommand)('permission-request')));
582
- hook
583
- .command('permission-denied')
584
- .description('Forward a PermissionDenied hook event to Lumo (reads JSON from stdin)')
585
- .action(wrap(() => (0, hook_1.hookCommand)('permission-denied')));
586
- hook
587
- .command('session-start')
588
- .description('Forward a SessionStart hook event to Lumo (reads JSON from stdin)')
589
- .action(wrap(() => (0, hook_1.hookCommand)('session-start')));
590
- hook
591
- .command('session-end')
592
- .description('Forward a SessionEnd hook event to Lumo (reads JSON from stdin)')
593
- .action(wrap(() => (0, hook_1.hookCommand)('session-end')));
594
- hook
595
- .command('subagent-start')
596
- .description('Forward a SubagentStart hook event to Lumo (reads JSON from stdin)')
597
- .action(wrap(() => (0, hook_1.hookCommand)('subagent-start')));
598
- hook
599
- .command('subagent-stop')
600
- .description('Forward a SubagentStop hook event to Lumo (reads JSON from stdin)')
601
- .action(wrap(() => (0, hook_1.hookCommand)('subagent-stop')));
602
- hook
603
- .command('worktree-create')
604
- .description('Forward a WorktreeCreate hook event to Lumo (reads JSON from stdin)')
605
- .action(wrap(() => (0, hook_1.hookCommand)('worktree-create')));
606
- hook
607
- .command('worktree-remove')
608
- .description('Forward a WorktreeRemove hook event to Lumo (reads JSON from stdin)')
609
- .action(wrap(() => (0, hook_1.hookCommand)('worktree-remove')));
610
- hook
611
- .command('file-changed')
612
- .description('Forward a FileChanged hook event to Lumo (reads JSON from stdin)')
613
- .action(wrap(() => (0, hook_1.hookCommand)('file-changed')));
614
- hook
615
- .command('config-change')
616
- .description('Forward a ConfigChange hook event to Lumo (reads JSON from stdin)')
617
- .action(wrap(() => (0, hook_1.hookCommand)('config-change')));
618
- hook
619
- .command('task-created')
620
- .description('Forward a TaskCreated hook event to Lumo (reads JSON from stdin)')
621
- .action(wrap(() => (0, hook_1.hookCommand)('task-created')));
622
- hook
623
- .command('task-completed')
624
- .description('Forward a TaskCompleted hook event to Lumo (reads JSON from stdin)')
625
- .action(wrap(() => (0, hook_1.hookCommand)('task-completed')));
626
- hook
627
- .command('post-tool-batch')
628
- .description('Forward a PostToolBatch hook event to Lumo (reads JSON from stdin)')
629
- .action(wrap(() => (0, hook_1.hookCommand)('post-tool-batch')));
630
- hook
631
- .command('user-prompt-expansion')
632
- .description('Forward a UserPromptExpansion hook event to Lumo (reads JSON from stdin)')
633
- .action(wrap(() => (0, hook_1.hookCommand)('user-prompt-expansion')));
634
- hook
635
- .command('notification')
636
- .description('Forward a Notification hook event to Lumo (reads JSON from stdin)')
637
- .action(wrap(() => (0, hook_1.hookCommand)('notification')));
638
- hook
639
- .command('elicitation')
640
- .description('Forward an Elicitation hook event to Lumo (reads JSON from stdin)')
641
- .action(wrap(() => (0, hook_1.hookCommand)('elicitation')));
642
- hook
643
- .command('elicitation-result')
644
- .description('Forward an ElicitationResult hook event to Lumo (reads JSON from stdin)')
645
- .action(wrap(() => (0, hook_1.hookCommand)('elicitation-result')));
646
- hook
647
- .command('cwd-changed')
648
- .description('Forward a CwdChanged hook event to Lumo (reads JSON from stdin)')
649
- .action(wrap(() => (0, hook_1.hookCommand)('cwd-changed')));
650
- hook
651
- .command('instructions-loaded')
652
- .description('Forward an InstructionsLoaded hook event to Lumo (reads JSON from stdin)')
653
- .action(wrap(() => (0, hook_1.hookCommand)('instructions-loaded')));
614
+ // Slug → help-text for every `lumo hook <slug>` subcommand. Each one accepts
615
+ // an optional `--agent <token>` flag (baked into settings.json by `lumo setup
616
+ // --agent <token>`); the value rides to the server as the X-Lumo-Agent header
617
+ // so auto-sedimented memories are attributed to the agent that produced them.
618
+ // Keep this list in sync with LUMO_HOOK_EVENTS (cli/src/lib/hooks-template.ts)
619
+ // and the server-side HookEventType slugs.
620
+ const HOOK_SUBCOMMANDS = [
621
+ [
622
+ 'pre-tool-use',
623
+ 'Forward a PreToolUse hook event to Lumo (reads JSON from stdin)',
624
+ ],
625
+ [
626
+ 'post-tool-use',
627
+ 'Forward a PostToolUse hook event to Lumo (reads JSON from stdin)',
628
+ ],
629
+ [
630
+ 'post-tool-use-failure',
631
+ 'Forward a PostToolUseFailure hook event to Lumo (reads JSON from stdin)',
632
+ ],
633
+ [
634
+ 'user-prompt-submit',
635
+ 'Forward a UserPromptSubmit hook event to Lumo (reads JSON from stdin)',
636
+ ],
637
+ ['stop', 'Forward a Stop hook event to Lumo (reads JSON from stdin)'],
638
+ [
639
+ 'stop-failure',
640
+ 'Forward a StopFailure hook event to Lumo (reads JSON from stdin)',
641
+ ],
642
+ [
643
+ 'permission-request',
644
+ 'Forward a PermissionRequest hook event to Lumo (reads JSON from stdin)',
645
+ ],
646
+ [
647
+ 'permission-denied',
648
+ 'Forward a PermissionDenied hook event to Lumo (reads JSON from stdin)',
649
+ ],
650
+ [
651
+ 'session-start',
652
+ 'Forward a SessionStart hook event to Lumo (reads JSON from stdin)',
653
+ ],
654
+ [
655
+ 'session-end',
656
+ 'Forward a SessionEnd hook event to Lumo (reads JSON from stdin)',
657
+ ],
658
+ [
659
+ 'subagent-start',
660
+ 'Forward a SubagentStart hook event to Lumo (reads JSON from stdin)',
661
+ ],
662
+ [
663
+ 'subagent-stop',
664
+ 'Forward a SubagentStop hook event to Lumo (reads JSON from stdin)',
665
+ ],
666
+ [
667
+ 'worktree-create',
668
+ 'Forward a WorktreeCreate hook event to Lumo (reads JSON from stdin)',
669
+ ],
670
+ [
671
+ 'worktree-remove',
672
+ 'Forward a WorktreeRemove hook event to Lumo (reads JSON from stdin)',
673
+ ],
674
+ [
675
+ 'file-changed',
676
+ 'Forward a FileChanged hook event to Lumo (reads JSON from stdin)',
677
+ ],
678
+ [
679
+ 'config-change',
680
+ 'Forward a ConfigChange hook event to Lumo (reads JSON from stdin)',
681
+ ],
682
+ [
683
+ 'task-created',
684
+ 'Forward a TaskCreated hook event to Lumo (reads JSON from stdin)',
685
+ ],
686
+ [
687
+ 'task-completed',
688
+ 'Forward a TaskCompleted hook event to Lumo (reads JSON from stdin)',
689
+ ],
690
+ [
691
+ 'post-tool-batch',
692
+ 'Forward a PostToolBatch hook event to Lumo (reads JSON from stdin)',
693
+ ],
694
+ [
695
+ 'user-prompt-expansion',
696
+ 'Forward a UserPromptExpansion hook event to Lumo (reads JSON from stdin)',
697
+ ],
698
+ [
699
+ 'notification',
700
+ 'Forward a Notification hook event to Lumo (reads JSON from stdin)',
701
+ ],
702
+ [
703
+ 'elicitation',
704
+ 'Forward an Elicitation hook event to Lumo (reads JSON from stdin)',
705
+ ],
706
+ [
707
+ 'elicitation-result',
708
+ 'Forward an ElicitationResult hook event to Lumo (reads JSON from stdin)',
709
+ ],
710
+ [
711
+ 'cwd-changed',
712
+ 'Forward a CwdChanged hook event to Lumo (reads JSON from stdin)',
713
+ ],
714
+ [
715
+ 'instructions-loaded',
716
+ 'Forward an InstructionsLoaded hook event to Lumo (reads JSON from stdin)',
717
+ ],
718
+ ];
719
+ for (const [slug, description] of HOOK_SUBCOMMANDS) {
720
+ hook
721
+ .command(slug)
722
+ .description(description)
723
+ .option('--agent <token>', 'Coding agent that owns this session (e.g. claude-code, codex). Baked in by `lumo setup --agent`.')
724
+ .action(wrap((opts) => (0, hook_1.hookCommand)(slug, opts.agent)));
725
+ }
654
726
  if (!isUpdateCheckWorker) {
655
727
  program.parseAsync(process.argv).catch(err => {
656
728
  const msg = err instanceof Error ? err.message : String(err);
657
- console.error(`Error: ${msg}`);
729
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
658
730
  process.exit(1);
659
731
  });
660
732
  }
@@ -5,7 +5,7 @@
5
5
  * mirrored here. Keep in sync with prisma `enum TaskArtifactAgent`.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.VALID_AGENT_TOKENS = exports.AGENT_LABELS = void 0;
8
+ exports.VALID_AGENT_TOKENS = exports.ENUM_TO_TOKEN = exports.AGENT_LABELS = void 0;
9
9
  exports.normalizeAgent = normalizeAgent;
10
10
  /** enum value → display label (matches lib/i18n en.json taskArtifact.agent.*) */
11
11
  exports.AGENT_LABELS = {
@@ -16,6 +16,15 @@ exports.AGENT_LABELS = {
16
16
  GITHUB_COPILOT: 'GitHub Copilot',
17
17
  WINDSURF: 'Windsurf',
18
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
+ };
19
28
  /** accepted CLI token (normalized) → enum value */
20
29
  const TOKEN_TO_ENUM = {
21
30
  'claude-code': 'CLAUDE_CODE',
@@ -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
- return url && url.length > 0 ? url : DEFAULT_API_URL;
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 text = await args.readFile(args.file);
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) {
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.editInEditor = editInEditor;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ /**
42
+ * Open `initial` in the user's $EDITOR (fallback vi → nano), return the edited
43
+ * text. Degrades gracefully: when stdin is not a TTY (piped / agent) or the
44
+ * editor exits non-zero, the original text is returned unchanged so the caller
45
+ * can proceed without an interactive editor.
46
+ */
47
+ async function editInEditor(initial) {
48
+ if (!process.stdin.isTTY)
49
+ return initial;
50
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
51
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumo-wrap-'));
52
+ const file = path.join(dir, 'progress.md');
53
+ try {
54
+ fs.writeFileSync(file, initial, 'utf8');
55
+ const result = (0, child_process_1.spawnSync)(editor, [file], { stdio: 'inherit' });
56
+ if (result.status !== 0)
57
+ return initial;
58
+ return fs.readFileSync(file, 'utf8');
59
+ }
60
+ catch {
61
+ return initial;
62
+ }
63
+ finally {
64
+ fs.rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ }
@@ -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.resolveApiUrl)();
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: {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatTaskListTable = formatTaskListTable;
4
+ const sanitize_1 = require("./sanitize");
4
5
  /**
5
6
  * Render a task list as a fixed-width table. Each row:
6
7
  * LUM-42 IN_PROGRESS HIGH project-name Title here
@@ -14,8 +15,8 @@ function formatTaskListTable(tasks) {
14
15
  identifier: `${t.teamIdentifier}-${t.number}`,
15
16
  status: t.status,
16
17
  priority: t.priority,
17
- project: t.project.name,
18
- title: t.title,
18
+ project: (0, sanitize_1.sanitizeField)(t.project.name),
19
+ title: (0, sanitize_1.sanitizeField)(t.title),
19
20
  }));
20
21
  const widths = {
21
22
  identifier: Math.max(...rows.map(r => r.identifier.length)),