@lumoai/cli 1.0.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 (61) hide show
  1. package/README.md +90 -0
  2. package/dist/cli/src/commands/auth-login.js +55 -0
  3. package/dist/cli/src/commands/auth-logout.js +14 -0
  4. package/dist/cli/src/commands/doc-bind.js +52 -0
  5. package/dist/cli/src/commands/doc-create.js +138 -0
  6. package/dist/cli/src/commands/doc-delete.js +52 -0
  7. package/dist/cli/src/commands/doc-list.js +91 -0
  8. package/dist/cli/src/commands/doc-move.js +113 -0
  9. package/dist/cli/src/commands/doc-share-list.js +62 -0
  10. package/dist/cli/src/commands/doc-share.js +77 -0
  11. package/dist/cli/src/commands/doc-show.js +99 -0
  12. package/dist/cli/src/commands/doc-unbind.js +47 -0
  13. package/dist/cli/src/commands/doc-unshare.js +71 -0
  14. package/dist/cli/src/commands/doc-update.js +144 -0
  15. package/dist/cli/src/commands/hook.js +21 -0
  16. package/dist/cli/src/commands/milestone-create.js +84 -0
  17. package/dist/cli/src/commands/milestone-delete.js +96 -0
  18. package/dist/cli/src/commands/milestone-list.js +55 -0
  19. package/dist/cli/src/commands/milestone-show.js +106 -0
  20. package/dist/cli/src/commands/milestone-update.js +167 -0
  21. package/dist/cli/src/commands/project-list.js +57 -0
  22. package/dist/cli/src/commands/session-attach.js +80 -0
  23. package/dist/cli/src/commands/session-detach.js +60 -0
  24. package/dist/cli/src/commands/session-status.js +58 -0
  25. package/dist/cli/src/commands/sprint-add.js +62 -0
  26. package/dist/cli/src/commands/sprint-close.js +151 -0
  27. package/dist/cli/src/commands/sprint-create.js +80 -0
  28. package/dist/cli/src/commands/sprint-delete.js +88 -0
  29. package/dist/cli/src/commands/sprint-list.js +85 -0
  30. package/dist/cli/src/commands/sprint-remove.js +57 -0
  31. package/dist/cli/src/commands/sprint-show.js +81 -0
  32. package/dist/cli/src/commands/sprint-start.js +68 -0
  33. package/dist/cli/src/commands/sprint-summary.js +138 -0
  34. package/dist/cli/src/commands/sprint-update.js +148 -0
  35. package/dist/cli/src/commands/task-comment.js +95 -0
  36. package/dist/cli/src/commands/task-context.js +137 -0
  37. package/dist/cli/src/commands/task-create.js +202 -0
  38. package/dist/cli/src/commands/task-list.js +94 -0
  39. package/dist/cli/src/commands/task-show.js +74 -0
  40. package/dist/cli/src/commands/task-update.js +468 -0
  41. package/dist/cli/src/commands/whoami.js +16 -0
  42. package/dist/cli/src/index.js +492 -0
  43. package/dist/cli/src/lib/api.js +33 -0
  44. package/dist/cli/src/lib/browser.js +33 -0
  45. package/dist/cli/src/lib/config.js +92 -0
  46. package/dist/cli/src/lib/doc-input.js +80 -0
  47. package/dist/cli/src/lib/doc-sort-order.js +23 -0
  48. package/dist/cli/src/lib/doc-tree.js +54 -0
  49. package/dist/cli/src/lib/format.js +33 -0
  50. package/dist/cli/src/lib/hook-log.js +81 -0
  51. package/dist/cli/src/lib/hook-runner.js +156 -0
  52. package/dist/cli/src/lib/markdown-tiptap.js +7 -0
  53. package/dist/cli/src/lib/prompt.js +71 -0
  54. package/dist/cli/src/lib/resolve-doc-id.js +34 -0
  55. package/dist/cli/src/lib/resolve-doc.js +27 -0
  56. package/dist/cli/src/lib/resolve-member.js +58 -0
  57. package/dist/cli/src/lib/resolve.js +170 -0
  58. package/dist/cli/src/lib/tag-resolver.js +49 -0
  59. package/dist/shared/src/index.js +35 -0
  60. package/dist/shared/src/markdown-tiptap.js +267 -0
  61. package/package.json +48 -0
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUuid = isUuid;
4
+ exports.slugify = slugify;
5
+ exports.resolveProjectId = resolveProjectId;
6
+ exports.resolveTeamId = resolveTeamId;
7
+ exports.resolveSprintId = resolveSprintId;
8
+ exports.resolveMilestoneId = resolveMilestoneId;
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ function isUuid(value) {
11
+ return UUID_RE.test(value);
12
+ }
13
+ /**
14
+ * Mirror of `lib/utils/slugify` (server). Single source of truth for CLI
15
+ * slugification — must match the server byte-for-byte so a slug printed by
16
+ * `lumo project list` resolves correctly when passed as `--project <slug>`.
17
+ */
18
+ function slugify(input) {
19
+ return input
20
+ .toLowerCase()
21
+ .trim()
22
+ .replace(/[^a-z0-9\s-]/g, '')
23
+ .replace(/\s+/g, '-')
24
+ .replace(/-+/g, '-')
25
+ .replace(/^-|-$/g, '');
26
+ }
27
+ async function fetchJson(base, token, path) {
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}${path}`, {
31
+ headers: { Authorization: `Bearer ${token}` },
32
+ });
33
+ }
34
+ catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ throw new Error(`Could not reach Lumo API at ${base} (${msg})`);
37
+ }
38
+ if (res.status === 401) {
39
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
40
+ }
41
+ if (!res.ok) {
42
+ throw new Error(`Request failed (HTTP ${res.status}): ${path}`);
43
+ }
44
+ return (await res.json());
45
+ }
46
+ /**
47
+ * Resolve `--project <ref>` to a project id.
48
+ * - `ref` undefined and workspace has 1 visible project → auto-select
49
+ * - `ref` undefined and workspace has >1 visible project → throw
50
+ * - `ref` set → match by name or slug, case-insensitive
51
+ */
52
+ async function resolveProjectId(base, token, ref) {
53
+ const { projects } = await fetchJson(base, token, '/api/projects');
54
+ const visible = projects.filter(p => !p.isIdea);
55
+ if (!ref) {
56
+ if (visible.length === 0)
57
+ throw new Error('workspace has no projects yet.');
58
+ if (visible.length === 1)
59
+ return visible[0].id;
60
+ throw new Error('workspace has multiple projects; pass --project <slug>.');
61
+ }
62
+ const needle = ref.trim().toLowerCase();
63
+ const match = visible.find(p => p.name.toLowerCase() === needle || slugify(p.name) === needle);
64
+ if (!match) {
65
+ throw new Error(`project "${ref}" not found. Try \`lumo project list\`.`);
66
+ }
67
+ return match.id;
68
+ }
69
+ /**
70
+ * Resolve `--team <ref>` to a team id + name.
71
+ * - `ref` undefined and workspace has 1 visible team → auto-select
72
+ * - `ref` undefined and workspace has >1 visible team → throw
73
+ * - `ref` is a UUID → return as-is (no fetch)
74
+ * - `ref` set → match by name or identifier (slug), case-insensitive
75
+ */
76
+ async function resolveTeamId(base, token, ref) {
77
+ if (ref !== undefined && isUuid(ref)) {
78
+ return { id: ref, name: '' };
79
+ }
80
+ const { teams } = await fetchJson(base, token, '/api/teams');
81
+ if (!ref) {
82
+ if (teams.length === 0)
83
+ throw new Error('workspace has no teams yet.');
84
+ if (teams.length === 1)
85
+ return { id: teams[0].id, name: teams[0].name };
86
+ throw new Error('workspace has multiple teams; pass --team <slug>.');
87
+ }
88
+ const needle = ref.trim().toLowerCase();
89
+ const match = teams.find(t => t.name.toLowerCase() === needle ||
90
+ t.identifier.toLowerCase() === needle);
91
+ if (!match) {
92
+ throw new Error(`team "${ref}" not found. Try \`lumo team list\`.`);
93
+ }
94
+ return { id: match.id, name: match.name };
95
+ }
96
+ /**
97
+ * Resolve a sprint `<identifier>`:
98
+ * - If it's a UUID, return { id, number: 0, name: '', teamId: '' } — caller
99
+ * fetches /api/sprints/<id> for full data.
100
+ * - If it's a positive integer, look up via /api/sprints/by-number scoped
101
+ * to the caller's workspace.
102
+ *
103
+ * The /by-number endpoint hardcodes the product assumption that a workspace
104
+ * has at most one sprints-enabled team. If that constraint is ever lifted,
105
+ * this function will need a `--team` disambiguation step.
106
+ *
107
+ * `teamRef` is used to soften the disambiguation hint when --team was already
108
+ * attempted; today the endpoint still disambiguates via workspace_url only.
109
+ */
110
+ async function resolveSprintId(base, token, identifier, teamRef, workspaceSlug) {
111
+ if (isUuid(identifier)) {
112
+ return { id: identifier, number: 0, name: '', teamId: '' };
113
+ }
114
+ const n = Number(identifier);
115
+ if (!Number.isInteger(n) || n <= 0) {
116
+ throw new Error(`sprint "${identifier}" must be a positive integer or a UUID.`);
117
+ }
118
+ const path = `/api/sprints/by-number?workspaceUrl=${encodeURIComponent(workspaceSlug)}&number=${n}`;
119
+ let res;
120
+ try {
121
+ res = await fetch(`${base}${path}`, {
122
+ headers: { Authorization: `Bearer ${token}` },
123
+ });
124
+ }
125
+ catch (err) {
126
+ const msg = err instanceof Error ? err.message : String(err);
127
+ throw new Error(`Could not reach Lumo API at ${base} (${msg})`);
128
+ }
129
+ if (res.status === 404) {
130
+ // teamRef is currently informational only — surface the hint regardless,
131
+ // but soften the wording when --team was already attempted.
132
+ const hint = teamRef
133
+ ? 'Verify the sprint exists for the chosen --team, or pass the UUID.'
134
+ : 'If your workspace has multiple sprints-enabled teams, pass --team <name> or use the sprint UUID.';
135
+ throw new Error(`sprint #${n} not found in this workspace. ${hint}`);
136
+ }
137
+ if (res.status === 401) {
138
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
139
+ }
140
+ if (!res.ok) {
141
+ throw new Error(`sprint lookup failed (HTTP ${res.status})`);
142
+ }
143
+ const body = (await res.json());
144
+ return {
145
+ id: body.sprint.id,
146
+ number: body.sprint.number,
147
+ name: body.sprint.name,
148
+ teamId: body.sprint.teamId,
149
+ };
150
+ }
151
+ /**
152
+ * Resolve a milestone `<identifier>`:
153
+ * - If it's a UUID, return as-is (project not fetched).
154
+ * - Otherwise, resolve the project (via `--project <ref>` or auto-select),
155
+ * list its milestones, and match by name case-insensitively.
156
+ */
157
+ async function resolveMilestoneId(base, token, identifier, projectRef) {
158
+ if (isUuid(identifier)) {
159
+ // Caller will fetch /api/milestones/<id> to learn the projectId & name.
160
+ return { id: identifier, name: '', projectId: '' };
161
+ }
162
+ const projectId = await resolveProjectId(base, token, projectRef);
163
+ const { milestones } = await fetchJson(base, token, `/api/projects/${projectId}/milestones`);
164
+ const needle = identifier.trim().toLowerCase();
165
+ const match = milestones.find(m => m.name.toLowerCase() === needle);
166
+ if (!match) {
167
+ throw new Error(`no milestone matches "${identifier}" in this project. Try \`lumo milestone list\`.`);
168
+ }
169
+ return { id: match.id, name: match.name, projectId };
170
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTagRefs = resolveTagRefs;
4
+ const api_1 = require("./api");
5
+ async function resolveTagRefs(refs, deps) {
6
+ const fetchImpl = deps.fetchImpl ?? fetch;
7
+ // Trim + reject empty
8
+ const trimmed = [];
9
+ for (const raw of refs.names) {
10
+ const t = raw.trim();
11
+ if (t.length === 0) {
12
+ throw new Error('tag names must not be empty');
13
+ }
14
+ trimmed.push(t);
15
+ }
16
+ // Dedupe case-insensitively, preserve first-seen casing
17
+ const seen = new Set();
18
+ const unique = [];
19
+ for (const t of trimmed) {
20
+ const key = t.toLowerCase();
21
+ if (seen.has(key))
22
+ continue;
23
+ seen.add(key);
24
+ unique.push(t);
25
+ }
26
+ let nameIds = [];
27
+ if (unique.length > 0) {
28
+ const url = `${(0, api_1.trimTrailingSlash)(deps.apiUrl)}/api/tags/resolve`;
29
+ const res = await fetchImpl(url, {
30
+ method: 'POST',
31
+ headers: {
32
+ Authorization: `Bearer ${deps.token}`,
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ body: JSON.stringify({ names: unique }),
36
+ });
37
+ if (!res.ok) {
38
+ const body = await res.text();
39
+ throw new Error(`tag resolve failed: ${res.status} ${res.statusText}: ${body}`);
40
+ }
41
+ const json = (await res.json());
42
+ nameIds = json.tags.map(t => t.id);
43
+ }
44
+ // Merge with explicit ids, dedupe
45
+ const out = new Set(nameIds);
46
+ for (const id of refs.ids)
47
+ out.add(id);
48
+ return [...out];
49
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ // ── Agent Error types ────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
5
+ exports.userFriendlyError = userFriendlyError;
6
+ class AgentError extends Error {
7
+ code;
8
+ cause;
9
+ constructor(code, message, cause) {
10
+ super(message);
11
+ this.code = code;
12
+ this.cause = cause;
13
+ this.name = 'AgentError';
14
+ }
15
+ }
16
+ exports.AgentError = AgentError;
17
+ const USER_MESSAGES = {
18
+ SANDBOX_CREATE_FAILED: 'Could not start the execution environment. Please try again.',
19
+ CLONE_FAILED: 'Could not access the repository. Check your GitHub connection.',
20
+ AGENT_EXECUTION_FAILED: 'Agent encountered an error while writing code.',
21
+ PUSH_FAILED: 'Could not push to GitHub. Check repository permissions.',
22
+ PR_CREATE_FAILED: 'Code was written but PR creation failed. Check GitHub.',
23
+ TIMEOUT: 'Agent timed out after 10 minutes. The task may be too complex — try breaking it down.',
24
+ CLARIFY_TIMEOUT: 'Agent was waiting for clarification for 24 hours and has been stopped. Add more details to the spec and retry.',
25
+ SANDBOX_KILLED: 'The execution environment was shut down unexpectedly. This usually means the task took too long. Try breaking it into smaller steps.',
26
+ CONFIG_ERROR: 'Server configuration error. Please contact your administrator.',
27
+ UNKNOWN_ERROR: 'Something went wrong. Please try again.',
28
+ };
29
+ function userFriendlyError(code) {
30
+ return USER_MESSAGES[code] ?? USER_MESSAGES.UNKNOWN_ERROR;
31
+ }
32
+ // ── Markdown ↔ Tiptap converter ───────────────────────────────────────────────
33
+ var markdown_tiptap_1 = require("./markdown-tiptap");
34
+ Object.defineProperty(exports, "markdownToTiptap", { enumerable: true, get: function () { return markdown_tiptap_1.markdownToTiptap; } });
35
+ Object.defineProperty(exports, "tiptapToMarkdown", { enumerable: true, get: function () { return markdown_tiptap_1.tiptapToMarkdown; } });
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.markdownToHtml = markdownToHtml;
7
+ exports.markdownToTiptap = markdownToTiptap;
8
+ exports.tiptapToMarkdown = tiptapToMarkdown;
9
+ const markdown_it_1 = __importDefault(require("markdown-it"));
10
+ const md = new markdown_it_1.default({ html: false, linkify: true, breaks: false });
11
+ /**
12
+ * Render a markdown string to HTML.
13
+ *
14
+ * Shares the markdown-it instance with `markdownToTiptap` so the doc CLI
15
+ * pipeline (markdown → HTML for the Tiptap editor) and the agent pipeline
16
+ * (markdown → tiptap JSON) parse identically. Used by the documents API
17
+ * when a CLI caller posts `contentMarkdown`: the editor consumes the
18
+ * resulting HTML directly via `setContent` / `getHTML`.
19
+ */
20
+ function markdownToHtml(input) {
21
+ if (!input)
22
+ return '';
23
+ return md.render(input);
24
+ }
25
+ /**
26
+ * Parse a markdown string into a Tiptap JSON doc node.
27
+ * Supports paragraphs, headings (h1-h6), bullet/ordered lists, fenced
28
+ * code blocks, blockquotes, and inline marks (bold, italic, code, link).
29
+ * Unsupported nodes degrade to plain text inside a paragraph.
30
+ */
31
+ function markdownToTiptap(input) {
32
+ if (!input) {
33
+ return { type: 'doc', content: [] };
34
+ }
35
+ const tokens = md.parse(input, {});
36
+ const content = blocksFromTokens(tokens);
37
+ return { type: 'doc', content };
38
+ }
39
+ function blocksFromTokens(tokens) {
40
+ const out = [];
41
+ for (let i = 0; i < tokens.length; i++) {
42
+ const t = tokens[i];
43
+ switch (t.type) {
44
+ case 'paragraph_open': {
45
+ const end = findMatch(tokens, i, 'paragraph_close');
46
+ const inline = tokens[i + 1];
47
+ out.push({
48
+ type: 'paragraph',
49
+ content: inline ? inlineFromToken(inline) : [],
50
+ });
51
+ i = end;
52
+ break;
53
+ }
54
+ case 'heading_open': {
55
+ const level = parseInt(t.tag.replace('h', ''), 10);
56
+ const end = findMatch(tokens, i, 'heading_close');
57
+ const inline = tokens[i + 1];
58
+ out.push({
59
+ type: 'heading',
60
+ attrs: { level },
61
+ content: inline ? inlineFromToken(inline) : [],
62
+ });
63
+ i = end;
64
+ break;
65
+ }
66
+ case 'bullet_list_open':
67
+ case 'ordered_list_open': {
68
+ const isOrdered = t.type === 'ordered_list_open';
69
+ const closeType = isOrdered ? 'ordered_list_close' : 'bullet_list_close';
70
+ const end = findMatch(tokens, i, closeType);
71
+ const slice = tokens.slice(i + 1, end);
72
+ out.push({
73
+ type: isOrdered ? 'orderedList' : 'bulletList',
74
+ content: listItemsFromTokens(slice),
75
+ });
76
+ i = end;
77
+ break;
78
+ }
79
+ case 'fence': {
80
+ out.push({
81
+ type: 'codeBlock',
82
+ attrs: { language: t.info || null },
83
+ content: t.content ? [{ type: 'text', text: t.content }] : [],
84
+ });
85
+ break;
86
+ }
87
+ case 'blockquote_open': {
88
+ const end = findMatch(tokens, i, 'blockquote_close');
89
+ const slice = tokens.slice(i + 1, end);
90
+ out.push({ type: 'blockquote', content: blocksFromTokens(slice) });
91
+ i = end;
92
+ break;
93
+ }
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+ function listItemsFromTokens(tokens) {
101
+ const items = [];
102
+ for (let i = 0; i < tokens.length; i++) {
103
+ if (tokens[i]?.type === 'list_item_open') {
104
+ const end = findMatch(tokens, i, 'list_item_close');
105
+ const inner = tokens.slice(i + 1, end);
106
+ items.push({ type: 'listItem', content: blocksFromTokens(inner) });
107
+ i = end;
108
+ }
109
+ }
110
+ return items;
111
+ }
112
+ function inlineFromToken(t) {
113
+ if (!t.children) {
114
+ return t.content ? [{ type: 'text', text: t.content }] : [];
115
+ }
116
+ const out = [];
117
+ const markStack = [];
118
+ for (const child of t.children) {
119
+ switch (child.type) {
120
+ case 'text':
121
+ if (child.content) {
122
+ out.push(makeText(child.content, markStack));
123
+ }
124
+ break;
125
+ case 'softbreak':
126
+ case 'hardbreak':
127
+ out.push(makeText('\n', markStack));
128
+ break;
129
+ case 'code_inline':
130
+ out.push(makeText(child.content, [...markStack, { type: 'code' }]));
131
+ break;
132
+ case 'strong_open':
133
+ markStack.push({ type: 'bold' });
134
+ break;
135
+ case 'strong_close':
136
+ markStack.pop();
137
+ break;
138
+ case 'em_open':
139
+ markStack.push({ type: 'italic' });
140
+ break;
141
+ case 'em_close':
142
+ markStack.pop();
143
+ break;
144
+ case 'link_open': {
145
+ // markdown-it Token may expose attrGet or a raw attrs array
146
+ let href = '';
147
+ if (typeof child.attrGet === 'function') {
148
+ href = child.attrGet('href') ?? '';
149
+ }
150
+ else {
151
+ href = child.attrs?.find(a => a[0] === 'href')?.[1] ?? '';
152
+ }
153
+ markStack.push({ type: 'link', attrs: { href } });
154
+ break;
155
+ }
156
+ case 'link_close':
157
+ markStack.pop();
158
+ break;
159
+ default:
160
+ if (child.content) {
161
+ out.push(makeText(child.content, markStack));
162
+ }
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+ function makeText(text, marks) {
168
+ const node = { type: 'text', text };
169
+ if (marks.length > 0)
170
+ node.marks = marks.map(cloneMark);
171
+ return node;
172
+ }
173
+ function cloneMark(m) {
174
+ if (m.type === 'link')
175
+ return { type: 'link', attrs: { ...m.attrs } };
176
+ return { type: m.type };
177
+ }
178
+ function findMatch(tokens, start, closeType) {
179
+ const startLevel = tokens[start]?.level ?? 0;
180
+ for (let i = start + 1; i < tokens.length; i++) {
181
+ if (tokens[i]?.type === closeType && tokens[i]?.level === startLevel) {
182
+ return i;
183
+ }
184
+ }
185
+ return tokens.length - 1;
186
+ }
187
+ /**
188
+ * Render a Tiptap JSON doc node back to markdown text.
189
+ * Best-effort: paragraphs, headings, lists, code blocks, mention nodes.
190
+ * Returns '' for null/non-object input.
191
+ */
192
+ function tiptapToMarkdown(doc) {
193
+ if (!doc || typeof doc !== 'object')
194
+ return '';
195
+ const node = doc;
196
+ if (!Array.isArray(node.content))
197
+ return '';
198
+ const lines = node.content.map(renderBlock).filter(Boolean);
199
+ return lines.join('\n');
200
+ }
201
+ function renderBlock(block) {
202
+ switch (block.type) {
203
+ case 'paragraph':
204
+ return renderInlines(block.content ?? []) + '\n';
205
+ case 'heading': {
206
+ const level = block.attrs?.level ?? 1;
207
+ return '#'.repeat(level) + ' ' + renderInlines(block.content ?? []) + '\n';
208
+ }
209
+ case 'bulletList':
210
+ return ((block.content ?? [])
211
+ .map(item => '- ' + renderListItem(item))
212
+ .join('\n') + '\n');
213
+ case 'orderedList':
214
+ return ((block.content ?? [])
215
+ .map((item, idx) => `${idx + 1}. ` + renderListItem(item))
216
+ .join('\n') + '\n');
217
+ case 'codeBlock': {
218
+ const lang = block.attrs?.language ?? '';
219
+ const body = (block.content ?? []).map(c => c.text ?? '').join('');
220
+ return '```' + lang + '\n' + body + '```\n';
221
+ }
222
+ case 'blockquote':
223
+ return ((block.content ?? [])
224
+ .map(b => '> ' + renderBlock(b))
225
+ .join('')
226
+ .trimEnd() + '\n');
227
+ default:
228
+ return '';
229
+ }
230
+ }
231
+ function renderListItem(item) {
232
+ const blocks = item.content ?? [];
233
+ return blocks.map(b => renderInlines(b.content ?? [])).join(' ');
234
+ }
235
+ function renderInlines(nodes) {
236
+ return nodes.map(renderInline).join('');
237
+ }
238
+ function renderInline(node) {
239
+ if (node.type === 'mention') {
240
+ const ident = node.attrs?.identifier ?? '';
241
+ return ident ? `@${ident}` : '';
242
+ }
243
+ if (node.type !== 'text')
244
+ return '';
245
+ let text = node.text ?? '';
246
+ if (!node.marks)
247
+ return text;
248
+ for (const mark of node.marks) {
249
+ switch (mark.type) {
250
+ case 'bold':
251
+ text = `**${text}**`;
252
+ break;
253
+ case 'italic':
254
+ text = `*${text}*`;
255
+ break;
256
+ case 'code':
257
+ text = `\`${text}\``;
258
+ break;
259
+ case 'link': {
260
+ const href = mark.attrs?.href ?? '';
261
+ text = `[${text}](${href})`;
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ return text;
267
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@lumoai/cli",
3
+ "version": "1.0.0",
4
+ "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
+ "license": "MIT",
6
+ "author": "cli@uselumo.ai",
7
+ "homepage": "https://github.com/Lumo-Workspace/lumo/tree/main/cli#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Lumo-Workspace/lumo.git",
11
+ "directory": "cli"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Lumo-Workspace/lumo/issues"
15
+ },
16
+ "keywords": [
17
+ "lumo",
18
+ "cli",
19
+ "task-management",
20
+ "project-management",
21
+ "claude-code"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "bin": {
27
+ "lumo": "./dist/cli/src/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc && chmod +x dist/cli/src/index.js",
37
+ "dev": "tsc --watch",
38
+ "clean": "rm -rf dist",
39
+ "prepublishOnly": "npm run clean && npm run build"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^13.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25",
46
+ "typescript": "^5"
47
+ }
48
+ }