@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.
Files changed (61) hide show
  1. package/README.md +90 -0
  2. package/dist/cli/src/commands/auth-login.js +55 -0
  3. package/dist/cli/src/commands/auth-logout.js +14 -0
  4. package/dist/cli/src/commands/doc-bind.js +52 -0
  5. package/dist/cli/src/commands/doc-create.js +138 -0
  6. package/dist/cli/src/commands/doc-delete.js +52 -0
  7. package/dist/cli/src/commands/doc-list.js +91 -0
  8. package/dist/cli/src/commands/doc-move.js +113 -0
  9. package/dist/cli/src/commands/doc-share-list.js +62 -0
  10. package/dist/cli/src/commands/doc-share.js +77 -0
  11. package/dist/cli/src/commands/doc-show.js +99 -0
  12. package/dist/cli/src/commands/doc-unbind.js +47 -0
  13. package/dist/cli/src/commands/doc-unshare.js +71 -0
  14. package/dist/cli/src/commands/doc-update.js +144 -0
  15. package/dist/cli/src/commands/hook.js +21 -0
  16. package/dist/cli/src/commands/milestone-create.js +84 -0
  17. package/dist/cli/src/commands/milestone-delete.js +96 -0
  18. package/dist/cli/src/commands/milestone-list.js +55 -0
  19. package/dist/cli/src/commands/milestone-show.js +106 -0
  20. package/dist/cli/src/commands/milestone-update.js +167 -0
  21. package/dist/cli/src/commands/project-list.js +57 -0
  22. package/dist/cli/src/commands/session-attach.js +80 -0
  23. package/dist/cli/src/commands/session-detach.js +60 -0
  24. package/dist/cli/src/commands/session-status.js +58 -0
  25. package/dist/cli/src/commands/sprint-add.js +62 -0
  26. package/dist/cli/src/commands/sprint-close.js +151 -0
  27. package/dist/cli/src/commands/sprint-create.js +80 -0
  28. package/dist/cli/src/commands/sprint-delete.js +88 -0
  29. package/dist/cli/src/commands/sprint-list.js +85 -0
  30. package/dist/cli/src/commands/sprint-remove.js +57 -0
  31. package/dist/cli/src/commands/sprint-show.js +81 -0
  32. package/dist/cli/src/commands/sprint-start.js +68 -0
  33. package/dist/cli/src/commands/sprint-summary.js +138 -0
  34. package/dist/cli/src/commands/sprint-update.js +148 -0
  35. package/dist/cli/src/commands/task-comment.js +95 -0
  36. package/dist/cli/src/commands/task-context.js +137 -0
  37. package/dist/cli/src/commands/task-create.js +202 -0
  38. package/dist/cli/src/commands/task-list.js +94 -0
  39. package/dist/cli/src/commands/task-show.js +74 -0
  40. package/dist/cli/src/commands/task-update.js +468 -0
  41. package/dist/cli/src/commands/whoami.js +16 -0
  42. package/dist/cli/src/index.js +492 -0
  43. package/dist/cli/src/lib/api.js +33 -0
  44. package/dist/cli/src/lib/browser.js +33 -0
  45. package/dist/cli/src/lib/config.js +92 -0
  46. package/dist/cli/src/lib/doc-input.js +80 -0
  47. package/dist/cli/src/lib/doc-sort-order.js +23 -0
  48. package/dist/cli/src/lib/doc-tree.js +54 -0
  49. package/dist/cli/src/lib/format.js +33 -0
  50. package/dist/cli/src/lib/hook-log.js +81 -0
  51. package/dist/cli/src/lib/hook-runner.js +156 -0
  52. package/dist/cli/src/lib/markdown-tiptap.js +7 -0
  53. package/dist/cli/src/lib/prompt.js +71 -0
  54. package/dist/cli/src/lib/resolve-doc-id.js +34 -0
  55. package/dist/cli/src/lib/resolve-doc.js +27 -0
  56. package/dist/cli/src/lib/resolve-member.js +58 -0
  57. package/dist/cli/src/lib/resolve.js +170 -0
  58. package/dist/cli/src/lib/tag-resolver.js +49 -0
  59. package/dist/shared/src/index.js +35 -0
  60. package/dist/shared/src/markdown-tiptap.js +267 -0
  61. 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
+ }