@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
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Lumo CLI
|
|
2
|
+
|
|
3
|
+
Manage [Lumo](https://www.uselumo.ai) tasks, sessions, sprints, and documents from the terminal. Designed to pair with Claude Code so AI agents can bind to a task, load context, and update state without leaving the shell.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @lumoai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Verify:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
lumo --version
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
lumo auth login
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The CLI opens your browser to create an API key, then reads it from stdin. Keys are stored in `~/.lumo/credentials.json`.
|
|
24
|
+
|
|
25
|
+
Check who you're logged in as:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
lumo whoami
|
|
29
|
+
# Logged in as you@example.com
|
|
30
|
+
# Workspace: Acme (acme)
|
|
31
|
+
# Key: My laptop (lum_abc1...)
|
|
32
|
+
# API: https://www.uselumo.ai
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# What am I working on?
|
|
39
|
+
lumo task list
|
|
40
|
+
|
|
41
|
+
# Create a task
|
|
42
|
+
lumo task create "Fix Slack OAuth redirect" --priority high
|
|
43
|
+
|
|
44
|
+
# Mark it in progress
|
|
45
|
+
lumo task update LUM-42 --status in_progress
|
|
46
|
+
|
|
47
|
+
# Bind the current Claude Code session to the task
|
|
48
|
+
lumo session attach LUM-42
|
|
49
|
+
|
|
50
|
+
# Load full background (description, prior session summaries, memory)
|
|
51
|
+
lumo task context LUM-42
|
|
52
|
+
|
|
53
|
+
# When you're done
|
|
54
|
+
lumo task update LUM-42 --status done
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Group | Highlights |
|
|
60
|
+
| --------------- | ------------------------------------------------------------- |
|
|
61
|
+
| `auth` | `login`, `logout` |
|
|
62
|
+
| `whoami` | Show current identity + workspace |
|
|
63
|
+
| `task` | `create`, `update`, `list`, `show`, `comment`, `context` |
|
|
64
|
+
| `session` | `attach`, `status`, `detach` (binds Claude Code sessions) |
|
|
65
|
+
| `project` | `list` |
|
|
66
|
+
| `milestone` | `create`, `update`, `list`, `show`, `delete` |
|
|
67
|
+
| `sprint` | `create`, `start`, `close`, `list`, `show`, `add`, `remove`, `summary` |
|
|
68
|
+
| `doc` | `create`, `update`, `list`, `show`, `move`, `bind`, `share` |
|
|
69
|
+
|
|
70
|
+
Every command accepts `--help` for full flags and examples:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
lumo task create --help
|
|
74
|
+
lumo sprint close --help
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Environment Variables
|
|
78
|
+
|
|
79
|
+
| Variable | Default | Purpose |
|
|
80
|
+
| ------------------------- | ------------------------ | ------------------------------------------------------------- |
|
|
81
|
+
| `LUMO_API_URL` | `https://www.uselumo.ai` | Override the API endpoint (e.g. self-hosted Lumo instance). |
|
|
82
|
+
| `CLAUDE_CODE_SESSION_ID` | _(set by Claude Code)_ | Required for `session attach` / `status` / `detach`. |
|
|
83
|
+
|
|
84
|
+
## Claude Code Integration
|
|
85
|
+
|
|
86
|
+
Inside a Claude Code session, the agent skill auto-discovers the CLI and uses it to load task context, bind sessions, and update task state. See the [Lumo Claude Code skill](https://github.com/Lumo-Workspace/lumo/tree/main/.claude/skills/lumo) for the full agent contract.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authLogin = authLogin;
|
|
4
|
+
const prompt_1 = require("../lib/prompt");
|
|
5
|
+
const browser_1 = require("../lib/browser");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const config_1 = require("../lib/config");
|
|
8
|
+
const KEY_PREFIX = 'lum_';
|
|
9
|
+
async function authLogin() {
|
|
10
|
+
const apiUrl = (0, api_1.resolveApiUrl)();
|
|
11
|
+
const webUrl = (0, api_1.trimTrailingSlash)(apiUrl);
|
|
12
|
+
console.log('Log in to Lumo');
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(` 1. Open ${webUrl} and go to Settings → API Keys`);
|
|
15
|
+
console.log(` 2. Create a new key and copy it`);
|
|
16
|
+
console.log(` 3. Paste it below`);
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('Opening your browser…');
|
|
19
|
+
(0, browser_1.openBrowser)(webUrl);
|
|
20
|
+
console.log('');
|
|
21
|
+
const token = await (0, prompt_1.promptSecret)('API key (input hidden): ');
|
|
22
|
+
if (!token) {
|
|
23
|
+
console.log('Login cancelled.');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
if (!token.startsWith(KEY_PREFIX)) {
|
|
27
|
+
console.error(`Error: API key must start with "${KEY_PREFIX}"`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
let resp;
|
|
31
|
+
try {
|
|
32
|
+
resp = await (0, api_1.verifyToken)(apiUrl, token);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
console.error(`Error: ${msg}`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
(0, config_1.writeCredentials)({
|
|
40
|
+
token,
|
|
41
|
+
apiUrl,
|
|
42
|
+
userId: resp.user.id,
|
|
43
|
+
email: resp.user.email,
|
|
44
|
+
workspaceId: resp.workspace.id,
|
|
45
|
+
workspaceSlug: resp.workspace.slug,
|
|
46
|
+
workspaceName: resp.workspace.name,
|
|
47
|
+
apiKeyId: resp.apiKey.id,
|
|
48
|
+
apiKeyName: resp.apiKey.name,
|
|
49
|
+
apiKeyPrefix: resp.apiKey.prefix,
|
|
50
|
+
});
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(`✓ Logged in as ${resp.user.email}`);
|
|
53
|
+
console.log(` Workspace: ${resp.workspace.name}`);
|
|
54
|
+
console.log(` Key: ${resp.apiKey.name} (${resp.apiKey.prefix})`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authLogout = authLogout;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
async function authLogout() {
|
|
6
|
+
const creds = (0, config_1.readCredentials)();
|
|
7
|
+
if (!creds) {
|
|
8
|
+
console.log('Not logged in');
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const email = creds.email;
|
|
12
|
+
(0, config_1.deleteCredentials)();
|
|
13
|
+
console.log(`✓ Logged out (${email})`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatBindOutput = formatBindOutput;
|
|
4
|
+
exports.docBind = docBind;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
8
|
+
function formatBindOutput(args) {
|
|
9
|
+
if (args.alreadyBound)
|
|
10
|
+
return `Already bound ${args.docId} ↔ ${args.identifier}`;
|
|
11
|
+
const upgrade = args.upgradedFromContent ? ' (upgraded from content)' : '';
|
|
12
|
+
return `Bound ${args.docId} ↔ ${args.identifier}${upgrade}`;
|
|
13
|
+
}
|
|
14
|
+
async function docBind(docRef, task) {
|
|
15
|
+
if (!docRef || !task) {
|
|
16
|
+
console.error('Error: usage: lumo doc bind <doc> <task>');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const creds = (0, config_1.readCredentials)();
|
|
20
|
+
if (!creds) {
|
|
21
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
25
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
26
|
+
const docId = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, docRef);
|
|
27
|
+
if (!docId) {
|
|
28
|
+
console.error(`Error: Document not found: ${docRef}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${docId}/mentions`;
|
|
32
|
+
const res = await fetch(url, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: `Bearer ${creds.token}`,
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ taskIdentifier: task }),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const text = await res.text();
|
|
42
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
const { mention } = (await res.json());
|
|
46
|
+
console.log(formatBindOutput({
|
|
47
|
+
docId,
|
|
48
|
+
identifier: mention.taskIdentifier,
|
|
49
|
+
alreadyBound: mention.alreadyBound,
|
|
50
|
+
upgradedFromContent: mention.upgradedFromContent,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeScope = normalizeScope;
|
|
4
|
+
exports.formatCreatedDocLine = formatCreatedDocLine;
|
|
5
|
+
exports.docCreate = docCreate;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const doc_input_1 = require("../lib/doc-input");
|
|
9
|
+
const tag_resolver_1 = require("../lib/tag-resolver");
|
|
10
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
11
|
+
/** personal → PRIVATE, workspace → WORKSPACE. Null on unknown. */
|
|
12
|
+
function normalizeScope(value) {
|
|
13
|
+
const lower = (value ?? '').toLowerCase();
|
|
14
|
+
if (lower === 'personal')
|
|
15
|
+
return 'PRIVATE';
|
|
16
|
+
if (lower === 'workspace')
|
|
17
|
+
return 'WORKSPACE';
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function formatCreatedDocLine(doc) {
|
|
21
|
+
const escaped = doc.title.replace(/"/g, '\\"');
|
|
22
|
+
const head = `Created ${doc.id} "${escaped}" ${doc.url}`;
|
|
23
|
+
if (doc.tags && doc.tags.length > 0) {
|
|
24
|
+
return `${head}\nTags: ${doc.tags.join(', ')}`;
|
|
25
|
+
}
|
|
26
|
+
return head;
|
|
27
|
+
}
|
|
28
|
+
function docUrl(apiUrl, workspaceSlug, id) {
|
|
29
|
+
return `${(0, api_1.trimTrailingSlash)(apiUrl)}/workspace/${workspaceSlug}/documents/${id}`;
|
|
30
|
+
}
|
|
31
|
+
async function docCreate(title, opts) {
|
|
32
|
+
const creds = (0, config_1.readCredentials)();
|
|
33
|
+
if (!creds) {
|
|
34
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
let visibility = 'PRIVATE';
|
|
38
|
+
if (opts.scope !== undefined) {
|
|
39
|
+
const normalized = normalizeScope(opts.scope);
|
|
40
|
+
if (!normalized) {
|
|
41
|
+
console.error(`Error: invalid scope "${opts.scope}". Allowed: personal, workspace`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
visibility = normalized;
|
|
45
|
+
}
|
|
46
|
+
const content = await (0, doc_input_1.resolveDocContent)({
|
|
47
|
+
content: opts.content,
|
|
48
|
+
file: opts.file,
|
|
49
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
50
|
+
readStdin: doc_input_1.readStdinToString,
|
|
51
|
+
readFile: doc_input_1.readFileUtf8,
|
|
52
|
+
});
|
|
53
|
+
if (content.kind === 'error') {
|
|
54
|
+
console.error(`Error: ${content.message}`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
58
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
59
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents`;
|
|
60
|
+
let tagIds;
|
|
61
|
+
if ((opts.tag && opts.tag.length > 0) ||
|
|
62
|
+
(opts.tagId && opts.tagId.length > 0)) {
|
|
63
|
+
try {
|
|
64
|
+
tagIds = await (0, tag_resolver_1.resolveTagRefs)({ names: opts.tag ?? [], ids: opts.tagId ?? [] }, { apiUrl, token: creds.token });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`Error: ${err.message}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let resolvedParentId;
|
|
72
|
+
if (opts.parent !== undefined && opts.parent.length > 0) {
|
|
73
|
+
const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, opts.parent);
|
|
74
|
+
if (!id) {
|
|
75
|
+
console.error(`Error: --parent doc not found: ${opts.parent}`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
resolvedParentId = id;
|
|
79
|
+
}
|
|
80
|
+
const body = {
|
|
81
|
+
title: title?.trim() ?? '',
|
|
82
|
+
visibility,
|
|
83
|
+
};
|
|
84
|
+
if (content.kind === 'ok')
|
|
85
|
+
body.contentMarkdown = content.markdown;
|
|
86
|
+
if (opts.project !== undefined)
|
|
87
|
+
body.projectRef = opts.project;
|
|
88
|
+
if (tagIds && tagIds.length > 0)
|
|
89
|
+
body.tagIds = tagIds;
|
|
90
|
+
if (resolvedParentId !== undefined)
|
|
91
|
+
body.parentId = resolvedParentId;
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await fetch(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
Authorization: `Bearer ${creds.token}`,
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(body),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(`Error: network failure: ${err.message}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const text = await res.text();
|
|
109
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
const { document } = (await res.json());
|
|
113
|
+
const fullUrl = docUrl(apiUrl, creds.workspaceSlug ?? 'lumo', document.slug);
|
|
114
|
+
const responseTagNames = (document.docTags ?? []).map(t => t.name);
|
|
115
|
+
console.log(formatCreatedDocLine({
|
|
116
|
+
id: document.id,
|
|
117
|
+
title: document.title,
|
|
118
|
+
url: fullUrl,
|
|
119
|
+
tags: responseTagNames,
|
|
120
|
+
}));
|
|
121
|
+
if (opts.task) {
|
|
122
|
+
const bindUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${document.id}/mentions`;
|
|
123
|
+
const bindRes = await fetch(bindUrl, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${creds.token}`,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ taskIdentifier: opts.task }),
|
|
130
|
+
});
|
|
131
|
+
if (!bindRes.ok) {
|
|
132
|
+
const text = await bindRes.text();
|
|
133
|
+
console.error(`Warning: doc created but bind to ${opts.task} failed: ${bindRes.status} ${text}`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
console.log(`Bound ${document.id} ↔ ${opts.task}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.docDelete = docDelete;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const resolve_doc_1 = require("../lib/resolve-doc");
|
|
7
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
8
|
+
async function docDelete(reference, opts) {
|
|
9
|
+
if (!reference) {
|
|
10
|
+
console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --yes');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
if (!opts.yes) {
|
|
14
|
+
console.error('Error: Refusing to delete without --yes');
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
const creds = (0, config_1.readCredentials)();
|
|
18
|
+
if (!creds) {
|
|
19
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
23
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
24
|
+
// For a cuid reference we don't have the title; for a title we look up id+title at once.
|
|
25
|
+
let id;
|
|
26
|
+
let title = '';
|
|
27
|
+
if ((0, resolve_doc_1.isLikelyCuid)(reference)) {
|
|
28
|
+
id = reference;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const fetched = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
|
|
32
|
+
if (!fetched) {
|
|
33
|
+
console.error(`Error: Document not found: ${reference}`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
id = fetched;
|
|
37
|
+
// Pick up the title from the lookup payload by repeating the GET — small extra cost.
|
|
38
|
+
// (Alternative: change lookupDocId to return the full DocLike. For now keep it simple.)
|
|
39
|
+
title = reference;
|
|
40
|
+
}
|
|
41
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
|
|
42
|
+
method: 'DELETE',
|
|
43
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const escaped = title.replace(/"/g, '\\"');
|
|
51
|
+
console.log(`Deleted ${id}${title ? ` "${escaped}"` : ''}`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatDocListRows = formatDocListRows;
|
|
4
|
+
exports.formatDocListRowsAsTree = formatDocListRowsAsTree;
|
|
5
|
+
exports.docList = docList;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const doc_create_1 = require("./doc-create");
|
|
9
|
+
const doc_tree_1 = require("../lib/doc-tree");
|
|
10
|
+
function visibilityLabel(v) {
|
|
11
|
+
if (v === 'PRIVATE')
|
|
12
|
+
return 'PERSONAL';
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
function formatDocListRows(rows) {
|
|
16
|
+
if (rows.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
return rows.map(r => {
|
|
19
|
+
const label = visibilityLabel(r.visibility).padEnd(10, ' ');
|
|
20
|
+
const project = (r.project?.name ?? '-').padEnd(14, ' ');
|
|
21
|
+
return `${r.id} ${label} ${project} ${r.title}`;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function formatDocListRowsAsTree(rows) {
|
|
25
|
+
if (rows.length === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const tree = (0, doc_tree_1.buildTree)(rows);
|
|
28
|
+
const flat = (0, doc_tree_1.flattenWithDepth)(tree);
|
|
29
|
+
return flat.map(({ row, depth }) => {
|
|
30
|
+
const label = visibilityLabel(row.visibility).padEnd(10, ' ');
|
|
31
|
+
const project = (row.project?.name ?? '-').padEnd(14, ' ');
|
|
32
|
+
const indent = ' '.repeat(depth);
|
|
33
|
+
return `${row.id} ${label} ${project} ${indent}${row.title}`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function docList(opts) {
|
|
37
|
+
const creds = (0, config_1.readCredentials)();
|
|
38
|
+
if (!creds) {
|
|
39
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
43
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
44
|
+
let url;
|
|
45
|
+
if (opts.task) {
|
|
46
|
+
url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/${opts.task}/documents`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const params = new URLSearchParams();
|
|
50
|
+
if (opts.scope && opts.scope !== 'all') {
|
|
51
|
+
const v = (0, doc_create_1.normalizeScope)(opts.scope);
|
|
52
|
+
if (!v) {
|
|
53
|
+
console.error(`Error: invalid scope "${opts.scope}". Allowed: personal, workspace, all`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
params.set('visibility', v);
|
|
57
|
+
}
|
|
58
|
+
if (opts.project)
|
|
59
|
+
params.set('projectRef', opts.project);
|
|
60
|
+
const qs = params.toString();
|
|
61
|
+
url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents${qs ? `?${qs}` : ''}`;
|
|
62
|
+
}
|
|
63
|
+
const res = await fetch(url, {
|
|
64
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
console.error(`Error: ${res.status} ${res.statusText}: ${text}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const { documents } = (await res.json());
|
|
72
|
+
let rows = documents;
|
|
73
|
+
if (opts.task && opts.scope && opts.scope !== 'all') {
|
|
74
|
+
const v = (0, doc_create_1.normalizeScope)(opts.scope);
|
|
75
|
+
if (v)
|
|
76
|
+
rows = rows.filter(r => r.visibility === v);
|
|
77
|
+
}
|
|
78
|
+
if (opts.task && opts.project) {
|
|
79
|
+
rows = rows.filter(r => (r.project?.name ?? '').toLowerCase() === opts.project.toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
if (opts.limit) {
|
|
82
|
+
const n = parseInt(opts.limit, 10);
|
|
83
|
+
if (!Number.isNaN(n))
|
|
84
|
+
rows = rows.slice(0, n);
|
|
85
|
+
}
|
|
86
|
+
const lines = opts.tree
|
|
87
|
+
? formatDocListRowsAsTree(rows)
|
|
88
|
+
: formatDocListRows(rows);
|
|
89
|
+
for (const line of lines)
|
|
90
|
+
console.log(line);
|
|
91
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.docMove = docMove;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const resolve_doc_1 = require("../lib/resolve-doc");
|
|
7
|
+
const doc_sort_order_1 = require("../lib/doc-sort-order");
|
|
8
|
+
async function docMove(reference, opts) {
|
|
9
|
+
if (!reference) {
|
|
10
|
+
console.error('Error: usage: lumo doc move <doc> --parent <doc> | --root');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const hasParent = typeof opts.parent === 'string' && opts.parent.length > 0;
|
|
14
|
+
const hasRoot = Boolean(opts.root);
|
|
15
|
+
if (hasParent && hasRoot) {
|
|
16
|
+
console.error('Error: --parent and --root are mutually exclusive');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
if (!hasParent && !hasRoot) {
|
|
20
|
+
console.error('Error: specify --parent <doc> or --root');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const creds = (0, config_1.readCredentials)();
|
|
24
|
+
if (!creds) {
|
|
25
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
29
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
30
|
+
const listUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents`;
|
|
31
|
+
let listRes;
|
|
32
|
+
try {
|
|
33
|
+
listRes = await fetch(listUrl, {
|
|
34
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(`Error: network failure: ${err.message}`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
if (!listRes.ok) {
|
|
42
|
+
const text = await listRes.text();
|
|
43
|
+
console.error(`Error: ${listRes.status} ${listRes.statusText}: ${text}`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const { documents } = (await listRes.json());
|
|
47
|
+
const docMatch = (0, resolve_doc_1.resolveDoc)(reference, documents);
|
|
48
|
+
if (docMatch.kind === 'ambiguous') {
|
|
49
|
+
console.error(`Error: title "${reference}" matches ${docMatch.candidates.length} docs:`);
|
|
50
|
+
for (const c of docMatch.candidates) {
|
|
51
|
+
console.error(` ${c.id} ${c.title}`);
|
|
52
|
+
}
|
|
53
|
+
console.error('Re-run with the cuid.');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
if (docMatch.kind === 'not-found') {
|
|
57
|
+
console.error(`Error: Document not found: ${reference}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const docRow = documents.find(d => d.id === docMatch.doc.id);
|
|
61
|
+
let newParentId = null;
|
|
62
|
+
let parentTitleForOutput = 'root';
|
|
63
|
+
if (hasParent) {
|
|
64
|
+
const parentMatch = (0, resolve_doc_1.resolveDoc)(opts.parent, documents);
|
|
65
|
+
if (parentMatch.kind === 'ambiguous') {
|
|
66
|
+
console.error(`Error: --parent "${opts.parent}" matches ${parentMatch.candidates.length} docs:`);
|
|
67
|
+
for (const c of parentMatch.candidates) {
|
|
68
|
+
console.error(` ${c.id} ${c.title}`);
|
|
69
|
+
}
|
|
70
|
+
console.error('Re-run with the cuid.');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
if (parentMatch.kind === 'not-found') {
|
|
74
|
+
console.error(`Error: Document not found: ${opts.parent}`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const parentRow = documents.find(d => d.id === parentMatch.doc.id);
|
|
78
|
+
newParentId = parentRow.id;
|
|
79
|
+
parentTitleForOutput = `"${parentRow.title.replace(/"/g, '\\"')}"`;
|
|
80
|
+
}
|
|
81
|
+
const siblings = documents.filter(d => d.parentId === newParentId && d.id !== docRow.id);
|
|
82
|
+
const sortOrder = (0, doc_sort_order_1.pickNextSortOrder)(siblings);
|
|
83
|
+
const moveUrl = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${docRow.id}/move`;
|
|
84
|
+
let moveRes;
|
|
85
|
+
try {
|
|
86
|
+
moveRes = await fetch(moveUrl, {
|
|
87
|
+
method: 'PATCH',
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${creds.token}`,
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ parentId: newParentId, sortOrder }),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error(`Error: network failure: ${err.message}`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
if (!moveRes.ok) {
|
|
100
|
+
const text = await moveRes.text();
|
|
101
|
+
let msg = text;
|
|
102
|
+
try {
|
|
103
|
+
const json = JSON.parse(text);
|
|
104
|
+
if (json.error)
|
|
105
|
+
msg = json.error;
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
console.error(`Error: ${moveRes.status} ${moveRes.statusText}: ${msg}`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const escapedDocTitle = docRow.title.replace(/"/g, '\\"');
|
|
112
|
+
console.log(`Moved ${docRow.id} "${escapedDocTitle}" → ${parentTitleForOutput}`);
|
|
113
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatShareListRows = formatShareListRows;
|
|
4
|
+
exports.docShareList = docShareList;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_doc_id_1 = require("../lib/resolve-doc-id");
|
|
8
|
+
const resolve_member_1 = require("../lib/resolve-member");
|
|
9
|
+
function formatShareListRows(rows) {
|
|
10
|
+
if (rows.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
const nameWidth = Math.max(...rows.map(r => r.displayName.length));
|
|
13
|
+
return rows.map(r => {
|
|
14
|
+
const name = r.displayName.padEnd(nameWidth, ' ');
|
|
15
|
+
return `${name} ${r.role}`;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async function docShareList(docRef) {
|
|
19
|
+
if (!docRef) {
|
|
20
|
+
console.error('Error: usage: lumo doc share-list <doc>');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const creds = (0, config_1.readCredentials)();
|
|
24
|
+
if (!creds) {
|
|
25
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const envUrl = process.env.LUMO_API_URL?.trim();
|
|
29
|
+
const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
|
|
30
|
+
const docId = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, docRef);
|
|
31
|
+
if (!docId) {
|
|
32
|
+
console.error(`Error: Document not found: ${docRef}`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
// Shares endpoint returns memberId only; we cross-reference the workspace
|
|
36
|
+
// member directory to print a friendly display name. If the directory call
|
|
37
|
+
// fails (rare), we fall back to memberId so the user still sees something.
|
|
38
|
+
const [sharesRes, members] = await Promise.all([
|
|
39
|
+
fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${docId}/shares`, {
|
|
40
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
41
|
+
}),
|
|
42
|
+
(0, resolve_member_1.fetchMembers)(apiUrl, creds.token).catch(() => null),
|
|
43
|
+
]);
|
|
44
|
+
if (!sharesRes.ok) {
|
|
45
|
+
const text = await sharesRes.text();
|
|
46
|
+
console.error(`Error: ${sharesRes.status} ${sharesRes.statusText}: ${text}`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const { shares } = (await sharesRes.json());
|
|
50
|
+
const nameByMemberId = new Map();
|
|
51
|
+
if (members) {
|
|
52
|
+
for (const m of members)
|
|
53
|
+
nameByMemberId.set(m.memberId, m.displayName);
|
|
54
|
+
}
|
|
55
|
+
const rows = shares.map(s => ({
|
|
56
|
+
displayName: nameByMemberId.get(s.member.id) ?? s.member.id,
|
|
57
|
+
role: s.role,
|
|
58
|
+
}));
|
|
59
|
+
const lines = formatShareListRows(rows);
|
|
60
|
+
for (const line of lines)
|
|
61
|
+
console.log(line);
|
|
62
|
+
}
|