@lumoai/cli 1.5.0 → 1.6.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.md +189 -16
- package/dist/cli/src/commands/auth-login.js +4 -3
- package/dist/cli/src/commands/auth-logout.js +2 -1
- package/dist/cli/src/commands/doc-bind.js +3 -3
- package/dist/cli/src/commands/doc-create.js +5 -5
- package/dist/cli/src/commands/doc-delete.js +4 -4
- package/dist/cli/src/commands/doc-import-gdoc.js +82 -0
- package/dist/cli/src/commands/doc-list.js +7 -7
- package/dist/cli/src/commands/doc-move.js +8 -8
- package/dist/cli/src/commands/doc-share-list.js +11 -8
- package/dist/cli/src/commands/doc-share.js +7 -5
- package/dist/cli/src/commands/doc-show.js +6 -6
- package/dist/cli/src/commands/doc-sync.js +44 -0
- package/dist/cli/src/commands/doc-unbind.js +4 -4
- package/dist/cli/src/commands/doc-unshare.js +9 -7
- package/dist/cli/src/commands/doc-update.js +5 -5
- package/dist/cli/src/commands/hook.js +2 -2
- package/dist/cli/src/commands/memory-project-add.js +19 -4
- package/dist/cli/src/commands/memory-project-list.js +1 -2
- package/dist/cli/src/commands/memory-promote.js +3 -3
- package/dist/cli/src/commands/memory-rm.js +1 -2
- package/dist/cli/src/commands/memory-task-add.js +19 -4
- package/dist/cli/src/commands/memory-task-list.js +1 -2
- package/dist/cli/src/commands/milestone-create.js +4 -4
- package/dist/cli/src/commands/milestone-delete.js +5 -5
- package/dist/cli/src/commands/milestone-list.js +3 -3
- package/dist/cli/src/commands/milestone-show.js +5 -5
- package/dist/cli/src/commands/milestone-update.js +6 -5
- package/dist/cli/src/commands/project-list.js +3 -3
- package/dist/cli/src/commands/session-attach.js +5 -5
- package/dist/cli/src/commands/session-detach.js +3 -3
- package/dist/cli/src/commands/session-status.js +3 -3
- package/dist/cli/src/commands/session-wrap.js +32 -0
- package/dist/cli/src/commands/setup.js +33 -7
- package/dist/cli/src/commands/sprint-add.js +3 -3
- package/dist/cli/src/commands/sprint-close.js +5 -5
- package/dist/cli/src/commands/sprint-create.js +4 -4
- package/dist/cli/src/commands/sprint-delete.js +5 -5
- package/dist/cli/src/commands/sprint-list.js +3 -3
- package/dist/cli/src/commands/sprint-remove.js +3 -3
- package/dist/cli/src/commands/sprint-show.js +4 -4
- package/dist/cli/src/commands/sprint-start.js +4 -4
- package/dist/cli/src/commands/sprint-summary.js +7 -7
- package/dist/cli/src/commands/sprint-update.js +6 -5
- package/dist/cli/src/commands/task-artifact-add.js +17 -5
- package/dist/cli/src/commands/task-artifact-list.js +4 -4
- package/dist/cli/src/commands/task-artifact-rm.js +4 -4
- package/dist/cli/src/commands/task-artifact-show.js +8 -8
- package/dist/cli/src/commands/task-artifact-update.js +5 -5
- package/dist/cli/src/commands/task-comment-list.js +111 -0
- package/dist/cli/src/commands/task-comment.js +3 -3
- package/dist/cli/src/commands/task-context.js +29 -12
- package/dist/cli/src/commands/task-create.js +7 -7
- package/dist/cli/src/commands/task-figma-add.js +3 -2
- package/dist/cli/src/commands/task-figma-context.js +61 -0
- package/dist/cli/src/commands/task-figma-list.js +3 -2
- package/dist/cli/src/commands/task-figma-refresh.js +4 -3
- package/dist/cli/src/commands/task-figma-rm.js +3 -2
- package/dist/cli/src/commands/task-list.js +1 -2
- package/dist/cli/src/commands/task-pr-show.js +66 -0
- package/dist/cli/src/commands/task-show.js +8 -7
- package/dist/cli/src/commands/task-slack-show.js +59 -0
- package/dist/cli/src/commands/task-update.js +7 -7
- package/dist/cli/src/commands/task-web-show.js +64 -0
- package/dist/cli/src/commands/whoami.js +4 -3
- package/dist/cli/src/commands/wrap/progress-comment-section.js +81 -0
- package/dist/cli/src/index.js +174 -102
- package/dist/cli/src/lib/agent.js +10 -1
- package/dist/cli/src/lib/api.js +81 -1
- package/dist/cli/src/lib/config.js +2 -1
- package/dist/cli/src/lib/doc-input.js +12 -1
- package/dist/cli/src/lib/editor.js +66 -0
- package/dist/cli/src/lib/figma-api.js +1 -1
- package/dist/cli/src/lib/format.js +3 -2
- package/dist/cli/src/lib/hook-runner.js +64 -19
- package/dist/cli/src/lib/hooks-template.js +52 -7
- package/dist/cli/src/lib/memory-content.js +4 -3
- package/dist/cli/src/lib/path-guard.js +125 -0
- package/dist/cli/src/lib/progress-comment-api.js +47 -0
- package/dist/cli/src/lib/resolve-doc-id.js +2 -1
- package/dist/cli/src/lib/resolve-member.js +2 -1
- package/dist/cli/src/lib/sanitize.js +17 -0
- package/dist/cli/src/lib/tag-resolver.js +2 -1
- package/dist/cli/src/lib/update-check.js +2 -2
- package/dist/cli/src/lib/wrap-panel.js +15 -0
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ exports.runHookWithBody = runHookWithBody;
|
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const api_1 = require("./api");
|
|
8
8
|
const hook_log_1 = require("./hook-log");
|
|
9
|
+
const sanitize_1 = require("./sanitize");
|
|
10
|
+
const agent_1 = require("./agent");
|
|
9
11
|
/**
|
|
10
12
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
11
13
|
* logged, and `runHook` exits 0 — Claude Code is never blocked beyond
|
|
@@ -49,16 +51,40 @@ function readStdin() {
|
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Build the array of stdout lines to emit for a given hook path + response
|
|
52
|
-
* body. Returns an empty array for any path
|
|
54
|
+
* body. Returns an empty array for any path that emits nothing.
|
|
53
55
|
*
|
|
54
56
|
* For 'session-start' the array contains:
|
|
55
57
|
* [0] plain-text bind/unbound status line (always present when taskBinding exists)
|
|
56
|
-
* [1] (optional) hookSpecificOutput JSON when
|
|
58
|
+
* [1] (optional) hookSpecificOutput JSON when there is any additionalContext —
|
|
59
|
+
* the memory section and the PR-review-todos section, concatenated.
|
|
57
60
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
* For 'pre-tool-use' the array contains:
|
|
62
|
+
* [0] (optional) a PreToolUse hookSpecificOutput JSON carrying the parallel-
|
|
63
|
+
* edit collision warning as additionalContext, when the server returned a
|
|
64
|
+
* `collisionWarning` (LUM-150 step ③). Empty otherwise.
|
|
65
|
+
*
|
|
66
|
+
* The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
|
|
67
|
+
* runtime injects additionalContext into the conversation automatically.
|
|
60
68
|
*/
|
|
61
69
|
function formatHookStdoutLines(path, responseBody) {
|
|
70
|
+
if (path === 'pre-tool-use') {
|
|
71
|
+
if (responseBody == null || typeof responseBody !== 'object')
|
|
72
|
+
return [];
|
|
73
|
+
const warning = responseBody
|
|
74
|
+
.collisionWarning;
|
|
75
|
+
if (typeof warning !== 'string' || warning === '')
|
|
76
|
+
return [];
|
|
77
|
+
return [
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
hookSpecificOutput: {
|
|
80
|
+
hookEventName: 'PreToolUse',
|
|
81
|
+
// Server-built text routed back to stdout — sanitize untrusted free
|
|
82
|
+
// text before Claude Code consumes it (ANSI/control-char injection).
|
|
83
|
+
additionalContext: (0, sanitize_1.sanitizeField)(warning),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
62
88
|
if (path !== 'session-start')
|
|
63
89
|
return [];
|
|
64
90
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
@@ -70,16 +96,19 @@ function formatHookStdoutLines(path, responseBody) {
|
|
|
70
96
|
const sessionId = body.sessionId;
|
|
71
97
|
const tb = body.taskBinding;
|
|
72
98
|
if (tb && tb.bound === true) {
|
|
73
|
-
lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${tb.taskTitle}`);
|
|
99
|
+
lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
|
|
74
100
|
}
|
|
75
101
|
else if (tb && tb.bound === false) {
|
|
76
102
|
lines.push(`[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`);
|
|
77
103
|
}
|
|
78
|
-
|
|
104
|
+
// Memory + PR-review todos share one additionalContext block so Claude Code
|
|
105
|
+
// injects a single coherent context payload at session start.
|
|
106
|
+
const contextParts = [body.memorySection, body.reviewTodosSection].filter((s) => typeof s === 'string' && s !== '');
|
|
107
|
+
if (contextParts.length > 0) {
|
|
79
108
|
lines.push(JSON.stringify({
|
|
80
109
|
hookSpecificOutput: {
|
|
81
110
|
hookEventName: 'SessionStart',
|
|
82
|
-
additionalContext:
|
|
111
|
+
additionalContext: contextParts.join('\n\n'),
|
|
83
112
|
},
|
|
84
113
|
}));
|
|
85
114
|
}
|
|
@@ -92,16 +121,16 @@ function formatHookStdoutLines(path, responseBody) {
|
|
|
92
121
|
*
|
|
93
122
|
* This function NEVER throws and NEVER rejects.
|
|
94
123
|
*/
|
|
95
|
-
async function runHook(path) {
|
|
124
|
+
async function runHook(path, agentToken) {
|
|
96
125
|
const body = await readStdin();
|
|
97
|
-
return runHookWithBody(path, body);
|
|
126
|
+
return runHookWithBody(path, body, agentToken);
|
|
98
127
|
}
|
|
99
128
|
/**
|
|
100
129
|
* Test-friendly worker. Takes the hook body as an argument so tests can
|
|
101
130
|
* exercise the side-effect logic without poking process.stdin. Production
|
|
102
131
|
* code always goes through `runHook`, which feeds it real stdin.
|
|
103
132
|
*/
|
|
104
|
-
async function runHookWithBody(path, body) {
|
|
133
|
+
async function runHookWithBody(path, body, agentToken) {
|
|
105
134
|
try {
|
|
106
135
|
const creds = (0, config_1.readCredentials)();
|
|
107
136
|
if (!creds) {
|
|
@@ -111,27 +140,43 @@ async function runHookWithBody(path, body) {
|
|
|
111
140
|
// Allow `LUMO_API_URL` to override the baked-in creds.apiUrl for
|
|
112
141
|
// redirecting hooks at dev-env switch without re-running `lumo auth
|
|
113
142
|
// login`. The bearer token from creds.json is still used as-is.
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
// An insecure override (non-https, non-localhost) throws here and is
|
|
144
|
+
// caught by the outer try/catch → logged, POST skipped, hook still exits 0.
|
|
145
|
+
// The host-mismatch warning goes to hook.log (never stdout, which would
|
|
146
|
+
// pollute Claude Code).
|
|
147
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl, {
|
|
148
|
+
warn: message => (0, hook_log_1.logHookError)(`[${path}]`, message),
|
|
149
|
+
});
|
|
116
150
|
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/hooks/${path}`;
|
|
117
151
|
const controller = new AbortController();
|
|
118
152
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
153
|
+
// Tell the server which coding agent produced this hook. The token is
|
|
154
|
+
// baked into the hook command at `lumo setup --agent <token>`; we send
|
|
155
|
+
// the normalized enum so the server can record it on the session. An
|
|
156
|
+
// unrecognized/absent token is dropped — the server then falls back to
|
|
157
|
+
// its default (CLAUDE_CODE).
|
|
158
|
+
const headers = {
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
Authorization: `Bearer ${creds.token}`,
|
|
161
|
+
};
|
|
162
|
+
const agentEnum = agentToken ? (0, agent_1.normalizeAgent)(agentToken) : null;
|
|
163
|
+
if (agentEnum)
|
|
164
|
+
headers['X-Lumo-Agent'] = agentEnum;
|
|
119
165
|
try {
|
|
120
166
|
const res = await fetch(url, {
|
|
121
167
|
method: 'POST',
|
|
122
|
-
headers
|
|
123
|
-
'Content-Type': 'application/json',
|
|
124
|
-
Authorization: `Bearer ${creds.token}`,
|
|
125
|
-
},
|
|
168
|
+
headers,
|
|
126
169
|
body: body || '{}',
|
|
127
170
|
signal: controller.signal,
|
|
128
171
|
});
|
|
129
172
|
if (!res.ok) {
|
|
130
173
|
(0, hook_log_1.logHookError)(`[${path}]`, `HTTP ${res.status} from ${url}`);
|
|
131
174
|
}
|
|
132
|
-
else if (path === 'session-start') {
|
|
133
|
-
//
|
|
134
|
-
//
|
|
175
|
+
else if (path === 'session-start' || path === 'pre-tool-use') {
|
|
176
|
+
// Paths that turn the response body into stdout for Claude Code:
|
|
177
|
+
// session-start → bind status + injected context
|
|
178
|
+
// pre-tool-use → parallel-edit collision warning (LUM-150 ③)
|
|
179
|
+
// Only after a 2xx so a transient server failure emits nothing.
|
|
135
180
|
try {
|
|
136
181
|
const responseBody = await res.json();
|
|
137
182
|
for (const line of formatHookStdoutLines(path, responseBody)) {
|
|
@@ -35,35 +35,80 @@ exports.LUMO_HOOK_EVENTS = [
|
|
|
35
35
|
['CwdChanged', 'cwd-changed'],
|
|
36
36
|
['InstructionsLoaded', 'instructions-loaded'],
|
|
37
37
|
];
|
|
38
|
+
/** Default agent token baked into the hook commands when none is given. */
|
|
39
|
+
const DEFAULT_AGENT_TOKEN = 'claude-code';
|
|
40
|
+
/**
|
|
41
|
+
* A hook command belongs to Lumo when it is exactly `lumo hook <slug>` or
|
|
42
|
+
* carries trailing flags (`lumo hook <slug> --agent codex`). We match on the
|
|
43
|
+
* prefix so a re-run can recognize — and rewrite — an entry baked with a
|
|
44
|
+
* different (or no) `--agent` token.
|
|
45
|
+
*/
|
|
46
|
+
function isLumoHookCommand(command, slug) {
|
|
47
|
+
return (command === `lumo hook ${slug}` || command.startsWith(`lumo hook ${slug} `));
|
|
48
|
+
}
|
|
38
49
|
// Build the settings.json fragment we want to install. We do not use a
|
|
39
50
|
// matcher because the Lumo hook handlers ingest every event of a given type;
|
|
40
51
|
// adding a matcher would silently drop events that have no `tool_name` (e.g.
|
|
41
52
|
// SessionStart). Keep this in step with cli/src/commands/hook.ts dispatch.
|
|
42
|
-
|
|
53
|
+
//
|
|
54
|
+
// `agentToken` is baked into every command (`lumo hook <slug> --agent
|
|
55
|
+
// <token>`) so the hook tells the server which coding agent owns the session.
|
|
56
|
+
function buildLumoHookFragment(agentToken = DEFAULT_AGENT_TOKEN) {
|
|
43
57
|
const hooks = {};
|
|
44
58
|
for (const [event, slug] of exports.LUMO_HOOK_EVENTS) {
|
|
45
59
|
hooks[event] = [
|
|
46
|
-
{
|
|
60
|
+
{
|
|
61
|
+
hooks: [
|
|
62
|
+
{
|
|
63
|
+
type: 'command',
|
|
64
|
+
command: `lumo hook ${slug} --agent ${agentToken}`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
47
68
|
];
|
|
48
69
|
}
|
|
49
70
|
return { hooks };
|
|
50
71
|
}
|
|
51
|
-
function mergeLumoHooks(existing) {
|
|
72
|
+
function mergeLumoHooks(existing, agentToken = DEFAULT_AGENT_TOKEN) {
|
|
52
73
|
const base = existing
|
|
53
74
|
? JSON.parse(JSON.stringify(existing))
|
|
54
75
|
: {};
|
|
55
76
|
if (!base.hooks)
|
|
56
77
|
base.hooks = {};
|
|
57
|
-
const stats = {
|
|
78
|
+
const stats = {
|
|
79
|
+
addedEvents: [],
|
|
80
|
+
alreadyPresent: [],
|
|
81
|
+
updatedEvents: [],
|
|
82
|
+
};
|
|
58
83
|
for (const [event, slug] of exports.LUMO_HOOK_EVENTS) {
|
|
59
|
-
const ourCmd = `lumo hook ${slug}`;
|
|
84
|
+
const ourCmd = `lumo hook ${slug} --agent ${agentToken}`;
|
|
60
85
|
const groups = base.hooks[event] ?? [];
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
let exact = false;
|
|
87
|
+
let rewritten = false;
|
|
88
|
+
for (const g of groups) {
|
|
89
|
+
if (!Array.isArray(g.hooks))
|
|
90
|
+
continue;
|
|
91
|
+
for (const h of g.hooks) {
|
|
92
|
+
if (h.command === ourCmd) {
|
|
93
|
+
exact = true;
|
|
94
|
+
}
|
|
95
|
+
else if (isLumoHookCommand(h.command, slug)) {
|
|
96
|
+
// legacy flagless or a different agent token — bring it up to date
|
|
97
|
+
h.command = ourCmd;
|
|
98
|
+
rewritten = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (exact) {
|
|
63
103
|
stats.alreadyPresent.push(event);
|
|
64
104
|
base.hooks[event] = groups;
|
|
65
105
|
continue;
|
|
66
106
|
}
|
|
107
|
+
if (rewritten) {
|
|
108
|
+
stats.updatedEvents.push(event);
|
|
109
|
+
base.hooks[event] = groups;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
67
112
|
base.hooks[event] = [
|
|
68
113
|
...groups,
|
|
69
114
|
{ hooks: [{ type: 'command', command: ourCmd }] },
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Category/field metadata + builders for the `lumo memory` commands.
|
|
3
|
-
// Mirrors the four content shapes validated server-side by parseMemoryContent.
|
|
4
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
3
|
exports.buildMemoryContent = buildMemoryContent;
|
|
6
4
|
exports.formatMemoryList = formatMemoryList;
|
|
5
|
+
// Category/field metadata + builders for the `lumo memory` commands.
|
|
6
|
+
// Mirrors the four content shapes validated server-side by parseMemoryContent.
|
|
7
|
+
const sanitize_1 = require("./sanitize");
|
|
7
8
|
const trimmed = (v) => (v ?? '').trim();
|
|
8
9
|
/**
|
|
9
10
|
* Validate the per-category flags and assemble { category, content }. Required
|
|
@@ -69,7 +70,7 @@ function headline(category, content) {
|
|
|
69
70
|
: category === 'CONVENTION' ? 'rule'
|
|
70
71
|
: 'workflow';
|
|
71
72
|
const v = c[key];
|
|
72
|
-
return typeof v === 'string' && v.length > 0 ? v : '(unparseable)';
|
|
73
|
+
return typeof v === 'string' && v.length > 0 ? (0, sanitize_1.sanitizeField)(v) : '(unparseable)';
|
|
73
74
|
}
|
|
74
75
|
/** Fixed-width rows: id SCOPE CATEGORY headline source(auto|manual). */
|
|
75
76
|
function formatMemoryList(rows) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.matchSensitiveName = matchSensitiveName;
|
|
37
|
+
exports.checkArtifactFilePath = checkArtifactFilePath;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const SENSITIVE_SEGMENTS = ['.ssh', '.aws', '.gnupg'];
|
|
41
|
+
const SENSITIVE_BASENAMES = [
|
|
42
|
+
'id_rsa',
|
|
43
|
+
'id_dsa',
|
|
44
|
+
'id_ecdsa',
|
|
45
|
+
'id_ed25519',
|
|
46
|
+
'credentials',
|
|
47
|
+
'.npmrc',
|
|
48
|
+
'.netrc',
|
|
49
|
+
'.pgpass',
|
|
50
|
+
'.htpasswd',
|
|
51
|
+
];
|
|
52
|
+
const SENSITIVE_EXTENSIONS = ['.pem', '.key', '.pfx', '.p12'];
|
|
53
|
+
/**
|
|
54
|
+
* Pure denylist check. Returns a human-readable reason when the path looks
|
|
55
|
+
* like a secret, or null when it is acceptable. Matches the basename
|
|
56
|
+
* (case-insensitive) and path segments; no filesystem access.
|
|
57
|
+
*/
|
|
58
|
+
function matchSensitiveName(p) {
|
|
59
|
+
const segments = p.split(/[\\/]+/).filter(Boolean);
|
|
60
|
+
for (const seg of segments) {
|
|
61
|
+
if (SENSITIVE_SEGMENTS.includes(seg.toLowerCase())) {
|
|
62
|
+
return `path contains ${seg}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const base = (segments[segments.length - 1] ?? '').toLowerCase();
|
|
66
|
+
if (base === '.env' || base.startsWith('.env.'))
|
|
67
|
+
return 'matches .env';
|
|
68
|
+
if (SENSITIVE_BASENAMES.includes(base))
|
|
69
|
+
return `matches ${base}`;
|
|
70
|
+
for (const ext of SENSITIVE_EXTENSIONS) {
|
|
71
|
+
if (base.endsWith(ext))
|
|
72
|
+
return `matches *${ext}`;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decide whether `rawPath` is safe to read and upload as an artifact body.
|
|
78
|
+
* Two layers: (1) it must resolve to a real path inside the project directory
|
|
79
|
+
* (cwd), and (2) neither the raw nor the canonical path may match the
|
|
80
|
+
* sensitive-filename denylist. realpath defeats symlinks that escape the tree.
|
|
81
|
+
* NOTE: there is a TOCTOU window between this check and the caller's read;
|
|
82
|
+
* the caller reads the returned canonical `resolved` path to narrow it.
|
|
83
|
+
*/
|
|
84
|
+
function checkArtifactFilePath(rawPath, deps) {
|
|
85
|
+
const cwd = deps?.cwd ?? process.cwd;
|
|
86
|
+
const realpath = deps?.realpath ?? fs.realpathSync;
|
|
87
|
+
// 1. Fail fast on the raw path (no fs access).
|
|
88
|
+
const rawHit = matchSensitiveName(rawPath);
|
|
89
|
+
if (rawHit)
|
|
90
|
+
return { ok: false, reason: 'sensitive', detail: rawHit };
|
|
91
|
+
const root = cwd();
|
|
92
|
+
const absolute = path.resolve(root, rawPath);
|
|
93
|
+
// 2. Canonicalize. realpath throws if the path does not exist / is unreadable.
|
|
94
|
+
let resolved;
|
|
95
|
+
let rootReal;
|
|
96
|
+
try {
|
|
97
|
+
resolved = realpath(absolute);
|
|
98
|
+
rootReal = realpath(root);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ok: false, reason: 'unreadable', detail: 'path does not resolve' };
|
|
102
|
+
}
|
|
103
|
+
// 3. Re-check the denylist on the canonical path (catches innocent-looking
|
|
104
|
+
// symlinks pointing at a secret).
|
|
105
|
+
const canonHit = matchSensitiveName(resolved);
|
|
106
|
+
if (canonHit)
|
|
107
|
+
return { ok: false, reason: 'sensitive', detail: canonHit };
|
|
108
|
+
// The path must be a file inside the project, not the project root itself.
|
|
109
|
+
if (resolved === rootReal) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
reason: 'outside-project',
|
|
113
|
+
detail: 'path resolves to the project root, not a file',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// 4. Confinement: the canonical path must be inside the project root.
|
|
117
|
+
if (resolved !== rootReal && !resolved.startsWith(rootReal + path.sep)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
reason: 'outside-project',
|
|
121
|
+
detail: 'resolves outside project directory',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { ok: true, resolved };
|
|
125
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchProgressDraft = fetchProgressDraft;
|
|
4
|
+
exports.postProgressComment = postProgressComment;
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
function base(creds) {
|
|
7
|
+
return (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
8
|
+
}
|
|
9
|
+
/** GET the unposted progress draft for the session. Throws on transport / non-200. */
|
|
10
|
+
async function fetchProgressDraft(creds, sessionId) {
|
|
11
|
+
const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/turn-summaries`;
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
14
|
+
});
|
|
15
|
+
if (res.status === 401)
|
|
16
|
+
throw new Error('API key invalid or revoked. Run `lumo auth login`.');
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
throw new Error(`progress draft fetch failed (HTTP ${res.status})`);
|
|
19
|
+
return (await res.json());
|
|
20
|
+
}
|
|
21
|
+
/** POST the (possibly edited) body + watermark. Throws the server message on non-201. */
|
|
22
|
+
async function postProgressComment(creds, sessionId, payload) {
|
|
23
|
+
const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/progress-comment`;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${creds.token}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
});
|
|
32
|
+
if (res.status === 401)
|
|
33
|
+
throw new Error('API key invalid or revoked. Run `lumo auth login`.');
|
|
34
|
+
if (res.status !== 201) {
|
|
35
|
+
let serverMsg = null;
|
|
36
|
+
try {
|
|
37
|
+
const errBody = (await res.json());
|
|
38
|
+
if (typeof errBody.error === 'string')
|
|
39
|
+
serverMsg = errBody.error;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// body wasn't JSON
|
|
43
|
+
}
|
|
44
|
+
throw new Error(serverMsg ?? `progress comment failed (HTTP ${res.status})`);
|
|
45
|
+
}
|
|
46
|
+
return (await res.json());
|
|
47
|
+
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.lookupDocId = lookupDocId;
|
|
4
4
|
const api_1 = require("./api");
|
|
5
5
|
const resolve_doc_1 = require("./resolve-doc");
|
|
6
|
+
const sanitize_1 = require("./sanitize");
|
|
6
7
|
/**
|
|
7
8
|
* Resolve a user-typed doc reference (cuid OR case-insensitive title) to a
|
|
8
9
|
* document id. Fetches GET /api/documents to perform a title lookup when
|
|
@@ -25,7 +26,7 @@ async function lookupDocId(apiUrl, token, reference) {
|
|
|
25
26
|
if (result.kind === 'ambiguous') {
|
|
26
27
|
console.error(`Error: title "${reference}" matches ${result.candidates.length} docs:`);
|
|
27
28
|
for (const c of result.candidates) {
|
|
28
|
-
console.error(` ${c.id} ${c.title}`);
|
|
29
|
+
console.error(` ${c.id} ${(0, sanitize_1.sanitizeField)(c.title)}`);
|
|
29
30
|
}
|
|
30
31
|
console.error('Re-run with the cuid.');
|
|
31
32
|
return null;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.fetchMembers = fetchMembers;
|
|
4
4
|
exports.resolveMember = resolveMember;
|
|
5
5
|
const api_1 = require("./api");
|
|
6
|
+
const sanitize_1 = require("./sanitize");
|
|
6
7
|
/**
|
|
7
8
|
* Fetch the workspace member directory once. Used by doc-share commands to
|
|
8
9
|
* resolve a free-form member ref → memberId locally, because the document
|
|
@@ -16,7 +17,7 @@ async function fetchMembers(apiUrl, token) {
|
|
|
16
17
|
});
|
|
17
18
|
if (!res.ok) {
|
|
18
19
|
const text = await res.text();
|
|
19
|
-
throw new Error(`failed to load members (${res.status} ${res.statusText}): ${text}`);
|
|
20
|
+
throw new Error(`failed to load members (${res.status} ${res.statusText}): ${(0, sanitize_1.sanitizeField)(text)}`);
|
|
20
21
|
}
|
|
21
22
|
const body = (await res.json());
|
|
22
23
|
return body.members.map(m => ({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeField = sanitizeField;
|
|
4
|
+
// Matches C0 control chars EXCEPT tab (\x09) and newline (\x0a), DEL (\x7f),
|
|
5
|
+
// and the C1 control range (\x80-\x9f). Includes ESC (\x1b) and the 8-bit CSI
|
|
6
|
+
// introducer (\x9b) — both ANSI escape-sequence injection vectors. U+0080-U+009F
|
|
7
|
+
// are never legitimate visible text, so stripping them is safe.
|
|
8
|
+
const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
9
|
+
/**
|
|
10
|
+
* Strip terminal control characters from untrusted, server-returned fields
|
|
11
|
+
* before printing them, to prevent ANSI escape-sequence injection. Tab and
|
|
12
|
+
* newline are preserved so fixed-width tables and multi-line bodies render
|
|
13
|
+
* correctly. Callers handle optional values, e.g. `sanitizeField(name ?? '')`.
|
|
14
|
+
*/
|
|
15
|
+
function sanitizeField(value) {
|
|
16
|
+
return value.replace(CONTROL_CHARS, '');
|
|
17
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveTagRefs = resolveTagRefs;
|
|
4
4
|
const api_1 = require("./api");
|
|
5
|
+
const sanitize_1 = require("./sanitize");
|
|
5
6
|
async function resolveTagRefs(refs, deps) {
|
|
6
7
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
7
8
|
// Trim + reject empty
|
|
@@ -36,7 +37,7 @@ async function resolveTagRefs(refs, deps) {
|
|
|
36
37
|
});
|
|
37
38
|
if (!res.ok) {
|
|
38
39
|
const body = await res.text();
|
|
39
|
-
throw new Error(`tag resolve failed: ${res.status} ${res.statusText}: ${body}`);
|
|
40
|
+
throw new Error(`tag resolve failed: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(body)}`);
|
|
40
41
|
}
|
|
41
42
|
const json = (await res.json());
|
|
42
43
|
nameIds = json.tags.map(t => t.id);
|
|
@@ -40,10 +40,10 @@ exports.maybeRefreshInBackground = maybeRefreshInBackground;
|
|
|
40
40
|
exports.runBackgroundRefresh = runBackgroundRefresh;
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
-
const os = __importStar(require("os"));
|
|
44
43
|
const https = __importStar(require("https"));
|
|
45
44
|
const child_process_1 = require("child_process");
|
|
46
|
-
const
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
const CACHE_FILE = path.join((0, config_1.configDir)(), 'update-check.json');
|
|
47
47
|
const CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24;
|
|
48
48
|
function readCache() {
|
|
49
49
|
try {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runWrapPanel = runWrapPanel;
|
|
4
|
+
/** Run each section in order; print its title, prepare, then run if it has content. */
|
|
5
|
+
async function runWrapPanel(sections, opts) {
|
|
6
|
+
for (const section of sections) {
|
|
7
|
+
process.stdout.write(`\n━━ ${section.title} ━━\n`);
|
|
8
|
+
const hasContent = await section.prepare();
|
|
9
|
+
if (!hasContent) {
|
|
10
|
+
process.stdout.write('(无内容)\n');
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
await section.run(opts);
|
|
14
|
+
}
|
|
15
|
+
}
|