@lumoai/cli 1.0.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/README.md +90 -0
- package/dist/cli/src/commands/auth-login.js +55 -0
- package/dist/cli/src/commands/auth-logout.js +14 -0
- package/dist/cli/src/commands/doc-bind.js +52 -0
- package/dist/cli/src/commands/doc-create.js +138 -0
- package/dist/cli/src/commands/doc-delete.js +52 -0
- package/dist/cli/src/commands/doc-list.js +91 -0
- package/dist/cli/src/commands/doc-move.js +113 -0
- package/dist/cli/src/commands/doc-share-list.js +62 -0
- package/dist/cli/src/commands/doc-share.js +77 -0
- package/dist/cli/src/commands/doc-show.js +99 -0
- package/dist/cli/src/commands/doc-unbind.js +47 -0
- package/dist/cli/src/commands/doc-unshare.js +71 -0
- package/dist/cli/src/commands/doc-update.js +144 -0
- package/dist/cli/src/commands/hook.js +21 -0
- package/dist/cli/src/commands/milestone-create.js +84 -0
- package/dist/cli/src/commands/milestone-delete.js +96 -0
- package/dist/cli/src/commands/milestone-list.js +55 -0
- package/dist/cli/src/commands/milestone-show.js +106 -0
- package/dist/cli/src/commands/milestone-update.js +167 -0
- package/dist/cli/src/commands/project-list.js +57 -0
- package/dist/cli/src/commands/session-attach.js +80 -0
- package/dist/cli/src/commands/session-detach.js +60 -0
- package/dist/cli/src/commands/session-status.js +58 -0
- package/dist/cli/src/commands/sprint-add.js +62 -0
- package/dist/cli/src/commands/sprint-close.js +151 -0
- package/dist/cli/src/commands/sprint-create.js +80 -0
- package/dist/cli/src/commands/sprint-delete.js +88 -0
- package/dist/cli/src/commands/sprint-list.js +85 -0
- package/dist/cli/src/commands/sprint-remove.js +57 -0
- package/dist/cli/src/commands/sprint-show.js +81 -0
- package/dist/cli/src/commands/sprint-start.js +68 -0
- package/dist/cli/src/commands/sprint-summary.js +138 -0
- package/dist/cli/src/commands/sprint-update.js +148 -0
- package/dist/cli/src/commands/task-comment.js +95 -0
- package/dist/cli/src/commands/task-context.js +137 -0
- package/dist/cli/src/commands/task-create.js +202 -0
- package/dist/cli/src/commands/task-list.js +94 -0
- package/dist/cli/src/commands/task-show.js +74 -0
- package/dist/cli/src/commands/task-update.js +468 -0
- package/dist/cli/src/commands/whoami.js +16 -0
- package/dist/cli/src/index.js +492 -0
- package/dist/cli/src/lib/api.js +33 -0
- package/dist/cli/src/lib/browser.js +33 -0
- package/dist/cli/src/lib/config.js +92 -0
- package/dist/cli/src/lib/doc-input.js +80 -0
- package/dist/cli/src/lib/doc-sort-order.js +23 -0
- package/dist/cli/src/lib/doc-tree.js +54 -0
- package/dist/cli/src/lib/format.js +33 -0
- package/dist/cli/src/lib/hook-log.js +81 -0
- package/dist/cli/src/lib/hook-runner.js +156 -0
- package/dist/cli/src/lib/markdown-tiptap.js +7 -0
- package/dist/cli/src/lib/prompt.js +71 -0
- package/dist/cli/src/lib/resolve-doc-id.js +34 -0
- package/dist/cli/src/lib/resolve-doc.js +27 -0
- package/dist/cli/src/lib/resolve-member.js +58 -0
- package/dist/cli/src/lib/resolve.js +170 -0
- package/dist/cli/src/lib/tag-resolver.js +49 -0
- package/dist/shared/src/index.js +35 -0
- package/dist/shared/src/markdown-tiptap.js +267 -0
- package/package.json +48 -0
|
@@ -0,0 +1,80 @@
|
|
|
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.resolveDocContent = resolveDocContent;
|
|
37
|
+
exports.readStdinToString = readStdinToString;
|
|
38
|
+
exports.readFileUtf8 = readFileUtf8;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
/**
|
|
41
|
+
* Pick one of --content / --file / stdin as the markdown source.
|
|
42
|
+
* Explicit flags win: if --content or --file is set, stdin is ignored
|
|
43
|
+
* (whether the shell is interactive or not). --content and --file are
|
|
44
|
+
* mutually exclusive. stdin is only consulted when neither flag is set
|
|
45
|
+
* and the shell is non-TTY.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveDocContent(args) {
|
|
48
|
+
const hasContent = args.content !== undefined;
|
|
49
|
+
const hasFile = args.file !== undefined && args.file.length > 0;
|
|
50
|
+
if (hasContent && hasFile) {
|
|
51
|
+
return {
|
|
52
|
+
kind: 'error',
|
|
53
|
+
message: '--content and --file are mutually exclusive (pick one)',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (hasContent)
|
|
57
|
+
return { kind: 'ok', markdown: args.content };
|
|
58
|
+
if (hasFile) {
|
|
59
|
+
const text = await args.readFile(args.file);
|
|
60
|
+
return { kind: 'ok', markdown: text };
|
|
61
|
+
}
|
|
62
|
+
if (!args.stdinIsTTY) {
|
|
63
|
+
const text = await args.readStdin();
|
|
64
|
+
return { kind: 'ok', markdown: text };
|
|
65
|
+
}
|
|
66
|
+
return { kind: 'none' };
|
|
67
|
+
}
|
|
68
|
+
/** Read stdin to end as UTF-8 string. */
|
|
69
|
+
function readStdinToString() {
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
let buf = '';
|
|
72
|
+
process.stdin.setEncoding('utf8');
|
|
73
|
+
process.stdin.on('data', chunk => (buf += chunk));
|
|
74
|
+
process.stdin.on('end', () => resolve(buf));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Default readFile wrapper for production CLI use. */
|
|
78
|
+
function readFileUtf8(path) {
|
|
79
|
+
return fs.promises.readFile(path, 'utf8');
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickNextSortOrder = pickNextSortOrder;
|
|
4
|
+
/**
|
|
5
|
+
* Compute the sortOrder to assign to a new/relocated doc so it lands at the
|
|
6
|
+
* end of its sibling list. Empty siblings → 0. Otherwise max(sortOrder) + 1.
|
|
7
|
+
*
|
|
8
|
+
* The "siblings" passed in MUST be the docs already under the target parent
|
|
9
|
+
* (or under root). The caller filters this out of the GET /api/documents
|
|
10
|
+
* payload before calling — keeping this function pure makes it cheap to
|
|
11
|
+
* unit-test in the future.
|
|
12
|
+
*/
|
|
13
|
+
function pickNextSortOrder(siblings) {
|
|
14
|
+
if (siblings.length === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
let max = siblings[0].sortOrder;
|
|
17
|
+
for (let i = 1; i < siblings.length; i++) {
|
|
18
|
+
const v = siblings[i].sortOrder;
|
|
19
|
+
if (v > max)
|
|
20
|
+
max = v;
|
|
21
|
+
}
|
|
22
|
+
return max + 1;
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTree = buildTree;
|
|
4
|
+
exports.flattenWithDepth = flattenWithDepth;
|
|
5
|
+
/**
|
|
6
|
+
* Group a flat list of docs into a forest. Docs whose parentId is null OR
|
|
7
|
+
* whose parent is absent from the input become roots ("virtual roots" in
|
|
8
|
+
* spec parlance). Siblings are sorted by sortOrder ascending; ties broken
|
|
9
|
+
* by id to keep ordering stable when sortOrder collides.
|
|
10
|
+
*/
|
|
11
|
+
function buildTree(rows) {
|
|
12
|
+
const byId = new Map();
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
byId.set(row.id, { row, children: [] });
|
|
15
|
+
}
|
|
16
|
+
const roots = [];
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
const node = byId.get(row.id);
|
|
19
|
+
const parent = row.parentId ? byId.get(row.parentId) : undefined;
|
|
20
|
+
if (parent) {
|
|
21
|
+
parent.children.push(node);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
roots.push(node);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const sortNodes = (nodes) => {
|
|
28
|
+
nodes.sort((a, b) => {
|
|
29
|
+
const ao = a.row.sortOrder ?? 0;
|
|
30
|
+
const bo = b.row.sortOrder ?? 0;
|
|
31
|
+
if (ao !== bo)
|
|
32
|
+
return ao - bo;
|
|
33
|
+
return a.row.id < b.row.id ? -1 : a.row.id > b.row.id ? 1 : 0;
|
|
34
|
+
});
|
|
35
|
+
for (const n of nodes)
|
|
36
|
+
sortNodes(n.children);
|
|
37
|
+
};
|
|
38
|
+
sortNodes(roots);
|
|
39
|
+
return roots;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pre-order traversal yielding rows tagged with depth. Roots → depth 0.
|
|
43
|
+
*/
|
|
44
|
+
function flattenWithDepth(roots) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const visit = (node, depth) => {
|
|
47
|
+
out.push({ row: node.row, depth });
|
|
48
|
+
for (const child of node.children)
|
|
49
|
+
visit(child, depth + 1);
|
|
50
|
+
};
|
|
51
|
+
for (const r of roots)
|
|
52
|
+
visit(r, 0);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatTaskListTable = formatTaskListTable;
|
|
4
|
+
/**
|
|
5
|
+
* Render a task list as a fixed-width table. Each row:
|
|
6
|
+
* LUM-42 IN_PROGRESS HIGH project-name Title here
|
|
7
|
+
* Columns are right-padded to the widest cell in the result set so output
|
|
8
|
+
* lines up regardless of identifier or project-name length.
|
|
9
|
+
*/
|
|
10
|
+
function formatTaskListTable(tasks) {
|
|
11
|
+
if (tasks.length === 0)
|
|
12
|
+
return 'No tasks.';
|
|
13
|
+
const rows = tasks.map(t => ({
|
|
14
|
+
identifier: `${t.teamIdentifier}-${t.number}`,
|
|
15
|
+
status: t.status,
|
|
16
|
+
priority: t.priority,
|
|
17
|
+
project: t.project.name,
|
|
18
|
+
title: t.title,
|
|
19
|
+
}));
|
|
20
|
+
const widths = {
|
|
21
|
+
identifier: Math.max(...rows.map(r => r.identifier.length)),
|
|
22
|
+
status: Math.max(...rows.map(r => r.status.length)),
|
|
23
|
+
priority: Math.max(...rows.map(r => r.priority.length)),
|
|
24
|
+
project: Math.max(...rows.map(r => r.project.length)),
|
|
25
|
+
};
|
|
26
|
+
return rows
|
|
27
|
+
.map(r => `${r.identifier.padEnd(widths.identifier)} ` +
|
|
28
|
+
`${r.status.padEnd(widths.status)} ` +
|
|
29
|
+
`${r.priority.padEnd(widths.priority)} ` +
|
|
30
|
+
`${r.project.padEnd(widths.project)} ` +
|
|
31
|
+
r.title)
|
|
32
|
+
.join('\n');
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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.logHookError = logHookError;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
function logDir() {
|
|
41
|
+
return path.join(os.homedir(), '.lumo');
|
|
42
|
+
}
|
|
43
|
+
function logPath() {
|
|
44
|
+
return path.join(logDir(), 'hook.log');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort append to `~/.lumo/hook.log`.
|
|
48
|
+
*
|
|
49
|
+
* Hook commands must never emit to stderr (that would pollute Claude Code's
|
|
50
|
+
* terminal output). All errors — network, auth, timeout, filesystem — are
|
|
51
|
+
* appended here instead. If the log write itself fails, we swallow that too:
|
|
52
|
+
* a broken log is better than a broken hook.
|
|
53
|
+
*
|
|
54
|
+
* @param context Short label identifying the call site (e.g. `"[pre-tool-use]"`)
|
|
55
|
+
* @param error Either an `Error`, a string, or anything else; stringified internally
|
|
56
|
+
*/
|
|
57
|
+
function logHookError(context, error) {
|
|
58
|
+
try {
|
|
59
|
+
const dir = logDir();
|
|
60
|
+
if (!fs.existsSync(dir))
|
|
61
|
+
fs.mkdirSync(dir, { mode: 0o700, recursive: true });
|
|
62
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
63
|
+
const line = `${new Date().toISOString()} ${context} ${msg}\n`;
|
|
64
|
+
const p = logPath();
|
|
65
|
+
const existed = fs.existsSync(p);
|
|
66
|
+
fs.appendFileSync(p, line, { mode: 0o600 });
|
|
67
|
+
// Best-effort permission tightening: appendFileSync's mode is only honored
|
|
68
|
+
// on initial create, so chmod existing files to match config.ts style.
|
|
69
|
+
if (existed) {
|
|
70
|
+
try {
|
|
71
|
+
fs.chmodSync(p, 0o600);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best-effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Log writes are best-effort; never throw
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatHookStdoutLines = formatHookStdoutLines;
|
|
4
|
+
exports.runHook = runHook;
|
|
5
|
+
exports.runHookWithBody = runHookWithBody;
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const api_1 = require("./api");
|
|
8
|
+
const hook_log_1 = require("./hook-log");
|
|
9
|
+
/**
|
|
10
|
+
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
11
|
+
* logged, and `runHook` exits 0 — Claude Code is never blocked beyond
|
|
12
|
+
* this ceiling.
|
|
13
|
+
*
|
|
14
|
+
* Sizing history:
|
|
15
|
+
* - 1000ms (initial): too tight, false timeouts on cold starts
|
|
16
|
+
* - 1500ms (per plan): still below observed p95 for cold
|
|
17
|
+
* Next.js + Prisma + Clerk on dev; server actually inserts the row
|
|
18
|
+
* but the client aborts before reading the 200, so `hook.log` fills
|
|
19
|
+
* with "This operation was aborted" noise
|
|
20
|
+
* - 3000ms (now): matches observed p95 on dev; catches the full
|
|
21
|
+
* happy path without being long enough to make users feel per-tool
|
|
22
|
+
* hook lag. Revisit when Day 4 makes hook POSTs fire-and-forget so
|
|
23
|
+
* the client can exit 0 before the server finishes.
|
|
24
|
+
*/
|
|
25
|
+
const TIMEOUT_MS = 3000;
|
|
26
|
+
/**
|
|
27
|
+
* Read the full hook JSON from stdin.
|
|
28
|
+
*
|
|
29
|
+
* Resolves to an empty string on EOF if no bytes were produced (e.g. tests
|
|
30
|
+
* or a client that invoked the command with no stdin attached). This lets
|
|
31
|
+
* the runner POST `{}` rather than hanging.
|
|
32
|
+
*/
|
|
33
|
+
function readStdin() {
|
|
34
|
+
return new Promise(resolve => {
|
|
35
|
+
// If stdin is a TTY (nothing piped in), resolve immediately so the hook
|
|
36
|
+
// doesn't hang when invoked outside of Claude Code.
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
resolve('');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let data = '';
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', chunk => {
|
|
44
|
+
data += chunk;
|
|
45
|
+
});
|
|
46
|
+
process.stdin.on('end', () => resolve(data));
|
|
47
|
+
process.stdin.on('error', () => resolve(data));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the array of stdout lines to emit for a given hook path + response
|
|
52
|
+
* body. Returns an empty array for any path other than 'session-start'.
|
|
53
|
+
*
|
|
54
|
+
* For 'session-start' the array contains:
|
|
55
|
+
* [0] plain-text bind/unbound status line (always present when taskBinding exists)
|
|
56
|
+
* [1] (optional) hookSpecificOutput JSON when memorySection is a non-empty string
|
|
57
|
+
*
|
|
58
|
+
* The JSON on line [1] conforms to Claude Code's hookSpecificOutput envelope so
|
|
59
|
+
* the runtime injects additionalContext into the conversation automatically.
|
|
60
|
+
*/
|
|
61
|
+
function formatHookStdoutLines(path, responseBody) {
|
|
62
|
+
if (path !== 'session-start')
|
|
63
|
+
return [];
|
|
64
|
+
if (responseBody == null || typeof responseBody !== 'object')
|
|
65
|
+
return [];
|
|
66
|
+
const body = responseBody;
|
|
67
|
+
if (!body.sessionId)
|
|
68
|
+
return [];
|
|
69
|
+
const lines = [];
|
|
70
|
+
const sessionId = body.sessionId;
|
|
71
|
+
const tb = body.taskBinding;
|
|
72
|
+
if (tb && tb.bound === true) {
|
|
73
|
+
lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${tb.taskTitle}`);
|
|
74
|
+
}
|
|
75
|
+
else if (tb && tb.bound === false) {
|
|
76
|
+
lines.push(`[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof body.memorySection === 'string' && body.memorySection !== '') {
|
|
79
|
+
lines.push(JSON.stringify({
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: 'SessionStart',
|
|
82
|
+
additionalContext: body.memorySection,
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* POST the hook body to /api/hooks/<path> with a short timeout. All errors
|
|
90
|
+
* — credential missing, network failure, timeout, non-2xx — are routed to
|
|
91
|
+
* hook.log and then the caller exits 0.
|
|
92
|
+
*
|
|
93
|
+
* This function NEVER throws and NEVER rejects.
|
|
94
|
+
*/
|
|
95
|
+
async function runHook(path) {
|
|
96
|
+
const body = await readStdin();
|
|
97
|
+
return runHookWithBody(path, body);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Test-friendly worker. Takes the hook body as an argument so tests can
|
|
101
|
+
* exercise the side-effect logic without poking process.stdin. Production
|
|
102
|
+
* code always goes through `runHook`, which feeds it real stdin.
|
|
103
|
+
*/
|
|
104
|
+
async function runHookWithBody(path, body) {
|
|
105
|
+
try {
|
|
106
|
+
const creds = (0, config_1.readCredentials)();
|
|
107
|
+
if (!creds) {
|
|
108
|
+
(0, hook_log_1.logHookError)(`[${path}]`, 'no credentials; skipping');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Allow `LUMO_API_URL` to override the baked-in creds.apiUrl for
|
|
112
|
+
// redirecting hooks at dev-env switch without re-running `lumo auth
|
|
113
|
+
// login`. The bearer token from creds.json is still used as-is.
|
|
114
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
115
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
116
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/hooks/${path}`;
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
Authorization: `Bearer ${creds.token}`,
|
|
125
|
+
},
|
|
126
|
+
body: body || '{}',
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
(0, hook_log_1.logHookError)(`[${path}]`, `HTTP ${res.status} from ${url}`);
|
|
131
|
+
}
|
|
132
|
+
else if (path === 'session-start') {
|
|
133
|
+
// Per-hook side effects fire only after a 2xx response so a transient
|
|
134
|
+
// server failure doesn't desync local state from server state.
|
|
135
|
+
try {
|
|
136
|
+
const responseBody = await res.json();
|
|
137
|
+
for (const line of formatHookStdoutLines(path, responseBody)) {
|
|
138
|
+
process.stdout.write(line + '\n');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// response body parse failure is non-fatal
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
(0, hook_log_1.logHookError)(`[${path}]`, err);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
(0, hook_log_1.logHookError)(`[${path}] runner`, err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tiptapToMarkdown = exports.markdownToTiptap = void 0;
|
|
4
|
+
// Single source of truth lives in shared/src/markdown-tiptap.ts
|
|
5
|
+
var markdown_tiptap_1 = require("../../../shared/src/markdown-tiptap");
|
|
6
|
+
Object.defineProperty(exports, "markdownToTiptap", { enumerable: true, get: function () { return markdown_tiptap_1.markdownToTiptap; } });
|
|
7
|
+
Object.defineProperty(exports, "tiptapToMarkdown", { enumerable: true, get: function () { return markdown_tiptap_1.tiptapToMarkdown; } });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptSecret = promptSecret;
|
|
4
|
+
/**
|
|
5
|
+
* Prompt the user for a secret with echo disabled.
|
|
6
|
+
*
|
|
7
|
+
* Returns the trimmed input. Exits the process with code 130 on Ctrl+C.
|
|
8
|
+
* Falls back to a regular line-read when stdin is not a TTY (piped input).
|
|
9
|
+
*/
|
|
10
|
+
function promptSecret(message) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
const stdin = process.stdin;
|
|
13
|
+
const stdout = process.stdout;
|
|
14
|
+
if (!stdin.isTTY) {
|
|
15
|
+
let buf = '';
|
|
16
|
+
stdin.setEncoding('utf8');
|
|
17
|
+
const onData = (chunk) => {
|
|
18
|
+
buf += chunk;
|
|
19
|
+
const nl = buf.indexOf('\n');
|
|
20
|
+
if (nl !== -1) {
|
|
21
|
+
stdin.pause();
|
|
22
|
+
stdin.removeListener('data', onData);
|
|
23
|
+
resolve(buf.slice(0, nl).trim());
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
stdin.on('data', onData);
|
|
27
|
+
stdin.on('end', () => resolve(buf.trim()));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
stdout.write(message);
|
|
31
|
+
const wasRaw = stdin.isRaw;
|
|
32
|
+
stdin.setRawMode(true);
|
|
33
|
+
stdin.resume();
|
|
34
|
+
stdin.setEncoding('utf8');
|
|
35
|
+
let buf = '';
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
stdin.setRawMode(wasRaw);
|
|
38
|
+
stdin.removeListener('data', onData);
|
|
39
|
+
stdin.pause();
|
|
40
|
+
stdout.write('\n');
|
|
41
|
+
};
|
|
42
|
+
const onData = (chunk) => {
|
|
43
|
+
for (const ch of chunk) {
|
|
44
|
+
if (ch === '\u0003') {
|
|
45
|
+
// Ctrl+C — treat as cancel, exit cleanly
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve('');
|
|
48
|
+
process.exit(130);
|
|
49
|
+
}
|
|
50
|
+
if (ch === '\u0004') {
|
|
51
|
+
cleanup();
|
|
52
|
+
resolve(buf.trim());
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '\r' || ch === '\n') {
|
|
56
|
+
cleanup();
|
|
57
|
+
resolve(buf.trim());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (ch === '\u0008' || ch === '\u007f') {
|
|
61
|
+
buf = buf.slice(0, -1);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch.charCodeAt(0) < 0x20)
|
|
65
|
+
continue;
|
|
66
|
+
buf += ch;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
stdin.on('data', onData);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lookupDocId = lookupDocId;
|
|
4
|
+
const api_1 = require("./api");
|
|
5
|
+
const resolve_doc_1 = require("./resolve-doc");
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a user-typed doc reference (cuid OR case-insensitive title) to a
|
|
8
|
+
* document id. Fetches GET /api/documents to perform a title lookup when
|
|
9
|
+
* needed. Returns null on not-found or ambiguity (and prints a helpful
|
|
10
|
+
* error in the ambiguous case so the caller can just exit).
|
|
11
|
+
*/
|
|
12
|
+
async function lookupDocId(apiUrl, token, reference) {
|
|
13
|
+
if ((0, resolve_doc_1.isLikelyCuid)(reference))
|
|
14
|
+
return reference;
|
|
15
|
+
const listUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents`;
|
|
16
|
+
const res = await fetch(listUrl, {
|
|
17
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return null;
|
|
21
|
+
const { documents } = (await res.json());
|
|
22
|
+
const result = (0, resolve_doc_1.resolveDoc)(reference, documents);
|
|
23
|
+
if (result.kind === 'found')
|
|
24
|
+
return result.doc.id;
|
|
25
|
+
if (result.kind === 'ambiguous') {
|
|
26
|
+
console.error(`Error: title "${reference}" matches ${result.candidates.length} docs:`);
|
|
27
|
+
for (const c of result.candidates) {
|
|
28
|
+
console.error(` ${c.id} ${c.title}`);
|
|
29
|
+
}
|
|
30
|
+
console.error('Re-run with the cuid.');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isLikelyCuid = isLikelyCuid;
|
|
4
|
+
exports.resolveDoc = resolveDoc;
|
|
5
|
+
/** A loose check — a real cuid is 'c' + 24 base36 chars. */
|
|
6
|
+
function isLikelyCuid(s) {
|
|
7
|
+
return /^c[a-z0-9]{20,}$/i.test(s);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a user-typed doc reference against a list. If the input looks
|
|
11
|
+
* like a cuid, do exact-id match; otherwise do case-insensitive title
|
|
12
|
+
* match. Multiple title hits return 'ambiguous'.
|
|
13
|
+
*/
|
|
14
|
+
function resolveDoc(input, docs) {
|
|
15
|
+
const trimmed = input.trim();
|
|
16
|
+
if (isLikelyCuid(trimmed)) {
|
|
17
|
+
const doc = docs.find(d => d.id === trimmed);
|
|
18
|
+
return doc ? { kind: 'found', doc } : { kind: 'not-found' };
|
|
19
|
+
}
|
|
20
|
+
const lower = trimmed.toLowerCase();
|
|
21
|
+
const hits = docs.filter(d => d.title.toLowerCase() === lower);
|
|
22
|
+
if (hits.length === 0)
|
|
23
|
+
return { kind: 'not-found' };
|
|
24
|
+
if (hits.length === 1)
|
|
25
|
+
return { kind: 'found', doc: hits[0] };
|
|
26
|
+
return { kind: 'ambiguous', candidates: hits };
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchMembers = fetchMembers;
|
|
4
|
+
exports.resolveMember = resolveMember;
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the workspace member directory once. Used by doc-share commands to
|
|
8
|
+
* resolve a free-form member ref → memberId locally, because the document
|
|
9
|
+
* shares endpoint expects WorkspaceMember.id (cuid) and does not do
|
|
10
|
+
* server-side ref resolution the way /api/tasks does for assigneeRef.
|
|
11
|
+
*/
|
|
12
|
+
async function fetchMembers(apiUrl, token) {
|
|
13
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/workspaces/members`;
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
throw new Error(`failed to load members (${res.status} ${res.statusText}): ${text}`);
|
|
20
|
+
}
|
|
21
|
+
const body = (await res.json());
|
|
22
|
+
return body.members.map(m => ({
|
|
23
|
+
memberId: m.id,
|
|
24
|
+
displayName: m.name,
|
|
25
|
+
email: m.email,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve `me`, an email, or a display name against the directory.
|
|
30
|
+
* Match is case-insensitive. `me` resolves via the caller's stored email.
|
|
31
|
+
* Email matches win over name matches so `alice@x.com` is unambiguous even
|
|
32
|
+
* if a display name happens to collide.
|
|
33
|
+
*/
|
|
34
|
+
function resolveMember(members, ref, myEmail) {
|
|
35
|
+
const trimmed = ref.trim();
|
|
36
|
+
if (trimmed.length === 0)
|
|
37
|
+
return { kind: 'not_found', query: ref };
|
|
38
|
+
if (trimmed.toLowerCase() === 'me') {
|
|
39
|
+
const me = members.find(m => (m.email ?? '').toLowerCase() === myEmail.toLowerCase());
|
|
40
|
+
if (!me)
|
|
41
|
+
return { kind: 'not_found', query: 'me' };
|
|
42
|
+
return { kind: 'ok', member: me };
|
|
43
|
+
}
|
|
44
|
+
const lower = trimmed.toLowerCase();
|
|
45
|
+
const byEmail = members.filter(m => (m.email ?? '').toLowerCase() === lower);
|
|
46
|
+
if (byEmail.length === 1)
|
|
47
|
+
return { kind: 'ok', member: byEmail[0] };
|
|
48
|
+
if (byEmail.length > 1) {
|
|
49
|
+
return { kind: 'ambiguous', query: ref, candidates: byEmail };
|
|
50
|
+
}
|
|
51
|
+
const byName = members.filter(m => m.displayName.toLowerCase() === lower);
|
|
52
|
+
if (byName.length === 1)
|
|
53
|
+
return { kind: 'ok', member: byName[0] };
|
|
54
|
+
if (byName.length > 1) {
|
|
55
|
+
return { kind: 'ambiguous', query: ref, candidates: byName };
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'not_found', query: ref };
|
|
58
|
+
}
|