@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
package/assets/skill.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化", "task artifact", "artifact add", "artifact list", "artifact show", "artifact rm", "artifact delete", "artifact update", "update artifact", "edit artifact", "change artifact kind", "change artifact source", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "编辑 artifact", "修改 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task memory", "project memory", "lumo memory".'
3
+ description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化", "task artifact", "artifact add", "artifact list", "artifact show", "artifact rm", "artifact delete", "artifact update", "update artifact", "edit artifact", "change artifact kind", "change artifact source", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "编辑 artifact", "修改 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task memory", "project memory", "lumo memory", "retrieval", "取全文", "load full content", "拉全文", "task slack show", "看 thread", "看 slack thread", "show slack thread", "slack 全文", "task web show", "show web link body", "web 正文", "抓网页正文", "task figma context", "figma metadata", "figma 元数据", "figma 设计上下文", "task comments list", "list comments", "看评论", "评论流", "task pr show", "查看 PR", "show pr", "PR 详情", "pr metadata", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入 google 文档", "同步 google 文档", "session wrap", "wrap up session", "收尾", "post progress", "把进度发出去", "progress comment", "进度评论".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -17,15 +17,18 @@ which lumo && lumo whoami
17
17
 
18
18
  ## Onboarding
19
19
 
20
- ### `lumo setup [--user|--project] [--force]` — install skill + hooks
20
+ ### `lumo setup [--user|--project] [--force] [--agent <token>]` — install skill + hooks
21
21
 
22
- Bootstraps Lumo into a Claude Code installation. Copies the bundled `SKILL.md` into `<scope>/.claude/skills/lumo/` and idempotently merges 25 hook entries into `<scope>/.claude/settings.json`. Existing user permissions and non-Lumo hook entries are preserved.
22
+ Bootstraps Lumo into a coding-agent installation. Copies the bundled `SKILL.md` into `<scope>/.claude/skills/lumo/` and idempotently merges 25 hook entries into `<scope>/.claude/settings.json`. Existing user permissions and non-Lumo hook entries are preserved.
23
+
24
+ `--agent <token>` records which coding agent these hooks run under and is **baked into every hook command** (`lumo hook <slug> --agent <token>`). Each hook then sends the agent to the server, where it's stored on the Session and inherited by auto-sedimented memories — so a memory is attributed to the agent that produced it instead of the default. Valid tokens: `claude-code | codex | cursor | gemini-cli | github-copilot | windsurf` (case-insensitive; `gemini` and `copilot` are accepted aliases). **Defaults to `claude-code`.** Re-running setup with a different `--agent` rewrites the token in place (no duplicate hook entries), and a legacy flagless entry is upgraded on the next run.
23
25
 
24
26
  ```bash
25
- npx @lumoai/cli setup # interactive: prompts user/project
26
- npx @lumoai/cli setup --user # write to ~/.claude/
27
- npx @lumoai/cli setup --project # write to ./.claude/
28
- npx @lumoai/cli setup --force # overwrite SKILL.md if it differs from bundled
27
+ npx @lumoai/cli setup # interactive: prompts user/project; agent=claude-code
28
+ npx @lumoai/cli setup --user # write to ~/.claude/
29
+ npx @lumoai/cli setup --project # write to ./.claude/
30
+ npx @lumoai/cli setup --force # overwrite SKILL.md if it differs from bundled
31
+ npx @lumoai/cli setup --agent codex # bake agent=codex into the hook commands
29
32
  ```
30
33
 
31
34
  Use when:
@@ -33,8 +36,9 @@ Use when:
33
36
  - The user asks "how do I set up lumo", "install the lumo skill", "wire up lumo hooks"
34
37
  - A fresh checkout of a project that uses Lumo doesn't have `.claude/skills/lumo/SKILL.md` yet
35
38
  - After `npm install -g @lumoai/cli`, before the first `lumo auth login`
39
+ - Wiring Lumo into a non-Claude-Code agent (Codex / Cursor / …) — pass `--agent <token>` so its memories are attributed correctly
36
40
 
37
- If stdin is not a TTY (CI, piped invocation), the default scope is `project` and no prompt is shown.
41
+ If stdin is not a TTY (CI, piped invocation), the default scope is `project` and no prompt is shown. An unrecognized `--agent` token aborts before writing anything.
38
42
 
39
43
  ## Authentication
40
44
 
@@ -118,17 +122,97 @@ The command prints a markdown document to stdout containing:
118
122
 
119
123
  1. **Task header** — identifier, title, status, description
120
124
  2. **Memory section** — cross-session learnings accumulated over prior sessions; treat as trusted background context
