@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.
- package/README.md +90 -0
- package/dist/cli/src/commands/auth-login.js +55 -0
- package/dist/cli/src/commands/auth-logout.js +14 -0
- package/dist/cli/src/commands/doc-bind.js +52 -0
- package/dist/cli/src/commands/doc-create.js +138 -0
- package/dist/cli/src/commands/doc-delete.js +52 -0
- package/dist/cli/src/commands/doc-list.js +91 -0
- package/dist/cli/src/commands/doc-move.js +113 -0
- package/dist/cli/src/commands/doc-share-list.js +62 -0
- package/dist/cli/src/commands/doc-share.js +77 -0
- package/dist/cli/src/commands/doc-show.js +99 -0
- package/dist/cli/src/commands/doc-unbind.js +47 -0
- package/dist/cli/src/commands/doc-unshare.js +71 -0
- package/dist/cli/src/commands/doc-update.js +144 -0
- package/dist/cli/src/commands/hook.js +21 -0
- package/dist/cli/src/commands/milestone-create.js +84 -0
- package/dist/cli/src/commands/milestone-delete.js +96 -0
- package/dist/cli/src/commands/milestone-list.js +55 -0
- package/dist/cli/src/commands/milestone-show.js +106 -0
- package/dist/cli/src/commands/milestone-update.js +167 -0
- package/dist/cli/src/commands/project-list.js +57 -0
- package/dist/cli/src/commands/session-attach.js +80 -0
- package/dist/cli/src/commands/session-detach.js +60 -0
- package/dist/cli/src/commands/session-status.js +58 -0
- package/dist/cli/src/commands/sprint-add.js +62 -0
- package/dist/cli/src/commands/sprint-close.js +151 -0
- package/dist/cli/src/commands/sprint-create.js +80 -0
- package/dist/cli/src/commands/sprint-delete.js +88 -0
- package/dist/cli/src/commands/sprint-list.js +85 -0
- package/dist/cli/src/commands/sprint-remove.js +57 -0
- package/dist/cli/src/commands/sprint-show.js +81 -0
- package/dist/cli/src/commands/sprint-start.js +68 -0
- package/dist/cli/src/commands/sprint-summary.js +138 -0
- package/dist/cli/src/commands/sprint-update.js +148 -0
- package/dist/cli/src/commands/task-comment.js +95 -0
- package/dist/cli/src/commands/task-context.js +137 -0
- package/dist/cli/src/commands/task-create.js +202 -0
- package/dist/cli/src/commands/task-list.js +94 -0
- package/dist/cli/src/commands/task-show.js +74 -0
- package/dist/cli/src/commands/task-update.js +468 -0
- package/dist/cli/src/commands/whoami.js +16 -0
- package/dist/cli/src/index.js +492 -0
- package/dist/cli/src/lib/api.js +33 -0
- package/dist/cli/src/lib/browser.js +33 -0
- package/dist/cli/src/lib/config.js +92 -0
- package/dist/cli/src/lib/doc-input.js +80 -0
- package/dist/cli/src/lib/doc-sort-order.js +23 -0
- package/dist/cli/src/lib/doc-tree.js +54 -0
- package/dist/cli/src/lib/format.js +33 -0
- package/dist/cli/src/lib/hook-log.js +81 -0
- package/dist/cli/src/lib/hook-runner.js +156 -0
- package/dist/cli/src/lib/markdown-tiptap.js +7 -0
- package/dist/cli/src/lib/prompt.js +71 -0
- package/dist/cli/src/lib/resolve-doc-id.js +34 -0
- package/dist/cli/src/lib/resolve-doc.js +27 -0
- package/dist/cli/src/lib/resolve-member.js +58 -0
- package/dist/cli/src/lib/resolve.js +170 -0
- package/dist/cli/src/lib/tag-resolver.js +49 -0
- package/dist/shared/src/index.js +35 -0
- package/dist/shared/src/markdown-tiptap.js +267 -0
- 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
|
+
}
|