felo-ai 0.2.30 → 0.2.31
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/felo-livedoc/README.md +13 -0
- package/felo-livedoc/SKILL.md +81 -2
- package/felo-livedoc/scripts/run_livedoc.mjs +193 -3
- package/package.json +1 -1
- package/src/cli.js +215 -0
- package/src/livedoc.js +287 -0
package/felo-livedoc/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Manage knowledge bases (LiveDocs) and their resources via the Felo API.
|
|
|
11
11
|
- Semantic retrieval across knowledge base resources
|
|
12
12
|
- Route relevant resources by query for targeted retrieval
|
|
13
13
|
- Full CRUD for resources within a LiveDoc
|
|
14
|
+
- Manage README content for each LiveDoc
|
|
15
|
+
- Task management: create, update, delete tasks with comments and change history
|
|
14
16
|
|
|
15
17
|
**When to use:**
|
|
16
18
|
- Building or managing a knowledge base
|
|
@@ -87,8 +89,19 @@ felo livedoc route SHORT_ID --query "latest AI research" --max-resources 5
|
|
|
87
89
|
| `add-urls <short_id>` | Add URL resources (max 10) |
|
|
88
90
|
| `upload <short_id>` | Upload a file resource |
|
|
89
91
|
| `remove-resource <short_id> <resource_id>` | Delete a resource |
|
|
92
|
+
| `update-resource <short_id> <resource_id>` | Update resource title, snippet, or thumbnail |
|
|
90
93
|
| `retrieve <short_id>` | Semantic retrieval (auto-routes if no `--resource-ids`) |
|
|
91
94
|
| `route <short_id>` | Route relevant resource IDs by query |
|
|
95
|
+
| `get-readme <short_id>` | Get README content |
|
|
96
|
+
| `update-readme <short_id>` | Create or replace README |
|
|
97
|
+
| `append-readme <short_id>` | Append content to README |
|
|
98
|
+
| `delete-readme <short_id>` | Delete README |
|
|
99
|
+
| `tasks <short_id>` | List tasks (filter by `--status`, `--labels`) |
|
|
100
|
+
| `create-task <short_id>` | Create a task |
|
|
101
|
+
| `update-task <short_id> <task_id>` | Partially update a task |
|
|
102
|
+
| `delete-task <short_id> <task_id>` | Delete a task |
|
|
103
|
+
| `task-records <short_id> <task_id>` | List task records (comments + change history) |
|
|
104
|
+
| `add-task-comment <short_id> <task_id>` | Add a comment to a task |
|
|
92
105
|
|
|
93
106
|
---
|
|
94
107
|
|
package/felo-livedoc/SKILL.md
CHANGED
|
@@ -14,10 +14,12 @@ Trigger this skill when users want to:
|
|
|
14
14
|
- **Semantic retrieval:** Search across knowledge base resources using natural language queries
|
|
15
15
|
- **Route resources:** Find relevant resource IDs by query for targeted retrieval
|
|
16
16
|
- **Resource management:** List, view, or delete resources within a LiveDoc
|
|
17
|
+
- **README management:** Get, create, update, append, or delete a LiveDoc's README
|
|
18
|
+
- **Task management:** Create, update, delete tasks; add comments; view change history
|
|
17
19
|
|
|
18
20
|
**Trigger words:**
|
|
19
|
-
- English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval, route resources
|
|
20
|
-
- 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索,
|
|
21
|
+
- English: knowledge base, livedoc, live doc, upload document, add URL, semantic search, retrieve, knowledge retrieval, route resources, readme, task, task management, comment
|
|
22
|
+
- 简体中文: 知识库, 文档库, 上传文档, 添加链接, 语义检索, 知识检索, 任务, 任务管理, 评论
|
|
21
23
|
|
|
22
24
|
**Explicit commands:** `/felo-livedoc`, "livedoc", "felo livedoc"
|
|
23
25
|
|
|
@@ -111,6 +113,12 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs resource SHORT_ID RES
|
|
|
111
113
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs remove-resource SHORT_ID RESOURCE_ID
|
|
112
114
|
```
|
|
113
115
|
|
|
116
|
+
**Update a resource (title/snippet/thumbnail):**
|
|
117
|
+
```bash
|
|
118
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-resource SHORT_ID RESOURCE_ID --title "New Title"
|
|
119
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-resource SHORT_ID RESOURCE_ID --snippet "New summary" --thumbnail "https://example.com/thumb.png"
|
|
120
|
+
```
|
|
121
|
+
|
|
114
122
|
### Semantic Retrieval
|
|
115
123
|
|
|
116
124
|
**Route relevant resources by query:**
|
|
@@ -147,6 +155,67 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs content SHORT_ID RESO
|
|
|
147
155
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information"
|
|
148
156
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information" --max-chunk 5
|
|
149
157
|
```
|
|
158
|
+
|
|
159
|
+
### README Management
|
|
160
|
+
|
|
161
|
+
**Get README:**
|
|
162
|
+
```bash
|
|
163
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs get-readme SHORT_ID
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Create or replace README:**
|
|
167
|
+
```bash
|
|
168
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-readme SHORT_ID --content "# My KB\n\nThis is the README."
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Append to README:**
|
|
172
|
+
```bash
|
|
173
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs append-readme SHORT_ID --content "\n\n## New Section\n\nAdditional content."
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Delete README:**
|
|
177
|
+
```bash
|
|
178
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs delete-readme SHORT_ID
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Task Management
|
|
182
|
+
|
|
183
|
+
**List tasks (with optional filters):**
|
|
184
|
+
```bash
|
|
185
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID
|
|
186
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID --status 0
|
|
187
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs tasks SHORT_ID --labels "docs,priority-high"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Create a task:**
|
|
191
|
+
```bash
|
|
192
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs create-task SHORT_ID --title "Write docs" --status 0 --sort 0
|
|
193
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs create-task SHORT_ID --title "Write docs" --status 0 --sort 0 --description "API docs" --labels "docs"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Task status values: `0`=TODO, `1`=IN_PROGRESS, `2`=DONE
|
|
197
|
+
|
|
198
|
+
**Update a task (partial update):**
|
|
199
|
+
```bash
|
|
200
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-task SHORT_ID TASK_ID --status 1
|
|
201
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs update-task SHORT_ID TASK_ID --title "New title" --labels "docs,done"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Delete a task:**
|
|
205
|
+
```bash
|
|
206
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs delete-task SHORT_ID TASK_ID
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**List task records (comments + change history):**
|
|
210
|
+
```bash
|
|
211
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs task-records SHORT_ID TASK_ID
|
|
212
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs task-records SHORT_ID TASK_ID --record-type comment
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Add a comment to a task:**
|
|
216
|
+
```bash
|
|
217
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-task-comment SHORT_ID TASK_ID --content "This is a comment."
|
|
218
|
+
```
|
|
150
219
|
### Options
|
|
151
220
|
|
|
152
221
|
All commands support:
|
|
@@ -194,6 +263,16 @@ The API returns JSON with this structure:
|
|
|
194
263
|
- `LIVEDOC_RESOURCE_UPLOAD_FAILED` — File upload failed
|
|
195
264
|
- `LIVEDOC_RESOURCE_ADD_URLS_FAILED` — URL addition failed
|
|
196
265
|
- `LIVEDOC_RESOURCE_RETRIEVE_FAILED` — Semantic retrieval failed
|
|
266
|
+
- `LIVEDOC_README_GET_FAILED` — Failed to get README
|
|
267
|
+
- `LIVEDOC_README_UPDATE_FAILED` — Failed to create or update README
|
|
268
|
+
- `LIVEDOC_README_DELETE_FAILED` — Failed to delete README
|
|
269
|
+
- `LIVEDOC_TASK_LIST_FAILED` — Failed to list tasks
|
|
270
|
+
- `LIVEDOC_TASK_CREATE_FAILED` — Failed to create task
|
|
271
|
+
- `LIVEDOC_TASK_UPDATE_FAILED` — Failed to update task
|
|
272
|
+
- `LIVEDOC_TASK_DELETE_FAILED` — Failed to delete task
|
|
273
|
+
- `LIVEDOC_TASK_NOT_FOUND` — Task does not exist
|
|
274
|
+
- `LIVEDOC_TASK_RECORD_LIST_FAILED` — Failed to list task records
|
|
275
|
+
- `LIVEDOC_TASK_COMMENT_CREATE_FAILED` — Failed to add comment
|
|
197
276
|
|
|
198
277
|
### Missing API Key
|
|
199
278
|
|
|
@@ -127,6 +127,28 @@ function formatRetrieveResult(r) {
|
|
|
127
127
|
return out;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function formatTask(t) {
|
|
131
|
+
if (!t) return '';
|
|
132
|
+
let out = `### ${t.title || '(untitled)'}\n`;
|
|
133
|
+
out += `- Task ID: \`${t.id}\`\n`;
|
|
134
|
+
out += `- Status: ${t.status === 0 ? 'TODO' : t.status === 1 ? 'IN_PROGRESS' : t.status === 2 ? 'DONE' : t.status}\n`;
|
|
135
|
+
if (t.sort != null) out += `- Sort: ${t.sort}\n`;
|
|
136
|
+
if (t.description) out += `- Description: ${t.description}\n`;
|
|
137
|
+
if (t.labels?.length) out += `- Labels: ${t.labels.join(', ')}\n`;
|
|
138
|
+
if (t.created_at) out += `- Created: ${t.created_at}\n`;
|
|
139
|
+
out += '\n';
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatTaskRecord(r) {
|
|
144
|
+
if (!r) return '';
|
|
145
|
+
let out = `- [${r.record_type}] `;
|
|
146
|
+
if (r.content) out += r.content;
|
|
147
|
+
else if (r.meta) out += JSON.stringify(r.meta);
|
|
148
|
+
out += ` (id: ${r.id}, ${r.created_at || ''})\n`;
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
// ── CLI ──
|
|
131
153
|
|
|
132
154
|
function usage() {
|
|
@@ -144,11 +166,22 @@ function usage() {
|
|
|
144
166
|
' add-urls <short_id> Add URLs (--urls required, comma-separated, max 10)',
|
|
145
167
|
' upload <short_id> Upload file (--file required, --convert optional)',
|
|
146
168
|
' remove-resource <short_id> <resource_id> Delete a resource',
|
|
169
|
+
' update-resource <short_id> <resource_id> Update resource title/snippet/thumbnail',
|
|
147
170
|
' retrieve <short_id> Semantic search (--query required, --resource-ids optional)',
|
|
148
171
|
' route <short_id> Route relevant resources by query (--query required)',
|
|
149
172
|
' download <short_id> <resource_id> Download source file to disk',
|
|
150
173
|
' content <short_id> <resource_id> Get text content of a resource',
|
|
151
174
|
' ppt-retrieve <short_id> PPT page deep retrieval (--resource-id, --page-number, --query required)',
|
|
175
|
+
' get-readme <short_id> Get README content',
|
|
176
|
+
' update-readme <short_id> Create or replace README (--content required)',
|
|
177
|
+
' append-readme <short_id> Append to README (--content required)',
|
|
178
|
+
' delete-readme <short_id> Delete README',
|
|
179
|
+
' tasks <short_id> List tasks (--status, --labels optional)',
|
|
180
|
+
' create-task <short_id> Create a task (--title, --status, --sort required)',
|
|
181
|
+
' update-task <short_id> <task_id> Partially update a task',
|
|
182
|
+
' delete-task <short_id> <task_id> Delete a task',
|
|
183
|
+
' task-records <short_id> <task_id> List task records (comments + history)',
|
|
184
|
+
' add-task-comment <short_id> <task_id> Add a comment (--content required)',
|
|
152
185
|
'',
|
|
153
186
|
'Options:',
|
|
154
187
|
' --name <name> LiveDoc name',
|
|
@@ -158,8 +191,8 @@ function usage() {
|
|
|
158
191
|
' --page <n> Page number',
|
|
159
192
|
' --size <n> Page size',
|
|
160
193
|
' --type <type> Resource type filter',
|
|
161
|
-
' --content <text> Document content',
|
|
162
|
-
' --title <title> Document title',
|
|
194
|
+
' --content <text> Document/README/comment content',
|
|
195
|
+
' --title <title> Document/task title',
|
|
163
196
|
' --urls <urls> Comma-separated URLs',
|
|
164
197
|
' --file <path> File path to upload',
|
|
165
198
|
' --convert Convert uploaded file to document',
|
|
@@ -171,6 +204,10 @@ function usage() {
|
|
|
171
204
|
' --max-chunk <n> Max chunks to return (ppt-retrieve, default 3)',
|
|
172
205
|
' --expires-in <s> Presigned URL expiry in seconds (download, default 3600)',
|
|
173
206
|
' --output <path> Output file path (download, default: filename from response)',
|
|
207
|
+
' --status <n> Task status: 0=TODO, 1=IN_PROGRESS, 2=DONE',
|
|
208
|
+
' --sort <n> Task sort order (non-negative integer)',
|
|
209
|
+
' --labels <labels> Comma-separated labels (tasks)',
|
|
210
|
+
' --record-type <type> Record type filter: comment, edit, status_change',
|
|
174
211
|
' -j, --json Output raw JSON',
|
|
175
212
|
' -t, --timeout <ms> Timeout in ms (default: 60000)',
|
|
176
213
|
' --help Show this help',
|
|
@@ -181,7 +218,8 @@ function parseArgs(argv) {
|
|
|
181
218
|
action: '', positional: [], name: '', description: '', icon: '',
|
|
182
219
|
keyword: '', page: '', size: '', type: '', content: '', title: '',
|
|
183
220
|
urls: '', file: '', convert: false, query: '', resourceIds: '', maxResources: '',
|
|
184
|
-
resourceId: '', pageNumber: '', maxChunk: '', expiresIn: '',
|
|
221
|
+
resourceId: '', pageNumber: '', maxChunk: '', expiresIn: '', output: '',
|
|
222
|
+
status: '', sort: '', labels: '', recordType: '',
|
|
185
223
|
json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
|
|
186
224
|
};
|
|
187
225
|
const positional = [];
|
|
@@ -208,6 +246,11 @@ function parseArgs(argv) {
|
|
|
208
246
|
else if (a === '--page-number') out.pageNumber = argv[++i] || '';
|
|
209
247
|
else if (a === '--max-chunk') out.maxChunk = argv[++i] || '';
|
|
210
248
|
else if (a === '--expires-in') out.expiresIn = argv[++i] || '';
|
|
249
|
+
else if (a === '--output') out.output = argv[++i] || '';
|
|
250
|
+
else if (a === '--status') out.status = argv[++i] || '';
|
|
251
|
+
else if (a === '--sort') out.sort = argv[++i] || '';
|
|
252
|
+
else if (a === '--labels') out.labels = argv[++i] || '';
|
|
253
|
+
else if (a === '--record-type') out.recordType = argv[++i] || '';
|
|
211
254
|
else if (a === '-t' || a === '--timeout') {
|
|
212
255
|
const n = parseInt(argv[++i] || '', 10);
|
|
213
256
|
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
@@ -374,6 +417,19 @@ async function main() {
|
|
|
374
417
|
code = 0;
|
|
375
418
|
break;
|
|
376
419
|
}
|
|
420
|
+
case 'update-resource': {
|
|
421
|
+
if (!shortId || !resourceId) { console.error('ERROR: short_id and resource_id are required'); break; }
|
|
422
|
+
spinnerId = startSpinner('Updating resource');
|
|
423
|
+
const body = {};
|
|
424
|
+
if (args.title !== undefined) body.title = args.title;
|
|
425
|
+
if (args.snippet !== undefined) body.snippet = args.snippet;
|
|
426
|
+
if (args.thumbnail !== undefined) body.thumbnail = args.thumbnail;
|
|
427
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}/resources/${resourceId}`, body, apiKey, apiBase, timeoutMs);
|
|
428
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
429
|
+
else { process.stdout.write('Resource updated!\n\n'); process.stdout.write(formatResource(payload?.data)); }
|
|
430
|
+
code = 0;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
377
433
|
case 'retrieve': {
|
|
378
434
|
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
379
435
|
if (!args.query) { console.error('ERROR: --query is required'); break; }
|
|
@@ -490,6 +546,140 @@ async function main() {
|
|
|
490
546
|
code = 0;
|
|
491
547
|
break;
|
|
492
548
|
}
|
|
549
|
+
case 'get-readme': {
|
|
550
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
551
|
+
spinnerId = startSpinner('Fetching README');
|
|
552
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
553
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
554
|
+
else { process.stdout.write(payload?.data?.content || '(empty)\n'); }
|
|
555
|
+
code = 0;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case 'update-readme': {
|
|
559
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
560
|
+
if (args.content === undefined || args.content === '') { console.error('ERROR: --content is required'); break; }
|
|
561
|
+
spinnerId = startSpinner('Updating README');
|
|
562
|
+
await apiRequest('PUT', `/livedocs/${shortId}/readme`, { content: args.content }, apiKey, apiBase, timeoutMs);
|
|
563
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
564
|
+
else { process.stdout.write('README updated.\n'); }
|
|
565
|
+
code = 0;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'append-readme': {
|
|
569
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
570
|
+
if (!args.content) { console.error('ERROR: --content is required'); break; }
|
|
571
|
+
spinnerId = startSpinner('Appending to README');
|
|
572
|
+
await apiRequest('POST', `/livedocs/${shortId}/readme/append`, { content: args.content }, apiKey, apiBase, timeoutMs);
|
|
573
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
574
|
+
else { process.stdout.write('README appended.\n'); }
|
|
575
|
+
code = 0;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'delete-readme': {
|
|
579
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
580
|
+
spinnerId = startSpinner('Deleting README');
|
|
581
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
582
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
583
|
+
else { process.stdout.write('README deleted.\n'); }
|
|
584
|
+
code = 0;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'tasks': {
|
|
588
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
589
|
+
spinnerId = startSpinner('Listing tasks');
|
|
590
|
+
const params = new URLSearchParams();
|
|
591
|
+
if (args.status !== '') params.set('status', args.status);
|
|
592
|
+
if (args.labels) args.labels.split(',').map(l => l.trim()).filter(Boolean).forEach(l => params.append('labels', l));
|
|
593
|
+
if (args.page) params.set('page', args.page);
|
|
594
|
+
if (args.size) params.set('size', args.size);
|
|
595
|
+
const qs = params.toString();
|
|
596
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
597
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
598
|
+
else {
|
|
599
|
+
const items = payload?.data?.items || [];
|
|
600
|
+
if (!items.length) { process.stderr.write('No tasks found.\n'); }
|
|
601
|
+
else {
|
|
602
|
+
process.stdout.write(`Found ${payload.data.total || items.length} task(s)\n\n`);
|
|
603
|
+
for (const t of items) process.stdout.write(formatTask(t));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
code = 0;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case 'create-task': {
|
|
610
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
611
|
+
if (!args.title) { console.error('ERROR: --title is required'); break; }
|
|
612
|
+
if (args.status === '') { console.error('ERROR: --status is required'); break; }
|
|
613
|
+
if (args.sort === '') { console.error('ERROR: --sort is required'); break; }
|
|
614
|
+
spinnerId = startSpinner('Creating task');
|
|
615
|
+
const body = { title: args.title, status: parseInt(args.status, 10), sort: parseInt(args.sort, 10) };
|
|
616
|
+
if (args.description) body.description = args.description;
|
|
617
|
+
if (args.labels) body.labels = args.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
618
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks`, body, apiKey, apiBase, timeoutMs);
|
|
619
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
620
|
+
else { process.stdout.write('Task created!\n\n'); process.stdout.write(formatTask(payload?.data)); }
|
|
621
|
+
code = 0;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'update-task': {
|
|
625
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
626
|
+
if (!resourceId) { console.error('ERROR: task_id is required'); break; }
|
|
627
|
+
spinnerId = startSpinner('Updating task');
|
|
628
|
+
const body = {};
|
|
629
|
+
if (args.title) body.title = args.title;
|
|
630
|
+
if (args.description !== undefined) body.description = args.description;
|
|
631
|
+
if (args.status !== '') body.status = parseInt(args.status, 10);
|
|
632
|
+
if (args.sort !== '') body.sort = parseInt(args.sort, 10);
|
|
633
|
+
if (args.labels !== undefined) body.labels = args.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
634
|
+
const payload = await apiRequest('PATCH', `/livedocs/${shortId}/tasks/${resourceId}`, body, apiKey, apiBase, timeoutMs);
|
|
635
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
636
|
+
else { process.stdout.write('Task updated!\n\n'); process.stdout.write(formatTask(payload?.data)); }
|
|
637
|
+
code = 0;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case 'delete-task': {
|
|
641
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
642
|
+
if (!resourceId) { console.error('ERROR: task_id is required'); break; }
|
|
643
|
+
spinnerId = startSpinner('Deleting task');
|
|
644
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/tasks/${resourceId}`, null, apiKey, apiBase, timeoutMs);
|
|
645
|
+
if (json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); }
|
|
646
|
+
else { process.stdout.write(`Task \`${resourceId}\` deleted.\n`); }
|
|
647
|
+
code = 0;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'task-records': {
|
|
651
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
652
|
+
if (!resourceId) { console.error('ERROR: task_id is required'); break; }
|
|
653
|
+
spinnerId = startSpinner('Fetching task records');
|
|
654
|
+
const params = new URLSearchParams();
|
|
655
|
+
if (args.recordType) params.set('record_type', args.recordType);
|
|
656
|
+
if (args.page) params.set('page', args.page);
|
|
657
|
+
if (args.size) params.set('size', args.size);
|
|
658
|
+
const qs = params.toString();
|
|
659
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks/${resourceId}/records${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
660
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
661
|
+
else {
|
|
662
|
+
const items = payload?.data?.items || [];
|
|
663
|
+
if (!items.length) { process.stderr.write('No records found.\n'); }
|
|
664
|
+
else {
|
|
665
|
+
process.stdout.write(`Found ${payload.data.total || items.length} record(s)\n\n`);
|
|
666
|
+
for (const r of items) process.stdout.write(formatTaskRecord(r));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
code = 0;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
case 'add-task-comment': {
|
|
673
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
674
|
+
if (!resourceId) { console.error('ERROR: task_id is required'); break; }
|
|
675
|
+
if (!args.content) { console.error('ERROR: --content is required'); break; }
|
|
676
|
+
spinnerId = startSpinner('Adding comment');
|
|
677
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks/${resourceId}/comments`, { content: args.content }, apiKey, apiBase, timeoutMs);
|
|
678
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
679
|
+
else { process.stdout.write('Comment added.\n'); process.stdout.write(formatTaskRecord(payload?.data)); }
|
|
680
|
+
code = 0;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
493
683
|
default:
|
|
494
684
|
console.error(`Unknown action: ${action}`);
|
|
495
685
|
usage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Felo AI CLI - real-time search, PPT generation, SuperAgent conversation, LiveDoc management, web fetch, YouTube subtitles, LiveDoc knowledge base, and X (Twitter) search from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
package/src/cli.js
CHANGED
|
@@ -673,6 +673,27 @@ livedocCmd
|
|
|
673
673
|
flushStdioThenExit(code);
|
|
674
674
|
});
|
|
675
675
|
|
|
676
|
+
livedocCmd
|
|
677
|
+
.command("update-resource <short_id> <resource_id>")
|
|
678
|
+
.description("Update a resource's title, snippet, or thumbnail")
|
|
679
|
+
.option("--title <title>", "new resource title (max 500 characters)")
|
|
680
|
+
.option("--snippet <text>", "new resource summary (max 2000 characters)")
|
|
681
|
+
.option("--thumbnail <url>", "new thumbnail URL (max 2000 characters)")
|
|
682
|
+
.option("-j, --json", "output raw JSON")
|
|
683
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
684
|
+
.action(async (shortId, resourceId, opts) => {
|
|
685
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
686
|
+
const code = await livedoc.updateResource(shortId, resourceId, {
|
|
687
|
+
title: opts.title,
|
|
688
|
+
snippet: opts.snippet,
|
|
689
|
+
thumbnail: opts.thumbnail,
|
|
690
|
+
json: opts.json,
|
|
691
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
692
|
+
});
|
|
693
|
+
process.exitCode = code;
|
|
694
|
+
flushStdioThenExit(code);
|
|
695
|
+
});
|
|
696
|
+
|
|
676
697
|
livedocCmd
|
|
677
698
|
.command("retrieve <short_id>")
|
|
678
699
|
.description("Semantic search across resources")
|
|
@@ -766,6 +787,200 @@ livedocCmd
|
|
|
766
787
|
flushStdioThenExit(code);
|
|
767
788
|
});
|
|
768
789
|
|
|
790
|
+
// ── LiveDoc README ──
|
|
791
|
+
|
|
792
|
+
livedocCmd
|
|
793
|
+
.command("get-readme <short_id>")
|
|
794
|
+
.description("Get the README of a LiveDoc")
|
|
795
|
+
.option("-j, --json", "output raw JSON")
|
|
796
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
797
|
+
.action(async (shortId, opts) => {
|
|
798
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
799
|
+
const code = await livedoc.getReadme(shortId, {
|
|
800
|
+
json: opts.json,
|
|
801
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
802
|
+
});
|
|
803
|
+
process.exitCode = code;
|
|
804
|
+
flushStdioThenExit(code);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
livedocCmd
|
|
808
|
+
.command("update-readme <short_id>")
|
|
809
|
+
.description("Create or replace the README of a LiveDoc")
|
|
810
|
+
.requiredOption("--content <text>", "README content (Markdown)")
|
|
811
|
+
.option("-j, --json", "output raw JSON")
|
|
812
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
813
|
+
.action(async (shortId, opts) => {
|
|
814
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
815
|
+
const code = await livedoc.upsertReadme(shortId, {
|
|
816
|
+
content: opts.content,
|
|
817
|
+
json: opts.json,
|
|
818
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
819
|
+
});
|
|
820
|
+
process.exitCode = code;
|
|
821
|
+
flushStdioThenExit(code);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
livedocCmd
|
|
825
|
+
.command("append-readme <short_id>")
|
|
826
|
+
.description("Append content to the README of a LiveDoc")
|
|
827
|
+
.requiredOption("--content <text>", "content to append (Markdown)")
|
|
828
|
+
.option("-j, --json", "output raw JSON")
|
|
829
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
830
|
+
.action(async (shortId, opts) => {
|
|
831
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
832
|
+
const code = await livedoc.appendReadme(shortId, {
|
|
833
|
+
content: opts.content,
|
|
834
|
+
json: opts.json,
|
|
835
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
836
|
+
});
|
|
837
|
+
process.exitCode = code;
|
|
838
|
+
flushStdioThenExit(code);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
livedocCmd
|
|
842
|
+
.command("delete-readme <short_id>")
|
|
843
|
+
.description("Delete the README of a LiveDoc")
|
|
844
|
+
.option("-j, --json", "output raw JSON")
|
|
845
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
846
|
+
.action(async (shortId, opts) => {
|
|
847
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
848
|
+
const code = await livedoc.deleteReadme(shortId, {
|
|
849
|
+
json: opts.json,
|
|
850
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
851
|
+
});
|
|
852
|
+
process.exitCode = code;
|
|
853
|
+
flushStdioThenExit(code);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ── LiveDoc Tasks ──
|
|
857
|
+
|
|
858
|
+
livedocCmd
|
|
859
|
+
.command("tasks <short_id>")
|
|
860
|
+
.description("List tasks in a LiveDoc")
|
|
861
|
+
.option("--status <n>", "filter by status: 0=TODO, 1=IN_PROGRESS, 2=DONE")
|
|
862
|
+
.option("--labels <labels>", "filter by labels (comma-separated)")
|
|
863
|
+
.option("--page <n>", "page number")
|
|
864
|
+
.option("--size <n>", "page size")
|
|
865
|
+
.option("-j, --json", "output raw JSON")
|
|
866
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
867
|
+
.action(async (shortId, opts) => {
|
|
868
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
869
|
+
const code = await livedoc.listTasks(shortId, {
|
|
870
|
+
status: opts.status,
|
|
871
|
+
labels: opts.labels,
|
|
872
|
+
page: opts.page,
|
|
873
|
+
size: opts.size,
|
|
874
|
+
json: opts.json,
|
|
875
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
876
|
+
});
|
|
877
|
+
process.exitCode = code;
|
|
878
|
+
flushStdioThenExit(code);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
livedocCmd
|
|
882
|
+
.command("create-task <short_id>")
|
|
883
|
+
.description("Create a task in a LiveDoc")
|
|
884
|
+
.requiredOption("--title <title>", "task title")
|
|
885
|
+
.requiredOption("--status <n>", "task status: 0=TODO, 1=IN_PROGRESS, 2=DONE")
|
|
886
|
+
.requiredOption("--sort <n>", "sort order (non-negative integer)")
|
|
887
|
+
.option("--description <desc>", "task description")
|
|
888
|
+
.option("--labels <labels>", "comma-separated labels (max 10)")
|
|
889
|
+
.option("-j, --json", "output raw JSON")
|
|
890
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
891
|
+
.action(async (shortId, opts) => {
|
|
892
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
893
|
+
const code = await livedoc.createTask(shortId, {
|
|
894
|
+
title: opts.title,
|
|
895
|
+
status: opts.status,
|
|
896
|
+
sort: opts.sort,
|
|
897
|
+
description: opts.description,
|
|
898
|
+
labels: opts.labels,
|
|
899
|
+
json: opts.json,
|
|
900
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
901
|
+
});
|
|
902
|
+
process.exitCode = code;
|
|
903
|
+
flushStdioThenExit(code);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
livedocCmd
|
|
907
|
+
.command("update-task <short_id> <task_id>")
|
|
908
|
+
.description("Partially update a task (only provided fields are changed)")
|
|
909
|
+
.option("--title <title>", "new task title")
|
|
910
|
+
.option("--description <desc>", "new task description")
|
|
911
|
+
.option("--status <n>", "new status: 0=TODO, 1=IN_PROGRESS, 2=DONE")
|
|
912
|
+
.option("--sort <n>", "new sort order")
|
|
913
|
+
.option("--labels <labels>", "new comma-separated labels")
|
|
914
|
+
.option("-j, --json", "output raw JSON")
|
|
915
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
916
|
+
.action(async (shortId, taskId, opts) => {
|
|
917
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
918
|
+
const code = await livedoc.updateTask(shortId, taskId, {
|
|
919
|
+
title: opts.title,
|
|
920
|
+
description: opts.description,
|
|
921
|
+
status: opts.status,
|
|
922
|
+
sort: opts.sort,
|
|
923
|
+
labels: opts.labels,
|
|
924
|
+
json: opts.json,
|
|
925
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
926
|
+
});
|
|
927
|
+
process.exitCode = code;
|
|
928
|
+
flushStdioThenExit(code);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
livedocCmd
|
|
932
|
+
.command("delete-task <short_id> <task_id>")
|
|
933
|
+
.description("Delete a task")
|
|
934
|
+
.option("-j, --json", "output raw JSON")
|
|
935
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
936
|
+
.action(async (shortId, taskId, opts) => {
|
|
937
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
938
|
+
const code = await livedoc.deleteTask(shortId, taskId, {
|
|
939
|
+
json: opts.json,
|
|
940
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
941
|
+
});
|
|
942
|
+
process.exitCode = code;
|
|
943
|
+
flushStdioThenExit(code);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
livedocCmd
|
|
947
|
+
.command("task-records <short_id> <task_id>")
|
|
948
|
+
.description("List records (comments + change history) for a task")
|
|
949
|
+
.option("--record-type <type>", "filter by type: comment, edit, status_change")
|
|
950
|
+
.option("--page <n>", "page number")
|
|
951
|
+
.option("--size <n>", "page size")
|
|
952
|
+
.option("-j, --json", "output raw JSON")
|
|
953
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
954
|
+
.action(async (shortId, taskId, opts) => {
|
|
955
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
956
|
+
const code = await livedoc.listTaskRecords(shortId, taskId, {
|
|
957
|
+
recordType: opts.recordType,
|
|
958
|
+
page: opts.page,
|
|
959
|
+
size: opts.size,
|
|
960
|
+
json: opts.json,
|
|
961
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
962
|
+
});
|
|
963
|
+
process.exitCode = code;
|
|
964
|
+
flushStdioThenExit(code);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
livedocCmd
|
|
968
|
+
.command("add-task-comment <short_id> <task_id>")
|
|
969
|
+
.description("Add a comment to a task")
|
|
970
|
+
.requiredOption("--content <text>", "comment content")
|
|
971
|
+
.option("-j, --json", "output raw JSON")
|
|
972
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
973
|
+
.action(async (shortId, taskId, opts) => {
|
|
974
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
975
|
+
const code = await livedoc.createTaskComment(shortId, taskId, {
|
|
976
|
+
content: opts.content,
|
|
977
|
+
json: opts.json,
|
|
978
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
979
|
+
});
|
|
980
|
+
process.exitCode = code;
|
|
981
|
+
flushStdioThenExit(code);
|
|
982
|
+
});
|
|
983
|
+
|
|
769
984
|
program
|
|
770
985
|
.command("summarize")
|
|
771
986
|
.description("Summarize text or URL (coming when API is available)")
|
package/src/livedoc.js
CHANGED
|
@@ -375,6 +375,31 @@ export async function removeResource(shortId, resourceId, opts = {}) {
|
|
|
375
375
|
} finally { stopSpinner(spinnerId); }
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
export async function updateResource(shortId, resourceId, opts = {}) {
|
|
379
|
+
const apiKey = await getApiKey();
|
|
380
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
381
|
+
if (!shortId || !resourceId) { process.stderr.write('ERROR: short_id and resource_id are required.\n'); return 1; }
|
|
382
|
+
|
|
383
|
+
const apiBase = await getApiBase();
|
|
384
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
385
|
+
const spinnerId = startSpinner('Updating resource');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const body = {};
|
|
389
|
+
if (opts.title !== undefined) body.title = opts.title;
|
|
390
|
+
if (opts.snippet !== undefined) body.snippet = opts.snippet;
|
|
391
|
+
if (opts.thumbnail !== undefined) body.thumbnail = opts.thumbnail;
|
|
392
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}/resources/${resourceId}`, body, apiKey, apiBase, timeoutMs);
|
|
393
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
394
|
+
process.stdout.write('Resource updated!\n\n');
|
|
395
|
+
process.stdout.write(formatResource(payload?.data));
|
|
396
|
+
return 0;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
process.stderr.write(`Failed to update resource: ${err?.message || err}\n`);
|
|
399
|
+
return 1;
|
|
400
|
+
} finally { stopSpinner(spinnerId); }
|
|
401
|
+
}
|
|
402
|
+
|
|
378
403
|
export async function route(shortId, opts = {}) {
|
|
379
404
|
const apiKey = await getApiKey();
|
|
380
405
|
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
@@ -532,6 +557,268 @@ export async function getResourceContent(shortId, resourceId, opts = {}) {
|
|
|
532
557
|
} finally { stopSpinner(spinnerId); }
|
|
533
558
|
}
|
|
534
559
|
|
|
560
|
+
// ── README ──
|
|
561
|
+
|
|
562
|
+
export async function getReadme(shortId, opts = {}) {
|
|
563
|
+
const apiKey = await getApiKey();
|
|
564
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
565
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
566
|
+
|
|
567
|
+
const apiBase = await getApiBase();
|
|
568
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
569
|
+
const spinnerId = startSpinner('Fetching README');
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
573
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
574
|
+
const content = payload?.data?.content ?? '';
|
|
575
|
+
process.stdout.write(content || '(empty)\n');
|
|
576
|
+
return 0;
|
|
577
|
+
} catch (err) {
|
|
578
|
+
process.stderr.write(`Failed to get README: ${err?.message || err}\n`);
|
|
579
|
+
return 1;
|
|
580
|
+
} finally { stopSpinner(spinnerId); }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export async function upsertReadme(shortId, opts = {}) {
|
|
584
|
+
const apiKey = await getApiKey();
|
|
585
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
586
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
587
|
+
if (opts.content === undefined || opts.content === null) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
588
|
+
|
|
589
|
+
const apiBase = await getApiBase();
|
|
590
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
591
|
+
const spinnerId = startSpinner('Updating README');
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}/readme`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
595
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
596
|
+
process.stdout.write('README updated.\n');
|
|
597
|
+
return 0;
|
|
598
|
+
} catch (err) {
|
|
599
|
+
process.stderr.write(`Failed to update README: ${err?.message || err}\n`);
|
|
600
|
+
return 1;
|
|
601
|
+
} finally { stopSpinner(spinnerId); }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export async function appendReadme(shortId, opts = {}) {
|
|
605
|
+
const apiKey = await getApiKey();
|
|
606
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
607
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
608
|
+
if (!opts.content) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
609
|
+
|
|
610
|
+
const apiBase = await getApiBase();
|
|
611
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
612
|
+
const spinnerId = startSpinner('Appending to README');
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/readme/append`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
616
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
617
|
+
process.stdout.write('README appended.\n');
|
|
618
|
+
return 0;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
process.stderr.write(`Failed to append README: ${err?.message || err}\n`);
|
|
621
|
+
return 1;
|
|
622
|
+
} finally { stopSpinner(spinnerId); }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function deleteReadme(shortId, opts = {}) {
|
|
626
|
+
const apiKey = await getApiKey();
|
|
627
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
628
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
629
|
+
|
|
630
|
+
const apiBase = await getApiBase();
|
|
631
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
632
|
+
const spinnerId = startSpinner('Deleting README');
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
636
|
+
if (opts.json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); return 0; }
|
|
637
|
+
process.stdout.write('README deleted.\n');
|
|
638
|
+
return 0;
|
|
639
|
+
} catch (err) {
|
|
640
|
+
process.stderr.write(`Failed to delete README: ${err?.message || err}\n`);
|
|
641
|
+
return 1;
|
|
642
|
+
} finally { stopSpinner(spinnerId); }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Tasks ──
|
|
646
|
+
|
|
647
|
+
function formatTask(t) {
|
|
648
|
+
if (!t) return '';
|
|
649
|
+
let out = `### ${t.title || '(untitled)'}\n`;
|
|
650
|
+
out += `- Task ID: \`${t.id}\`\n`;
|
|
651
|
+
out += `- Status: ${t.status === 0 ? 'TODO' : t.status === 1 ? 'IN_PROGRESS' : t.status === 2 ? 'DONE' : t.status}\n`;
|
|
652
|
+
if (t.sort != null) out += `- Sort: ${t.sort}\n`;
|
|
653
|
+
if (t.description) out += `- Description: ${t.description}\n`;
|
|
654
|
+
if (t.labels?.length) out += `- Labels: ${t.labels.join(', ')}\n`;
|
|
655
|
+
if (t.created_at) out += `- Created: ${t.created_at}\n`;
|
|
656
|
+
out += '\n';
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function formatTaskRecord(r) {
|
|
661
|
+
if (!r) return '';
|
|
662
|
+
let out = `- [${r.record_type}] `;
|
|
663
|
+
if (r.content) out += r.content;
|
|
664
|
+
else if (r.meta) out += JSON.stringify(r.meta);
|
|
665
|
+
out += ` (id: ${r.id}, ${r.created_at || ''})\n`;
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export async function listTasks(shortId, opts = {}) {
|
|
670
|
+
const apiKey = await getApiKey();
|
|
671
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
672
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
673
|
+
|
|
674
|
+
const apiBase = await getApiBase();
|
|
675
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
676
|
+
const spinnerId = startSpinner('Listing tasks');
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const params = new URLSearchParams();
|
|
680
|
+
if (opts.status !== undefined && opts.status !== '') params.set('status', opts.status);
|
|
681
|
+
if (opts.labels) opts.labels.split(',').map(l => l.trim()).filter(Boolean).forEach(l => params.append('labels', l));
|
|
682
|
+
if (opts.page) params.set('page', opts.page);
|
|
683
|
+
if (opts.size) params.set('size', opts.size);
|
|
684
|
+
const qs = params.toString();
|
|
685
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
686
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
687
|
+
const items = payload?.data?.items || [];
|
|
688
|
+
if (!items.length) { process.stderr.write('No tasks found.\n'); return 0; }
|
|
689
|
+
process.stdout.write(`Found ${payload.data.total || items.length} task(s)\n\n`);
|
|
690
|
+
for (const t of items) process.stdout.write(formatTask(t));
|
|
691
|
+
return 0;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
process.stderr.write(`Failed to list tasks: ${err?.message || err}\n`);
|
|
694
|
+
return 1;
|
|
695
|
+
} finally { stopSpinner(spinnerId); }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export async function createTask(shortId, opts = {}) {
|
|
699
|
+
const apiKey = await getApiKey();
|
|
700
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
701
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
702
|
+
if (!opts.title) { process.stderr.write('ERROR: --title is required.\n'); return 1; }
|
|
703
|
+
if (opts.status === undefined || opts.status === '') { process.stderr.write('ERROR: --status is required.\n'); return 1; }
|
|
704
|
+
if (opts.sort === undefined || opts.sort === '') { process.stderr.write('ERROR: --sort is required.\n'); return 1; }
|
|
705
|
+
|
|
706
|
+
const apiBase = await getApiBase();
|
|
707
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
708
|
+
const spinnerId = startSpinner('Creating task');
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const body = { title: opts.title, status: parseInt(opts.status, 10), sort: parseInt(opts.sort, 10) };
|
|
712
|
+
if (opts.description) body.description = opts.description;
|
|
713
|
+
if (opts.labels) body.labels = opts.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
714
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks`, body, apiKey, apiBase, timeoutMs);
|
|
715
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
716
|
+
process.stdout.write('Task created!\n\n');
|
|
717
|
+
process.stdout.write(formatTask(payload?.data));
|
|
718
|
+
return 0;
|
|
719
|
+
} catch (err) {
|
|
720
|
+
process.stderr.write(`Failed to create task: ${err?.message || err}\n`);
|
|
721
|
+
return 1;
|
|
722
|
+
} finally { stopSpinner(spinnerId); }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function updateTask(shortId, taskId, opts = {}) {
|
|
726
|
+
const apiKey = await getApiKey();
|
|
727
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
728
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
729
|
+
|
|
730
|
+
const apiBase = await getApiBase();
|
|
731
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
732
|
+
const spinnerId = startSpinner('Updating task');
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const body = {};
|
|
736
|
+
if (opts.title) body.title = opts.title;
|
|
737
|
+
if (opts.description !== undefined) body.description = opts.description;
|
|
738
|
+
if (opts.status !== undefined && opts.status !== '') body.status = parseInt(opts.status, 10);
|
|
739
|
+
if (opts.sort !== undefined && opts.sort !== '') body.sort = parseInt(opts.sort, 10);
|
|
740
|
+
if (opts.labels !== undefined) body.labels = opts.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
741
|
+
const payload = await apiRequest('PATCH', `/livedocs/${shortId}/tasks/${taskId}`, body, apiKey, apiBase, timeoutMs);
|
|
742
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
743
|
+
process.stdout.write('Task updated!\n\n');
|
|
744
|
+
process.stdout.write(formatTask(payload?.data));
|
|
745
|
+
return 0;
|
|
746
|
+
} catch (err) {
|
|
747
|
+
process.stderr.write(`Failed to update task: ${err?.message || err}\n`);
|
|
748
|
+
return 1;
|
|
749
|
+
} finally { stopSpinner(spinnerId); }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function deleteTask(shortId, taskId, opts = {}) {
|
|
753
|
+
const apiKey = await getApiKey();
|
|
754
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
755
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
756
|
+
|
|
757
|
+
const apiBase = await getApiBase();
|
|
758
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
759
|
+
const spinnerId = startSpinner('Deleting task');
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/tasks/${taskId}`, null, apiKey, apiBase, timeoutMs);
|
|
763
|
+
if (opts.json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); return 0; }
|
|
764
|
+
process.stdout.write(`Task \`${taskId}\` deleted.\n`);
|
|
765
|
+
return 0;
|
|
766
|
+
} catch (err) {
|
|
767
|
+
process.stderr.write(`Failed to delete task: ${err?.message || err}\n`);
|
|
768
|
+
return 1;
|
|
769
|
+
} finally { stopSpinner(spinnerId); }
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export async function listTaskRecords(shortId, taskId, opts = {}) {
|
|
773
|
+
const apiKey = await getApiKey();
|
|
774
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
775
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
776
|
+
|
|
777
|
+
const apiBase = await getApiBase();
|
|
778
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
779
|
+
const spinnerId = startSpinner('Fetching task records');
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const params = new URLSearchParams();
|
|
783
|
+
if (opts.recordType) params.set('record_type', opts.recordType);
|
|
784
|
+
if (opts.page) params.set('page', opts.page);
|
|
785
|
+
if (opts.size) params.set('size', opts.size);
|
|
786
|
+
const qs = params.toString();
|
|
787
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks/${taskId}/records${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
788
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
789
|
+
const items = payload?.data?.items || [];
|
|
790
|
+
if (!items.length) { process.stderr.write('No records found.\n'); return 0; }
|
|
791
|
+
process.stdout.write(`Found ${payload.data.total || items.length} record(s)\n\n`);
|
|
792
|
+
for (const r of items) process.stdout.write(formatTaskRecord(r));
|
|
793
|
+
return 0;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
process.stderr.write(`Failed to list task records: ${err?.message || err}\n`);
|
|
796
|
+
return 1;
|
|
797
|
+
} finally { stopSpinner(spinnerId); }
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export async function createTaskComment(shortId, taskId, opts = {}) {
|
|
801
|
+
const apiKey = await getApiKey();
|
|
802
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
803
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
804
|
+
if (!opts.content) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
805
|
+
|
|
806
|
+
const apiBase = await getApiBase();
|
|
807
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
808
|
+
const spinnerId = startSpinner('Adding comment');
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks/${taskId}/comments`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
812
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
813
|
+
process.stdout.write('Comment added.\n');
|
|
814
|
+
process.stdout.write(formatTaskRecord(payload?.data));
|
|
815
|
+
return 0;
|
|
816
|
+
} catch (err) {
|
|
817
|
+
process.stderr.write(`Failed to add comment: ${err?.message || err}\n`);
|
|
818
|
+
return 1;
|
|
819
|
+
} finally { stopSpinner(spinnerId); }
|
|
820
|
+
}
|
|
821
|
+
|
|
535
822
|
export async function pptRetrieve(shortId, opts = {}) {
|
|
536
823
|
const apiKey = await getApiKey();
|
|
537
824
|
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|