@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,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSprintSummary = formatSprintSummary;
|
|
4
|
+
exports.sprintSummary = sprintSummary;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_1 = require("../lib/resolve");
|
|
8
|
+
function formatSprintSummary(input) {
|
|
9
|
+
const { number, name, stats, tldr, report } = input;
|
|
10
|
+
const lines = [`Sprint: #${number} ${name}`];
|
|
11
|
+
if (stats === null && tldr === null && report === null) {
|
|
12
|
+
lines.push('', '(no summary generated yet)');
|
|
13
|
+
return lines.join('\n');
|
|
14
|
+
}
|
|
15
|
+
if (stats !== null) {
|
|
16
|
+
lines.push(`Done: ${stats.done} / ${stats.total}`);
|
|
17
|
+
}
|
|
18
|
+
if (tldr) {
|
|
19
|
+
lines.push('', tldr);
|
|
20
|
+
}
|
|
21
|
+
if (report) {
|
|
22
|
+
lines.push('', report);
|
|
23
|
+
}
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
async function sprintSummary(identifier, opts) {
|
|
27
|
+
const creds = (0, config_1.readCredentials)();
|
|
28
|
+
if (!creds) {
|
|
29
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
33
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
34
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
35
|
+
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
36
|
+
let resolved;
|
|
37
|
+
try {
|
|
38
|
+
resolved = await (0, resolve_1.resolveSprintId)(base, creds.token, identifier, opts.team, workspaceSlug);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
// Fill in number/name if identifier was a UUID (same pattern as sprint-start.ts).
|
|
45
|
+
let displayNumber = resolved.number;
|
|
46
|
+
let displayName = resolved.name;
|
|
47
|
+
if (!displayName) {
|
|
48
|
+
const r = await fetch(`${base}/api/sprints/${resolved.id}`, {
|
|
49
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
50
|
+
});
|
|
51
|
+
if (r.ok) {
|
|
52
|
+
const { sprint } = (await r.json());
|
|
53
|
+
displayNumber = sprint.number;
|
|
54
|
+
displayName = sprint.name;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Optional retry: POST to /summary/retry first. Expect 202 (queued).
|
|
58
|
+
if (opts.retry) {
|
|
59
|
+
let retryRes;
|
|
60
|
+
try {
|
|
61
|
+
retryRes = await fetch(`${base}/api/sprints/${resolved.id}/summary/retry`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
if (retryRes.status === 401) {
|
|
72
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
if (!retryRes.ok && retryRes.status !== 202) {
|
|
76
|
+
let errMsg = `summary retry failed (HTTP ${retryRes.status})`;
|
|
77
|
+
try {
|
|
78
|
+
const errBody = (await retryRes.json());
|
|
79
|
+
if (errBody.error)
|
|
80
|
+
errMsg = errBody.error;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
console.error(`Error: ${errMsg}`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
// 202 means queued; regeneration is async. The GET below may still return
|
|
89
|
+
// the old summary or 404 — warn the user.
|
|
90
|
+
console.error('Note: summary regeneration queued. Result below may still be the previous summary.');
|
|
91
|
+
}
|
|
92
|
+
// GET /api/sprints/<id>/summary — returns SprintSummary row directly or 404.
|
|
93
|
+
let summaryRes;
|
|
94
|
+
try {
|
|
95
|
+
summaryRes = await fetch(`${base}/api/sprints/${resolved.id}/summary`, {
|
|
96
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
if (summaryRes.status === 401) {
|
|
105
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
let stats = null;
|
|
109
|
+
let tldr = null;
|
|
110
|
+
let report = null;
|
|
111
|
+
if (summaryRes.ok) {
|
|
112
|
+
const body = (await summaryRes.json());
|
|
113
|
+
stats = body.stats ?? null;
|
|
114
|
+
tldr = body.tldr ?? null;
|
|
115
|
+
report = body.report ?? null;
|
|
116
|
+
}
|
|
117
|
+
else if (summaryRes.status !== 404) {
|
|
118
|
+
let errMsg = `sprint summary failed (HTTP ${summaryRes.status})`;
|
|
119
|
+
try {
|
|
120
|
+
const errBody = (await summaryRes.json());
|
|
121
|
+
if (errBody.error)
|
|
122
|
+
errMsg = errBody.error;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
console.error(`Error: ${errMsg}`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
// 404 → fall through with all-null; formatSprintSummary renders the friendly marker.
|
|
131
|
+
process.stdout.write(formatSprintSummary({
|
|
132
|
+
number: displayNumber,
|
|
133
|
+
name: displayName,
|
|
134
|
+
stats,
|
|
135
|
+
tldr,
|
|
136
|
+
report,
|
|
137
|
+
}) + '\n');
|
|
138
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSprintUpdatePayload = buildSprintUpdatePayload;
|
|
4
|
+
exports.formatSprintUpdateSummary = formatSprintUpdateSummary;
|
|
5
|
+
exports.sprintUpdate = sprintUpdate;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const resolve_1 = require("../lib/resolve");
|
|
9
|
+
function buildSprintUpdatePayload(opts) {
|
|
10
|
+
const payload = {};
|
|
11
|
+
const flagsGiven = [];
|
|
12
|
+
if (opts.name !== undefined) {
|
|
13
|
+
payload.name = opts.name;
|
|
14
|
+
flagsGiven.push('--name');
|
|
15
|
+
}
|
|
16
|
+
if (opts.start !== undefined) {
|
|
17
|
+
payload.startDate = opts.start;
|
|
18
|
+
flagsGiven.push('--start');
|
|
19
|
+
}
|
|
20
|
+
if (opts.end !== undefined) {
|
|
21
|
+
payload.endDate = opts.end;
|
|
22
|
+
flagsGiven.push('--end');
|
|
23
|
+
}
|
|
24
|
+
return { payload, flagsGiven };
|
|
25
|
+
}
|
|
26
|
+
function fmtDate(v) {
|
|
27
|
+
if (!v)
|
|
28
|
+
return '-';
|
|
29
|
+
return v.slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
function formatSprintUpdateSummary(before, after) {
|
|
32
|
+
const changes = [];
|
|
33
|
+
if (after.name !== undefined && after.name !== before.name) {
|
|
34
|
+
changes.push(`name "${before.name}" → "${after.name}"`);
|
|
35
|
+
}
|
|
36
|
+
if (after.startDate !== undefined) {
|
|
37
|
+
changes.push(`start → ${fmtDate(after.startDate)}`);
|
|
38
|
+
}
|
|
39
|
+
if (after.endDate !== undefined) {
|
|
40
|
+
changes.push(`end → ${fmtDate(after.endDate)}`);
|
|
41
|
+
}
|
|
42
|
+
return `Updated sprint #${before.number} "${before.name}": ${changes.join(', ')}`;
|
|
43
|
+
}
|
|
44
|
+
async function sprintUpdate(identifier, opts) {
|
|
45
|
+
// Reject empty strings — these fields can't be cleared.
|
|
46
|
+
if (opts.name !== undefined && opts.name === '') {
|
|
47
|
+
console.error('Error: --name cannot be empty.');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
if (opts.start !== undefined && opts.start === '') {
|
|
51
|
+
console.error('Error: --start cannot be empty.');
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (opts.end !== undefined && opts.end === '') {
|
|
55
|
+
console.error('Error: --end cannot be empty.');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const { payload, flagsGiven } = buildSprintUpdatePayload(opts);
|
|
59
|
+
if (flagsGiven.length === 0) {
|
|
60
|
+
console.error('Error: provide at least one field to update (--name, --start, --end)');
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const creds = (0, config_1.readCredentials)();
|
|
64
|
+
if (!creds) {
|
|
65
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
69
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
70
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
71
|
+
let sprintId;
|
|
72
|
+
let resolvedNumber;
|
|
73
|
+
let resolvedName;
|
|
74
|
+
try {
|
|
75
|
+
const resolved = await (0, resolve_1.resolveSprintId)(base, creds.token, identifier, opts.team, creds.workspaceSlug);
|
|
76
|
+
sprintId = resolved.id;
|
|
77
|
+
resolvedNumber = resolved.number;
|
|
78
|
+
resolvedName = resolved.name;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
// Fetch current values to populate the before-snapshot for the summary.
|
|
85
|
+
// When resolveSprintId short-circuited on UUID, number/name are empty — we
|
|
86
|
+
// always GET to get a consistent snapshot regardless of identifier form.
|
|
87
|
+
let beforeRes;
|
|
88
|
+
try {
|
|
89
|
+
beforeRes = await fetch(`${base}/api/sprints/${sprintId}`, {
|
|
90
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
if (beforeRes.status === 401) {
|
|
99
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
if (!beforeRes.ok) {
|
|
103
|
+
console.error(`Error: sprint fetch failed (HTTP ${beforeRes.status})`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const { sprint: before } = (await beforeRes.json());
|
|
107
|
+
// Use resolved values if available (number-based lookup already returned them).
|
|
108
|
+
const beforeSnapshot = {
|
|
109
|
+
number: before.number || resolvedNumber,
|
|
110
|
+
name: before.name || resolvedName,
|
|
111
|
+
...(before.startDate !== undefined && { startDate: before.startDate }),
|
|
112
|
+
...(before.endDate !== undefined && { endDate: before.endDate }),
|
|
113
|
+
};
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = await fetch(`${base}/api/sprints/${sprintId}`, {
|
|
117
|
+
method: 'PATCH',
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${creds.token}`,
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify(payload),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
if (res.status === 401) {
|
|
131
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
let errMsg = `sprint update failed (HTTP ${res.status})`;
|
|
136
|
+
try {
|
|
137
|
+
const errBody = (await res.json());
|
|
138
|
+
if (errBody.error)
|
|
139
|
+
errMsg = errBody.error;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// body wasn't JSON; keep the status-only message
|
|
143
|
+
}
|
|
144
|
+
console.error(`Error: ${errMsg}`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
process.stdout.write(formatSprintUpdateSummary(beforeSnapshot, payload) + '\n');
|
|
148
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taskComment = taskComment;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
/**
|
|
7
|
+
* `lumo task comment <LUM-N> <body>`
|
|
8
|
+
*
|
|
9
|
+
* Two HTTP calls: resolve LUM-N → task DB id, then POST the comment to
|
|
10
|
+
* `/api/tasks/:id/comments`. The comments endpoint accepts both Clerk and
|
|
11
|
+
* bearer auth (dual-auth) — see lib/auth/identity.ts.
|
|
12
|
+
*
|
|
13
|
+
* Body is taken verbatim as the comment text. Multi-line bodies work via
|
|
14
|
+
* shell quoting. We do not support markdown @-mention chip syntax from the
|
|
15
|
+
* CLI; plain text only.
|
|
16
|
+
*/
|
|
17
|
+
async function taskComment(identifier, body) {
|
|
18
|
+
if (!identifier) {
|
|
19
|
+
console.error('Error: missing <identifier>. Usage: lumo task comment <LUM-42> "comment body"');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
if (!body || body.trim().length === 0) {
|
|
23
|
+
console.error('Error: missing <body>. Usage: lumo task comment <LUM-42> "comment body"');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const creds = (0, config_1.readCredentials)();
|
|
27
|
+
if (!creds) {
|
|
28
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
32
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
33
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
34
|
+
// 1. Resolve LUM-N → DB id (the comments endpoint takes DB id).
|
|
35
|
+
let resolveRes;
|
|
36
|
+
try {
|
|
37
|
+
resolveRes = await fetch(`${base}/api/tasks/resolve/${encodeURIComponent(identifier)}`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (resolveRes.status === 401) {
|
|
45
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
if (resolveRes.status === 404) {
|
|
49
|
+
console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
if (!resolveRes.ok) {
|
|
53
|
+
console.error(`Error: resolve failed (HTTP ${resolveRes.status})`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const resolved = (await resolveRes.json());
|
|
57
|
+
// 2. POST the comment.
|
|
58
|
+
let postRes;
|
|
59
|
+
try {
|
|
60
|
+
postRes = await fetch(`${base}/api/tasks/${encodeURIComponent(resolved.id)}/comments`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${creds.token}`,
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify({ body }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
if (postRes.status === 401) {
|
|
75
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
if (postRes.status !== 201) {
|
|
79
|
+
let serverMsg = null;
|
|
80
|
+
try {
|
|
81
|
+
const errBody = (await postRes.json());
|
|
82
|
+
if (typeof errBody.error === 'string')
|
|
83
|
+
serverMsg = errBody.error;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Body wasn't JSON
|
|
87
|
+
}
|
|
88
|
+
console.error(serverMsg
|
|
89
|
+
? `Error: ${serverMsg}`
|
|
90
|
+
: `Error: comment create failed (HTTP ${postRes.status})`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
const data = (await postRes.json());
|
|
94
|
+
process.stdout.write(`Commented on ${resolved.identifier} (comment ${data.comment.id})\n`);
|
|
95
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taskContext = taskContext;
|
|
4
|
+
exports.formatTaskContextMarkdown = formatTaskContextMarkdown;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
async function taskContext(identifier) {
|
|
8
|
+
if (!identifier) {
|
|
9
|
+
console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
const creds = (0, config_1.readCredentials)();
|
|
13
|
+
if (!creds) {
|
|
14
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
18
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
19
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/context/${encodeURIComponent(identifier)}`;
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetch(url, {
|
|
23
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
if (res.status === 401) {
|
|
32
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 404) {
|
|
36
|
+
console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
console.error(`Error: context fetch failed (HTTP ${res.status})`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
const now = new Date();
|
|
45
|
+
process.stdout.write(formatTaskContextMarkdown(data, now));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render a TaskContextResponse as the agent-facing markdown handoff. Pure
|
|
49
|
+
* function; the `now` parameter makes "X ago" phrasing deterministic in
|
|
50
|
+
* tests.
|
|
51
|
+
*/
|
|
52
|
+
function formatTaskContextMarkdown(data, now) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(`# Task: ${data.task.identifier}`);
|
|
55
|
+
lines.push(`**Title**: ${data.task.title}`);
|
|
56
|
+
lines.push(`**Status**: ${data.task.status}`);
|
|
57
|
+
if (data.task.milestone) {
|
|
58
|
+
const target = data.task.milestone.targetDate
|
|
59
|
+
? `, target ${data.task.milestone.targetDate.slice(0, 10)}`
|
|
60
|
+
: '';
|
|
61
|
+
lines.push(`**Milestone**: ${data.task.milestone.name} (${data.task.milestone.status}${target})`);
|
|
62
|
+
}
|
|
63
|
+
if (data.task.description && data.task.description.trim().length > 0) {
|
|
64
|
+
lines.push(`**Description**: ${data.task.description}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
// Frontload memory before sessions: it's cold context the agent should see
|
|
68
|
+
// first. Server returns "" when empty, in which case we skip the section.
|
|
69
|
+
if (data.memorySection && data.memorySection.trim().length > 0) {
|
|
70
|
+
lines.push(data.memorySection.trimEnd());
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
if (data.slackContextSection && data.slackContextSection.trim().length > 0) {
|
|
74
|
+
lines.push(data.slackContextSection.trimEnd());
|
|
75
|
+
lines.push('');
|
|
76
|
+
}
|
|
77
|
+
if (data.webLinkSection && data.webLinkSection.trim().length > 0) {
|
|
78
|
+
lines.push(data.webLinkSection.trimEnd());
|
|
79
|
+
lines.push('');
|
|
80
|
+
}
|
|
81
|
+
if (data.sessions.length === 0) {
|
|
82
|
+
lines.push('## Previous Sessions (0)');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('_No prior coding sessions for this task._');
|
|
85
|
+
lines.push('');
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
lines.push(`## Previous Sessions (${data.sessions.length})`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
for (const s of data.sessions) {
|
|
91
|
+
const shortId = s.id.slice(0, 8);
|
|
92
|
+
const ago = relativeTime(new Date(s.lastActivityAt), now);
|
|
93
|
+
const dur = formatDuration(s.durationMs);
|
|
94
|
+
lines.push(`### Session ${shortId} · ${ago} · ${dur}`);
|
|
95
|
+
lines.push(`**Summary**: ${s.headline}`);
|
|
96
|
+
if (s.unresolved.length > 0) {
|
|
97
|
+
lines.push('**Unresolved**:');
|
|
98
|
+
for (const u of s.unresolved)
|
|
99
|
+
lines.push(`- ${u}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
function formatDuration(ms) {
|
|
106
|
+
if (ms < 60_000) {
|
|
107
|
+
return `${Math.max(1, Math.round(ms / 1000))}s`;
|
|
108
|
+
}
|
|
109
|
+
const totalMinutes = Math.round(ms / 60_000);
|
|
110
|
+
if (totalMinutes < 60)
|
|
111
|
+
return `${totalMinutes}min`;
|
|
112
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
113
|
+
const minutes = totalMinutes % 60;
|
|
114
|
+
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}min`;
|
|
115
|
+
}
|
|
116
|
+
function relativeTime(then, now) {
|
|
117
|
+
const diffMs = now.getTime() - then.getTime();
|
|
118
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
119
|
+
if (seconds < 60)
|
|
120
|
+
return 'just now';
|
|
121
|
+
const minutes = Math.floor(seconds / 60);
|
|
122
|
+
if (minutes < 60)
|
|
123
|
+
return `${minutes}min ago`;
|
|
124
|
+
const hours = Math.floor(minutes / 60);
|
|
125
|
+
if (hours < 24)
|
|
126
|
+
return `${hours}h ago`;
|
|
127
|
+
const days = Math.floor(hours / 24);
|
|
128
|
+
if (days === 1)
|
|
129
|
+
return 'yesterday';
|
|
130
|
+
if (days < 7)
|
|
131
|
+
return `${days} days ago`;
|
|
132
|
+
const weeks = Math.floor(days / 7);
|
|
133
|
+
if (weeks < 5)
|
|
134
|
+
return `${weeks}w ago`;
|
|
135
|
+
const months = Math.floor(days / 30);
|
|
136
|
+
return `${months}mo ago`;
|
|
137
|
+
}
|