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.
@@ -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
 
@@ -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.30",
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; }