@lumoai/cli 1.5.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.
Files changed (81) hide show
  1. package/assets/skill.md +159 -15
  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/setup.js +33 -7
  34. package/dist/cli/src/commands/sprint-add.js +3 -3
  35. package/dist/cli/src/commands/sprint-close.js +5 -5
  36. package/dist/cli/src/commands/sprint-create.js +4 -4
  37. package/dist/cli/src/commands/sprint-delete.js +5 -5
  38. package/dist/cli/src/commands/sprint-list.js +3 -3
  39. package/dist/cli/src/commands/sprint-remove.js +3 -3
  40. package/dist/cli/src/commands/sprint-show.js +4 -4
  41. package/dist/cli/src/commands/sprint-start.js +4 -4
  42. package/dist/cli/src/commands/sprint-summary.js +7 -7
  43. package/dist/cli/src/commands/sprint-update.js +6 -5
  44. package/dist/cli/src/commands/task-artifact-add.js +17 -5
  45. package/dist/cli/src/commands/task-artifact-list.js +4 -4
  46. package/dist/cli/src/commands/task-artifact-rm.js +4 -4
  47. package/dist/cli/src/commands/task-artifact-show.js +8 -8
  48. package/dist/cli/src/commands/task-artifact-update.js +5 -5
  49. package/dist/cli/src/commands/task-comment-list.js +111 -0
  50. package/dist/cli/src/commands/task-comment.js +3 -3
  51. package/dist/cli/src/commands/task-context.js +24 -12
  52. package/dist/cli/src/commands/task-create.js +7 -7
  53. package/dist/cli/src/commands/task-figma-add.js +3 -2
  54. package/dist/cli/src/commands/task-figma-context.js +61 -0
  55. package/dist/cli/src/commands/task-figma-list.js +3 -2
  56. package/dist/cli/src/commands/task-figma-refresh.js +4 -3
  57. package/dist/cli/src/commands/task-figma-rm.js +3 -2
  58. package/dist/cli/src/commands/task-list.js +1 -2
  59. package/dist/cli/src/commands/task-pr-show.js +66 -0
  60. package/dist/cli/src/commands/task-show.js +8 -7
  61. package/dist/cli/src/commands/task-slack-show.js +59 -0
  62. package/dist/cli/src/commands/task-update.js +7 -7
  63. package/dist/cli/src/commands/task-web-show.js +64 -0
  64. package/dist/cli/src/commands/whoami.js +4 -3
  65. package/dist/cli/src/index.js +167 -102
  66. package/dist/cli/src/lib/agent.js +10 -1
  67. package/dist/cli/src/lib/api.js +81 -1
  68. package/dist/cli/src/lib/config.js +2 -1
  69. package/dist/cli/src/lib/doc-input.js +12 -1
  70. package/dist/cli/src/lib/figma-api.js +1 -1
  71. package/dist/cli/src/lib/format.js +3 -2
  72. package/dist/cli/src/lib/hook-runner.js +26 -10
  73. package/dist/cli/src/lib/hooks-template.js +52 -7
  74. package/dist/cli/src/lib/memory-content.js +4 -3
  75. package/dist/cli/src/lib/path-guard.js +125 -0
  76. package/dist/cli/src/lib/resolve-doc-id.js +2 -1
  77. package/dist/cli/src/lib/resolve-member.js +2 -1
  78. package/dist/cli/src/lib/sanitize.js +17 -0
  79. package/dist/cli/src/lib/tag-resolver.js +2 -1
  80. package/dist/cli/src/lib/update-check.js +2 -2
  81. 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 文档".'
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
 
@@ -129,6 +133,83 @@ The command prints a markdown document to stdout containing:
129
133
  - Focus on the **most recent 1–2 sessions** for relevant state; older sessions are for historical reference only
130
134
  - If there are **no prior sessions**, this is a fresh start — read the task description carefully and ask clarifying questions if needed
131
135
 
