@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,202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertSameTeam = assertSameTeam;
4
+ exports.normalizePriority = normalizePriority;
5
+ exports.formatCreatedTaskLine = formatCreatedTaskLine;
6
+ exports.taskCreate = taskCreate;
7
+ const config_1 = require("../lib/config");
8
+ const api_1 = require("../lib/api");
9
+ const tag_resolver_1 = require("../lib/tag-resolver");
10
+ const resolve_1 = require("../lib/resolve");
11
+ const ALLOWED_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
12
+ /**
13
+ * Assert that the sprint belongs to the same team as the newly created task.
14
+ * Throws with a human-readable message on mismatch (caller console.errors it).
15
+ */
16
+ function assertSameTeam(task, sprint) {
17
+ if (task.teamId !== sprint.teamId) {
18
+ throw new Error(`Sprint binding skipped: sprint #${sprint.number} belongs to team "${sprint.teamId}", task belongs to "${task.teamId}"`);
19
+ }
20
+ }
21
+ /**
22
+ * Bind a newly created task to a sprint.
23
+ * - Resolves the sprint ref (number or UUID).
24
+ * - If resolveSprintId returned the UUID placeholder (teamId === ''), does a
25
+ * follow-up GET /api/sprints/<id> to fill in { teamId, number, name }.
26
+ * - Asserts same-team, then POSTs to /api/sprints/<sprintId>/tasks.
27
+ * Returns the sprint info on success, or throws on failure.
28
+ */
29
+ async function bindTaskToSprint(base, token, workspaceSlug, sprintRef, task) {
30
+ let sprint = await (0, resolve_1.resolveSprintId)(base, token, sprintRef, undefined, workspaceSlug);
31
+ // UUID path: resolveSprintId returns a placeholder with teamId === ''
32
+ if (sprint.teamId === '') {
33
+ const sprintRes = await fetch(`${base}/api/sprints/${sprint.id}`, {
34
+ headers: { Authorization: `Bearer ${token}` },
35
+ });
36
+ if (!sprintRes.ok) {
37
+ throw new Error(`sprint lookup failed (HTTP ${sprintRes.status})`);
38
+ }
39
+ const { sprint: full } = (await sprintRes.json());
40
+ sprint = { id: full.id, number: full.number, name: full.name, teamId: full.teamId };
41
+ }
42
+ assertSameTeam(task, sprint);
43
+ const bindRes = await fetch(`${base}/api/sprints/${sprint.id}/tasks`, {
44
+ method: 'POST',
45
+ headers: {
46
+ Authorization: `Bearer ${token}`,
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ body: JSON.stringify({ taskId: task.id }),
50
+ });
51
+ if (!bindRes.ok) {
52
+ let errMsg = `sprint bind failed (HTTP ${bindRes.status})`;
53
+ try {
54
+ const errBody = (await bindRes.json());
55
+ if (errBody.error)
56
+ errMsg = errBody.error;
57
+ }
58
+ catch {
59
+ // ignore
60
+ }
61
+ throw new Error(errMsg);
62
+ }
63
+ return sprint;
64
+ }
65
+ /**
66
+ * Uppercase + validate a priority string. Returns null on unknown values so
67
+ * the caller can print a context-aware error.
68
+ */
69
+ function normalizePriority(value) {
70
+ if (!value)
71
+ return null;
72
+ const upper = value.toUpperCase();
73
+ return ALLOWED_PRIORITIES.includes(upper)
74
+ ? upper
75
+ : null;
76
+ }
77
+ /**
78
+ * Format the single-line success output. Title is double-quoted; embedded
79
+ * double-quotes are backslash-escaped so the output stays parseable as one
80
+ * shell-safe token group. An optional Tags line is appended when tags are
81
+ * present.
82
+ */
83
+ function formatCreatedTaskLine(task) {
84
+ const escapedTitle = task.title.replace(/"/g, '\\"');
85
+ const head = `Created ${task.identifier} "${escapedTitle}" ${task.url}`;
86
+ if (task.tags && task.tags.length > 0) {
87
+ return `${head}\nTags: ${task.tags.join(', ')}`;
88
+ }
89
+ return head;
90
+ }
91
+ async function taskCreate(title, opts) {
92
+ if (!title || title.trim().length === 0) {
93
+ console.error('Error: missing <title>. Usage: lumo task create <title> [options]');
94
+ return 1;
95
+ }
96
+ const creds = (0, config_1.readCredentials)();
97
+ if (!creds) {
98
+ console.error('Error: not logged in. Run `lumo auth login` first.');
99
+ return 1;
100
+ }
101
+ // Priority: default LOW, validate if provided
102
+ let priority = 'LOW';
103
+ if (opts.priority !== undefined) {
104
+ const normalized = normalizePriority(opts.priority);
105
+ if (!normalized) {
106
+ console.error(`Error: invalid priority "${opts.priority}". Allowed: low, medium, high, urgent`);
107
+ return 1;
108
+ }
109
+ priority = normalized;
110
+ }
111
+ const envUrl = process.env.LUMO_API_URL?.trim();
112
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
113
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
114
+ const url = `${base}/api/tasks`;
115
+ let tagIds;
116
+ if ((opts.tag && opts.tag.length > 0) ||
117
+ (opts.tagId && opts.tagId.length > 0)) {
118
+ try {
119
+ tagIds = await (0, tag_resolver_1.resolveTagRefs)({ names: opts.tag ?? [], ids: opts.tagId ?? [] }, { apiUrl, token: creds.token });
120
+ }
121
+ catch (err) {
122
+ console.error(`Error: ${err.message}`);
123
+ return 1;
124
+ }
125
+ }
126
+ const body = {
127
+ title: title.trim(),
128
+ priority,
129
+ };
130
+ if (opts.description !== undefined)
131
+ body.description = opts.description;
132
+ if (opts.project !== undefined)
133
+ body.projectRef = opts.project;
134
+ if (opts.assignee !== undefined)
135
+ body.assigneeRef = opts.assignee;
136
+ if (opts.milestone !== undefined)
137
+ body.milestoneRef = opts.milestone;
138
+ if (tagIds && tagIds.length > 0)
139
+ body.tagIds = tagIds;
140
+ let res;
141
+ try {
142
+ res = await fetch(url, {
143
+ method: 'POST',
144
+ headers: {
145
+ Authorization: `Bearer ${creds.token}`,
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: JSON.stringify(body),
149
+ });
150
+ }
151
+ catch (err) {
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
154
+ return 1;
155
+ }
156
+ if (res.status === 401) {
157
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
158
+ return 1;
159
+ }
160
+ if (res.status === 201) {
161
+ const data = (await res.json());
162
+ const responseTagNames = (data.task.taskTags ?? []).map(t => t.name);
163
+ process.stdout.write(formatCreatedTaskLine({ ...data.task, tags: responseTagNames }) + '\n');
164
+ // Sprint binding (optional): if --sprint was passed, bind the new task.
165
+ if (opts.sprint) {
166
+ const workspaceSlug = creds.workspaceSlug ?? '';
167
+ try {
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`);
170
+ }
171
+ catch (err) {
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ // Spec's literal mismatch output — print bare, no "Error:" prefix
174
+ if (msg.startsWith('Sprint binding skipped:')) {
175
+ console.error(msg);
176
+ }
177
+ else {
178
+ console.error(`Error: ${msg}`);
179
+ }
180
+ return 1;
181
+ }
182
+ }
183
+ return;
184
+ }
185
+ // Surface server error message when present, otherwise the status
186
+ let serverMsg = null;
187
+ try {
188
+ const errBody = (await res.json());
189
+ if (typeof errBody.error === 'string')
190
+ serverMsg = errBody.error;
191
+ }
192
+ catch {
193
+ // Body wasn't JSON; fall through to status-only message
194
+ }
195
+ if (serverMsg) {
196
+ console.error(`Error: ${serverMsg}`);
197
+ }
198
+ else {
199
+ console.error(`Error: task create failed (HTTP ${res.status})`);
200
+ }
201
+ return 1;
202
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTaskListTable = void 0;
4
+ exports.resolveTaskListEndpoint = resolveTaskListEndpoint;
5
+ exports.taskList = taskList;
6
+ const config_1 = require("../lib/config");
7
+ const api_1 = require("../lib/api");
8
+ const format_1 = require("../lib/format");
9
+ const resolve_1 = require("../lib/resolve");
10
+ var format_2 = require("../lib/format");
11
+ Object.defineProperty(exports, "formatTaskListTable", { enumerable: true, get: function () { return format_2.formatTaskListTable; } });
12
+ const ALLOWED_STATUSES = ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'];
13
+ function resolveTaskListEndpoint(base, milestoneId) {
14
+ if (!milestoneId)
15
+ return `${base}/api/tasks/me`;
16
+ return `${base}/api/milestones/${milestoneId}/tasks?assigned=me`;
17
+ }
18
+ function normalizeStatusFilter(value) {
19
+ if (!value)
20
+ return null;
21
+ const upper = value.toUpperCase().replace(/-/g, '_');
22
+ return ALLOWED_STATUSES.includes(upper)
23
+ ? upper
24
+ : null;
25
+ }
26
+ async function taskList(opts) {
27
+ let statusFilter;
28
+ if (opts.status !== undefined) {
29
+ const n = normalizeStatusFilter(opts.status);
30
+ if (!n) {
31
+ console.error(`Error: invalid status "${opts.status}". Allowed: todo, in_progress, in_review, done`);
32
+ return 1;
33
+ }
34
+ statusFilter = n;
35
+ }
36
+ const limit = opts.limit !== undefined ? parseInt(opts.limit, 10) : undefined;
37
+ if (limit !== undefined && (Number.isNaN(limit) || limit < 1)) {
38
+ console.error(`Error: invalid --limit "${opts.limit}" (expected a positive integer)`);
39
+ return 1;
40
+ }
41
+ const creds = (0, config_1.readCredentials)();
42
+ if (!creds) {
43
+ console.error('Error: not logged in. Run `lumo auth login` first.');
44
+ return 1;
45
+ }
46
+ const envUrl = process.env.LUMO_API_URL?.trim();
47
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
48
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
49
+ let milestoneId;
50
+ if (opts.milestone !== undefined) {
51
+ try {
52
+ const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, opts.milestone, opts.project);
53
+ milestoneId = resolved.id;
54
+ }
55
+ catch (err) {
56
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
57
+ return 1;
58
+ }
59
+ }
60
+ const url = resolveTaskListEndpoint(base, milestoneId);
61
+ let res;
62
+ try {
63
+ res = await fetch(url, {
64
+ headers: { Authorization: `Bearer ${creds.token}` },
65
+ });
66
+ }
67
+ catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
70
+ return 1;
71
+ }
72
+ if (res.status === 401) {
73
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
74
+ return 1;
75
+ }
76
+ if (!res.ok) {
77
+ console.error(`Error: task list failed (HTTP ${res.status})`);
78
+ return 1;
79
+ }
80
+ const data = (await res.json());
81
+ // Server returns the full assigned-to-me set. Filters are client-side
82
+ // because /api/tasks/me doesn't accept status/project query params yet
83
+ // — when it does, switch to query params and drop this filter block.
84
+ let tasks = data.tasks;
85
+ if (statusFilter)
86
+ tasks = tasks.filter(t => t.status === statusFilter);
87
+ if (opts.project) {
88
+ const ref = opts.project.toLowerCase();
89
+ tasks = tasks.filter(t => t.project.name.toLowerCase() === ref);
90
+ }
91
+ if (limit !== undefined)
92
+ tasks = tasks.slice(0, limit);
93
+ process.stdout.write((0, format_1.formatTaskListTable)(tasks) + '\n');
94
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTaskShow = formatTaskShow;
4
+ exports.taskShow = taskShow;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ /**
8
+ * Render task detail as a key:value block. Multi-line description is
9
+ * indented under a `Description:` label so the structure stays scannable.
10
+ */
11
+ function formatTaskShow(task) {
12
+ const lines = [];
13
+ lines.push(`${task.identifier} ${task.title}`);
14
+ lines.push(`Status: ${task.status}`);
15
+ lines.push(`Priority: ${task.priority}`);
16
+ lines.push(`Project: ${task.project.name}`);
17
+ if (task.assignee) {
18
+ const detail = task.assignee.email && task.assignee.type === 'member'
19
+ ? `${task.assignee.label} <${task.assignee.email}>`
20
+ : task.assignee.label;
21
+ lines.push(`Assignee: ${detail}`);
22
+ }
23
+ else {
24
+ lines.push('Assignee: (unassigned)');
25
+ }
26
+ lines.push(`URL: ${task.url}`);
27
+ if (task.description && task.description.trim().length > 0) {
28
+ lines.push('');
29
+ lines.push('Description:');
30
+ for (const line of task.description.split('\n')) {
31
+ lines.push(` ${line}`);
32
+ }
33
+ }
34
+ return lines.join('\n');
35
+ }
36
+ async function taskShow(identifier) {
37
+ if (!identifier) {
38
+ console.error('Error: missing <identifier>. Usage: lumo task show <LUM-42>');
39
+ return 1;
40
+ }
41
+ const creds = (0, config_1.readCredentials)();
42
+ if (!creds) {
43
+ console.error('Error: not logged in. Run `lumo auth login` first.');
44
+ return 1;
45
+ }
46
+ const envUrl = process.env.LUMO_API_URL?.trim();
47
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
48
+ const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/by-identifier/${encodeURIComponent(identifier)}`;
49
+ let res;
50
+ try {
51
+ res = await fetch(url, {
52
+ headers: { Authorization: `Bearer ${creds.token}` },
53
+ });
54
+ }
55
+ catch (err) {
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
58
+ return 1;
59
+ }
60
+ if (res.status === 401) {
61
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
62
+ return 1;
63
+ }
64
+ if (res.status === 404) {
65
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
66
+ return 1;
67
+ }
68
+ if (!res.ok) {
69
+ console.error(`Error: task show failed (HTTP ${res.status})`);
70
+ return 1;
71
+ }
72
+ const data = (await res.json());
73
+ process.stdout.write(formatTaskShow(data.task) + '\n');
74
+ }