@comment-io/cli 0.1.1-alpha.9 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,10 +60,10 @@ The canonical reference for the agent-facing REST API is served at `/llms.txt` (
60
60
  - `PATCH /docs/:id` — edit via `{ old_string, new_string }` patches
61
61
  - `POST /docs/:id/comments` — comment / suggest / reply
62
62
 
63
- ## CommentFS Local Sync
63
+ ## Local Sync Files
64
64
 
65
- CommentFS can project selected Comment.io docs into read-only local markdown
66
- files under `~/Comment Docs`.
65
+ The Go `comment` CLI can mirror read-only markdown projections for the current
66
+ library sync scope, **My Files and Shared With Me**, under `~/Comment Docs`.
67
67
 
68
68
  Install the CLI:
69
69
 
@@ -72,14 +72,21 @@ npm install -g '@comment-io/cli@^0.1.1'
72
72
  ```
73
73
 
74
74
  ```bash
75
- comment sync login --api-key <usk_...>
76
- comment sync
75
+ comment sync login
76
+ comment sync once
77
+ comment sync enable
77
78
  comment sync watch
78
- comment sync logout
79
+ comment sync logout [--purge-local]
79
80
  ```
80
81
 
81
- Enable **Sync locally** on a document first. Local files are not an edit path;
82
- edit through the UI or API. See `docs/COMMENTFS-SYNC-USAGE.md`.
82
+ Approve the device in Settings when `comment sync login` prints the browser
83
+ code. `comment sync enable` lets the Go bus worker run local sync; install or
84
+ run the bus daemon with `comment bus install` or `comment bus run` for
85
+ persistent background sync. Local files are not an edit path; edit through the
86
+ UI or REST API. Local edits are preserved under `~/.comment-io/sync/recovery`
87
+ and the server version is restored on the next sync. Use
88
+ `comment sync logout --purge-local` only when you also want to remove verified
89
+ clean local projections.
83
90
 
84
91
  ## Local Notification Daemon
85
92
 
@@ -120,8 +127,7 @@ comment run --runtime claude --profile <handle>
120
127
  comment run --runtime codex --profile <handle>
121
128
  ```
122
129
 
123
- The npm package ships the Go bus binary for macOS and Linux while keeping the
124
- legacy `comment sync ...` commands available through the Node wrapper.
130
+ The npm package ships the Go bus and local sync binary for macOS and Linux.
125
131
 
126
132
  ## Docs
127
133
 
package/bin/comment.js CHANGED
@@ -1,15 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'node:fs';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { createRequire } from 'node:module';
5
4
  import { dirname, resolve } from 'node:path';
6
5
  import { fileURLToPath } from 'node:url';
7
6
 
8
7
  const binDir = dirname(fileURLToPath(import.meta.url));
9
8
  const packageRoot = resolve(binDir, '..');
10
- const require = createRequire(import.meta.url);
11
- const tsxCli = require.resolve('tsx/cli');
12
- const syncCli = resolve(packageRoot, 'scripts', 'commentfs-sync.ts');
13
9
  const args = process.argv.slice(2);
14
10
 
15
11
  function run(command, commandArgs, options = {}) {
@@ -41,10 +37,6 @@ function goTarget() {
41
37
  return `${platform}-${arch}`;
42
38
  }
43
39
 
44
- if (args[0] === 'sync') {
45
- run(process.execPath, [tsxCli, syncCli, ...args]);
46
- }
47
-
48
40
  const target = goTarget();
49
41
  const exe = process.platform === 'win32' ? '.exe' : '';
50
42
  const bundledBinary = target ? resolve(packageRoot, 'dist', `comment-${target}${exe}`) : '';
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comment-io/cli",
3
- "version": "0.1.1-alpha.9",
3
+ "version": "0.1.2",
4
4
  "description": "Comment.io CLI and local notification daemon",
5
5
  "private": false,
6
6
  "type": "module",
@@ -29,18 +29,12 @@
29
29
  "files": [
30
30
  "bin/",
31
31
  "dist/",
32
- "scripts/commentfs-sync.ts",
33
- "shared/commentfs-sync.ts",
34
- "docs/COMMENTFS-SYNC-USAGE.md",
35
32
  "README.md",
36
33
  "LICENSE"
37
34
  ],
38
35
  "scripts": {
39
36
  "prepack": "node ../../scripts/prepare-cli-package.mjs"
40
37
  },
41
- "dependencies": {
42
- "tsx": "^4.21.0"
43
- },
44
38
  "engines": {
45
39
  "node": ">=20"
46
40
  }
@@ -1,97 +0,0 @@
1
- # CommentFS Read-Only Local Sync
2
-
3
- CommentFS projects selected Comment.io documents into local markdown files. The
4
- online Comment.io document is canonical. Local markdown files are read-only
5
- snapshots for search, context, indexing, and agent inspection.
6
-
7
- Do not edit synced markdown files directly. Humans should edit through the
8
- Comment.io UI. Agents should edit through the Comment.io API.
9
-
10
- ## Setup
11
-
12
- The CommentFS UI is currently gated behind `VITE_ENABLE_COMMENTFS_UI=true` so it
13
- can be enabled on staging without exposing the flow in production.
14
-
15
- 1. Open Comment.io settings and generate a CommentFS key.
16
- 2. Configure the local CLI:
17
-
18
- ```bash
19
- comment sync login --api-key <usk_...>
20
- ```
21
-
22
- For staging or another deployment, include the base URL:
23
-
24
- ```bash
25
- comment sync login --api-key <usk_...> --base-url https://staging.example.com
26
- ```
27
-
28
- To remove the stored sync key from this computer:
29
-
30
- ```bash
31
- comment sync logout
32
- ```
33
-
34
- 3. Open a document's access panel and enable **Sync locally**.
35
- 4. Run one sync:
36
-
37
- ```bash
38
- comment sync
39
- ```
40
-
41
- 5. Keep projections updated:
42
-
43
- ```bash
44
- comment sync watch
45
- ```
46
-
47
- By default files are written under `~/Comment Docs`.
48
-
49
- ## Useful Commands
50
-
51
- ```bash
52
- comment sync status
53
- comment sync logout
54
- comment sync repair
55
- comment sync explain <path>
56
- comment sync recover <path>
57
- comment sync watch --interval 10s --full-interval 5m
58
- ```
59
-
60
- `comment sync repair` restores read-only permissions on existing projections.
61
- `comment sync explain` points a markdown or sidecar path back to its source
62
- document and API docs. `comment sync recover` explains a preserved local edit
63
- artifact.
64
-
65
- ## Sidecars
66
-
67
- Each sync root has a `.comment/` folder. Important files include:
68
-
69
- - `.comment/manifest.json`: local projection manifest.
70
- - `.comment/docs/<slug>/status.json`: sync health, source URL, revision, sidecar
71
- paths, and recovery metadata.
72
- - `.comment/docs/<slug>/edit.md`: short edit instructions and API doc links.
73
- - `.comment/docs/<slug>/authorship.json`: authorship/provenance metadata.
74
- - `.comment/docs/<slug>/comments.json`: comment and suggestion metadata.
75
- - `.comment/docs/<slug>/participants.json`: participant metadata.
76
- - `.comment/recovery/*.local.md`: preserved local text from unsupported local
77
- edits.
78
-
79
- ## Local Edits
80
-
81
- If a local tool changes a synced markdown file, the next sync preserves that
82
- local text under `.comment/recovery/` and restores the canonical Comment.io
83
- version. It does not upload local markdown edits.
84
-
85
- To apply an intended change, open the document in Comment.io or use the API docs
86
- linked from the sidecar `api_docs_url`.
87
-
88
- ## Auth Notes
89
-
90
- `COMMENT_IO_USER_API_KEY` and `usk_` keys are only for read-only projection sync.
91
- They can poll selected docs and fetch projections. They cannot write documents.
92
- `comment sync logout` removes the stored `usk_` key from this computer; revoke
93
- the key in Comment.io to invalidate it everywhere.
94
-
95
- Agent edits need an edit-capable Comment.io credential such as
96
- `COMMENT_IO_AGENT_SECRET`, a registered agent credential, or an edit-capable
97
- per-document token.
@@ -1,342 +0,0 @@
1
- #!/usr/bin/env node
2
- import { homedir } from 'node:os';
3
- import { resolve } from 'node:path';
4
- import {
5
- DEFAULT_COMMENTFS_BASE_URL,
6
- DEFAULT_SYNC_ROOT,
7
- deleteCommentFsUserApiKey,
8
- readCommentFsConfig,
9
- explainCommentFsPath,
10
- getCommentFsStatus,
11
- recoverCommentFsPath,
12
- repairCommentFsPermissions,
13
- saveCommentFsUserApiKey,
14
- syncConfiguredCommentDocs,
15
- syncConfiguredCommentDocsSettled,
16
- syncOneCommentDoc,
17
- syncRemoteSelectedCommentDocsSettled,
18
- } from '../shared/commentfs-sync.js';
19
-
20
- interface ParsedArgs {
21
- command: string[];
22
- options: Record<string, string | true>;
23
- }
24
-
25
- function parseArgs(argv: string[]): ParsedArgs {
26
- const command: string[] = [];
27
- const options: Record<string, string | true> = {};
28
- for (let i = 0; i < argv.length; i += 1) {
29
- const arg = argv[i];
30
- if (arg.startsWith('--')) {
31
- const key = arg.slice(2);
32
- const next = argv[i + 1];
33
- if (next && !next.startsWith('--')) {
34
- options[key] = next;
35
- i += 1;
36
- } else {
37
- options[key] = true;
38
- }
39
- } else {
40
- command.push(arg);
41
- }
42
- }
43
- return { command, options };
44
- }
45
-
46
- function optionString(options: Record<string, string | true>, key: string): string | undefined {
47
- const value = options[key];
48
- return typeof value === 'string' ? value : undefined;
49
- }
50
-
51
- function usage(): string {
52
- return [
53
- 'Usage:',
54
- ' comment sync add <doc-url-or-slug> [--root <folder>] [--token <token>] [--base-url <url>] [--agent <handle>] [--filename <name.md>]',
55
- ' comment sync login --api-key <user-api-key> [--base-url <url>]',
56
- ' comment sync logout',
57
- ' comment sync [--root <folder>] [--api-key <user-api-key>]',
58
- ' comment sync status [--root <folder>]',
59
- ' comment sync repair [--root <folder>]',
60
- ' comment sync recover <path>',
61
- ' comment sync watch [--root <folder>] [--interval <seconds>] [--full-interval <seconds>] [--max-backoff <seconds>] [--once] [--api-key <user-api-key>] [--base-url <url>]',
62
- ' comment sync explain <path>',
63
- '',
64
- 'Local markdown files are read-only projections. Edit through Comment.io UI or API.',
65
- `Default root: ${DEFAULT_SYNC_ROOT}`,
66
- ].join('\n');
67
- }
68
-
69
- async function main(): Promise<void> {
70
- const { command, options } = parseArgs(process.argv.slice(2));
71
- if (options.help || command[0] === 'help') {
72
- console.log(usage());
73
- return;
74
- }
75
-
76
- if (command[0] !== 'sync') {
77
- throw new Error(`Unknown command.\n\n${usage()}`);
78
- }
79
-
80
- const rootDir = optionString(options, 'root');
81
-
82
- if (command[1] === 'login') {
83
- const apiKey = optionString(options, 'api-key');
84
- if (!apiKey) throw new Error(`Missing --api-key.\n\n${usage()}`);
85
- const saved = await saveCommentFsUserApiKey({
86
- userApiKey: apiKey,
87
- baseUrl: optionString(options, 'base-url'),
88
- });
89
- console.log(`saved: ${saved.path}`);
90
- console.log(`base_url: ${saved.config.baseUrl}`);
91
- return;
92
- }
93
-
94
- if (command[1] === 'logout') {
95
- const result = await deleteCommentFsUserApiKey();
96
- console.log(`${result.removed ? 'removed' : 'not logged in'}: ${result.path}`);
97
- console.log(result.removed
98
- ? 'CommentFS user API key removed from this computer.'
99
- : 'No CommentFS user API key was stored on this computer.');
100
- return;
101
- }
102
-
103
- if (command[1] === 'add') {
104
- const input = command[2];
105
- if (!input) throw new Error(`Missing doc URL or slug.\n\n${usage()}`);
106
- const result = await syncOneCommentDoc({
107
- rootDir,
108
- input,
109
- token: optionString(options, 'token'),
110
- baseUrl: optionString(options, 'base-url'),
111
- agentHandle: optionString(options, 'agent'),
112
- filename: optionString(options, 'filename'),
113
- });
114
- printResult(result);
115
- return;
116
- }
117
-
118
- if (command[1] === 'explain') {
119
- const inputPath = command[2];
120
- if (!inputPath) throw new Error(`Missing path.\n\n${usage()}`);
121
- const result = await explainCommentFsPath(inputPath);
122
- console.log(result.explanation);
123
- return;
124
- }
125
-
126
- if (command[1] === 'recover') {
127
- const inputPath = command[2];
128
- if (!inputPath) throw new Error(`Missing path.\n\n${usage()}`);
129
- const result = await recoverCommentFsPath(inputPath);
130
- console.log(result.instructions);
131
- console.log(`markdown: ${result.markdownPath}`);
132
- console.log(`status: ${result.statusPath}`);
133
- console.log(`edit: ${result.editPath}`);
134
- return;
135
- }
136
-
137
- if (command[1] === 'watch') {
138
- await runWatch({
139
- rootDir,
140
- baseUrl: optionString(options, 'base-url'),
141
- apiKey: optionString(options, 'api-key'),
142
- intervalMs: parseIntervalMs(optionString(options, 'interval') ?? '10s'),
143
- fullIntervalMs: parseIntervalMs(optionString(options, 'full-interval') ?? '5m'),
144
- maxBackoffMs: parseIntervalMs(optionString(options, 'max-backoff') ?? '1m'),
145
- once: options.once === true,
146
- });
147
- return;
148
- }
149
-
150
- if (command[1] === 'status') {
151
- const status = await getCommentFsStatus(rootDir);
152
- console.log(`root: ${status.rootDir}`);
153
- console.log(`manifest: ${status.manifestPath}`);
154
- if (status.docs.length === 0) {
155
- console.log('No Comment.io docs configured.');
156
- return;
157
- }
158
- for (const doc of status.docs) {
159
- const health = doc.syncHealth?.status ?? 'unknown';
160
- const revision = doc.revision === null ? 'unknown' : String(doc.revision);
161
- const mode = doc.readOnly ? 'read-only' : 'writable-or-missing';
162
- console.log(`${health}: ${doc.title} (${doc.slug}) revision ${revision} ${mode}`);
163
- console.log(` markdown: ${doc.markdownPath}`);
164
- console.log(` status: ${doc.statusPath}`);
165
- if (doc.localChange) console.log(` recovery: ${doc.localChange.recoveryFile}`);
166
- if (doc.syncHealth?.lastError) console.log(` error: ${doc.syncHealth.lastError}`);
167
- }
168
- return;
169
- }
170
-
171
- if (command[1] === 'repair') {
172
- const results = await repairCommentFsPermissions(rootDir);
173
- if (results.length === 0) {
174
- console.log('No Comment.io docs configured.');
175
- return;
176
- }
177
- for (const result of results) {
178
- if (!result.existed) console.log(`missing: ${result.title} (${result.markdownPath})`);
179
- else if (result.repaired) console.log(`repaired: ${result.title} (${result.markdownPath})`);
180
- else console.log(`ok: ${result.title} (${result.markdownPath})`);
181
- }
182
- return;
183
- }
184
-
185
- if (command.length === 1) {
186
- const apiKey = optionString(options, 'api-key');
187
- const results = await syncConfiguredCommentDocs({ rootDir, userApiKey: apiKey });
188
- if (results.length === 0) {
189
- console.log('No Comment.io docs configured. Run `comment sync add <doc-url-or-slug>` first.');
190
- return;
191
- }
192
- for (const result of results) printResult(result);
193
- return;
194
- }
195
-
196
- throw new Error(`Unknown sync subcommand.\n\n${usage()}`);
197
- }
198
-
199
- function printResult(result: {
200
- ok: boolean;
201
- title: string;
202
- revision?: number;
203
- changed?: boolean;
204
- localChangeDetected?: boolean;
205
- recoveryPath?: string;
206
- disabled?: boolean;
207
- markdownPath: string;
208
- statusPath: string;
209
- error?: string;
210
- }): void {
211
- if (!result.ok) {
212
- console.log(`error: ${result.title}`);
213
- console.log(`markdown: ${result.markdownPath}`);
214
- console.log(`status: ${result.statusPath}`);
215
- console.log(`message: ${result.error ?? 'unknown error'}`);
216
- return;
217
- }
218
- if (result.disabled) {
219
- console.log(`unselected: ${result.title}`);
220
- console.log(`removed: ${result.markdownPath}`);
221
- console.log(`status: ${result.statusPath}`);
222
- if (result.recoveryPath) console.log(`recovery: ${result.recoveryPath}`);
223
- return;
224
- }
225
- const state = result.localChangeDetected ? 'recovered-local-change' : result.changed ? 'updated' : 'unchanged';
226
- console.log(`${state}: ${result.title} (revision ${result.revision})`);
227
- console.log(`markdown: ${result.markdownPath}`);
228
- console.log(`status: ${result.statusPath}`);
229
- if (result.recoveryPath) console.log(`recovery: ${result.recoveryPath}`);
230
- }
231
-
232
- async function runWatch(options: {
233
- rootDir?: string;
234
- baseUrl?: string;
235
- apiKey?: string;
236
- intervalMs: number;
237
- fullIntervalMs: number;
238
- maxBackoffMs: number;
239
- once: boolean;
240
- }): Promise<void> {
241
- const configured = await readCommentFsConfig().catch(() => null);
242
- const apiKey = options.apiKey ?? process.env.COMMENT_IO_USER_API_KEY ?? configured?.userApiKey;
243
- const baseUrl = (options.baseUrl ?? configured?.baseUrl ?? DEFAULT_COMMENTFS_BASE_URL).replace(/\/$/, '');
244
- const rootDir = resolve(options.rootDir ?? DEFAULT_SYNC_ROOT);
245
- const authMode = apiKey ? 'user_api_key' : 'configured_doc_credentials';
246
- const maxBackoffMs = Math.max(options.maxBackoffMs, options.intervalMs);
247
- let lastFullSyncAt = 0;
248
- let consecutiveFailures = 0;
249
- let emptyPolls = 0;
250
-
251
- console.log('watch: starting CommentFS read-only projection sync');
252
- console.log(`root: ${rootDir}`);
253
- console.log(`base_url: ${apiKey ? baseUrl : 'per-doc manifest URLs'}`);
254
- console.log(`auth_mode: ${authMode}`);
255
- console.log(`interval: ${formatDuration(options.intervalMs)}`);
256
- console.log(`full_reconcile_interval: ${apiKey ? formatDuration(options.fullIntervalMs) : 'not used without a user API key'}`);
257
- console.log(`max_backoff: ${formatDuration(maxBackoffMs)}`);
258
-
259
- do {
260
- try {
261
- const full = Boolean(apiKey && (lastFullSyncAt === 0 || Date.now() - lastFullSyncAt >= options.fullIntervalMs));
262
- const results = apiKey
263
- ? await syncRemoteSelectedCommentDocsSettled({
264
- rootDir,
265
- baseUrl,
266
- userApiKey: apiKey,
267
- full,
268
- })
269
- : await syncConfiguredCommentDocsSettled({ rootDir });
270
- if (full) lastFullSyncAt = Date.now();
271
-
272
- const failures = results.filter((result) => !result.ok).length;
273
- if (failures === 0) consecutiveFailures = 0;
274
- else consecutiveFailures += 1;
275
-
276
- if (results.length === 0) {
277
- emptyPolls += 1;
278
- if (emptyPolls === 1 || emptyPolls % 6 === 0 || options.once) {
279
- console.log(`${watchStamp()} ${apiKey
280
- ? 'No remote Comment.io sync changes.'
281
- : 'No Comment.io docs configured. Run `comment sync add <doc-url-or-slug>` first.'}`);
282
- }
283
- } else {
284
- emptyPolls = 0;
285
- console.log(`${watchStamp()} ${full ? 'full reconcile' : 'poll'} returned ${results.length} result${results.length === 1 ? '' : 's'}.`);
286
- for (const result of results) printResult(result);
287
- }
288
-
289
- if (options.once) {
290
- if (failures > 0) process.exitCode = 1;
291
- return;
292
- }
293
- await sleep(nextWatchDelay(options.intervalMs, maxBackoffMs, consecutiveFailures));
294
- } catch (error) {
295
- consecutiveFailures += 1;
296
- const delayMs = nextWatchDelay(options.intervalMs, maxBackoffMs, consecutiveFailures);
297
- console.error(`${watchStamp()} sync poll failed: ${error instanceof Error ? error.message : String(error)}`);
298
- if (options.once) {
299
- process.exitCode = 1;
300
- return;
301
- }
302
- console.error(`${watchStamp()} retrying in ${formatDuration(delayMs)}.`);
303
- await sleep(delayMs);
304
- }
305
- } while (true);
306
- }
307
-
308
- function parseIntervalMs(value: string): number {
309
- const trimmed = value.trim().toLowerCase();
310
- const match = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(trimmed);
311
- if (!match) throw new Error(`Invalid interval: ${value}`);
312
- const amount = Number(match[1]);
313
- const unit = match[2] ?? 's';
314
- const ms = unit === 'm' ? amount * 60_000 : unit === 's' ? amount * 1000 : amount;
315
- if (!Number.isFinite(ms) || ms < 100) throw new Error('Interval must be at least 100ms.');
316
- return Math.round(ms);
317
- }
318
-
319
- function nextWatchDelay(intervalMs: number, maxBackoffMs: number, consecutiveFailures: number): number {
320
- if (consecutiveFailures <= 0) return intervalMs;
321
- const multiplier = 2 ** Math.min(consecutiveFailures - 1, 6);
322
- return Math.min(maxBackoffMs, Math.max(intervalMs, intervalMs * multiplier));
323
- }
324
-
325
- function formatDuration(ms: number): string {
326
- if (ms % 60_000 === 0) return `${ms / 60_000}m`;
327
- if (ms % 1000 === 0) return `${ms / 1000}s`;
328
- return `${ms}ms`;
329
- }
330
-
331
- function watchStamp(): string {
332
- return `[${new Date().toISOString()}]`;
333
- }
334
-
335
- function sleep(ms: number): Promise<void> {
336
- return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
337
- }
338
-
339
- main().catch((error) => {
340
- console.error(error instanceof Error ? error.message : String(error));
341
- process.exit(1);
342
- });