136
+ ## Context Retrieval (按需取全文)
137
+
138
+ LUM-122 split task context injection into tiers. `lumo task context <LUM-N>`
139
+ now emits a **cheap inline card** for each source — a short summary or just
140
+ metadata — instead of dumping full bodies. Slack, docs, artifacts, and comments
141
+ get an **LLM summary**; web links, Figma, and PRs get **metadata only**. Each
142
+ card ends with the **retrieval command** you run to pull the heavy content on
143
+ demand.
144
+
145
+ **How to use it:** when the inline card is not enough and you need the full
146
+ Slack thread, the web page body, the Figma metadata, the entire comment thread,
147
+ or the PR detail — run the matching command below. Pass the same identifier
148
+ (`LUM-N`) plus the id the card shows for that source (a Slack `contextId`, a web
149
+ `linkId`, a Figma `linkId`, or a PR `number`).
150
+
151
+ All five are **read-only** (no live Slack/GitHub/Figma calls except the web body
152
+ fetch). Web/Figma/PR are v1 metadata-degraded: they print a `note:` explaining
153
+ that live content needs an external integration.
154
+
155
+ ### `lumo task slack show <identifier> <contextId>` — full Slack thread snapshot
156
+
157
+ Prints the **stored** thread snapshot (no live Slack call), one line per message
158
+ as `author: text`. Author falls back to `@<userId>` when the display name is
159
+ missing. Empty snapshot prints `(no messages in stored snapshot)`.
160
+
161
+ ```bash
162
+ lumo task slack show LUM-42 ctx_abc123
163
+ ```
164
+
165
+ ### `lumo task web show <identifier> <linkId>` — fetched web link body
166
+
167
+ Fetches the page body on demand behind the SSRF guard (cached after first read)
168
+ and prints it as plain text. Empty body prints `(empty body)`. Fetch failures
169
+ (blocked host, timeout) print the server's error message.
170
+
171
+ ```bash
172
+ lumo task web show LUM-42 wl_abc123
173
+ ```
174
+
175
+ ### `lumo task figma context <identifier> <linkId>` — Figma link metadata
176
+
177
+ **v1 metadata fallback.** Prints the cached design metadata as `file:` /
178
+ `frame:` / `url:` / `synced:` (and `syncError:` if the last sync failed) lines.
179
+ Live design context (layers, variables, code connect) requires the Figma MCP
180
+ server, so the command ends with a `note:` saying so.
181
+
182
+ ```bash
183
+ lumo task figma context LUM-42 cfl_abc123
184
+ ```
185
+
186
+ ### `lumo task comments list <identifier>` — full comment thread
187
+
188
+ Prints the **entire** comment thread: each comment as `author · createdAt`
189
+ followed by its plain-text body (comment bodies are stored as HTML and stripped
190
+ to text). Replies are indented two spaces under their parent. Author falls back
191
+ to `unknown`. No comments prints `(no comments)`.
192
+
193
+ ```bash
194
+ lumo task comments list LUM-42
195
+ ```
196
+
197
+ **Plural, and distinct from `task comment`.** `task comments list` _reads_ the
198
+ whole thread (this retrieval command). `task comment <identifier> <body>`
199
+ _writes_ a single new comment (see Task Management). Don't confuse the two —
200
+ the plural `comments` is read-only.
201
+
202
+ ### `lumo task pr show <identifier> <number>` — synced PR metadata
203
+
204
+ **v1 metadata fallback.** Prints the synced PR record: a `#<number> (repo)
205
+ title` header, then `state:` (with ` · draft` when draft), `ci:`, `author:`,
206
+ `branch: <head> → <base>`, and `url:` lines. The live diff + review comments
207
+ require the GitHub integration, so the command ends with a `note:` saying so.
208
+
209
+ ```bash
210
+ lumo task pr show LUM-42 128
211
+ ```
212
+
132
213
  ## Task Management
133
214
 
134
215
  ### `lumo task create <title> [flags]` — create a new task
@@ -301,7 +382,7 @@ Record Claude Code spec-engineering products (spec / plan / design …) on a tas
301
382
 
302
383
  #### `lumo task artifact add <task> --kind <kind> --title <title> --file <path> --source <source> --agent <agent>`
303
384
 
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.
385
+ 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
386
 
306
387
  `--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
388
 
@@ -518,7 +599,7 @@ Use this when the user wants to write a new document from the terminal. Title is
518
599
  | `--tag-id <cuid>` | string (repeatable) | Attach tag by id. Combines with `--tag`. Max 20 per call. |
519
600
  | `--parent <doc>` | string | cuid or case-insensitive title. Files the new doc under this parent. Omit for root. |
520
601
 
521
- The three content channels (`--content`, `--file`, stdin) are mutually exclusive — specify at most one.
602
+ 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
603
 
523
604
  Examples:
524
605
 
@@ -566,6 +647,8 @@ The `Tags:` line is omitted when no tags were attached.
566
647
 
567
648
  `--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
649
 
650
+ 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.
651
+
569
652
  Examples:
570
653
 