121
- 3. **Previous sessions** — ordered newest-first, each with:
125
+ 3. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below)
126
+ 4. **PR Review 待办** — mirrored PR review comments as a checkbox todo list: each line-level reviewer comment (shown as `` `file:line` `` + the reviewer's ask + a link to the GitHub comment) and each `changes_requested` review summary (shown as "🛑 整体要求改动"). Present only when the task's PR(s) have review comments. This same block is **auto-injected at session start** (alongside the memory section) when the session is bound to a task — so reviewer asks surface without re-running `task context`.
127
+ 5. **Previous sessions** — ordered newest-first, each with:
122
128
  - A headline summary of what was done
123
129
  - Unresolved items (carry-over TODOs from that session)
124
130
 
125
131
  ### How to use the context
126
132
 
127
133
  - **Unresolved items** from the most recent session are the highest-priority carry-overs — address them before starting new work unless the user says otherwise
134
+ - **PR Review 待办** items are reviewer-requested changes — treat each unchecked box as a TODO to resolve, then reply on the PR (a Lumo comment mirrors back to GitHub)
128
135
  - **Memory section** provides validated context that persists across sessions — use it to avoid re-learning decisions or constraints
129
136
  - Focus on the **most recent 1–2 sessions** for relevant state; older sessions are for historical reference only
130
137
  - If there are **no prior sessions**, this is a fresh start — read the task description carefully and ask clarifying questions if needed
131
138
 
139
+ ## Context Retrieval (按需取全文)
140
+
141
+ LUM-122 split task context injection into tiers. `lumo task context <LUM-N>`
142
+ now emits a **cheap inline card** for each source — a short summary or just
143
+ metadata — instead of dumping full bodies. Slack, docs, artifacts, and comments
144
+ get an **LLM summary**; web links, Figma, and PRs get **metadata only**. Each
145
+ card ends with the **retrieval command** you run to pull the heavy content on
146
+ demand.
147
+
148
+ **How to use it:** when the inline card is not enough and you need the full
149
+ Slack thread, the web page body, the Figma metadata, the entire comment thread,
150
+ or the PR detail — run the matching command below. Pass the same identifier
151
+ (`LUM-N`) plus the id the card shows for that source (a Slack `contextId`, a web
152
+ `linkId`, a Figma `linkId`, or a PR `number`).
153
+
154
+ All five are **read-only** (no live Slack/GitHub/Figma calls except the web body
155
+ fetch). Web/Figma/PR are v1 metadata-degraded: they print a `note:` explaining
156
+ that live content needs an external integration.
157
+
158
+ ### `lumo task slack show <identifier> <contextId>` — full Slack thread snapshot
159
+
160
+ Prints the **stored** thread snapshot (no live Slack call), one line per message
161
+ as `author: text`. Author falls back to `@<userId>` when the display name is
162
+ missing. Empty snapshot prints `(no messages in stored snapshot)`.
163
+
164
+ ```bash
165
+ lumo task slack show LUM-42 ctx_abc123
166
+ ```
167
+
168
+ ### `lumo task web show <identifier> <linkId>` — fetched web link body
169
+
170
+ Fetches the page body on demand behind the SSRF guard (cached after first read)
171
+ and prints it as plain text. Empty body prints `(empty body)`. Fetch failures
172
+ (blocked host, timeout) print the server's error message.
173
+
174
+ ```bash
175
+ lumo task web show LUM-42 wl_abc123
176
+ ```
177
+
178
+ ### `lumo task figma context <identifier> <linkId>` — Figma link metadata
179
+
180
+ **v1 metadata fallback.** Prints the cached design metadata as `file:` /
181
+ `frame:` / `url:` / `synced:` (and `syncError:` if the last sync failed) lines.
182
+ Live design context (layers, variables, code connect) requires the Figma MCP
183
+ server, so the command ends with a `note:` saying so.
184
+
185
+ ```bash
186
+ lumo task figma context LUM-42 cfl_abc123
187
+ ```
188
+
189
+ ### `lumo task comments list <identifier>` — full comment thread
190
+
191
+ Prints the **entire** comment thread: each comment as `author · createdAt`
192
+ followed by its plain-text body (comment bodies are stored as HTML and stripped
193
+ to text). Replies are indented two spaces under their parent. Author falls back
194
+ to `unknown`. No comments prints `(no comments)`.
195
+
196
+ ```bash
197
+ lumo task comments list LUM-42
198
+ ```
199
+
200
+ **Plural, and distinct from `task comment`.** `task comments list` _reads_ the
201
+ whole thread (this retrieval command). `task comment <identifier> <body>`
202
+ _writes_ a single new comment (see Task Management). Don't confuse the two —
203
+ the plural `comments` is read-only.
204
+
205
+ ### `lumo task pr show <identifier> <number>` — synced PR metadata
206
+
207
+ **v1 metadata fallback.** Prints the synced PR record: a `#<number> (repo)
208
+ title` header, then `state:` (with ` · draft` when draft), `ci:`, `author:`,
209
+ `branch: <head> → <base>`, and `url:` lines. The live diff + review comments
210
+ require the GitHub integration, so the command ends with a `note:` saying so.
211
+
212
+ ```bash
213
+ lumo task pr show LUM-42 128
214
+ ```
215
+
132
216
  ## Task Management
133
217
 
134
218
  ### `lumo task create <title> [flags]` — create a new task
@@ -301,7 +385,7 @@ Record Claude Code spec-engineering products (spec / plan / design …) on a tas
301
385
 
302
386
  #### `lumo task artifact add <task> --kind <kind> --title <title> --file <path> --source <source> --agent <agent>`
303
387
 
304
- Attaches an artifact to a task. `--kind`, `--title`, `--source` are stored verbatim — **`kind` is opaque** (no enumeration; `spec` / `plan` / `requirements` / anything is accepted). `--source` is **required** and names the spec-generation framework that produced the artifact, written as its **formal name** (`Superpowers` / `Spec Kit` / `BMad` / `OpenSpec` / `GSD` / …) — it is also opaque (no enumeration), but there is **no default**, so you must pass it. Quote names that contain spaces (`--source "Spec Kit"`). `--file` supplies the body (file contents). Each call appends to the end of the task's artifact list — call once per artifact (e.g. Superpowers: one `spec`, one `plan`). The `<task>` (e.g. `LUM-42`) is resolved server-side.
388
+ Attaches an artifact to a task. `--kind`, `--title`, `--source` are stored verbatim — **`kind` is opaque** (no enumeration; `spec` / `plan` / `requirements` / anything is accepted). `--source` is **required** and names the spec-generation framework that produced the artifact, written as its **formal name** (`Superpowers` / `Spec Kit` / `BMad` / `OpenSpec` / `GSD` / …) — it is also opaque (no enumeration), but there is **no default**, so you must pass it. Quote names that contain spaces (`--source "Spec Kit"`). `--file` supplies the body (file contents). Each call appends to the end of the task's artifact list — call once per artifact (e.g. Superpowers: one `spec`, one `plan`). The `<task>` (e.g. `LUM-42`) is resolved server-side. **`--file` is sandboxed:** the CLI rejects any path that resolves **outside the current project directory** (parent-traversal, absolute paths, escaping symlinks) or that matches a **sensitive-file denylist** (`.env*`, `id_rsa`/`id_ed25519`, `*.pem`/`*.key`, `.aws`/`.ssh` contents, `credentials`, `.npmrc`, …). There is no override flag — pass only project-local, non-secret files (e.g. `docs/spec.md`). To record an out-of-tree file, copy it into the project first.
305
389
 
306
390
  `--agent` is **required** and names the coding tool that produced the artifact (enum). Valid values: `claude-code | codex | cursor | gemini-cli | github-copilot | windsurf` (case-insensitive; `gemini` and `copilot` are accepted aliases).
307
391
 
@@ -518,7 +602,7 @@ Use this when the user wants to write a new document from the terminal. Title is
518
602
  | `--tag-id <cuid>` | string (repeatable) | Attach tag by id. Combines with `--tag`. Max 20 per call. |
519
603
  | `--parent <doc>` | string | cuid or case-insensitive title. Files the new doc under this parent. Omit for root. |
520
604
 
521
- The three content channels (`--content`, `--file`, stdin) are mutually exclusive — specify at most one.
605
+ The three content channels (`--content`, `--file`, stdin) are mutually exclusive — specify at most one. **`--file` is sandboxed:** the CLI rejects a path that resolves outside the current project directory (parent-traversal, absolute, escaping symlinks) or matches a sensitive-file denylist (`.env*`, private keys, `*.pem`/`*.key`, `credentials`, `.ssh`/`.aws` contents, …). Pass only project-local, non-secret files; there is no override flag.
522
606
 
523
607
  Examples:
524
608
 
@@ -566,6 +650,8 @@ The `Tags:` line is omitted when no tags were attached.
566
650
 
567
651
  `--tag` / `--tag-id` (bulk replace) are mutually exclusive with `--add-tag` / `--add-tag-id` / `--remove-tag` / `--remove-tag-id`. The CLI errors before any network call if both families are mixed.
568
652
 
653
+ Like `doc create`, `--file` is sandboxed: the CLI rejects paths that resolve outside the project directory or match the sensitive-file denylist (`.env*`, private keys, `credentials`, …). No override flag.
654
+
569
655
  Examples:
570
656
 
571
657
  ```bash
@@ -734,6 +820,63 @@ lumo doc share-list "RFC"
734
820
  - After `doc create --scope personal`, if the user mentions teammates needing access, suggest `doc share` rather than `doc update --scope workspace` when only specific members should see it
735
821
  - Before `doc unshare`, run `doc share-list` if the user hasn't named a specific member
736
822
 
823
+ ### `lumo doc import-gdoc <url> [--scope personal|workspace] [--task LUM-N]` — import a Google Doc
824
+
825
+ One-way import of a Google Doc into Lumo. The doc is exported from Google as markdown and turned into a native Lumo document (markdown → HTML), storing the source `googleDocId` and importer so it can be re-synced later (`lumo doc sync`). `<url>` accepts a Google Doc URL or a bare doc id.
826
+
827
+ | Flag | Type | Notes |
828
+ | ----------------- | ------ | ------------------------------------------------------------------------------------------ |
829
+ | `--scope <scope>` | enum | `personal` (→ PRIVATE) or `workspace` (→ WORKSPACE). Omit to use the server default scope. |
830
+ | `--task <LUM-N>` | string | Bind the imported doc to this task immediately after import. |
831
+
832
+ **Over-share note:** once imported, the content follows **Lumo's** sharing model (PRIVATE / SHARED / WORKSPACE) and is **no longer gated by Google permissions**. Importing a `workspace`-scoped doc can therefore expose it to everyone in the workspace even if the Google Doc was restricted — the command prints this reminder on success.
833
+
834
+ Requires a connected Google Drive integration; connect it in the Web UI at `/settings/integrations`. There is no CLI `google auth` command.
835
+
836
+ ```bash
837
+ lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit"
838
+ lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit" --scope workspace --task LUM-127
839
+ ```
840
+
841
+ Output:
842
+
843
+ ```
844
+ Imported cmd_xxx "Quarterly Plan" https://www.uselumo.ai/workspace/lumo/documents/quarterly-plan-42
845
+ Note: imported content follows Lumo sharing and is no longer gated by Google permissions.
846
+ Bound cmd_xxx ↔ LUM-127
847
+ ```
848
+
849
+ The `Bound ... ↔ LUM-N` line appears only when `--task` is supplied.
850
+
851
+ ### When to suggest `doc import-gdoc`
852
+
853
+ - User pastes a Google Doc URL or says "import this Google Doc", "pull this gdoc into Lumo", "把这篇 Google 文档导入".
854
+ - User wants a Google Doc tracked alongside Lumo tasks/docs — suggest import (with `--task LUM-N` if a task is in context) and remind them about the over-share semantics for `--scope workspace`.
855
+
856
+ ### `lumo doc sync <doc>` — re-sync an imported doc from Google
857
+
858
+ Re-imports a previously imported Google Doc and **overwrites the Lumo body** with the current Google content. **One-way and destructive** — any edits made to the doc inside Lumo are discarded; Google is the source of truth for synced docs.
859
+
860
+ Sync always runs as the **importer** (owner model): it re-exports using the importer's stored Google token, not the token of whoever runs the command. If the importer has lost access to the Google Doc, sync fails with a clear error.
861
+
862
+ `<doc>` accepts a doc cuid or a case-insensitive title; ambiguous titles fail with a candidate list — re-run with the cuid.
863
+
864
+ ```bash
865
+ lumo doc sync cmd_xxx
866
+ lumo doc sync "Quarterly Plan"
867
+ ```
868
+
869
+ Output:
870
+
871
+ ```
872
+ Synced cmd_xxx "Quarterly Plan" from Google
873
+ ```
874
+
875
+ ### When to suggest `doc sync`
876
+
877
+ - User says "re-sync the Google Doc", "pull the latest from Google", "refresh the imported doc", "更新一下从 Google 导入的文档".
878
+ - After the user mentions the Google Doc changed upstream. Warn first that local Lumo edits to that doc will be overwritten (one-way, destructive).
879
+
737
880
  ### Out of scope (CLI v1)
738
881
 
739
882
  The CLI does **not** currently support:
@@ -904,11 +1047,15 @@ lumo task memory list [LUM-N] [--category trap|decision|convention|procedural] [
904
1047
  lumo project memory list [<project>] [--category ...] [-n N]
905
1048
 
906
1049
  # Add (per-category fields; <task>/<project> default to the session-bound task)
907
- lumo task memory add [LUM-N] --category trap --trigger "..." --outcome "..." [--workaround "..."]
908
- lumo task memory add [LUM-N] --category decision --what "..." --why "..." [--alternatives "..."] [--implications "..."]
909
- lumo task memory add [LUM-N] --category convention --rule "..." --applies "..."
910
- lumo task memory add [LUM-N] --category procedural --workflow "..." --trigger "..." [--step "..." --step "..."]
911
- lumo project memory add [<project>] --category ... # same flags; records at PROJECT scope
1050
+ lumo task memory add [LUM-N] --category trap --trigger "..." --outcome "..." [--workaround "..."] [--agent <agent>]
1051
+ lumo task memory add [LUM-N] --category decision --what "..." --why "..." [--alternatives "..."] [--implications "..."] [--agent <agent>]
1052
+ lumo task memory add [LUM-N] --category convention --rule "..." --applies "..." [--agent <agent>]
1053
+ lumo task memory add [LUM-N] --category procedural --workflow "..." --trigger "..." [--step "..." --step "..."] [--agent <agent>]
1054
+ lumo project memory add [<project>] --category ... [--agent <agent>] # same flags; records at PROJECT scope
1055
+
1056
+ # --agent values: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)
1057
+ # Aliases: gemini → gemini-cli, copilot → github-copilot (case-insensitive)
1058
+ # Omitting --agent records the memory as produced by Claude Code.
912
1059
 
913
1060
  # Single-memory ops (memoryId from `... memory list` column 1)
914
1061
  lumo memory promote <memoryId> # TASK → PROJECT
@@ -995,6 +1142,32 @@ lumo session detach
995
1142
 
996
1143
  When to suggest: the user wants to stop tagging the current session with the active task (e.g., switching to unrelated exploratory work without binding to a different task).
997
1144
 
1145
+ ### `lumo session wrap [--yes] [--dry-run]` — draft + post a progress comment at wrap-up
1146
+
1147
+ Session-end wrap-up panel. Reads back the current Claude Code session's per-turn
1148
+ `turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
1149
+ every turn **since the last progress comment** into one bulleted body, and — after
1150
+ a `[y] 发送 / [e] 编辑 / [s] 跳过` confirmation — posts it as a comment on the
1151
+ session's bound task. A server-side watermark (`Session.lastProgressCommentAt`)
1152
+ means re-running never re-posts the same turns.
1153
+
1154
+ ```bash
1155
+ lumo session wrap # interactive: preview draft, choose y / e / s
1156
+ lumo session wrap --yes # post the drafted body without prompting (agent-friendly)
1157
+ lumo session wrap --dry-run # print the draft only; never posts, never advances watermark
1158
+ ```
1159
+
1160
+ - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
1161
+ task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
1162
+ summaries, the panel prints "(无内容)" and posts nothing.
1163
+ - `[e] 编辑` opens `$EDITOR` (fallback vi/nano) on the drafted body; the edited
1164
+ text is posted and the watermark still advances to the turns the draft covered.
1165
+ - Non-TTY without `--yes`: prints the draft and does **not** post (safe default).
1166
+
1167
+ When to suggest: at the end of a working session on a bound task, to record what
1168
+ was done as a progress comment — offer `lumo session wrap` rather than composing
1169
+ a `task comment` by hand.
1170
+
998
1171
  ### When to suggest session binding
999
1172
 
1000
1173
  - If the user mentions a task ID (e.g., "let's work on LUM-42") and no session is currently bound, **suggest running `lumo session attach`**.
@@ -5,6 +5,7 @@ const prompt_1 = require("../lib/prompt");
5
5
  const browser_1 = require("../lib/browser");
6
6
  const api_1 = require("../lib/api");
7
7
  const config_1 = require("../lib/config");
8
+ const sanitize_1 = require("../lib/sanitize");
8
9
  const KEY_PREFIX = 'lum_';
9
10
  async function authLogin() {
10
11
  const apiUrl = (0, api_1.resolveApiUrl)();
@@ -49,7 +50,7 @@ async function authLogin() {
49
50
  apiKeyPrefix: resp.apiKey.prefix,
50
51
  });
51
52
  console.log('');
52
- console.log(`✓ Logged in as ${resp.user.email}`);
53
- console.log(` Workspace: ${resp.workspace.name}`);
54
- console.log(` Key: ${resp.apiKey.name} (${resp.apiKey.prefix})`);
53
+ console.log(`✓ Logged in as ${(0, sanitize_1.sanitizeField)(resp.user.email)}`);
54
+ console.log(` Workspace: ${(0, sanitize_1.sanitizeField)(resp.workspace.name)}`);
55
+ console.log(` Key: ${(0, sanitize_1.sanitizeField)(resp.apiKey.name)} (${resp.apiKey.prefix})`);
55
56
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.authLogout = authLogout;
4
4
  const config_1 = require("../lib/config");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  async function authLogout() {
6
7
  const creds = (0, config_1.readCredentials)();
7
8
  if (!creds) {
@@ -10,5 +11,5 @@ async function authLogout() {
10
11
  }
11
12
  const email = creds.email;
12
13
  (0, config_1.deleteCredentials)();
13
- console.log(`✓ Logged out (${email})`);
14
+ console.log(`✓ Logged out (${(0, sanitize_1.sanitizeField)(email)})`);
14
15
  }
@@ -5,6 +5,7 @@ exports.docBind = docBind;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
7
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
8
+ const sanitize_1 = require("../lib/sanitize");
8
9
  function formatBindOutput(args) {
9
10
  if (args.alreadyBound)
10
11
  return `Already bound ${args.docId} ↔ ${args.identifier}`;
@@ -21,8 +22,7 @@ async function docBind(docRef, task) {
21
22
  console.error('Error: not logged in. Run `lumo auth login` first.');
22
23
  return 1;
23
24
  }
24
- const envUrl = process.env.LUMO_API_URL?.trim();
25
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
25
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
26
26
  const docId = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, docRef);
27
27
  if (!docId) {
28
28
  console.error(`Error: Document not found: ${docRef}`);
@@ -39,7 +39,7 @@ async function docBind(docRef, task) {
39
39
  });
40
40
  if (!res.ok) {
41
41
  const text = await res.text();
42
- console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
42
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
43
43
  return 1;
44
44
  }
45
45
  const { mention } = (await res.json());
@@ -8,6 +8,7 @@ const api_1 = require("../lib/api");
8
8
  const doc_input_1 = require("../lib/doc-input");
9
9
  const tag_resolver_1 = require("../lib/tag-resolver");
10
10
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
11
+ const sanitize_1 = require("../lib/sanitize");
11
12
  /** personal → PRIVATE, workspace → WORKSPACE. Null on unknown. */
12
13
  function normalizeScope(value) {
13
14
  const lower = (value ?? '').toLowerCase();
@@ -18,10 +19,10 @@ function normalizeScope(value) {
18
19
  return null;
19
20
  }
20
21
  function formatCreatedDocLine(doc) {
21
- const escaped = doc.title.replace(/"/g, '\\"');
22
+ const escaped = (0, sanitize_1.sanitizeField)(doc.title).replace(/"/g, '\\"');
22
23
  const head = `Created ${doc.id} "${escaped}" ${doc.url}`;
23
24
  if (doc.tags && doc.tags.length > 0) {
24
- return `${head}\nTags: ${doc.tags.join(', ')}`;
25
+ return `${head}\nTags: ${doc.tags.map(sanitize_1.sanitizeField).join(', ')}`;
25
26
  }
26
27
  return head;
27
28
  }
@@ -54,8 +55,7 @@ async function docCreate(title, opts) {
54
55
  console.error(`Error: ${content.message}`);
55
56
  return 1;
56
57
  }
57
- const envUrl = process.env.LUMO_API_URL?.trim();
58
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
58
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
59
59
  const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents`;
60
60
  let tagIds;
61
61
  if ((opts.tag && opts.tag.length > 0) ||
@@ -106,7 +106,7 @@ async function docCreate(title, opts) {
106
106
  }
107
107
  if (!res.ok) {
108
108
  const text = await res.text();
109
- console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
109
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
110
110
  return 1;
111
111
  }
112
112
  const { document } = (await res.json());
@@ -5,6 +5,7 @@ const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const resolve_doc_1 = require("../lib/resolve-doc");
7
7
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
8
+ const sanitize_1 = require("../lib/sanitize");
8
9
  async function docDelete(reference, opts) {
9
10
  if (!reference) {
10
11
  console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --yes');
@@ -19,8 +20,7 @@ async function docDelete(reference, opts) {
19
20
  console.error('Error: not logged in. Run `lumo auth login` first.');
20
21
  return 1;
21
22
  }
22
- const envUrl = process.env.LUMO_API_URL?.trim();
23
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
23
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
24
24
  // For a cuid reference we don't have the title; for a title we look up id+title at once.
25
25
  let id;
26
26
  let title = '';
@@ -44,9 +44,9 @@ async function docDelete(reference, opts) {
44
44
  });
45
45
  if (!res.ok) {
46
46
  const text = await res.text();
47
- console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
47
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
48
48
  return 1;
49
49
  }
50
- const escaped = title.replace(/"/g, '\\"');
50
+ const escaped = (0, sanitize_1.sanitizeField)(title).replace(/"/g, '\\"');
51
51
  console.log(`Deleted ${id}${title ? ` "${escaped}"` : ''}`);
52
52
  }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatImportedLine = formatImportedLine;
4
+ exports.docImportGdoc = docImportGdoc;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ function formatImportedLine(doc) {
9
+ const escaped = (0, sanitize_1.sanitizeField)(doc.title).replace(/"/g, '\\"');
10
+ return (`Imported ${doc.id} "${escaped}" ${doc.url}\n` +
11
+ `Note: imported content follows Lumo sharing and is no longer gated by Google permissions.`);
12
+ }
13
+ function normalizeScope(value) {
14
+ if (value === undefined)
15
+ return undefined;
16
+ const lower = value.toLowerCase();
17
+ if (lower === 'personal')
18
+ return 'PRIVATE';
19
+ if (lower === 'workspace')
20
+ return 'WORKSPACE';
21
+ return undefined;
22
+ }
23
+ async function docImportGdoc(url, opts) {
24
+ const creds = (0, config_1.readCredentials)();
25
+ if (!creds) {
26
+ console.error('Error: not logged in. Run `lumo auth login` first.');
27
+ return 1;
28
+ }
29
+ if (!url || url.trim().length === 0) {
30
+ console.error('Error: a Google Doc URL or id is required.');
31
+ return 1;
32
+ }
33
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
34
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
35
+ const body = { url: url.trim() };
36
+ const scope = normalizeScope(opts.scope);
37
+ if (scope)
38
+ body.visibility = scope;
39
+ let res;
40
+ try {
41
+ res = await fetch(`${base}/api/documents/import-google`, {
42
+ method: 'POST',
43
+ headers: {
44
+ Authorization: `Bearer ${creds.token}`,
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ body: JSON.stringify(body),
48
+ });
49
+ }
50
+ catch (err) {
51
+ console.error(`Error: network failure: ${err.message}`);
52
+ return 1;
53
+ }
54
+ if (!res.ok) {
55
+ const text = await res.text();
56
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
57
+ return 1;
58
+ }
59
+ const { document } = (await res.json());
60
+ const fullUrl = `${base}/workspace/${creds.workspaceSlug ?? 'lumo'}/documents/${document.slug}`;
61
+ console.log(formatImportedLine({
62
+ id: document.id,
63
+ title: document.title,
64
+ url: fullUrl,
65
+ }));
66
+ if (opts.task) {
67
+ const bindRes = await fetch(`${base}/api/documents/${document.id}/mentions`, {
68
+ method: 'POST',
69
+ headers: {
70
+ Authorization: `Bearer ${creds.token}`,
71
+ 'Content-Type': 'application/json',
72
+ },
73
+ body: JSON.stringify({ taskIdentifier: opts.task }),
74
+ });
75
+ if (!bindRes.ok) {
76
+ const text = await bindRes.text();
77
+ console.error(`Warning: imported but bind to ${opts.task} failed: ${bindRes.status} ${(0, sanitize_1.sanitizeField)(text)}`);
78
+ return 1;
79
+ }
80
+ console.log(`Bound ${document.id} ↔ ${opts.task}`);
81
+ }
82
+ }
@@ -7,6 +7,7 @@ const config_1 = require("../lib/config");
7
7
  const api_1 = require("../lib/api");
8
8
  const doc_create_1 = require("./doc-create");
9
9
  const doc_tree_1 = require("../lib/doc-tree");
10
+ const sanitize_1 = require("../lib/sanitize");
10
11
  function visibilityLabel(v) {
11
12
  if (v === 'PRIVATE')
12
13
  return 'PERSONAL';
@@ -17,8 +18,8 @@ function formatDocListRows(rows) {
17
18
  return [];
18
19
  return rows.map(r => {
19
20
  const label = visibilityLabel(r.visibility).padEnd(10, ' ');
20
- const project = (r.project?.name ?? '-').padEnd(14, ' ');
21
- return `${r.id} ${label} ${project} ${r.title}`;
21
+ const project = (0, sanitize_1.sanitizeField)(r.project?.name ?? '-').padEnd(14, ' ');
22
+ return `${r.id} ${label} ${project} ${(0, sanitize_1.sanitizeField)(r.title)}`;
22
23
  });
23
24
  }
24
25
  function formatDocListRowsAsTree(rows) {
@@ -28,9 +29,9 @@ function formatDocListRowsAsTree(rows) {
28
29
  const flat = (0, doc_tree_1.flattenWithDepth)(tree);
29
30
  return flat.map(({ row, depth }) => {
30
31
  const label = visibilityLabel(row.visibility).padEnd(10, ' ');
31
- const project = (row.project?.name ?? '-').padEnd(14, ' ');
32
+ const project = (0, sanitize_1.sanitizeField)(row.project?.name ?? '-').padEnd(14, ' ');
32
33
  const indent = ' '.repeat(depth);
33
- return `${row.id} ${label} ${project} ${indent}${row.title}`;
34
+ return `${row.id} ${label} ${project} ${indent}${(0, sanitize_1.sanitizeField)(row.title)}`;
34
35
  });
35
36
  }
36
37
  async function docList(opts) {
@@ -39,8 +40,7 @@ async function docList(opts) {
39
40
  console.error('Error: not logged in. Run `lumo auth login` first.');
40
41
  return 1;
41
42
  }
42
- const envUrl = process.env.LUMO_API_URL?.trim();
43
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
43
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
44
44
  let url;
45
45
  if (opts.task) {
46
46
  url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/${opts.task}/documents`;
@@ -65,7 +65,7 @@ async function docList(opts) {
65
65
  });
66
66
  if (!res.ok) {
67
67
  const text = await res.text();
68
- console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
68
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
69
69
  return 1;
70
70
  }
71
71
  const { documents } = (await res.json());
@@ -5,6 +5,7 @@ const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const resolve_doc_1 = require("../lib/resolve-doc");
7
7
  const doc_sort_order_1 = require("../lib/doc-sort-order");
8
+ const sanitize_1 = require("../lib/sanitize");
8
9
  async function docMove(reference, opts) {
9
10
  if (!reference) {
10
11
  console.error('Error: usage: lumo doc move <doc> --parent <doc> | --root');
@@ -25,8 +26,7 @@ async function docMove(reference, opts) {
25
26
  console.error('Error: not logged in. Run `lumo auth login` first.');
26
27
  return 1;
27
28
  }
28
- const envUrl = process.env.LUMO_API_URL?.trim();
29
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
29
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
30
30
  const listUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents`;
31
31
  let listRes;
32
32
  try {
@@ -40,7 +40,7 @@ async function docMove(reference, opts) {
40
40
  }
41
41
  if (!listRes.ok) {
42
42
  const text = await listRes.text();
43
- console.error(`Error: ${listRes.status} ${listRes.statusText}: ${text}`);
43
+ console.error(`Error: ${listRes.status} ${listRes.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
44
44
  return 1;
45
45
  }
46
46
  const { documents } = (await listRes.json());
@@ -48,7 +48,7 @@ async function docMove(reference, opts) {
48
48
  if (docMatch.kind === 'ambiguous') {
49
49
  console.error(`Error: title "${reference}" matches ${docMatch.candidates.length} docs:`);
50
50
  for (const c of docMatch.candidates) {
51
- console.error(` ${c.id} ${c.title}`);
51
+ console.error(` ${c.id} ${(0, sanitize_1.sanitizeField)(c.title)}`);
52
52
  }
53
53
  console.error('Re-run with the cuid.');
54
54
  return 1;
@@ -65,7 +65,7 @@ async function docMove(reference, opts) {
65
65
  if (parentMatch.kind === 'ambiguous') {
66
66
  console.error(`Error: --parent "${opts.parent}" matches ${parentMatch.candidates.length} docs:`);
67
67
  for (const c of parentMatch.candidates) {
68
- console.error(` ${c.id} ${c.title}`);
68
+ console.error(` ${c.id} ${(0, sanitize_1.sanitizeField)(c.title)}`);
69
69
  }
70
70
  console.error('Re-run with the cuid.');
71
71
  return 1;
@@ -76,7 +76,7 @@ async function docMove(reference, opts) {
76
76
  }
77
77
  const parentRow = documents.find(d => d.id === parentMatch.doc.id);
78
78
  newParentId = parentRow.id;
79
- parentTitleForOutput = `"${parentRow.title.replace(/"/g, '\\"')}"`;
79
+ parentTitleForOutput = `"${(0, sanitize_1.sanitizeField)(parentRow.title).replace(/"/g, '\\"')}"`;
80
80
  }
81
81
  const siblings = documents.filter(d => d.parentId === newParentId && d.id !== docRow.id);
82
82
  const sortOrder = (0, doc_sort_order_1.pickNextSortOrder)(siblings);
@@ -105,9 +105,9 @@ async function docMove(reference, opts) {
105
105
  msg = json.error;
106
106
  }
107
107
  catch { }
108
- console.error(`Error: ${moveRes.status} ${moveRes.statusText}: ${msg}`);
108
+ console.error(`Error: ${moveRes.status} ${moveRes.statusText}: ${(0, sanitize_1.sanitizeField)(msg)}`);
109
109
  return 1;
110
110
  }
111
- const escapedDocTitle = docRow.title.replace(/"/g, '\\"');
111
+ const escapedDocTitle = (0, sanitize_1.sanitizeField)(docRow.title).replace(/"/g, '\\"');
112
112
  console.log(`Moved ${docRow.id} "${escapedDocTitle}" → ${parentTitleForOutput}`);
113
113
  }