@lumoai/cli 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/assets/skill.md +228 -17
  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 +86 -0
  19. package/dist/cli/src/commands/memory-project-list.js +58 -0
  20. package/dist/cli/src/commands/memory-promote.js +52 -0
  21. package/dist/cli/src/commands/memory-rm.js +42 -0
  22. package/dist/cli/src/commands/memory-task-add.js +99 -0
  23. package/dist/cli/src/commands/memory-task-list.js +61 -0
  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 +35 -6
  45. package/dist/cli/src/commands/task-artifact-list.js +5 -3
  46. package/dist/cli/src/commands/task-artifact-rm.js +4 -4
  47. package/dist/cli/src/commands/task-artifact-show.js +9 -7
  48. package/dist/cli/src/commands/task-artifact-update.js +15 -6
  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 +239 -102
  66. package/dist/cli/src/lib/agent.js +58 -0
  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 +88 -0
  75. package/dist/cli/src/lib/path-guard.js +125 -0
  76. package/dist/cli/src/lib/resolve-bound-task.js +31 -0
  77. package/dist/cli/src/lib/resolve-doc-id.js +2 -1
  78. package/dist/cli/src/lib/resolve-member.js +2 -1
  79. package/dist/cli/src/lib/resolve-project.js +24 -0
  80. package/dist/cli/src/lib/sanitize.js +17 -0
  81. package/dist/cli/src/lib/tag-resolver.js +2 -1
  82. package/dist/cli/src/lib/update-check.js +2 -2
  83. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.taskContext = taskContext;
4
4
  exports.formatTaskContextMarkdown = formatTaskContextMarkdown;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
