@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.
@@ -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
- lines.push(`Next: lumo session attach ${first.identifier} && lumo task context ${first.identifier}`);
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
- process.stdout.write('\nNext steps:\n');
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
- process.stdout.write(' • Install the CLI globally so Claude Code hooks can find it:\n' +
252
- ' npm install -g @lumoai/cli\n');
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
- process.stdout.write(' • Authenticate so the CLI can sync with the Lumo server:\n' +
256
- ' lumo auth login\n');
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
- process.stdout.write(' • All set. Open Claude Code in this directory — the SKILL.md and\n' +
260
- ' hooks are wired in.\n');
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('\n');
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 task comments list ${id} --full`,
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