@lumoai/cli 1.55.0 → 1.56.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/assets/skill/SKILL.md +71 -0
- package/assets/skill/references/priority.md +6 -5
- package/assets/skill/references/tasks.md +15 -3
- package/dist/cli/src/commands/idea-comment.js +127 -0
- package/dist/cli/src/commands/idea-figma-add.js +30 -0
- package/dist/cli/src/commands/idea-figma-context.js +65 -0
- package/dist/cli/src/commands/idea-figma-list.js +32 -0
- package/dist/cli/src/commands/idea-figma-refresh.js +40 -0
- package/dist/cli/src/commands/idea-figma-rm.js +19 -0
- package/dist/cli/src/commands/idea-slack-add.js +66 -0
- package/dist/cli/src/commands/idea-slack-rm.js +63 -0
- package/dist/cli/src/commands/idea-slack-show.js +65 -0
- package/dist/cli/src/commands/idea-web-add.js +66 -0
- package/dist/cli/src/commands/idea-web-rm.js +63 -0
- package/dist/cli/src/commands/idea-web-show.js +71 -0
- package/dist/cli/src/commands/idea.js +140 -0
- package/dist/cli/src/commands/initiative.js +132 -0
- package/dist/cli/src/commands/next.js +9 -1
- package/dist/cli/src/commands/session-attach.js +4 -0
- package/dist/cli/src/commands/setup.js +17 -8
- package/dist/cli/src/commands/task-comment-list.js +2 -1
- package/dist/cli/src/commands/task-context.js +5 -1
- package/dist/cli/src/commands/task-create.js +18 -1
- package/dist/cli/src/commands/task-update.js +16 -0
- package/dist/cli/src/commands/worktree-add.js +6 -2
- package/dist/cli/src/index.js +118 -3
- package/dist/cli/src/lib/idea-figma-api.js +65 -0
- package/dist/cli/src/lib/next-steps.js +55 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ideaSlackShow = ideaSlackShow;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const report_pull_1 = require("../lib/report-pull");
|
|
8
|
+
/**
|
|
9
|
+
* `lumo idea slack show <LUM-I1> <context-id>`
|
|
10
|
+
*
|
|
11
|
+
* Tier-2 retrieval for the cheap inline Slack card on an idea. Fetches the
|
|
12
|
+
* stored thread snapshot (no live Slack call) from
|
|
13
|
+
* `/api/ideas/:id/slack-contexts/:contextId/snapshot` and prints one line per
|
|
14
|
+
* message as `author: text`, falling back to `@<userId>` when the display
|
|
15
|
+
* name is missing. Mirrors `taskSlackShow` (LUM-681).
|
|
16
|
+
*/
|
|
17
|
+
async function ideaSlackShow(identifier, contextId) {
|
|
18
|
+
if (!identifier || !contextId) {
|
|
19
|
+
console.error('Error: usage: lumo idea slack show <LUM-I1> <context-id>');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const creds = (0, config_1.readCredentials)();
|
|
23
|
+
if (!creds) {
|
|
24
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
28
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/slack-contexts/${encodeURIComponent(contextId)}/snapshot`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (res.status === 401) {
|
|
39
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
if (res.status === 404) {
|
|
43
|
+
console.error(`Error: idea ${identifier} or slack context ${contextId} not found in workspace ${creds.workspaceSlug}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
console.error(`Error: slack show failed (HTTP ${res.status})`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const { snapshot } = (await res.json());
|
|
51
|
+
const messages = snapshot?.messages ?? [];
|
|
52
|
+
if (messages.length === 0) {
|
|
53
|
+
console.log('(no messages in stored snapshot)');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
for (const m of messages) {
|
|
57
|
+
const author = (0, sanitize_1.sanitizeField)(m.userName ?? '@' + m.userId);
|
|
58
|
+
console.log(`${author}: ${(0, sanitize_1.sanitizeField)(m.text)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// LUM-681: mirrors LUM-500 disclosure-funnel stamping for idea fragments.
|
|
62
|
+
// The contextId arg == lineage SLACK_CONTEXT fragmentId. Fire-and-forget —
|
|
63
|
+
// never blocks, swallows failures.
|
|
64
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'SLACK_CONTEXT', fragmentId: contextId });
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ideaWebAdd = ideaWebAdd;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
/**
|
|
8
|
+
* `lumo idea web add <LUM-I1> <url>`
|
|
9
|
+
*
|
|
10
|
+
* Attaches a web link to an idea by POSTing the url to
|
|
11
|
+
* `/api/ideas/:id/web-links`. Mirrors the task-side flow (LUM-681).
|
|
12
|
+
*/
|
|
13
|
+
async function ideaWebAdd(identifier, url) {
|
|
14
|
+
if (!identifier || !url) {
|
|
15
|
+
console.error('Error: usage: lumo idea web add <LUM-I1> <url>');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const creds = (0, config_1.readCredentials)();
|
|
19
|
+
if (!creds) {
|
|
20
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
24
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/web-links`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${creds.token}`,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ url }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
if (res.status === 401) {
|
|
42
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (res.status === 404) {
|
|
46
|
+
console.error(`Error: idea ${identifier} not found in workspace ${creds.workspaceSlug}`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
let serverMsg = null;
|
|
51
|
+
try {
|
|
52
|
+
const errBody = (await res.json());
|
|
53
|
+
if (typeof errBody.error === 'string')
|
|
54
|
+
serverMsg = errBody.error;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* not JSON */
|
|
58
|
+
}
|
|
59
|
+
console.error(serverMsg
|
|
60
|
+
? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
|
|
61
|
+
: `Error: web add failed (HTTP ${res.status})`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const { webLink } = (await res.json());
|
|
65
|
+
console.log(`Linked web page to ${identifier} (link ${webLink.id})`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ideaWebRm = ideaWebRm;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
/**
|
|
8
|
+
* `lumo idea web rm <LUM-I1> <link-id>`
|
|
9
|
+
*
|
|
10
|
+
* Removes a web link from an idea via
|
|
11
|
+
* `DELETE /api/ideas/:id/web-links/:linkId`. Mirrors the task-side shape
|
|
12
|
+
* (LUM-681); no task-side `rm` equivalent exists yet, so this follows the
|
|
13
|
+
* DELETE route contract directly.
|
|
14
|
+
*/
|
|
15
|
+
async function ideaWebRm(identifier, linkId) {
|
|
16
|
+
if (!identifier || !linkId) {
|
|
17
|
+
console.error('Error: usage: lumo idea web rm <LUM-I1> <link-id>');
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
const creds = (0, config_1.readCredentials)();
|
|
21
|
+
if (!creds) {
|
|
22
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
26
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
27
|
+
let res;
|
|
28
|
+
try {
|
|
29
|
+
res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/web-links/${encodeURIComponent(linkId)}`, {
|
|
30
|
+
method: 'DELETE',
|
|
31
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
if (res.status === 401) {
|
|
40
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (res.status === 404) {
|
|
44
|
+
console.error(`Error: idea ${identifier} or web link ${linkId} not found in workspace ${creds.workspaceSlug}`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (!res.ok && res.status !== 204) {
|
|
48
|
+
let serverMsg = null;
|
|
49
|
+
try {
|
|
50
|
+
const errBody = (await res.json());
|
|
51
|
+
if (typeof errBody.error === 'string')
|
|
52
|
+
serverMsg = errBody.error;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* not JSON */
|
|
56
|
+
}
|
|
57
|
+
console.error(serverMsg
|
|
58
|
+
? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
|
|
59
|
+
: `Error: web rm failed (HTTP ${res.status})`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
console.log(`Removed web link ${linkId} from ${identifier}`);
|
|
63
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ideaWebShow = ideaWebShow;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const report_pull_1 = require("../lib/report-pull");
|
|
8
|
+
/**
|
|
9
|
+
* `lumo idea web show <LUM-I1> <link-id>`
|
|
10
|
+
*
|
|
11
|
+
* Tier-2 retrieval for the cheap inline WebLink card on an idea. Fetches the
|
|
12
|
+
* page body (cached, or fetched behind the SSRF guard on first read) from
|
|
13
|
+
* `/api/ideas/:id/web-links/:linkId/body` and prints it as plain text.
|
|
14
|
+
* Mirrors `taskWebShow` (LUM-681).
|
|
15
|
+
*/
|
|
16
|
+
async function ideaWebShow(identifier, linkId) {
|
|
17
|
+
if (!identifier || !linkId) {
|
|
18
|
+
console.error('Error: usage: lumo idea web show <LUM-I1> <link-id>');
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const creds = (0, config_1.readCredentials)();
|
|
22
|
+
if (!creds) {
|
|
23
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
27
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
28
|
+
let res;
|
|
29
|
+
try {
|
|
30
|
+
res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/web-links/${encodeURIComponent(linkId)}/body`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (res.status === 401) {
|
|
38
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
if (res.status === 404) {
|
|
42
|
+
console.error(`Error: idea ${identifier} or web link ${linkId} not found in workspace ${creds.workspaceSlug}`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
let serverMsg = null;
|
|
47
|
+
try {
|
|
48
|
+
const errBody = (await res.json());
|
|
49
|
+
if (typeof errBody.error === 'string')
|
|
50
|
+
serverMsg = errBody.error;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* not JSON */
|
|
54
|
+
}
|
|
55
|
+
console.error(serverMsg
|
|
56
|
+
? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
|
|
57
|
+
: `Error: web show failed (HTTP ${res.status})`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const { body } = (await res.json());
|
|
61
|
+
if (!body || body.trim().length === 0) {
|
|
62
|
+
console.log('(empty body)');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log((0, sanitize_1.sanitizeField)(body));
|
|
66
|
+
}
|
|
67
|
+
// LUM-681: mirrors LUM-500 disclosure-funnel stamping for idea fragments.
|
|
68
|
+
// The linkId arg == lineage WEB_LINK fragmentId. Fire-and-forget — never
|
|
69
|
+
// blocks output, swallows failures.
|
|
70
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'WEB_LINK', fragmentId: linkId });
|
|
71
|
+
}
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatIdeaList = formatIdeaList;
|
|
3
4
|
exports.formatCapturedIdeaLine = formatCapturedIdeaLine;
|
|
4
5
|
exports.ideaCapture = ideaCapture;
|
|
6
|
+
exports.ideaList = ideaList;
|
|
7
|
+
exports.ideaUpdate = ideaUpdate;
|
|
5
8
|
const config_1 = require("../lib/config");
|
|
6
9
|
const api_1 = require("../lib/api");
|
|
7
10
|
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
11
|
const sanitize_1 = require("../lib/sanitize");
|
|
12
|
+
/**
|
|
13
|
+
* The four Idea lifecycle states (LUM-679), lower-cased for CLI input. The
|
|
14
|
+
* command normalizes case before sending, so `--status Developing` and
|
|
15
|
+
* `--status developing` both resolve. Mirrors `lib/validation/idea.ts`.
|
|
16
|
+
*/
|
|
17
|
+
const IDEA_STATUS_VALUES = [
|
|
18
|
+
'captured',
|
|
19
|
+
'developing',
|
|
20
|
+
'planned',
|
|
21
|
+
'dropped',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Render the idea pool as aligned `<id> <status> <statement>` lines. The
|
|
25
|
+
* server already returns the pool newest-first, so this preserves order.
|
|
26
|
+
* Empty pool → a friendly one-liner.
|
|
27
|
+
*/
|
|
28
|
+
function formatIdeaList(ideas) {
|
|
29
|
+
if (ideas.length === 0)
|
|
30
|
+
return 'No ideas.\n';
|
|
31
|
+
const idWidth = Math.max(...ideas.map(i => i.identifier.length));
|
|
32
|
+
const statusWidth = Math.max(...ideas.map(i => i.status.length));
|
|
33
|
+
return (ideas
|
|
34
|
+
.map(i => {
|
|
35
|
+
const id = i.identifier.padEnd(idWidth);
|
|
36
|
+
const status = i.status.padEnd(statusWidth);
|
|
37
|
+
return `${id} ${status} ${(0, sanitize_1.sanitizeField)(i.statement)}`;
|
|
38
|
+
})
|
|
39
|
+
.join('\n') + '\n');
|
|
40
|
+
}
|
|
9
41
|
/**
|
|
10
42
|
* Success line for a captured idea. Echoes the human-referenceable id
|
|
11
43
|
* (`<TEAM>-I<n>`, e.g. `LUM-I42`) — the `I` prefix keeps it visibly distinct
|
|
@@ -87,3 +119,111 @@ async function ideaCapture(statement, opts) {
|
|
|
87
119
|
}
|
|
88
120
|
return 1;
|
|
89
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* `lumo idea list` — print the team's idea pool newest-first, each line
|
|
124
|
+
* showing the `<TEAM>-I<n>` id, status (CAPTURED|PLANNED) and statement.
|
|
125
|
+
* Reads `GET /api/ideas`, which returns the whole pool already ordered
|
|
126
|
+
* newest-first.
|
|
127
|
+
*/
|
|
128
|
+
async function ideaList() {
|
|
129
|
+
const creds = (0, config_1.readCredentials)();
|
|
130
|
+
if (!creds) {
|
|
131
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
135
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
136
|
+
const url = `${base}/api/ideas`;
|
|
137
|
+
let res;
|
|
138
|
+
try {
|
|
139
|
+
res = await fetch(url, {
|
|
140
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
145
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
if (res.status === 401) {
|
|
149
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
console.error(`Error: idea list failed (HTTP ${res.status})`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
const data = (await res.json());
|
|
157
|
+
process.stdout.write(formatIdeaList(data.ideas));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `lumo idea update <id> --status <STATUS>` — move an idea through its
|
|
161
|
+
* lifecycle (LUM-679). `<id>` is the `<TEAM>-I<n>` identifier (a bare `<n>`
|
|
162
|
+
* also resolves). The status is case-insensitive; the server enforces the
|
|
163
|
+
* allowed transitions (CAPTURED↔DEVELOPING→PLANNED, any→DROPPED, DROPPED
|
|
164
|
+
* terminal) and rejects an illegal move with a clear error.
|
|
165
|
+
*/
|
|
166
|
+
async function ideaUpdate(idRef, opts) {
|
|
167
|
+
if (!idRef || idRef.trim().length === 0) {
|
|
168
|
+
console.error('Error: missing <id>. Usage: lumo idea update <LUM-I42> --status <captured|developing|planned|dropped>');
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
const status = opts.status?.trim().toLowerCase();
|
|
172
|
+
if (!status) {
|
|
173
|
+
console.error('Error: --status is required. One of: captured | developing | planned | dropped');
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
if (!IDEA_STATUS_VALUES.includes(status)) {
|
|
177
|
+
console.error(`Error: invalid status "${(0, sanitize_1.sanitizeField)(opts.status ?? '')}". One of: captured | developing | planned | dropped`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const creds = (0, config_1.readCredentials)();
|
|
181
|
+
if (!creds) {
|
|
182
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
186
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
187
|
+
const url = `${base}/api/ideas/${encodeURIComponent(idRef.trim())}`;
|
|
188
|
+
let res;
|
|
189
|
+
try {
|
|
190
|
+
res = await fetch(url, {
|
|
191
|
+
method: 'PATCH',
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${creds.token}`,
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({ status: status.toUpperCase() }),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
201
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (res.status === 401) {
|
|
205
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
if (res.ok) {
|
|
209
|
+
const data = (await res.json());
|
|
210
|
+
process.stdout.write(`✓ 想法 ${data.idea.identifier} → ${data.idea.status}\n`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let serverMsg = null;
|
|
214
|
+
try {
|
|
215
|
+
const errBody = (await res.json());
|
|
216
|
+
if (typeof errBody.error === 'string')
|
|
217
|
+
serverMsg = errBody.error;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Body wasn't JSON; fall through to status-only message
|
|
221
|
+
}
|
|
222
|
+
if (serverMsg) {
|
|
223
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.error(`Error: idea update failed (HTTP ${res.status})`);
|
|
227
|
+
}
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCreatedInitiativeLine = formatCreatedInitiativeLine;
|
|
4
|
+
exports.formatInitiativeList = formatInitiativeList;
|
|
5
|
+
exports.initiativeCreate = initiativeCreate;
|
|
6
|
+
exports.initiativeList = initiativeList;
|
|
7
|
+
const config_1 = require("../lib/config");
|
|
8
|
+
const api_1 = require("../lib/api");
|
|
9
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
10
|
+
/**
|
|
11
|
+
* Success line for a created initiative. Echoes the human-referenceable id
|
|
12
|
+
* (`<TEAM>-INIT-<n>`, e.g. `LUM-INIT-3`) — the `INIT` infix keeps it visibly
|
|
13
|
+
* distinct from task (`LUM-3`) and idea (`LUM-I3`) ids.
|
|
14
|
+
*/
|
|
15
|
+
function formatCreatedInitiativeLine(init) {
|
|
16
|
+
return `✓ Initiative ${init.identifier} created`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Render the initiatives as aligned `<id> <status> <goal>` lines. The server
|
|
20
|
+
* returns them newest-first, so this preserves order. Empty → a friendly line.
|
|
21
|
+
*/
|
|
22
|
+
function formatInitiativeList(initiatives) {
|
|
23
|
+
if (initiatives.length === 0)
|
|
24
|
+
return 'No initiatives.\n';
|
|
25
|
+
const idWidth = Math.max(...initiatives.map(i => i.identifier.length));
|
|
26
|
+
const statusWidth = Math.max(...initiatives.map(i => i.status.length));
|
|
27
|
+
return (initiatives
|
|
28
|
+
.map(i => {
|
|
29
|
+
const id = i.identifier.padEnd(idWidth);
|
|
30
|
+
const status = i.status.padEnd(statusWidth);
|
|
31
|
+
return `${id} ${status} ${(0, sanitize_1.sanitizeField)(i.name)}`;
|
|
32
|
+
})
|
|
33
|
+
.join('\n') + '\n');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `lumo initiative create "<goal>" [--assumption <text>]` — create a
|
|
37
|
+
* team-level Initiative directly, without a converter plan run. Prints the
|
|
38
|
+
* created `<TEAM>-INIT-<n>` id.
|
|
39
|
+
*/
|
|
40
|
+
async function initiativeCreate(goal, opts) {
|
|
41
|
+
if (!goal || goal.trim().length === 0) {
|
|
42
|
+
console.error('Error: missing <goal>. Usage: lumo initiative create "<goal>" [--assumption <text>]');
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
const creds = (0, config_1.readCredentials)();
|
|
46
|
+
if (!creds) {
|
|
47
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
51
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
52
|
+
const url = `${base}/api/initiatives`;
|
|
53
|
+
const body = { name: goal.trim() };
|
|
54
|
+
if (opts.assumption !== undefined)
|
|
55
|
+
body.assumptionNote = opts.assumption;
|
|
56
|
+
let res;
|
|
57
|
+
try {
|
|
58
|
+
res = await fetch(url, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${creds.token}`,
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body),
|
|
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.status === 201) {
|
|
77
|
+
const data = (await res.json());
|
|
78
|
+
process.stdout.write(formatCreatedInitiativeLine(data.initiative) + '\n');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let serverMsg = null;
|
|
82
|
+
try {
|
|
83
|
+
const errBody = (await res.json());
|
|
84
|
+
if (typeof errBody.error === 'string')
|
|
85
|
+
serverMsg = errBody.error;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Body wasn't JSON; fall through to status-only message
|
|
89
|
+
}
|
|
90
|
+
if (serverMsg) {
|
|
91
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.error(`Error: initiative create failed (HTTP ${res.status})`);
|
|
95
|
+
}
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `lumo initiative list` — print the team's initiatives newest-first, each
|
|
100
|
+
* line showing the `<TEAM>-INIT-<n>` id, status (ACTIVE|DONE|DROPPED) and goal.
|
|
101
|
+
*/
|
|
102
|
+
async function initiativeList() {
|
|
103
|
+
const creds = (0, config_1.readCredentials)();
|
|
104
|
+
if (!creds) {
|
|
105
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
109
|
+
const base = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
110
|
+
const url = `${base}/api/initiatives`;
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await fetch(url, {
|
|
114
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
if (res.status === 401) {
|
|
123
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
console.error(`Error: initiative list failed (HTTP ${res.status})`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
const data = (await res.json());
|
|
131
|
+
process.stdout.write(formatInitiativeList(data.initiatives));
|
|
132
|
+
}
|
|
@@ -7,6 +7,7 @@ const config_1 = require("../lib/config");
|
|
|
7
7
|
const api_1 = require("../lib/api");
|
|
8
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
9
9
|
const rank_tasks_1 = require("../lib/rank-tasks");
|
|
10
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
10
11
|
const claimable_filter_1 = require("../lib/claimable-filter");
|
|
11
12
|
/**
|
|
12
13
|
* `lumo next [-n, --count <N>]` — recommend the next task(s) to work on.
|
|
@@ -132,7 +133,14 @@ poolNoun = 'open') {
|
|
|
132
133
|
lines.push(` ↳ ${r.reasons.join(' · ')}`);
|
|
133
134
|
});
|
|
134
135
|
lines.push('');
|
|
135
|
-
|
|
136
|
+
// LUM-686: was a hand-written string here — the original drift source this
|
|
137
|
+
// feature exists to remove. Same advice, now through the shared renderer.
|
|
138
|
+
lines.push((0, next_steps_1.formatNextSteps)([
|
|
139
|
+
{
|
|
140
|
+
command: `lumo session attach ${first.identifier} && lumo task context ${first.identifier}`,
|
|
141
|
+
why: 'start on the top-ranked task',
|
|
142
|
+
},
|
|
143
|
+
]));
|
|
136
144
|
if (ranked.length > 1) {
|
|
137
145
|
lines.push('(or pick any other LUM-N from the list)');
|
|
138
146
|
}
|
|
@@ -4,6 +4,7 @@ exports.sessionAttach = sessionAttach;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
7
8
|
const resolve_project_1 = require("../lib/resolve-project");
|
|
8
9
|
const memory_auto_1 = require("../lib/memory-auto");
|
|
9
10
|
/**
|
|
@@ -166,6 +167,9 @@ async function sessionAttach(identifier, options) {
|
|
|
166
167
|
catch {
|
|
167
168
|
// best-effort — the bind already succeeded; never surface a sync error here
|
|
168
169
|
}
|
|
170
|
+
// Last, after the contract/memory/sync sections — the block reads as the
|
|
171
|
+
// closing "so do this now" line rather than interrupting them (LUM-686).
|
|
172
|
+
(0, next_steps_1.emitNextSteps)(body.nextSteps ?? [], options ?? {});
|
|
169
173
|
}
|
|
170
174
|
/**
|
|
171
175
|
* LUM-640: the `--steward` form — bind this session to a MILESTONE as a
|
|
@@ -41,6 +41,7 @@ const os = __importStar(require("os"));
|
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
43
|
const hooks_template_1 = require("../lib/hooks-template");
|
|
44
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
44
45
|
const git_hook_template_1 = require("../lib/git-hook-template");
|
|
45
46
|
const line_prompt_1 = require("../lib/line-prompt");
|
|
46
47
|
const agent_1 = require("../lib/agent");
|
|
@@ -246,20 +247,28 @@ function printPostInstall() {
|
|
|
246
247
|
const onPath = isLumoOnPath();
|
|
247
248
|
const credsPath = path.join((0, config_1.configDir)(), 'credentials.json');
|
|
248
249
|
const authed = fs.existsSync(credsPath);
|
|
249
|
-
|
|
250
|
+
// LUM-686: same conditional advice, now assembled as NextStep[] and handed
|
|
251
|
+
// to the shared renderer. Locally static — setup has no task state to judge.
|
|
252
|
+
const steps = [];
|
|
250
253
|
if (!onPath || isRunningUnderNpx()) {
|
|
251
|
-
|
|
252
|
-
'
|
|
254
|
+
steps.push({
|
|
255
|
+
command: 'npm install -g @lumoai/cli',
|
|
256
|
+
why: 'install the CLI globally so Claude Code hooks can find it',
|
|
257
|
+
});
|
|
253
258
|
}
|
|
254
259
|
if (!authed) {
|
|
255
|
-
|
|
256
|
-
'
|
|
260
|
+
steps.push({
|
|
261
|
+
command: 'lumo auth login',
|
|
262
|
+
why: 'authenticate so the CLI can sync with the Lumo server',
|
|
263
|
+
});
|
|
257
264
|
}
|
|
258
265
|
if (onPath && authed && !isRunningUnderNpx()) {
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
steps.push({
|
|
267
|
+
command: null,
|
|
268
|
+
why: 'All set. Open Claude Code in this directory — the SKILL.md and hooks are wired in.',
|
|
269
|
+
});
|
|
261
270
|
}
|
|
262
|
-
process.stdout.write(
|
|
271
|
+
process.stdout.write(`\n${(0, next_steps_1.formatNextSteps)(steps)}\n\n`);
|
|
263
272
|
}
|
|
264
273
|
function isLumoOnPath() {
|
|
265
274
|
try {
|
|
@@ -54,11 +54,12 @@ function formatCommentThread(comments, opts = {}) {
|
|
|
54
54
|
if (opts.full)
|
|
55
55
|
return blocks.join('\n\n');
|
|
56
56
|
const id = opts.identifier ?? '<LUM-N>';
|
|
57
|
+
const fetchCommand = opts.fetchCommand ?? 'task comments list';
|
|
57
58
|
return (0, output_budget_1.truncateUnitsToBudget)({
|
|
58
59
|
units: blocks,
|
|
59
60
|
maxTokens: opts.maxTokens,
|
|
60
61
|
unitNoun: 'comments',
|
|
61
|
-
fetchHint: `read the whole thread with: lumo
|
|
62
|
+
fetchHint: `read the whole thread with: lumo ${fetchCommand} ${id} --full`,
|
|
62
63
|
separator: '\n\n',
|
|
63
64
|
}).text;
|
|
64
65
|
}
|
|
@@ -5,9 +5,10 @@ exports.formatTaskContextMarkdown = formatTaskContextMarkdown;
|
|
|
5
5
|
const config_1 = require("../lib/config");
|
|
6
6
|
const api_1 = require("../lib/api");
|
|
7
7
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
8
9
|
const format_1 = require("../lib/format");
|
|
9
10
|
const output_budget_1 = require("../../../shared/src/output-budget");
|
|
10
|
-
async function taskContext(identifier) {
|
|
11
|
+
async function taskContext(identifier, options) {
|
|
11
12
|
if (!identifier) {
|
|
12
13
|
console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
|
|
13
14
|
return 1;
|
|
@@ -45,6 +46,9 @@ async function taskContext(identifier) {
|
|
|
45
46
|
const data = (await res.json());
|
|
46
47
|
const now = new Date();
|
|
47
48
|
process.stdout.write(formatTaskContextMarkdown(data, now));
|
|
49
|
+
// Emitted outside the rendered markdown so the output budget can never drop
|
|
50
|
+
// it — the closing "so do this now" line is the cheapest section here.
|
|
51
|
+
(0, next_steps_1.emitNextSteps)(data.nextSteps ?? [], options ?? {});
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
54
|
* Render a TaskContextResponse as the agent-facing markdown handoff. Pure
|