571
654
  ```bash
@@ -734,6 +817,63 @@ lumo doc share-list "RFC"
734
817
  - 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
818
  - Before `doc unshare`, run `doc share-list` if the user hasn't named a specific member
736
819
 
820
+ ### `lumo doc import-gdoc <url> [--scope personal|workspace] [--task LUM-N]` — import a Google Doc
821
+
822
+ 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.
823
+
824
+ | Flag | Type | Notes |
825
+ | ----------------- | ------ | ------------------------------------------------------------------------------------------ |
826
+ | `--scope <scope>` | enum | `personal` (→ PRIVATE) or `workspace` (→ WORKSPACE). Omit to use the server default scope. |
827
+ | `--task <LUM-N>` | string | Bind the imported doc to this task immediately after import. |
828
+
829
+ **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.
830
+
831
+ Requires a connected Google Drive integration; connect it in the Web UI at `/settings/integrations`. There is no CLI `google auth` command.
832
+
833
+ ```bash
834
+ lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit"
835
+ lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit" --scope workspace --task LUM-127
836
+ ```
837
+
838
+ Output:
839
+
840
+ ```
841
+ Imported cmd_xxx "Quarterly Plan" https://www.uselumo.ai/workspace/lumo/documents/quarterly-plan-42
842
+ Note: imported content follows Lumo sharing and is no longer gated by Google permissions.
843
+ Bound cmd_xxx ↔ LUM-127
844
+ ```
845
+
846
+ The `Bound ... ↔ LUM-N` line appears only when `--task` is supplied.
847
+
848
+ ### When to suggest `doc import-gdoc`
849
+
850
+ - User pastes a Google Doc URL or says "import this Google Doc", "pull this gdoc into Lumo", "把这篇 Google 文档导入".
851
+ - 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`.
852
+
853
+ ### `lumo doc sync <doc>` — re-sync an imported doc from Google
854
+
855
+ 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.
856
+
857
+ 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.
858
+
859
+ `<doc>` accepts a doc cuid or a case-insensitive title; ambiguous titles fail with a candidate list — re-run with the cuid.
860
+
861
+ ```bash
862
+ lumo doc sync cmd_xxx
863
+ lumo doc sync "Quarterly Plan"
864
+ ```
865
+
866
+ Output:
867
+
868
+ ```
869
+ Synced cmd_xxx "Quarterly Plan" from Google
870
+ ```
871
+
872
+ ### When to suggest `doc sync`
873
+
874
+ - User says "re-sync the Google Doc", "pull the latest from Google", "refresh the imported doc", "更新一下从 Google 导入的文档".
875
+ - After the user mentions the Google Doc changed upstream. Warn first that local Lumo edits to that doc will be overwritten (one-way, destructive).
876
+
737
877
  ### Out of scope (CLI v1)
738
878
 
739
879
  The CLI does **not** currently support:
@@ -904,11 +1044,15 @@ lumo task memory list [LUM-N] [--category trap|decision|convention|procedural] [
904
1044
  lumo project memory list [<project>] [--category ...] [-n N]
905
1045
 
906
1046
  # 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
1047
+ lumo task memory add [LUM-N] --category trap --trigger "..." --outcome "..." [--workaround "..."] [--agent <agent>]
1048
+ lumo task memory add [LUM-N] --category decision --what "..." --why "..." [--alternatives "..."] [--implications "..."] [--agent <agent>]
1049
+ lumo task memory add [LUM-N] --category convention --rule "..." --applies "..." [--agent <agent>]
1050
+ lumo task memory add [LUM-N] --category procedural --workflow "..." --trigger "..." [--step "..." --step "..."] [--agent <agent>]
1051
+ lumo project memory add [<project>] --category ... [--agent <agent>] # same flags; records at PROJECT scope
1052
+
1053
+ # --agent values: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)
1054
+ # Aliases: gemini → gemini-cli, copilot → github-copilot (case-insensitive)
1055
+ # Omitting --agent records the memory as produced by Claude Code.
912
1056
 
913
1057
  # Single-memory ops (memoryId from `... memory list` column 1)
914
1058
  lumo memory promote <memoryId> # TASK → PROJECT
@@ -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
  }
@@ -6,14 +6,18 @@ 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
8
  const resolve_member_1 = require("../lib/resolve-member");
9
+ const sanitize_1 = require("../lib/sanitize");
9
10
  function formatShareListRows(rows) {
10
11
  if (rows.length === 0)
11
12
  return [];
12
- const nameWidth = Math.max(...rows.map(r => r.displayName.length));
13
- return rows.map(r => {
14
- const name = r.displayName.padEnd(nameWidth, ' ');
15
- return `${name} ${r.role}`;
16
- });
13
+ // Sanitize first, then measure/pad off the sanitized string so the
14
+ // column width matches the printed cell (control chars stripped).
15
+ const sanitized = rows.map(r => ({
16
+ name: (0, sanitize_1.sanitizeField)(r.displayName),
17
+ role: r.role,
18
+ }));
19
+ const nameWidth = Math.max(...sanitized.map(r => r.name.length));
20
+ return sanitized.map(r => `${r.name.padEnd(nameWidth, ' ')} ${r.role}`);
17
21
  }
18
22
  async function docShareList(docRef) {
19
23
  if (!docRef) {
@@ -25,8 +29,7 @@ async function docShareList(docRef) {
25
29
  console.error('Error: not logged in. Run `lumo auth login` first.');
26
30
  return 1;
27
31
  }
28
- const envUrl = process.env.LUMO_API_URL?.trim();
29
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
32
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
30
33
  const docId = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, docRef);
31
34
  if (!docId) {
32
35
  console.error(`Error: Document not found: ${docRef}`);
@@ -43,7 +46,7 @@ async function docShareList(docRef) {
43
46
  ]);
44
47
  if (!sharesRes.ok) {
45
48
  const text = await sharesRes.text();
46
- console.error(`Error: ${sharesRes.status} ${sharesRes.statusText}: ${text}`);
49
+ console.error(`Error: ${sharesRes.status} ${sharesRes.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
47
50
  return 1;
48
51
  }
49
52
  const { shares } = (await sharesRes.json());