@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.
- package/assets/skill/SKILL.md +73 -2
- package/assets/skill/references/criteria.md +1 -1
- package/assets/skill/references/priority.md +6 -5
- package/assets/skill/references/tasks.md +15 -3
- package/assets/skill/references/verify.md +18 -13
- package/assets/skill/references/worktree.md +19 -5
- 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 +16 -4
- package/dist/cli/src/commands/worktree-list.js +1 -1
- package/dist/cli/src/commands/worktree-rm.js +10 -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/dist/cli/src/lib/worktree-ref.js +53 -0
- package/dist/cli/src/lib/worktree.js +13 -11
- package/package.json +1 -1
|
@@ -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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertSameTeam = assertSameTeam;
|
|
4
4
|
exports.normalizePriority = normalizePriority;
|
|
5
|
+
exports.formatCreateOutput = formatCreateOutput;
|
|
5
6
|
exports.formatCreatedTaskLine = formatCreatedTaskLine;
|
|
6
7
|
exports.taskCreate = taskCreate;
|
|
7
8
|
const config_1 = require("../lib/config");
|
|
@@ -9,6 +10,7 @@ const api_1 = require("../lib/api");
|
|
|
9
10
|
const tag_resolver_1 = require("../lib/tag-resolver");
|
|
10
11
|
const resolve_1 = require("../lib/resolve");
|
|
11
12
|
const sanitize_1 = require("../lib/sanitize");
|
|
13
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
12
14
|
const ALLOWED_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
|
|
13
15
|
/**
|
|
14
16
|
* Assert that the sprint belongs to the same team as the newly created task.
|
|
@@ -80,6 +82,21 @@ function normalizePriority(value) {
|
|
|
80
82
|
? upper
|
|
81
83
|
: null;
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Compose the full `task create` output: the result line first, then the
|
|
87
|
+
* server-computed next-step block (LUM-686). Exported so the render test
|
|
88
|
+
* exercises the real composition rather than re-assembling the parts itself.
|
|
89
|
+
*
|
|
90
|
+
* The result line stays byte-identical and first — a reader taking line 1 is
|
|
91
|
+
* unaffected by the block below it.
|
|
92
|
+
*/
|
|
93
|
+
function formatCreateOutput(task, steps = [], opts = {}, env = process.env) {
|
|
94
|
+
const head = formatCreatedTaskLine(task);
|
|
95
|
+
if ((0, next_steps_1.nextStepsSilenced)(env, opts))
|
|
96
|
+
return head;
|
|
97
|
+
const block = (0, next_steps_1.formatNextSteps)(steps);
|
|
98
|
+
return block === '' ? head : `${head}\n\n${block}`;
|
|
99
|
+
}
|
|
83
100
|
/**
|
|
84
101
|
* Format the single-line success output. Title is double-quoted; embedded
|
|
85
102
|
* double-quotes are backslash-escaped so the output stays parseable as one
|
|
@@ -173,7 +190,7 @@ async function taskCreate(title, opts) {
|
|
|
173
190
|
if (res.status === 201) {
|
|
174
191
|
const data = (await res.json());
|
|
175
192
|
const responseTagNames = (data.task.taskTags ?? []).map(t => t.name);
|
|
176
|
-
process.stdout.write(
|
|
193
|
+
process.stdout.write(formatCreateOutput({ ...data.task, tags: responseTagNames }, data.nextSteps ?? [], opts) + '\n');
|
|
177
194
|
// Sprint binding (optional): if --sprint was passed, bind the new task.
|
|
178
195
|
if (opts.sprint) {
|
|
179
196
|
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
@@ -11,6 +11,7 @@ const task_create_1 = require("./task-create");
|
|
|
11
11
|
const tag_resolver_1 = require("../lib/tag-resolver");
|
|
12
12
|
const resolve_1 = require("../lib/resolve");
|
|
13
13
|
const sanitize_1 = require("../lib/sanitize");
|
|
14
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
14
15
|
const ALLOWED_STATUSES = ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'];
|
|
15
16
|
/**
|
|
16
17
|
* Pure function: given the task's current sprint binding and the resolved
|
|
@@ -101,7 +102,21 @@ function formatUpdatedTaskLine(task) {
|
|
|
101
102
|
}
|
|
102
103
|
return head;
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Thin wrapper so the next-step block always lands last (LUM-686). The update
|
|
107
|
+
* flow has several success exits — a plain PATCH, plus the sprint bind/unbind
|
|
108
|
+
* branches — and hints belong after whatever the command printed, not wedged
|
|
109
|
+
* before the Sprint line. Only the wrapper emits; the inner run just collects.
|
|
110
|
+
*/
|
|
104
111
|
async function taskUpdate(identifier, opts) {
|
|
112
|
+
const collected = { steps: [] };
|
|
113
|
+
const code = await runTaskUpdate(identifier, opts, collected);
|
|
114
|
+
// Success only — a failed update's advice would be noise on top of an error.
|
|
115
|
+
if (code === undefined)
|
|
116
|
+
(0, next_steps_1.emitNextSteps)(collected.steps, opts);
|
|
117
|
+
return code;
|
|
118
|
+
}
|
|
119
|
+
async function runTaskUpdate(identifier, opts, collected) {
|
|
105
120
|
if (!identifier || identifier.trim().length === 0) {
|
|
106
121
|
console.error('Error: missing <identifier>. Usage: lumo task update <LUM-42> [options]');
|
|
107
122
|
return 1;
|
|
@@ -249,6 +264,7 @@ async function taskUpdate(identifier, opts) {
|
|
|
249
264
|
if (responseTagNames !== undefined)
|
|
250
265
|
taskLine.tags = responseTagNames;
|
|
251
266
|
process.stdout.write(formatUpdatedTaskLine(taskLine) + '\n');
|
|
267
|
+
collected.steps = data.nextSteps ?? [];
|
|
252
268
|
}
|
|
253
269
|
else {
|
|
254
270
|
let serverMsg = null;
|
|
@@ -37,15 +37,20 @@ exports.worktreeAdd = worktreeAdd;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const child_process_1 = require("child_process");
|
|
40
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
40
41
|
const worktree_1 = require("../lib/worktree");
|
|
42
|
+
const worktree_ref_1 = require("../lib/worktree-ref");
|
|
41
43
|
function printGuidance(dir, branch) {
|
|
42
44
|
console.log('');
|
|
43
45
|
console.log(`✓ Worktree ready: ${dir}`);
|
|
44
46
|
console.log(` branch: ${branch}`);
|
|
45
47
|
console.log(` node_modules: symlinked to main checkout`);
|
|
46
48
|
console.log('');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
// Locally-static source: no server round-trip to carry steps, but the same
|
|
50
|
+
// renderer so the block looks identical everywhere (LUM-686).
|
|
51
|
+
console.log((0, next_steps_1.formatNextSteps)([
|
|
52
|
+
{ command: `cd ${dir}`, why: 'work from the isolated checkout' },
|
|
53
|
+
]));
|
|
49
54
|
console.log('');
|
|
50
55
|
console.log('Gotchas baked into this worktree:');
|
|
51
56
|
console.log(' • prisma: the generated client is SHARED via the node_modules symlink.');
|
|
@@ -55,9 +60,16 @@ function printGuidance(dir, branch) {
|
|
|
55
60
|
console.log(' cli/package.json haste collision silently runs the wrong tests.');
|
|
56
61
|
}
|
|
57
62
|
async function worktreeAdd(rawId, slug, opts) {
|
|
58
|
-
|
|
63
|
+
let taskId;
|
|
64
|
+
try {
|
|
65
|
+
taskId = await (0, worktree_ref_1.resolveWorktreeTaskId)(rawId);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
59
71
|
if (!taskId) {
|
|
60
|
-
console.error(`Error: "${rawId}" is not a task id (expected LUM-
|
|
72
|
+
console.error(`Error: "${rawId}" is not a task id (expected <TEAM>-N like LUM-42 or SPEC-12, or a bare number)`);
|
|
61
73
|
return 1;
|
|
62
74
|
}
|
|
63
75
|
if (!(0, worktree_1.isMainCheckout)()) {
|
|
@@ -46,7 +46,7 @@ function nodeModulesLinked(dir) {
|
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
/** Pull a
|
|
49
|
+
/** Pull a `<TEAM>-N` id from a worktree dir basename or its branch, else '—'. */
|
|
50
50
|
function taskIdOf(dirBase, branch) {
|
|
51
51
|
return ((0, git_task_1.matchTaskIdentifier)(dirBase) ??
|
|
52
52
|
(branch ? (0, git_task_1.matchTaskIdentifier)(branch) : null) ??
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.worktreeRm = worktreeRm;
|
|
4
4
|
const worktree_1 = require("../lib/worktree");
|
|
5
|
+
const worktree_ref_1 = require("../lib/worktree-ref");
|
|
5
6
|
async function worktreeRm(rawId, opts) {
|
|
6
|
-
|
|
7
|
+
let taskId;
|
|
8
|
+
try {
|
|
9
|
+
taskId = await (0, worktree_ref_1.resolveWorktreeTaskId)(rawId);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
7
15
|
if (!taskId) {
|
|
8
|
-
console.error(`Error: "${rawId}" is not a task id (expected LUM-
|
|
16
|
+
console.error(`Error: "${rawId}" is not a task id (expected <TEAM>-N like LUM-42 or SPEC-12, or a bare number)`);
|
|
9
17
|
return 1;
|
|
10
18
|
}
|
|
11
19
|
if (!(0, worktree_1.isMainCheckout)()) {
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,6 +47,19 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const next_1 = require("./commands/next");
|
|
49
49
|
const idea_1 = require("./commands/idea");
|
|
50
|
+
const idea_slack_add_1 = require("./commands/idea-slack-add");
|
|
51
|
+
const idea_slack_show_1 = require("./commands/idea-slack-show");
|
|
52
|
+
const idea_slack_rm_1 = require("./commands/idea-slack-rm");
|
|
53
|
+
const idea_web_add_1 = require("./commands/idea-web-add");
|
|
54
|
+
const idea_web_show_1 = require("./commands/idea-web-show");
|
|
55
|
+
const idea_web_rm_1 = require("./commands/idea-web-rm");
|
|
56
|
+
const idea_figma_add_1 = require("./commands/idea-figma-add");
|
|
57
|
+
const idea_figma_list_1 = require("./commands/idea-figma-list");
|
|
58
|
+
const idea_figma_rm_1 = require("./commands/idea-figma-rm");
|
|
59
|
+
const idea_figma_refresh_1 = require("./commands/idea-figma-refresh");
|
|
60
|
+
const idea_figma_context_1 = require("./commands/idea-figma-context");
|
|
61
|
+
const idea_comment_1 = require("./commands/idea-comment");
|
|
62
|
+
const initiative_1 = require("./commands/initiative");
|
|
50
63
|
const plan_1 = require("./commands/plan");
|
|
51
64
|
const cost_1 = require("./commands/cost");
|
|
52
65
|
const priority_1 = require("./commands/priority");
|
|
@@ -203,8 +216,18 @@ const program = new commander_1.Command()
|
|
|
203
216
|
// point at --help instead of dead-ending on "unknown option". Subcommands
|
|
204
217
|
// created via .command() inherit these settings.
|
|
205
218
|
.showSuggestionAfterError(true)
|
|
206
|
-
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)')
|
|
219
|
+
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)')
|
|
220
|
+
// LUM-686: global mute for the trailing next-step block. `LUMO_NO_HINTS=1`
|
|
221
|
+
// does the same for a whole shell.
|
|
222
|
+
.option('--no-hints', 'suppress the trailing next-step suggestions');
|
|
207
223
|
exports.program = program;
|
|
224
|
+
// Commander does not merge program-level options into a subcommand's own
|
|
225
|
+
// opts(), so the global `--no-hints` is folded into the env channel before any
|
|
226
|
+
// action runs. One silencing path (`nextStepsSilenced`) instead of two.
|
|
227
|
+
program.hook('preAction', thisCommand => {
|
|
228
|
+
if (thisCommand.opts().hints === false)
|
|
229
|
+
process.env.LUMO_NO_HINTS = '1';
|
|
230
|
+
});
|
|
208
231
|
const auth = program.command('auth').description('Manage Lumo authentication');
|
|
209
232
|
auth
|
|
210
233
|
.command('login')
|
|
@@ -279,11 +302,103 @@ program
|
|
|
279
302
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
280
303
|
.option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
|
|
281
304
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
282
|
-
program
|
|
283
|
-
.command('idea
|
|
305
|
+
const ideaCmd = program
|
|
306
|
+
.command('idea [statement]')
|
|
284
307
|
.description('Capture a team-level idea into the pool (<10s, no friction). Grabs the current Claude Code session id and its bound task as provenance; prints an I-prefixed id (e.g. LUM-I42). The transformer (Spec 2) consumes the pool.')
|
|
285
308
|
.option('-c, --context <text>', 'Free-text origin context for the idea')
|
|
286
309
|
.action(wrap((statement, options) => (0, idea_1.ideaCapture)(statement, options)));
|
|
310
|
+
ideaCmd
|
|
311
|
+
.command('list')
|
|
312
|
+
.description('List the team idea pool newest-first — each line shows the LUM-I<n> id, status (CAPTURED|DEVELOPING|PLANNED|DROPPED) and statement.')
|
|
313
|
+
.action(wrap(() => (0, idea_1.ideaList)()));
|
|
314
|
+
ideaCmd
|
|
315
|
+
.command('update <id>')
|
|
316
|
+
.description('Move an idea through its lifecycle (LUM-679). Allowed: CAPTURED↔DEVELOPING→PLANNED and any state→DROPPED (DROPPED is terminal). <id> is the LUM-I<n> id (a bare number also resolves).')
|
|
317
|
+
.option('-s, --status <value>', 'New status: captured | developing | planned | dropped (case-insensitive)')
|
|
318
|
+
.action(wrap((id, options) => (0, idea_1.ideaUpdate)(id, options)));
|
|
319
|
+
// Idea evidence commands (LUM-681): mirrors `task slack/web/figma`, hitting
|
|
320
|
+
// `/api/ideas/:id/...` instead of `/api/tasks/:id/...`. Lets an agent attach
|
|
321
|
+
// Slack threads, web links, and Figma design links to an idea before it goes
|
|
322
|
+
// through the plan-run converter.
|
|
323
|
+
const ideaSlack = ideaCmd
|
|
324
|
+
.command('slack')
|
|
325
|
+
.description('Attach & inspect Slack context on an idea');
|
|
326
|
+
ideaSlack
|
|
327
|
+
.command('add <idea> <permalink>')
|
|
328
|
+
.description('Attach a Slack thread to an idea')
|
|
329
|
+
.action(wrap((id, p) => (0, idea_slack_add_1.ideaSlackAdd)(id, p)));
|
|
330
|
+
ideaSlack
|
|
331
|
+
.command('show <idea> <contextId>')
|
|
332
|
+
.description('Show the full stored Slack thread snapshot')
|
|
333
|
+
.action(wrap((id, ctx) => (0, idea_slack_show_1.ideaSlackShow)(id, ctx)));
|
|
334
|
+
ideaSlack
|
|
335
|
+
.command('rm <idea> <contextId>')
|
|
336
|
+
.description('Remove a Slack context from an idea')
|
|
337
|
+
.action(wrap((id, ctx) => (0, idea_slack_rm_1.ideaSlackRm)(id, ctx)));
|
|
338
|
+
const ideaWeb = ideaCmd
|
|
339
|
+
.command('web')
|
|
340
|
+
.description('Attach & inspect web links on an idea');
|
|
341
|
+
ideaWeb
|
|
342
|
+
.command('add <idea> <url>')
|
|
343
|
+
.description('Attach a web link to an idea')
|
|
344
|
+
.action(wrap((id, u) => (0, idea_web_add_1.ideaWebAdd)(id, u)));
|
|
345
|
+
ideaWeb
|
|
346
|
+
.command('show <idea> <linkId>')
|
|
347
|
+
.description('Show the fetched web link body as plain text')
|
|
348
|
+
.action(wrap((id, l) => (0, idea_web_show_1.ideaWebShow)(id, l)));
|
|
349
|
+
ideaWeb
|
|
350
|
+
.command('rm <idea> <linkId>')
|
|
351
|
+
.description('Remove a web link from an idea')
|
|
352
|
+
.action(wrap((id, l) => (0, idea_web_rm_1.ideaWebRm)(id, l)));
|
|
353
|
+
const ideaFigma = ideaCmd
|
|
354
|
+
.command('figma')
|
|
355
|
+
.description('Attach Figma links to an idea');
|
|
356
|
+
ideaFigma
|
|
357
|
+
.command('add <idea> <url>')
|
|
358
|
+
.description('Attach a Figma file/frame URL to an idea')
|
|
359
|
+
.action(wrap((id, u) => (0, idea_figma_add_1.ideaFigmaAdd)({ identifier: id, url: u })));
|
|
360
|
+
ideaFigma
|
|
361
|
+
.command('list <idea>')
|
|
362
|
+
.description('List Figma links on an idea')
|
|
363
|
+
.action(wrap(id => (0, idea_figma_list_1.ideaFigmaList)({ identifier: id })));
|
|
364
|
+
ideaFigma
|
|
365
|
+
.command('rm <idea> <link-id-or-url>')
|
|
366
|
+
.description('Remove a Figma link from an idea (idempotent)')
|
|
367
|
+
.action(wrap((id, l) => (0, idea_figma_rm_1.ideaFigmaRm)({ identifier: id, linkIdOrUrl: l })));
|
|
368
|
+
ideaFigma
|
|
369
|
+
.command('refresh <idea>')
|
|
370
|
+
.description('Re-fetch Figma metadata for every link on an idea')
|
|
371
|
+
.action(wrap(id => (0, idea_figma_refresh_1.ideaFigmaRefresh)({ identifier: id })));
|
|
372
|
+
ideaFigma
|
|
373
|
+
.command('context <idea> <linkId>')
|
|
374
|
+
.description('Show cached Figma design context')
|
|
375
|
+
.action(wrap((id, l) => (0, idea_figma_context_1.ideaFigmaContext)(id, l)));
|
|
376
|
+
ideaCmd
|
|
377
|
+
.command('comment <id> <body>')
|
|
378
|
+
.description('Comment on an idea (LUM-680). <id> is the LUM-I<n> id (a bare number also resolves). Body is plain text — quote it to pass spaces or newlines.')
|
|
379
|
+
.action(wrap((id, body) => (0, idea_comment_1.ideaComment)(id, body)));
|
|
380
|
+
// Plural `comments` parent (mirrors `task comments`): `idea comment <id> <body>`
|
|
381
|
+
// already exists for posting, and commander disallows a duplicate name.
|
|
382
|
+
const ideaComments = ideaCmd
|
|
383
|
+
.command('comments')
|
|
384
|
+
.description('Inspect an idea comment thread');
|
|
385
|
+
ideaComments
|
|
386
|
+
.command('list <id>')
|
|
387
|
+
.description('List an idea comment thread (capped to the output budget; --full prints every comment).')
|
|
388
|
+
.option('--full', 'Print every comment, bypassing the output-token cap')
|
|
389
|
+
.action(wrap((id, opts) => (0, idea_comment_1.ideaCommentList)(id, opts)));
|
|
390
|
+
const initiativeCmd = program
|
|
391
|
+
.command('initiative')
|
|
392
|
+
.description('Team-level Initiatives (LUM-INIT-<n>): create one directly (no plan run required) or list them. Converter-materialised initiatives show here too.');
|
|
393
|
+
initiativeCmd
|
|
394
|
+
.command('create <goal>')
|
|
395
|
+
.description('Create a team-level initiative directly from a goal statement — no plan run required. Prints the LUM-INIT-<n> id; --assumption records the bet behind it.')
|
|
396
|
+
.option('--assumption <text>', 'The bet/assumption behind the initiative (赌注旁注)')
|
|
397
|
+
.action(wrap((goal, options) => (0, initiative_1.initiativeCreate)(goal, options)));
|
|
398
|
+
initiativeCmd
|
|
399
|
+
.command('list')
|
|
400
|
+
.description('List the team initiatives newest-first — each line shows the LUM-INIT-<n> id, status (ACTIVE|DONE|DROPPED) and goal.')
|
|
401
|
+
.action(wrap(() => (0, initiative_1.initiativeList)()));
|
|
287
402
|
const planCmd = program
|
|
288
403
|
.command('plan')
|
|
289
404
|
.description('Start a design-thinking 转换器 run (聚类→对齐→成计划) and print the gate-A web deep-link. Refuses when a run is already active (see `lumo plan status`); --abandon-active abandons the prior run first. Editing/confirmation happen in web.')
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addIdeaFigmaLink = addIdeaFigmaLink;
|
|
4
|
+
exports.listIdeaFigmaLinks = listIdeaFigmaLinks;
|
|
5
|
+
exports.removeIdeaFigmaLink = removeIdeaFigmaLink;
|
|
6
|
+
exports.refreshIdeaFigmaLinks = refreshIdeaFigmaLinks;
|
|
7
|
+
const api_1 = require("./api");
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
function buildErr(status, body) {
|
|
10
|
+
const err = Object.assign(new Error(body.error ?? `HTTP ${status}`), {
|
|
11
|
+
status,
|
|
12
|
+
});
|
|
13
|
+
if (body.code !== undefined)
|
|
14
|
+
err.code = body.code;
|
|
15
|
+
return err;
|
|
16
|
+
}
|
|
17
|
+
async function call(path, init) {
|
|
18
|
+
const creds = (0, config_1.readCredentials)();
|
|
19
|
+
if (!creds)
|
|
20
|
+
throw new Error('Not logged in. Run: lumo auth login');
|
|
21
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
22
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}${path}`, {
|
|
23
|
+
...init,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${creds.token}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...(init.headers ?? {}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
let parsed = {};
|
|
32
|
+
try {
|
|
33
|
+
parsed = (await res.json());
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* non-JSON body */
|
|
37
|
+
}
|
|
38
|
+
throw buildErr(res.status, parsed);
|
|
39
|
+
}
|
|
40
|
+
return (await res.json());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Idea-side counterpart to `cli/src/lib/figma-api.ts` (LUM-681). Same shape,
|
|
44
|
+
* same error codes (`figma_not_connected` / `figma_needs_reauth`), only the
|
|
45
|
+
* URL prefix differs: `/api/ideas/:id/...` instead of `/api/tasks/:id/...`.
|
|
46
|
+
*/
|
|
47
|
+
async function addIdeaFigmaLink(identifier, url) {
|
|
48
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify({ url }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function listIdeaFigmaLinks(identifier) {
|
|
54
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
|
|
55
|
+
method: 'GET',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function removeIdeaFigmaLink(identifier, linkId) {
|
|
59
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/${encodeURIComponent(linkId)}`, { method: 'DELETE' });
|
|
60
|
+
}
|
|
61
|
+
async function refreshIdeaFigmaLinks(identifier) {
|
|
62
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/refresh`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NEXT_STEPS_MAX = void 0;
|
|
4
|
+
exports.nextStepsSilenced = nextStepsSilenced;
|
|
5
|
+
exports.formatNextSteps = formatNextSteps;
|
|
6
|
+
exports.emitNextSteps = emitNextSteps;
|
|
7
|
+
const sanitize_1 = require("./sanitize");
|
|
8
|
+
/** Mirrors NEXT_STEPS_MAX server-side; a defence in depth, not the only cap. */
|
|
9
|
+
exports.NEXT_STEPS_MAX = 3;
|
|
10
|
+
const HEADING = 'Next:';
|
|
11
|
+
/**
|
|
12
|
+
* True when the user has muted suggestions. Hints cost tokens on every command;
|
|
13
|
+
* anyone who finds them noise must be able to turn them off for good.
|
|
14
|
+
*/
|
|
15
|
+
function nextStepsSilenced(env = process.env, opts = {}) {
|
|
16
|
+
// Commander maps `--no-hints` to `opts.hints === false`.
|
|
17
|
+
if (opts.hints === false)
|
|
18
|
+
return true;
|
|
19
|
+
const flag = env.LUMO_NO_HINTS;
|
|
20
|
+
return flag !== undefined && flag !== '' && flag !== '0';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Render the trailing suggestion block. Returns "" when there is nothing to
|
|
24
|
+
* say, so callers can append unconditionally without emitting a bare heading.
|
|
25
|
+
*/
|
|
26
|
+
function formatNextSteps(steps) {
|
|
27
|
+
const capped = steps.slice(0, exports.NEXT_STEPS_MAX);
|
|
28
|
+
if (capped.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const lines = [HEADING];
|
|
31
|
+
for (const step of capped) {
|
|
32
|
+
const why = (0, sanitize_1.sanitizeField)(step.why);
|
|
33
|
+
lines.push(step.command === null
|
|
34
|
+
? ` ${why}`
|
|
35
|
+
: ` ${(0, sanitize_1.sanitizeField)(step.command)}\n ${why}`);
|
|
36
|
+
}
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write the block to **stdout**, after the command's own result line.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not stderr: suggestions are not errors, `2>/dev/null` would
|
|
43
|
+
* swallow them, some harnesses read any stderr output as failure, and with
|
|
44
|
+
* both streams redirected to one file the block can surface *above* the result
|
|
45
|
+
* it follows. Machine consumers are served by `--json` instead, where the
|
|
46
|
+
* steps ride as a field rather than as text.
|
|
47
|
+
*/
|
|
48
|
+
function emitNextSteps(steps, opts = {}, out = process.stdout, env = process.env) {
|
|
49
|
+
if (nextStepsSilenced(env, opts))
|
|
50
|
+
return;
|
|
51
|
+
const block = formatNextSteps(steps);
|
|
52
|
+
if (block === '')
|
|
53
|
+
return;
|
|
54
|
+
out.write(`\n${block}\n`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveWorktreeTaskId = resolveWorktreeTaskId;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
const worktree_1 = require("./worktree");
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a user-supplied worktree ref to a canonical `<TEAM>-<n>` id.
|
|
9
|
+
*
|
|
10
|
+
* - Explicit prefix (`SPEC-12`, `lum-267`) → normalized locally, NO network.
|
|
11
|
+
* - Bare number (`267`) → completed with the workspace's DEFAULT team prefix
|
|
12
|
+
* (GET /api/teams: the `isDefault` team, or the sole team in a single-team
|
|
13
|
+
* workspace). Worktree tooling is otherwise purely local; the API is touched
|
|
14
|
+
* only to disambiguate a prefix-less number.
|
|
15
|
+
*
|
|
16
|
+
* Returns null when `raw` is not a task-id shape at all (caller prints the
|
|
17
|
+
* "expected <TEAM>-N" usage error). Throws Error with a user-facing message
|
|
18
|
+
* when a bare number needs the default team but it can't be determined
|
|
19
|
+
* (not logged in / offline / ambiguous multi-team without a default) — every
|
|
20
|
+
* message points the user at passing an explicit prefix instead.
|
|
21
|
+
*/
|
|
22
|
+
async function resolveWorktreeTaskId(raw) {
|
|
23
|
+
const explicit = (0, worktree_1.normalizeTaskId)(raw);
|
|
24
|
+
if (explicit)
|
|
25
|
+
return explicit;
|
|
26
|
+
const bare = raw.trim().match(/^(\d+)$/);
|
|
27
|
+
if (!bare)
|
|
28
|
+
return null;
|
|
29
|
+
const n = bare[1];
|
|
30
|
+
const creds = (0, config_1.readCredentials)();
|
|
31
|
+
if (!creds) {
|
|
32
|
+
throw new Error(`a bare number needs your default team prefix — run \`lumo auth login\`, or pass an explicit id like LUM-${n}`);
|
|
33
|
+
}
|
|
34
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
35
|
+
let teams;
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${base}/api/teams`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
throw new Error(`HTTP ${res.status}`);
|
|
42
|
+
({ teams } = (await res.json()));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
throw new Error(`could not resolve the default team for bare id "${n}" (${msg}) — pass an explicit id like LUM-${n}`);
|
|
47
|
+
}
|
|
48
|
+
const def = teams.find(t => t.isDefault) ?? (teams.length === 1 ? teams[0] : undefined);
|
|
49
|
+
if (!def) {
|
|
50
|
+
throw new Error(`workspace has multiple teams and no default — pass an explicit id like SPEC-${n} instead of a bare number`);
|
|
51
|
+
}
|
|
52
|
+
return `${def.identifier.toUpperCase()}-${n}`;
|
|
53
|
+
}
|
|
@@ -46,19 +46,21 @@ exports.getGitCommonDir = getGitCommonDir;
|
|
|
46
46
|
exports.isMainCheckout = isMainCheckout;
|
|
47
47
|
exports.getRepoRoot = getRepoRoot;
|
|
48
48
|
/**
|
|
49
|
-
* Normalize a user-supplied task reference to canonical
|
|
50
|
-
* Accepts `LUM-267`, `
|
|
51
|
-
*
|
|
49
|
+
* Normalize a user-supplied task reference to canonical `<TEAM>-<n>` form.
|
|
50
|
+
* Accepts any team prefix (`LUM-267`, `SPEC-12`, `spec-12`), not just LUM —
|
|
51
|
+
* the prefix is a team's configured identifier (1–10 letters), matching the
|
|
52
|
+
* single-sourced shape in shared/src/task-identifier.ts (LUM-419). The prefix
|
|
53
|
+
* is upper-cased; the number is preserved.
|
|
54
|
+
*
|
|
55
|
+
* A bare number (`267`) returns null here: without team context the prefix
|
|
56
|
+
* cannot be inferred locally. Team-aware bare-number resolution (via the
|
|
57
|
+
* workspace's default team) lives in `resolveWorktreeTaskId` (worktree-ref.ts).
|
|
52
58
|
*/
|
|
53
59
|
function normalizeTaskId(input) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const bare = trimmed.match(/^(\d+)$/);
|
|
59
|
-
if (bare)
|
|
60
|
-
return `LUM-${bare[1]}`;
|
|
61
|
-
return null;
|
|
60
|
+
const m = input.trim().match(/^([A-Za-z]{1,10})-(\d+)$/);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
return `${m[1].toUpperCase()}-${m[2]}`;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Turn an arbitrary human slug into a branch/dir-safe segment: lowercase,
|