7
8
  async function taskContext(identifier) {
8
9
  if (!identifier) {
9
10
  console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
@@ -14,8 +15,7 @@ async function taskContext(identifier) {
14
15
  console.error('Error: not logged in. Run `lumo auth login` first.');
15
16
  return 1;
16
17
  }
17
- const envUrl = process.env.LUMO_API_URL?.trim();
18
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
18
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
19
19
  const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/context/${encodeURIComponent(identifier)}`;
20
20
  let res;
21
21
  try {
@@ -52,39 +52,51 @@ async function taskContext(identifier) {
52
52
  function formatTaskContextMarkdown(data, now) {
53
53
  const lines = [];
54
54
  lines.push(`# Task: ${data.task.identifier}`);
55
- lines.push(`**Title**: ${data.task.title}`);
55
+ lines.push(`**Title**: ${(0, sanitize_1.sanitizeField)(data.task.title)}`);
56
56
  lines.push(`**Status**: ${data.task.status}`);
57
57
  if (data.task.milestone) {
58
58
  const target = data.task.milestone.targetDate
59
59
  ? `, target ${data.task.milestone.targetDate.slice(0, 10)}`
60
60
  : '';
61
- lines.push(`**Milestone**: ${data.task.milestone.name} (${data.task.milestone.status}${target})`);
61
+ lines.push(`**Milestone**: ${(0, sanitize_1.sanitizeField)(data.task.milestone.name)} (${data.task.milestone.status}${target})`);
62
62
  }
63
63
  const body = data.task.descriptionMarkdown ?? data.task.description;
64
64
  if (body && body.trim().length > 0) {
65
- lines.push(`**Description**: ${body}`);
65
+ lines.push(`**Description**: ${(0, sanitize_1.sanitizeField)(body)}`);
66
66
  }
67
67
  lines.push('');
68
68
  // Frontload memory before sessions: it's cold context the agent should see
69
69
  // first. Server returns "" when empty, in which case we skip the section.
70
70
  if (data.memorySection && data.memorySection.trim().length > 0) {
71
- lines.push(data.memorySection.trimEnd());
71
+ lines.push((0, sanitize_1.sanitizeField)(data.memorySection.trimEnd()));
72
72
  lines.push('');
73
73
  }
74
74
  if (data.slackContextSection && data.slackContextSection.trim().length > 0) {
75
- lines.push(data.slackContextSection.trimEnd());
75
+ lines.push((0, sanitize_1.sanitizeField)(data.slackContextSection.trimEnd()));
76
76
  lines.push('');
77
77
  }
78
78
  if (data.webLinkSection && data.webLinkSection.trim().length > 0) {
79
- lines.push(data.webLinkSection.trimEnd());
79
+ lines.push((0, sanitize_1.sanitizeField)(data.webLinkSection.trimEnd()));
80
80
  lines.push('');
81
81
  }
82
82
  if (data.figmaSection && data.figmaSection.trim().length > 0) {
83
- lines.push(data.figmaSection.trimEnd());
83
+ lines.push((0, sanitize_1.sanitizeField)(data.figmaSection.trimEnd()));
84
84
  lines.push('');
85
85
  }
86
86
  if (data.artifactSection && data.artifactSection.trim().length > 0) {
87
- lines.push(data.artifactSection.trimEnd());
87
+ lines.push((0, sanitize_1.sanitizeField)(data.artifactSection.trimEnd()));
88
+ lines.push('');
89
+ }
90
+ if (data.docSection && data.docSection.trim().length > 0) {
91
+ lines.push((0, sanitize_1.sanitizeField)(data.docSection.trimEnd()));
92
+ lines.push('');
93
+ }
94
+ if (data.commentsSection && data.commentsSection.trim().length > 0) {
95
+ lines.push((0, sanitize_1.sanitizeField)(data.commentsSection.trimEnd()));
96
+ lines.push('');
97
+ }
98
+ if (data.prSection && data.prSection.trim().length > 0) {
99
+ lines.push((0, sanitize_1.sanitizeField)(data.prSection.trimEnd()));
88
100
  lines.push('');
89
101
  }
90
102
  if (data.sessions.length === 0) {
@@ -101,11 +113,11 @@ function formatTaskContextMarkdown(data, now) {
101
113
  const ago = relativeTime(new Date(s.lastActivityAt), now);
102
114
  const dur = formatDuration(s.durationMs);
103
115
  lines.push(`### Session ${shortId} · ${ago} · ${dur}`);
104
- lines.push(`**Summary**: ${s.headline}`);
116
+ lines.push(`**Summary**: ${(0, sanitize_1.sanitizeField)(s.headline)}`);
105
117
  if (s.unresolved.length > 0) {
106
118
  lines.push('**Unresolved**:');
107
119
  for (const u of s.unresolved)
108
- lines.push(`- ${u}`);
120
+ lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
109
121
  }
110
122
  lines.push('');
111
123
  }
@@ -8,6 +8,7 @@ const config_1 = require("../lib/config");
8
8
  const api_1 = require("../lib/api");
9
9
  const tag_resolver_1 = require("../lib/tag-resolver");
10
10
  const resolve_1 = require("../lib/resolve");
11
+ const sanitize_1 = require("../lib/sanitize");
11
12
  const ALLOWED_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
12
13
  /**
13
14
  * Assert that the sprint belongs to the same team as the newly created task.
@@ -81,10 +82,10 @@ function normalizePriority(value) {
81
82
  * present.
82
83
  */
83
84
  function formatCreatedTaskLine(task) {
84
- const escapedTitle = task.title.replace(/"/g, '\\"');
85
+ const escapedTitle = (0, sanitize_1.sanitizeField)(task.title).replace(/"/g, '\\"');
85
86
  const head = `Created ${task.identifier} "${escapedTitle}" ${task.url}`;
86
87
  if (task.tags && task.tags.length > 0) {
87
- return `${head}\nTags: ${task.tags.join(', ')}`;
88
+ return `${head}\nTags: ${task.tags.map(sanitize_1.sanitizeField).join(', ')}`;
88
89
  }
89
90
  return head;
90
91
  }
@@ -108,8 +109,7 @@ async function taskCreate(title, opts) {
108
109
  }
109
110
  priority = normalized;
110
111
  }
111
- const envUrl = process.env.LUMO_API_URL?.trim();
112
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
112
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
113
113
  const base = (0, api_1.trimTrailingSlash)(apiUrl);
114
114
  const url = `${base}/api/tasks`;
115
115
  let tagIds;
@@ -166,7 +166,7 @@ async function taskCreate(title, opts) {
166
166
  const workspaceSlug = creds.workspaceSlug ?? '';
167
167
  try {
168
168
  const sprint = await bindTaskToSprint(base, creds.token, workspaceSlug, opts.sprint, { id: data.task.id, teamId: data.task.teamId, identifier: data.task.identifier });
169
- process.stdout.write(`Sprint: #${sprint.number} "${sprint.name}"\n`);
169
+ process.stdout.write(`Sprint: #${sprint.number} "${(0, sanitize_1.sanitizeField)(sprint.name)}"\n`);
170
170
  }
171
171
  catch (err) {
172
172
  const msg = err instanceof Error ? err.message : String(err);
@@ -175,7 +175,7 @@ async function taskCreate(title, opts) {
175
175
  console.error(msg);
176
176
  }
177
177
  else {
178
- console.error(`Error: ${msg}`);
178
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(msg)}`);
179
179
  }
180
180
  return 1;
181
181
  }
@@ -193,7 +193,7 @@ async function taskCreate(title, opts) {
193
193
  // Body wasn't JSON; fall through to status-only message
194
194
  }
195
195
  if (serverMsg) {
196
- console.error(`Error: ${serverMsg}`);
196
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
197
197
  }
198
198
  else {
199
199
  console.error(`Error: task create failed (HTTP ${res.status})`);
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskFigmaAdd = taskFigmaAdd;
4
4
  const figma_api_1 = require("../lib/figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  async function taskFigmaAdd(args) {
6
7
  try {
7
8
  const { link } = await (0, figma_api_1.addFigmaLink)(args.identifier, args.url);
8
- const label = link.frameName
9
+ const label = (0, sanitize_1.sanitizeField)(link.frameName
9
10
  ? `${link.fileName} · ${link.frameName}`
10
- : `${link.fileName}${link.nodeId === '' ? ' (file-level)' : ''}`;
11
+ : `${link.fileName}${link.nodeId === '' ? ' (file-level)' : ''}`);
11
12
  console.log(`Linked ${args.identifier} ↔ Figma "${label}"`);
12
13
  console.log(` ${link.url}`);
13
14
  return 0;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFigmaContext = taskFigmaContext;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo task figma context <LUM-N> <link-id>`
9
+ *
10
+ * Tier-2 retrieval for the cheap inline Figma card. Reads the cached design
11
+ * metadata (file/frame name, url, thumbnail, last sync) from
12
+ * `/api/tasks/:id/figma-links/:linkId/context`. Live design context (layers,
13
+ * variables, code connect) needs the Figma MCP server, so the route degrades to
14
+ * metadata + a `note`; we print the note when present.
15
+ */
16
+ async function taskFigmaContext(identifier, linkId) {
17
+ if (!identifier || !linkId) {
18
+ console.error('Error: usage: lumo task figma context <LUM-42> <link-id>');
19
+ return 1;
20
+ }
21
+ const creds = (0, config_1.readCredentials)();
22
+ if (!creds) {
23
+ console.error('Error: not logged in. Run `lumo auth login` first.');
24
+ return 1;
25
+ }
26
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/figma-links/${encodeURIComponent(linkId)}/context`, { headers: { Authorization: `Bearer ${creds.token}` } });
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
35
+ return 1;
36
+ }
37
+ if (res.status === 401) {
38
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
39
+ return 1;
40
+ }
41
+ if (res.status === 404) {
42
+ console.error(`Error: task ${identifier} or figma link ${linkId} not found in workspace ${creds.workspaceSlug}`);
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ console.error(`Error: figma context failed (HTTP ${res.status})`);
47
+ return 1;
48
+ }
49
+ const { metadata, note } = (await res.json());
50
+ if (metadata.fileName)
51
+ console.log(`file: ${(0, sanitize_1.sanitizeField)(metadata.fileName)}`);
52
+ if (metadata.frameName)
53
+ console.log(`frame: ${(0, sanitize_1.sanitizeField)(metadata.frameName)}`);
54
+ console.log(`url: ${metadata.url}`);
55
+ if (metadata.lastSyncedAt)
56
+ console.log(`synced: ${metadata.lastSyncedAt}`);
57
+ if (metadata.lastSyncError)
58
+ console.log(`syncError: ${(0, sanitize_1.sanitizeField)(metadata.lastSyncError)}`);
59
+ if (note)
60
+ console.log(`\nnote: ${(0, sanitize_1.sanitizeField)(note)}`);
61
+ }
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskFigmaList = taskFigmaList;
4
4
  const figma_api_1 = require("../lib/figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  const STALE_MS = 25 * 24 * 3600 * 1000;
6
7
  function formatRow(id, fileName, frame, synced, stale) {
7
8
  return [
8
9
  id.padEnd(10),
9
- fileName.padEnd(20),
10
- frame.padEnd(24),
10
+ (0, sanitize_1.sanitizeField)(fileName).padEnd(20),
11
+ (0, sanitize_1.sanitizeField)(frame).padEnd(24),
11
12
  synced.padEnd(12),
12
13
  stale ? '⚠ thumbnail stale' : '',
13
14
  ]
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskFigmaRefresh = taskFigmaRefresh;
4
4
  const figma_api_1 = require("../lib/figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  async function taskFigmaRefresh(args) {
6
7
  try {
7
8
  const { links } = await (0, figma_api_1.listFigmaLinks)(args.identifier);
@@ -15,14 +16,14 @@ async function taskFigmaRefresh(args) {
15
16
  for (const r of results) {
16
17
  const link = byId.get(r.id);
17
18
  const label = link
18
- ? link.frameName
19
+ ? (0, sanitize_1.sanitizeField)(link.frameName
19
20
  ? `${link.fileName} · ${link.frameName}`
20
- : `${link.fileName} (file-level)`
21
+ : `${link.fileName} (file-level)`)
21
22
  : r.id;
22
23
  if (r.ok)
23
24
  console.log(` ✓ ${label}`);
24
25
  else
25
- console.log(` ✗ ${label} (${r.error})`);
26
+ console.log(` ✗ ${label} (${(0, sanitize_1.sanitizeField)(r.error ?? '')})`);
26
27
  }
27
28
  return 0;
28
29
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskFigmaRm = taskFigmaRm;
4
4
  const figma_api_1 = require("../lib/figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  async function taskFigmaRm(args) {
6
7
  const { links } = await (0, figma_api_1.listFigmaLinks)(args.identifier);
7
8
  const match = links.find(l => l.id === args.linkIdOrUrl || l.url === args.linkIdOrUrl);
@@ -10,9 +11,9 @@ async function taskFigmaRm(args) {
10
11
  return 0;
11
12
  }
12
13
  await (0, figma_api_1.removeFigmaLink)(args.identifier, match.id);
13
- const label = match.frameName
14
+ const label = (0, sanitize_1.sanitizeField)(match.frameName
14
15
  ? `${match.fileName} · ${match.frameName}`
15
- : `${match.fileName} (file-level)`;
16
+ : `${match.fileName} (file-level)`);
16
17
  console.log(`Removed Figma link from ${args.identifier}: "${label}"`);
17
18
  return 0;
18
19
  }
@@ -43,8 +43,7 @@ async function taskList(opts) {
43
43
  console.error('Error: not logged in. Run `lumo auth login` first.');
44
44
  return 1;
45
45
  }
46
- const envUrl = process.env.LUMO_API_URL?.trim();
47
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
46
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
48
47
  const base = (0, api_1.trimTrailingSlash)(apiUrl);
49
48
  let milestoneId;
50
49
  if (opts.milestone !== undefined) {
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskPrShow = taskPrShow;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo task pr show <LUM-N> <number>`
9
+ *
10
+ * Tier-2 retrieval for the cheap inline PR card. Reads the synced PR record
11
+ * (title, state, ciStatus, branches, author, url) from
12
+ * `/api/tasks/:id/pull-requests/:number`. Live diff + review comments need a
13
+ * GitHub token, so the route degrades to the stored record + a `note`; we print
14
+ * the note when present.
15
+ */
16
+ async function taskPrShow(identifier, prNumber) {
17
+ if (!identifier || !prNumber) {
18
+ console.error('Error: usage: lumo task pr show <LUM-42> <number>');
19
+ return 1;
20
+ }
21
+ const creds = (0, config_1.readCredentials)();
22
+ if (!creds) {
23
+ console.error('Error: not logged in. Run `lumo auth login` first.');
24
+ return 1;
25
+ }
26
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/pull-requests/${encodeURIComponent(prNumber)}`, { headers: { Authorization: `Bearer ${creds.token}` } });
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
35
+ return 1;
36
+ }
37
+ if (res.status === 401) {
38
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
39
+ return 1;
40
+ }
41
+ if (res.status === 400) {
42
+ console.error(`Error: invalid PR number "${prNumber}"`);
43
+ return 1;
44
+ }
45
+ if (res.status === 404) {
46
+ console.error(`Error: task ${identifier} or PR #${prNumber} not found in workspace ${creds.workspaceSlug}`);
47
+ return 1;
48
+ }
49
+ if (!res.ok) {
50
+ console.error(`Error: pr show failed (HTTP ${res.status})`);
51
+ return 1;
52
+ }
53
+ const { pullRequest: pr, note } = (await res.json());
54
+ console.log(`#${pr.number}${pr.repo ? ` (${(0, sanitize_1.sanitizeField)(pr.repo)})` : ''} ${(0, sanitize_1.sanitizeField)(pr.title ?? '')}`);
55
+ console.log(`state: ${pr.state ?? 'unknown'}${pr.isDraft ? ' · draft' : ''}`);
56
+ if (pr.ciStatus)
57
+ console.log(`ci: ${pr.ciStatus}`);
58
+ if (pr.authorLogin)
59
+ console.log(`author: ${(0, sanitize_1.sanitizeField)(pr.authorLogin)}`);
60
+ if (pr.headBranch || pr.baseBranch)
61
+ console.log(`branch: ${pr.headBranch ?? '?'} → ${pr.baseBranch ?? '?'}`);
62
+ if (pr.url)
63
+ console.log(`url: ${pr.url}`);
64
+ if (note)
65
+ console.log(`\nnote: ${(0, sanitize_1.sanitizeField)(note)}`);
66
+ }
@@ -4,20 +4,22 @@ exports.formatTaskShow = formatTaskShow;
4
4
  exports.taskShow = taskShow;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
7
8
  /**
8
9
  * Render task detail as a key:value block. Multi-line description is
9
10
  * indented under a `Description:` label so the structure stays scannable.
10
11
  */
11
12
  function formatTaskShow(task) {
12
13
  const lines = [];
13
- lines.push(`${task.identifier} ${task.title}`);
14
+ lines.push(`${task.identifier} ${(0, sanitize_1.sanitizeField)(task.title)}`);
14
15
  lines.push(`Status: ${task.status}`);
15
16
  lines.push(`Priority: ${task.priority}`);
16
- lines.push(`Project: ${task.project.name}`);
17
+ lines.push(`Project: ${(0, sanitize_1.sanitizeField)(task.project.name)}`);
17
18
  if (task.assignee) {
19
+ const label = (0, sanitize_1.sanitizeField)(task.assignee.label);
18
20
  const detail = task.assignee.email && task.assignee.type === 'member'
19
- ? `${task.assignee.label} <${task.assignee.email}>`
20
- : task.assignee.label;
21
+ ? `${label} <${(0, sanitize_1.sanitizeField)(task.assignee.email)}>`
22
+ : label;
21
23
  lines.push(`Assignee: ${detail}`);
22
24
  }
23
25
  else {
@@ -28,7 +30,7 @@ function formatTaskShow(task) {
28
30
  if (body && body.trim().length > 0) {
29
31
  lines.push('');
30
32
  lines.push('Description:');
31
- for (const line of body.split('\n')) {
33
+ for (const line of (0, sanitize_1.sanitizeField)(body).split('\n')) {
32
34
  lines.push(` ${line}`);
33
35
  }
34
36
  }
@@ -44,8 +46,7 @@ async function taskShow(identifier) {
44
46
  console.error('Error: not logged in. Run `lumo auth login` first.');
45
47
  return 1;
46
48
  }
47
- const envUrl = process.env.LUMO_API_URL?.trim();
48
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
49
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
49
50
  const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/by-identifier/${encodeURIComponent(identifier)}`;
50
51
  let res;
51
52
  try {
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskSlackShow = taskSlackShow;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo task slack show <LUM-N> <context-id>`
9
+ *
10
+ * Tier-2 retrieval for the cheap inline Slack card. Fetches the stored thread
11
+ * snapshot (no live Slack call) from
12
+ * `/api/tasks/:id/slack-contexts/:contextId/snapshot` and prints one line per
13
+ * message as `author: text`, falling back to `@<userId>` when the display name
14
+ * is missing.
15
+ */
16
+ async function taskSlackShow(identifier, contextId) {
17
+ if (!identifier || !contextId) {
18
+ console.error('Error: usage: lumo task slack show <LUM-42> <context-id>');
19
+ return 1;
20
+ }
21
+ const creds = (0, config_1.readCredentials)();
22
+ if (!creds) {
23
+ console.error('Error: not logged in. Run `lumo auth login` first.');
24
+ return 1;
25
+ }
26
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/slack-contexts/${encodeURIComponent(contextId)}/snapshot`, { headers: { Authorization: `Bearer ${creds.token}` } });
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
35
+ return 1;
36
+ }
37
+ if (res.status === 401) {
38
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
39
+ return 1;
40
+ }
41
+ if (res.status === 404) {
42
+ console.error(`Error: task ${identifier} or slack context ${contextId} not found in workspace ${creds.workspaceSlug}`);
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ console.error(`Error: slack show failed (HTTP ${res.status})`);
47
+ return 1;
48
+ }
49
+ const { snapshot } = (await res.json());
50
+ const messages = snapshot?.messages ?? [];
51
+ if (messages.length === 0) {
52
+ console.log('(no messages in stored snapshot)');
53
+ return;
54
+ }
55
+ for (const m of messages) {
56
+ const author = (0, sanitize_1.sanitizeField)(m.userName ?? '@' + m.userId);
57
+ console.log(`${author}: ${(0, sanitize_1.sanitizeField)(m.text)}`);
58
+ }
59
+ }
@@ -10,6 +10,7 @@ const api_1 = require("../lib/api");
10
10
  const task_create_1 = require("./task-create");
11
11
  const tag_resolver_1 = require("../lib/tag-resolver");
12
12
  const resolve_1 = require("../lib/resolve");
13
+ const sanitize_1 = require("../lib/sanitize");
13
14
  const ALLOWED_STATUSES = ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'];
14
15
  /**
15
16
  * Pure function: given the task's current sprint binding and the resolved
@@ -87,10 +88,10 @@ function buildUpdatePayload(opts) {
87
88
  * Appends a Tags line when tags are present.
88
89
  */
89
90
  function formatUpdatedTaskLine(task) {
90
- const escapedTitle = task.title.replace(/"/g, '\\"');
91
+ const escapedTitle = (0, sanitize_1.sanitizeField)(task.title).replace(/"/g, '\\"');
91
92
  const head = `Updated ${task.identifier} "${escapedTitle}" ${task.url}`;
92
93
  if (task.tags && task.tags.length > 0) {
93
- return `${head}\nTags: ${task.tags.join(', ')}`;
94
+ return `${head}\nTags: ${task.tags.map(sanitize_1.sanitizeField).join(', ')}`;
94
95
  }
95
96
  return head;
96
97
  }
@@ -146,8 +147,7 @@ async function taskUpdate(identifier, opts) {
146
147
  console.error('Error: not logged in. Run `lumo auth login` first.');
147
148
  return 1;
148
149
  }
149
- const envUrl = process.env.LUMO_API_URL?.trim();
150
- const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
150
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
151
151
  // Resolve tag refs into ids
152
152
  let tagIds;
153
153
  let addTagIds;
@@ -235,7 +235,7 @@ async function taskUpdate(identifier, opts) {
235
235
  // Body wasn't JSON; fall through to status-only message
236
236
  }
237
237
  if (serverMsg) {
238
- console.error(`Error: ${serverMsg}`);
238
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
239
239
  }
240
240
  else {
241
241
  console.error(`Error: task update failed (HTTP ${res.status})`);
@@ -365,7 +365,7 @@ async function taskUpdate(identifier, opts) {
365
365
  errMsg = b.error;
366
366
  }
367
367
  catch { /* ignore */ }
368
- console.error(`Error: ${errMsg}`);
368
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
369
369
  return 1;
370
370
  }
371
371
  const oldLabel = oldNumber !== null ? `#${oldNumber}` : action.oldSprintId;
@@ -397,7 +397,7 @@ async function taskUpdate(identifier, opts) {
397
397
  errMsg = b.error;
398
398
  }
399
399
  catch { /* ignore */ }
400
- console.error(`Error: ${errMsg}`);
400
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
401
401
  return 1;
402
402
  }
403
403
  // resolvedSprintNumber is set from the resolveSprintId call above
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskWebShow = taskWebShow;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo task web show <LUM-N> <link-id>`
9
+ *
10
+ * Tier-2 retrieval for the cheap inline WebLink card. Fetches the page body
11
+ * (cached, or fetched behind the SSRF guard on first read) from
12
+ * `/api/tasks/:id/web-links/:linkId/body` and prints it as plain text.
13
+ */
14
+ async function taskWebShow(identifier, linkId) {
15
+ if (!identifier || !linkId) {
16
+ console.error('Error: usage: lumo task web show <LUM-42> <link-id>');
17
+ return 1;
18
+ }
19
+ const creds = (0, config_1.readCredentials)();
20
+ if (!creds) {
21
+ console.error('Error: not logged in. Run `lumo auth login` first.');
22
+ return 1;
23
+ }
24
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
25
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
26
+ let res;
27
+ try {
28
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/web-links/${encodeURIComponent(linkId)}/body`, { headers: { Authorization: `Bearer ${creds.token}` } });
29
+ }
30
+ catch (err) {
31
+ const msg = err instanceof Error ? err.message : String(err);
32
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
33
+ return 1;
34
+ }
35
+ if (res.status === 401) {
36
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
37
+ return 1;
38
+ }
39
+ if (res.status === 404) {
40
+ console.error(`Error: task ${identifier} or web link ${linkId} not found in workspace ${creds.workspaceSlug}`);
41
+ return 1;
42
+ }
43
+ if (!res.ok) {
44
+ let serverMsg = null;
45
+ try {
46
+ const errBody = (await res.json());
47
+ if (typeof errBody.error === 'string')
48
+ serverMsg = errBody.error;
49
+ }
50
+ catch {
51
+ /* not JSON */
52
+ }
53
+ console.error(serverMsg
54
+ ? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
55
+ : `Error: web show failed (HTTP ${res.status})`);
56
+ return 1;
57
+ }
58
+ const { body } = (await res.json());
59
+ if (!body || body.trim().length === 0) {
60
+ console.log('(empty body)');
61
+ return;
62
+ }
63
+ console.log((0, sanitize_1.sanitizeField)(body));
64
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.whoami = whoami;
4
4
  const config_1 = require("../lib/config");
5
+ const sanitize_1 = require("../lib/sanitize");
5
6
  async function whoami() {
6
7
  const creds = (0, config_1.readCredentials)();
7
8
  if (!creds) {
@@ -9,8 +10,8 @@ async function whoami() {
9
10
  console.log('Run `lumo auth login` to log in');
10
11
  return 1;
11
12
  }
12
- console.log(`Logged in as ${creds.email}`);
13
- console.log(` Workspace: ${creds.workspaceName} (${creds.workspaceSlug})`);
14
- console.log(` Key: ${creds.apiKeyName} (${creds.apiKeyPrefix})`);
13
+ console.log(`Logged in as ${(0, sanitize_1.sanitizeField)(creds.email)}`);
14
+ console.log(` Workspace: ${(0, sanitize_1.sanitizeField)(creds.workspaceName)} (${creds.workspaceSlug})`);
15
+ console.log(` Key: ${(0, sanitize_1.sanitizeField)(creds.apiKeyName)} (${creds.apiKeyPrefix})`);
15
16
  console.log(` API: ${creds.apiUrl}`);
16
17
  }