@lumoai/cli 1.55.0 → 1.57.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 (36) hide show
  1. package/assets/skill/SKILL.md +73 -2
  2. package/assets/skill/references/criteria.md +1 -1
  3. package/assets/skill/references/priority.md +6 -5
  4. package/assets/skill/references/tasks.md +15 -3
  5. package/assets/skill/references/verify.md +18 -13
  6. package/assets/skill/references/worktree.md +19 -5
  7. package/dist/cli/src/commands/idea-comment.js +127 -0
  8. package/dist/cli/src/commands/idea-figma-add.js +30 -0
  9. package/dist/cli/src/commands/idea-figma-context.js +65 -0
  10. package/dist/cli/src/commands/idea-figma-list.js +32 -0
  11. package/dist/cli/src/commands/idea-figma-refresh.js +40 -0
  12. package/dist/cli/src/commands/idea-figma-rm.js +19 -0
  13. package/dist/cli/src/commands/idea-slack-add.js +66 -0
  14. package/dist/cli/src/commands/idea-slack-rm.js +63 -0
  15. package/dist/cli/src/commands/idea-slack-show.js +65 -0
  16. package/dist/cli/src/commands/idea-web-add.js +66 -0
  17. package/dist/cli/src/commands/idea-web-rm.js +63 -0
  18. package/dist/cli/src/commands/idea-web-show.js +71 -0
  19. package/dist/cli/src/commands/idea.js +140 -0
  20. package/dist/cli/src/commands/initiative.js +132 -0
  21. package/dist/cli/src/commands/next.js +9 -1
  22. package/dist/cli/src/commands/session-attach.js +4 -0
  23. package/dist/cli/src/commands/setup.js +17 -8
  24. package/dist/cli/src/commands/task-comment-list.js +2 -1
  25. package/dist/cli/src/commands/task-context.js +5 -1
  26. package/dist/cli/src/commands/task-create.js +18 -1
  27. package/dist/cli/src/commands/task-update.js +16 -0
  28. package/dist/cli/src/commands/worktree-add.js +16 -4
  29. package/dist/cli/src/commands/worktree-list.js +1 -1
  30. package/dist/cli/src/commands/worktree-rm.js +10 -2
  31. package/dist/cli/src/index.js +118 -3
  32. package/dist/cli/src/lib/idea-figma-api.js +65 -0
  33. package/dist/cli/src/lib/next-steps.js +55 -0
  34. package/dist/cli/src/lib/worktree-ref.js +53 -0
  35. package/dist/cli/src/lib/worktree.js +13 -11
  36. package/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaSlackAdd = ideaSlackAdd;
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 slack add <LUM-I1> <permalink>`
9
+ *
10
+ * Attaches a Slack thread to an idea by POSTing the permalink to
11
+ * `/api/ideas/:id/slack-contexts`. Mirrors the task-side flow (LUM-681).
12
+ */
13
+ async function ideaSlackAdd(identifier, permalink) {
14
+ if (!identifier || !permalink) {
15
+ console.error('Error: usage: lumo idea slack add <LUM-I1> <permalink>');
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)}/slack-contexts`, {
28
+ method: 'POST',
29
+ headers: {
30
+ Authorization: `Bearer ${creds.token}`,
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify({ permalink }),
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: slack add failed (HTTP ${res.status})`);
62
+ return 1;
63
+ }
64
+ const { context } = (await res.json());
65
+ console.log(`Linked Slack thread to ${identifier} (context ${context.id})`);
66
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaSlackRm = ideaSlackRm;
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 slack rm <LUM-I1> <context-id>`
9
+ *
10
+ * Removes a Slack context from an idea via
11
+ * `DELETE /api/ideas/:id/slack-contexts/:contextId`. Mirrors the task-side
12
+ * shape (LUM-681); no task-side `rm` equivalent exists yet, so this follows
13
+ * the DELETE route contract directly.
14
+ */
15
+ async function ideaSlackRm(identifier, contextId) {
16
+ if (!identifier || !contextId) {
17
+ console.error('Error: usage: lumo idea slack rm <LUM-I1> <context-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)}/slack-contexts/${encodeURIComponent(contextId)}`, {
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 slack context ${contextId} 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: slack rm failed (HTTP ${res.status})`);
60
+ return 1;
61
+ }
62
+ console.log(`Removed Slack context ${contextId} from ${identifier}`);
63
+ }
@@ -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
+ }