contextloop-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2253 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/lib/api-client.ts
7
+ import { ofetch, FetchError } from "ofetch";
8
+
9
+ // src/lib/errors.ts
10
+ var CLIError = class extends Error {
11
+ code;
12
+ hint;
13
+ constructor(code, message, hint) {
14
+ super(message);
15
+ this.name = "CLIError";
16
+ this.code = code;
17
+ this.hint = hint;
18
+ }
19
+ /**
20
+ * Get exit code for this error
21
+ */
22
+ getExitCode() {
23
+ const exitCodes = {
24
+ FILE_NOT_FOUND: 4,
25
+ FILE_READ_ERROR: 10,
26
+ FILE_WRITE_ERROR: 10,
27
+ STDIN_ERROR: 10,
28
+ CONFIG_ERROR: 6,
29
+ KEYCHAIN_ERROR: 2,
30
+ NETWORK_ERROR: 1,
31
+ AUTH_EXPIRED: 2,
32
+ PROJECT_NOT_SET: 6,
33
+ TIMEOUT: 12,
34
+ FILE_TOO_LARGE: 13
35
+ };
36
+ return exitCodes[this.code] ?? 1;
37
+ }
38
+ };
39
+ var APIError = class extends Error {
40
+ code;
41
+ details;
42
+ hint;
43
+ constructor(code, message, details, hint) {
44
+ super(message);
45
+ this.name = "APIError";
46
+ this.code = code;
47
+ this.details = details;
48
+ this.hint = hint;
49
+ }
50
+ /**
51
+ * Get exit code for this error
52
+ */
53
+ getExitCode() {
54
+ const exitCodes = {
55
+ UNAUTHORIZED: 2,
56
+ FORBIDDEN: 3,
57
+ NOT_FOUND: 4,
58
+ VALIDATION_ERROR: 5,
59
+ CONFLICT: 5,
60
+ RATE_LIMITED: 11,
61
+ INTERNAL_ERROR: 1
62
+ };
63
+ return exitCodes[this.code] ?? 1;
64
+ }
65
+ /**
66
+ * Get helpful hint based on error code
67
+ */
68
+ static getDefaultHint(code) {
69
+ const hints = {
70
+ UNAUTHORIZED: "Run 'contextloop auth login' to authenticate",
71
+ NOT_FOUND: "Use 'contextloop document list' to see available documents",
72
+ RATE_LIMITED: "Rate limit exceeded. Try again in a few seconds."
73
+ };
74
+ return hints[code];
75
+ }
76
+ };
77
+ function getExitCode(error) {
78
+ if (error instanceof CLIError || error instanceof APIError) {
79
+ return error.getExitCode();
80
+ }
81
+ return 1;
82
+ }
83
+ function formatError(error, format = "text") {
84
+ if (error instanceof CLIError || error instanceof APIError) {
85
+ if (format === "json") {
86
+ return JSON.stringify(
87
+ {
88
+ error: {
89
+ code: error.code,
90
+ message: error.message,
91
+ hint: error.hint,
92
+ ...error instanceof APIError && error.details ? { details: error.details } : {}
93
+ }
94
+ },
95
+ null,
96
+ 2
97
+ );
98
+ }
99
+ let output = `Error: ${error.message}`;
100
+ if (error.code) {
101
+ output += ` (${error.code})`;
102
+ }
103
+ if (error.hint) {
104
+ output += `
105
+ Hint: ${error.hint}`;
106
+ }
107
+ return output;
108
+ }
109
+ if (error instanceof Error) {
110
+ if (format === "json") {
111
+ return JSON.stringify(
112
+ {
113
+ error: {
114
+ code: "UNKNOWN_ERROR",
115
+ message: error.message
116
+ }
117
+ },
118
+ null,
119
+ 2
120
+ );
121
+ }
122
+ return `Error: ${error.message}`;
123
+ }
124
+ return `Error: ${String(error)}`;
125
+ }
126
+
127
+ // src/lib/credentials.ts
128
+ import * as fs from "fs";
129
+ import * as path from "path";
130
+ import * as crypto2 from "crypto";
131
+ import * as os from "os";
132
+ var SERVICE_NAME = "contextloop-cli";
133
+ var CREDENTIALS_FILENAME = "credentials.enc";
134
+ var KEY_API_KEY = "api-key";
135
+ var KEY_ACCESS_TOKEN = "access-token";
136
+ var KEY_REFRESH_TOKEN = "refresh-token";
137
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
138
+ var KEY_LENGTH = 32;
139
+ var IV_LENGTH = 16;
140
+ var AUTH_TAG_LENGTH = 16;
141
+ function getConfigDir() {
142
+ const home = os.homedir();
143
+ if (process.platform === "win32") {
144
+ return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "contextloop-cli");
145
+ }
146
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "contextloop-cli");
147
+ }
148
+ function getMachineKey() {
149
+ const machineId = `${os.hostname()}-${os.userInfo().username}-${SERVICE_NAME}`;
150
+ return crypto2.scryptSync(machineId, "contextloop-cli-salt", KEY_LENGTH);
151
+ }
152
+ function encrypt(data) {
153
+ const key = getMachineKey();
154
+ const iv = crypto2.randomBytes(IV_LENGTH);
155
+ const cipher = crypto2.createCipheriv(ENCRYPTION_ALGORITHM, key, iv);
156
+ const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
157
+ const authTag = cipher.getAuthTag();
158
+ return Buffer.concat([iv, authTag, encrypted]);
159
+ }
160
+ function decrypt(data) {
161
+ const key = getMachineKey();
162
+ const iv = data.subarray(0, IV_LENGTH);
163
+ const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
164
+ const encrypted = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
165
+ const decipher = crypto2.createDecipheriv(ENCRYPTION_ALGORITHM, key, iv);
166
+ decipher.setAuthTag(authTag);
167
+ return decipher.update(encrypted) + decipher.final("utf8");
168
+ }
169
+ var KeyringBackend = class {
170
+ name = "keyring";
171
+ Entry = null;
172
+ initialized = false;
173
+ available = false;
174
+ async init() {
175
+ if (this.initialized) return this.available;
176
+ this.initialized = true;
177
+ try {
178
+ const keyring = await import("@napi-rs/keyring");
179
+ this.Entry = keyring.Entry;
180
+ const testEntry = new this.Entry(SERVICE_NAME, "__test__");
181
+ try {
182
+ testEntry.getPassword();
183
+ } catch {
184
+ }
185
+ this.available = true;
186
+ } catch {
187
+ this.available = false;
188
+ }
189
+ return this.available;
190
+ }
191
+ async get(key) {
192
+ if (!await this.init() || !this.Entry) return null;
193
+ try {
194
+ const entry = new this.Entry(SERVICE_NAME, key);
195
+ return entry.getPassword();
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ async set(key, value) {
201
+ if (!await this.init() || !this.Entry) {
202
+ throw new Error("Keyring not available");
203
+ }
204
+ const entry = new this.Entry(SERVICE_NAME, key);
205
+ entry.setPassword(value);
206
+ }
207
+ async delete(key) {
208
+ if (!await this.init() || !this.Entry) return;
209
+ try {
210
+ const entry = new this.Entry(SERVICE_NAME, key);
211
+ entry.deletePassword();
212
+ } catch {
213
+ }
214
+ }
215
+ async isAvailable() {
216
+ return this.init();
217
+ }
218
+ };
219
+ var EncryptedFileBackend = class {
220
+ name = "encrypted-file";
221
+ configDir;
222
+ filePath;
223
+ constructor() {
224
+ this.configDir = getConfigDir();
225
+ this.filePath = path.join(this.configDir, CREDENTIALS_FILENAME);
226
+ }
227
+ readStore() {
228
+ try {
229
+ if (!fs.existsSync(this.filePath)) return {};
230
+ const encrypted = fs.readFileSync(this.filePath);
231
+ const decrypted = decrypt(encrypted);
232
+ return JSON.parse(decrypted);
233
+ } catch {
234
+ return {};
235
+ }
236
+ }
237
+ writeStore(store) {
238
+ if (!fs.existsSync(this.configDir)) {
239
+ fs.mkdirSync(this.configDir, { recursive: true, mode: 448 });
240
+ }
241
+ const encrypted = encrypt(JSON.stringify(store));
242
+ fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
243
+ }
244
+ async get(key) {
245
+ const store = this.readStore();
246
+ return store[key] ?? null;
247
+ }
248
+ async set(key, value) {
249
+ const store = this.readStore();
250
+ store[key] = value;
251
+ this.writeStore(store);
252
+ }
253
+ async delete(key) {
254
+ const store = this.readStore();
255
+ delete store[key];
256
+ if (Object.keys(store).length === 0) {
257
+ try {
258
+ fs.unlinkSync(this.filePath);
259
+ } catch {
260
+ }
261
+ } else {
262
+ this.writeStore(store);
263
+ }
264
+ }
265
+ };
266
+ var keyringBackend = null;
267
+ var fileBackend = null;
268
+ var activeBackend = null;
269
+ var backendInitialized = false;
270
+ async function getBackend() {
271
+ if (backendInitialized && activeBackend) {
272
+ return activeBackend;
273
+ }
274
+ keyringBackend = new KeyringBackend();
275
+ fileBackend = new EncryptedFileBackend();
276
+ if (await keyringBackend.isAvailable()) {
277
+ activeBackend = keyringBackend;
278
+ } else {
279
+ activeBackend = fileBackend;
280
+ }
281
+ backendInitialized = true;
282
+ return activeBackend;
283
+ }
284
+ async function storeApiKey(apiKey) {
285
+ try {
286
+ const backend = await getBackend();
287
+ await backend.set(KEY_API_KEY, apiKey);
288
+ } catch (error) {
289
+ throw new CLIError(
290
+ "KEYCHAIN_ERROR",
291
+ `Failed to store API key: ${error instanceof Error ? error.message : "Unknown error"}. You can use the CONTEXTLOOP_API_KEY environment variable instead.`
292
+ );
293
+ }
294
+ }
295
+ async function getApiKey() {
296
+ if (process.env.CONTEXTLOOP_API_KEY) {
297
+ return process.env.CONTEXTLOOP_API_KEY;
298
+ }
299
+ try {
300
+ const backend = await getBackend();
301
+ return await backend.get(KEY_API_KEY);
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+ async function storeTokens(accessToken, refreshToken) {
307
+ try {
308
+ const backend = await getBackend();
309
+ await backend.set(KEY_ACCESS_TOKEN, accessToken);
310
+ if (refreshToken) {
311
+ await backend.set(KEY_REFRESH_TOKEN, refreshToken);
312
+ }
313
+ } catch (error) {
314
+ throw new CLIError(
315
+ "KEYCHAIN_ERROR",
316
+ `Failed to store tokens: ${error instanceof Error ? error.message : "Unknown error"}`
317
+ );
318
+ }
319
+ }
320
+ async function getTokens() {
321
+ try {
322
+ const backend = await getBackend();
323
+ const accessToken = await backend.get(KEY_ACCESS_TOKEN);
324
+ const refreshToken = await backend.get(KEY_REFRESH_TOKEN);
325
+ return { accessToken, refreshToken };
326
+ } catch {
327
+ return { accessToken: null, refreshToken: null };
328
+ }
329
+ }
330
+ async function getCredentials() {
331
+ const apiKey = await getApiKey();
332
+ const { accessToken, refreshToken } = await getTokens();
333
+ return {
334
+ apiKey: apiKey ?? void 0,
335
+ accessToken: accessToken ?? void 0,
336
+ refreshToken: refreshToken ?? void 0
337
+ };
338
+ }
339
+ async function clearCredentials() {
340
+ try {
341
+ const backend = await getBackend();
342
+ await backend.delete(KEY_API_KEY);
343
+ await backend.delete(KEY_ACCESS_TOKEN);
344
+ await backend.delete(KEY_REFRESH_TOKEN);
345
+ } catch {
346
+ }
347
+ }
348
+
349
+ // src/lib/api-client.ts
350
+ var RATE_LIMIT_CONFIG = {
351
+ initialDelayMs: 1e3,
352
+ maxDelayMs: 6e4,
353
+ maxRetries: 5,
354
+ backoffMultiplier: 2,
355
+ jitterPercent: 0.1
356
+ };
357
+ function createAPIClient(config2, logger) {
358
+ const baseURL = config2.apiUrl.replace(/\/$/, "");
359
+ async function getAuthHeader() {
360
+ const apiKey = await getApiKey();
361
+ if (apiKey) {
362
+ return `Bearer ${apiKey}`;
363
+ }
364
+ const { accessToken } = await getTokens();
365
+ if (accessToken) {
366
+ return `Bearer ${accessToken}`;
367
+ }
368
+ return null;
369
+ }
370
+ function calculateRetryDelay(attempt, retryAfter) {
371
+ if (retryAfter) {
372
+ return retryAfter * 1e3;
373
+ }
374
+ const delay = Math.min(
375
+ RATE_LIMIT_CONFIG.initialDelayMs * Math.pow(RATE_LIMIT_CONFIG.backoffMultiplier, attempt),
376
+ RATE_LIMIT_CONFIG.maxDelayMs
377
+ );
378
+ const jitter = delay * RATE_LIMIT_CONFIG.jitterPercent * (Math.random() * 2 - 1);
379
+ return Math.floor(delay + jitter);
380
+ }
381
+ function sleep(ms) {
382
+ return new Promise((resolve) => setTimeout(resolve, ms));
383
+ }
384
+ async function request(method, path4, options = {}) {
385
+ const authHeader = await getAuthHeader();
386
+ const headers = {
387
+ "Content-Type": "application/json"
388
+ };
389
+ if (authHeader) {
390
+ headers["Authorization"] = authHeader;
391
+ }
392
+ const query = options.query ? Object.fromEntries(
393
+ Object.entries(options.query).filter(([, v]) => v !== void 0)
394
+ ) : void 0;
395
+ let lastError = null;
396
+ for (let attempt = 0; attempt <= RATE_LIMIT_CONFIG.maxRetries; attempt++) {
397
+ try {
398
+ logger.debug(`${method} ${path4}`, { query, attempt });
399
+ const response = await ofetch(`${baseURL}${path4}`, {
400
+ method,
401
+ headers,
402
+ body: options.body,
403
+ query,
404
+ timeout: 3e4
405
+ });
406
+ return response;
407
+ } catch (error) {
408
+ if (error instanceof FetchError) {
409
+ const status = error.response?.status;
410
+ const data = error.data;
411
+ if (status === 429) {
412
+ if (attempt < RATE_LIMIT_CONFIG.maxRetries) {
413
+ const retryAfter = error.response?.headers.get("Retry-After");
414
+ const delay = calculateRetryDelay(
415
+ attempt,
416
+ retryAfter ? parseInt(retryAfter, 10) : void 0
417
+ );
418
+ logger.warn(
419
+ `Rate limited. Retrying in ${Math.round(delay / 1e3)}s... (attempt ${attempt + 1}/${RATE_LIMIT_CONFIG.maxRetries})`
420
+ );
421
+ await sleep(delay);
422
+ continue;
423
+ }
424
+ throw new APIError(
425
+ "RATE_LIMITED",
426
+ "Rate limit exceeded. Please try again later.",
427
+ void 0,
428
+ `Try again in ${RATE_LIMIT_CONFIG.maxDelayMs / 1e3} seconds.`
429
+ );
430
+ }
431
+ const errorCode = mapStatusToErrorCode(status);
432
+ const message = data?.error?.message || error.message || "Request failed";
433
+ const hint = APIError.getDefaultHint(errorCode);
434
+ throw new APIError(errorCode, message, data?.error?.details, hint);
435
+ }
436
+ lastError = error instanceof Error ? error : new Error(String(error));
437
+ }
438
+ }
439
+ throw lastError || new Error("Request failed after retries");
440
+ }
441
+ function mapStatusToErrorCode(status) {
442
+ switch (status) {
443
+ case 401:
444
+ return "UNAUTHORIZED";
445
+ case 403:
446
+ return "FORBIDDEN";
447
+ case 404:
448
+ return "NOT_FOUND";
449
+ case 400:
450
+ case 422:
451
+ return "VALIDATION_ERROR";
452
+ case 409:
453
+ return "CONFLICT";
454
+ case 429:
455
+ return "RATE_LIMITED";
456
+ default:
457
+ return "INTERNAL_ERROR";
458
+ }
459
+ }
460
+ return {
461
+ // Auth methods
462
+ async validateApiKey(apiKey) {
463
+ const response = await ofetch(`${baseURL}/cli/auth/me`, {
464
+ headers: {
465
+ Authorization: `Bearer ${apiKey}`
466
+ }
467
+ });
468
+ return response;
469
+ },
470
+ async getCurrentUser() {
471
+ return request("GET", "/cli/auth/me");
472
+ },
473
+ async exchangeToken(apiKey) {
474
+ return request("POST", "/cli/auth/token", {
475
+ body: { apiKey }
476
+ });
477
+ },
478
+ // Project methods
479
+ async listProjects(params) {
480
+ return request("GET", "/cli/projects", {
481
+ query: {
482
+ page: params?.page,
483
+ pageSize: params?.pageSize
484
+ }
485
+ });
486
+ },
487
+ async getProject(projectId) {
488
+ return request("GET", `/cli/projects/${projectId}`);
489
+ },
490
+ // Document methods
491
+ async listDocuments(projectId, params) {
492
+ return request(
493
+ "GET",
494
+ `/cli/projects/${projectId}/documents`,
495
+ {
496
+ query: {
497
+ page: params?.page,
498
+ pageSize: params?.pageSize,
499
+ tree: params?.tree,
500
+ path: params?.path
501
+ }
502
+ }
503
+ );
504
+ },
505
+ async getDocument(projectId, documentId) {
506
+ return request(
507
+ "GET",
508
+ `/cli/projects/${projectId}/documents/${documentId}`
509
+ );
510
+ },
511
+ async getDocumentByPath(projectId, path4) {
512
+ return request("GET", `/cli/projects/${projectId}/documents`, {
513
+ query: { path: path4 }
514
+ });
515
+ },
516
+ async createDocument(projectId, data) {
517
+ return request("POST", `/cli/projects/${projectId}/documents`, {
518
+ body: data
519
+ });
520
+ },
521
+ async updateDocument(projectId, documentId, data) {
522
+ return request(
523
+ "PUT",
524
+ `/cli/projects/${projectId}/documents/${documentId}`,
525
+ { body: data }
526
+ );
527
+ },
528
+ async deleteDocument(projectId, documentId) {
529
+ await request(
530
+ "DELETE",
531
+ `/cli/projects/${projectId}/documents/${documentId}`
532
+ );
533
+ },
534
+ async listDocumentVersions(projectId, documentId) {
535
+ return request(
536
+ "GET",
537
+ `/cli/projects/${projectId}/documents/${documentId}/versions`
538
+ );
539
+ },
540
+ // Context methods
541
+ async listContext(projectId, params) {
542
+ return request(
543
+ "GET",
544
+ `/cli/projects/${projectId}/context`,
545
+ {
546
+ query: {
547
+ page: params?.page,
548
+ pageSize: params?.pageSize
549
+ }
550
+ }
551
+ );
552
+ },
553
+ async getContext(projectId, contextId) {
554
+ return request(
555
+ "GET",
556
+ `/cli/projects/${projectId}/context/${contextId}`
557
+ );
558
+ },
559
+ async createContext(projectId, data) {
560
+ return request("POST", `/cli/projects/${projectId}/context`, {
561
+ body: data
562
+ });
563
+ },
564
+ async deleteContext(projectId, contextId) {
565
+ await request(
566
+ "DELETE",
567
+ `/cli/projects/${projectId}/context/${contextId}`
568
+ );
569
+ },
570
+ // Comment methods
571
+ async listComments(projectId, documentId, params) {
572
+ return request(
573
+ "GET",
574
+ `/cli/projects/${projectId}/documents/${documentId}/comments`,
575
+ {
576
+ query: {
577
+ page: params?.page,
578
+ pageSize: params?.pageSize
579
+ }
580
+ }
581
+ );
582
+ },
583
+ async createComment(projectId, documentId, data) {
584
+ return request(
585
+ "POST",
586
+ `/cli/projects/${projectId}/documents/${documentId}/comments`,
587
+ { body: data }
588
+ );
589
+ },
590
+ async resolveComment(projectId, commentId) {
591
+ return request(
592
+ "PATCH",
593
+ `/cli/projects/${projectId}/comments/${commentId}/resolve`
594
+ );
595
+ }
596
+ };
597
+ }
598
+
599
+ // src/lib/config.ts
600
+ import Conf from "conf";
601
+ var DEFAULT_API_URL = "https://contextloop.io/api";
602
+ var LOCALHOST_URL = "http://localhost:3000/api";
603
+ var schema = {
604
+ apiUrl: {
605
+ type: "string",
606
+ default: DEFAULT_API_URL
607
+ },
608
+ defaultProjectId: {
609
+ type: "string"
610
+ },
611
+ defaultProjectSlug: {
612
+ type: "string"
613
+ },
614
+ tokenExpiry: {
615
+ type: "number"
616
+ }
617
+ };
618
+ var conf = new Conf({
619
+ projectName: "contextloop-cli",
620
+ schema,
621
+ defaults: {
622
+ apiUrl: DEFAULT_API_URL
623
+ }
624
+ });
625
+ function resolveApiUrl(envUrl) {
626
+ const url = envUrl || conf.get("apiUrl") || DEFAULT_API_URL;
627
+ if (url === "local" || url === "localhost") {
628
+ return LOCALHOST_URL;
629
+ }
630
+ return url;
631
+ }
632
+ function loadConfig() {
633
+ const config2 = {
634
+ apiUrl: resolveApiUrl(process.env.CONTEXTLOOP_API_URL),
635
+ defaultProjectId: process.env.CONTEXTLOOP_PROJECT_ID || conf.get("defaultProjectId"),
636
+ defaultProjectSlug: conf.get("defaultProjectSlug"),
637
+ tokenExpiry: conf.get("tokenExpiry")
638
+ };
639
+ return config2;
640
+ }
641
+ function saveConfig(updates) {
642
+ if (updates.apiUrl !== void 0) {
643
+ const url = updates.apiUrl === "local" || updates.apiUrl === "localhost" ? LOCALHOST_URL : updates.apiUrl;
644
+ conf.set("apiUrl", url);
645
+ }
646
+ if (updates.defaultProjectId !== void 0) {
647
+ conf.set("defaultProjectId", updates.defaultProjectId);
648
+ }
649
+ if (updates.defaultProjectSlug !== void 0) {
650
+ conf.set("defaultProjectSlug", updates.defaultProjectSlug);
651
+ }
652
+ if (updates.tokenExpiry !== void 0) {
653
+ conf.set("tokenExpiry", updates.tokenExpiry);
654
+ }
655
+ }
656
+ function clearConfig(keys) {
657
+ for (const key of keys) {
658
+ conf.delete(key);
659
+ }
660
+ }
661
+
662
+ // src/lib/logger.ts
663
+ import chalk from "chalk";
664
+ function createLogger(options) {
665
+ const isQuiet = options.quiet ?? false;
666
+ const isVerbose = options.verbose ?? false;
667
+ const useColor = options.color !== false;
668
+ const colorize = (fn, str) => {
669
+ return useColor ? fn(str) : str;
670
+ };
671
+ return {
672
+ info(message, ...args) {
673
+ if (!isQuiet) {
674
+ console.log(message, ...args);
675
+ }
676
+ },
677
+ warn(message, ...args) {
678
+ if (!isQuiet) {
679
+ console.warn(colorize(chalk.yellow, `Warning: ${message}`), ...args);
680
+ }
681
+ },
682
+ error(message, ...args) {
683
+ console.error(colorize(chalk.red, `Error: ${message}`), ...args);
684
+ },
685
+ debug(message, ...args) {
686
+ if (isVerbose) {
687
+ console.debug(colorize(chalk.gray, `[debug] ${message}`), ...args);
688
+ }
689
+ }
690
+ };
691
+ }
692
+
693
+ // src/commands/auth.ts
694
+ import chalk4 from "chalk";
695
+ import ora from "ora";
696
+
697
+ // src/lib/auth.ts
698
+ import http from "http";
699
+ import { URL } from "url";
700
+ import open from "open";
701
+ import chalk2 from "chalk";
702
+ var CALLBACK_PORT = 54320;
703
+ var CALLBACK_PATH = "/callback";
704
+ var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
705
+ var OAUTH_CLIENT_ID = "contextloop-cli";
706
+ function startCallbackServer(expectedState, logger) {
707
+ return new Promise((resolve, reject) => {
708
+ const server = http.createServer((req, res) => {
709
+ const url = new URL(req.url || "/", `http://localhost:${CALLBACK_PORT}`);
710
+ if (url.pathname !== CALLBACK_PATH) {
711
+ res.writeHead(404);
712
+ res.end("Not found");
713
+ return;
714
+ }
715
+ const code = url.searchParams.get("code");
716
+ const state = url.searchParams.get("state");
717
+ const error = url.searchParams.get("error");
718
+ const errorDescription = url.searchParams.get("error_description");
719
+ if (error) {
720
+ res.writeHead(400, { "Content-Type": "text/html" });
721
+ res.end(`
722
+ <html>
723
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
724
+ <h1 style="color: #ef4444;">Authentication Failed</h1>
725
+ <p>${errorDescription || error}</p>
726
+ <p>You can close this window.</p>
727
+ </body>
728
+ </html>
729
+ `);
730
+ server.close();
731
+ reject(
732
+ new CLIError(
733
+ "AUTH_EXPIRED",
734
+ errorDescription || error || "Authentication failed"
735
+ )
736
+ );
737
+ return;
738
+ }
739
+ if (!code) {
740
+ res.writeHead(400, { "Content-Type": "text/html" });
741
+ res.end(`
742
+ <html>
743
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
744
+ <h1 style="color: #ef4444;">Missing Authorization Code</h1>
745
+ <p>The callback did not include an authorization code.</p>
746
+ <p>You can close this window.</p>
747
+ </body>
748
+ </html>
749
+ `);
750
+ server.close();
751
+ reject(new CLIError("AUTH_EXPIRED", "Missing authorization code"));
752
+ return;
753
+ }
754
+ if (state !== expectedState) {
755
+ res.writeHead(400, { "Content-Type": "text/html" });
756
+ res.end(`
757
+ <html>
758
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
759
+ <h1 style="color: #ef4444;">Invalid State</h1>
760
+ <p>The state parameter did not match. This may be a security issue.</p>
761
+ <p>You can close this window.</p>
762
+ </body>
763
+ </html>
764
+ `);
765
+ server.close();
766
+ reject(new CLIError("AUTH_EXPIRED", "Invalid state parameter"));
767
+ return;
768
+ }
769
+ res.writeHead(200, { "Content-Type": "text/html" });
770
+ res.end(`
771
+ <html>
772
+ <body style="font-family: sans-serif; text-align: center; padding: 50px;">
773
+ <h1 style="color: #22c55e;">Authentication Successful!</h1>
774
+ <p>You can close this window and return to the CLI.</p>
775
+ </body>
776
+ </html>
777
+ `);
778
+ server.close();
779
+ resolve({ code, state: state || void 0 });
780
+ });
781
+ server.on("error", (err) => {
782
+ logger.debug(`Callback server error: ${err.message}`);
783
+ reject(
784
+ new CLIError(
785
+ "NETWORK_ERROR",
786
+ `Failed to start callback server: ${err.message}`
787
+ )
788
+ );
789
+ });
790
+ const timeout = setTimeout(() => {
791
+ server.close();
792
+ reject(
793
+ new CLIError(
794
+ "TIMEOUT",
795
+ "Authentication timed out. Please try again.",
796
+ "Run 'contextloop auth login' to try again"
797
+ )
798
+ );
799
+ }, AUTH_TIMEOUT_MS);
800
+ server.on("close", () => {
801
+ clearTimeout(timeout);
802
+ });
803
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
804
+ logger.debug(`Callback server listening on port ${CALLBACK_PORT}`);
805
+ });
806
+ });
807
+ }
808
+ function generateState() {
809
+ const array = new Uint8Array(32);
810
+ crypto.getRandomValues(array);
811
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
812
+ }
813
+ async function generatePKCE() {
814
+ const array = new Uint8Array(32);
815
+ crypto.getRandomValues(array);
816
+ const verifier = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join(
817
+ ""
818
+ );
819
+ const encoder = new TextEncoder();
820
+ const data = encoder.encode(verifier);
821
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
822
+ const hashArray = new Uint8Array(hashBuffer);
823
+ const challenge = btoa(String.fromCharCode(...hashArray)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
824
+ return { verifier, challenge };
825
+ }
826
+ async function interactiveLogin(config2, logger) {
827
+ const state = generateState();
828
+ const { verifier, challenge } = await generatePKCE();
829
+ const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
830
+ const baseUrl = config2.apiUrl.replace(/\/api$/, "");
831
+ const authUrl = new URL(`${config2.apiUrl}/oauth/authorize`);
832
+ authUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
833
+ authUrl.searchParams.set("redirect_uri", redirectUri);
834
+ authUrl.searchParams.set("response_type", "code");
835
+ authUrl.searchParams.set("state", state);
836
+ authUrl.searchParams.set("code_challenge", challenge);
837
+ authUrl.searchParams.set("code_challenge_method", "S256");
838
+ authUrl.searchParams.set("resource", `${baseUrl}/mcp/http`);
839
+ console.log(chalk2.cyan("Opening browser for authentication..."));
840
+ console.log(
841
+ chalk2.gray(`If the browser doesn't open, visit: ${authUrl.toString()}`)
842
+ );
843
+ console.log();
844
+ const serverPromise = startCallbackServer(state, logger);
845
+ try {
846
+ await open(authUrl.toString());
847
+ } catch (err) {
848
+ logger.debug(`Failed to open browser: ${err}`);
849
+ console.log(chalk2.yellow("Could not open browser automatically."));
850
+ console.log(`Please visit this URL manually:
851
+ ${authUrl.toString()}`);
852
+ }
853
+ console.log(chalk2.gray("Waiting for authentication..."));
854
+ const { code } = await serverPromise;
855
+ console.log(chalk2.gray("Exchanging authorization code for tokens..."));
856
+ const tokenUrl = `${config2.apiUrl}/oauth/token`;
857
+ const formData = new URLSearchParams({
858
+ grant_type: "authorization_code",
859
+ code,
860
+ redirect_uri: redirectUri,
861
+ client_id: OAUTH_CLIENT_ID,
862
+ code_verifier: verifier
863
+ });
864
+ const response = await fetch(tokenUrl, {
865
+ method: "POST",
866
+ headers: {
867
+ "Content-Type": "application/x-www-form-urlencoded"
868
+ },
869
+ body: formData.toString()
870
+ });
871
+ if (!response.ok) {
872
+ const error = await response.json().catch(() => ({}));
873
+ throw new CLIError(
874
+ "AUTH_EXPIRED",
875
+ error.error_description || error.error || "Failed to exchange authorization code",
876
+ "Run 'contextloop auth login' to try again"
877
+ );
878
+ }
879
+ const tokens = await response.json();
880
+ await storeTokens(tokens.access_token, tokens.refresh_token);
881
+ if (tokens.project_id) {
882
+ saveConfig({ defaultProjectId: tokens.project_id });
883
+ console.log(chalk2.green("Successfully authenticated!"));
884
+ console.log(chalk2.gray(`Default project set to: ${tokens.project_id}`));
885
+ } else {
886
+ console.log(chalk2.green("Successfully authenticated!"));
887
+ }
888
+ }
889
+ async function manualLogin(config2, logger) {
890
+ const state = generateState();
891
+ const { verifier, challenge } = await generatePKCE();
892
+ const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
893
+ const baseUrl = config2.apiUrl.replace(/\/api$/, "");
894
+ const authUrl = new URL(`${config2.apiUrl}/oauth/authorize`);
895
+ authUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
896
+ authUrl.searchParams.set("redirect_uri", redirectUri);
897
+ authUrl.searchParams.set("response_type", "code");
898
+ authUrl.searchParams.set("state", state);
899
+ authUrl.searchParams.set("code_challenge", challenge);
900
+ authUrl.searchParams.set("code_challenge_method", "S256");
901
+ authUrl.searchParams.set("resource", `${baseUrl}/mcp/http`);
902
+ console.log(chalk2.cyan("Manual authentication mode"));
903
+ console.log();
904
+ console.log("1. Visit this URL in a browser:");
905
+ console.log(chalk2.underline(authUrl.toString()));
906
+ console.log();
907
+ console.log(
908
+ `2. After authenticating, paste the full callback URL (starting with http://localhost:${CALLBACK_PORT}/callback?code=...)`
909
+ );
910
+ console.log();
911
+ const readline = await import("readline");
912
+ const rl = readline.createInterface({
913
+ input: process.stdin,
914
+ output: process.stdout
915
+ });
916
+ const callbackUrl = await new Promise((resolve) => {
917
+ rl.question("Callback URL: ", (answer) => {
918
+ rl.close();
919
+ resolve(answer.trim());
920
+ });
921
+ });
922
+ let url;
923
+ try {
924
+ url = new URL(callbackUrl);
925
+ } catch {
926
+ throw new CLIError("AUTH_EXPIRED", "Invalid callback URL");
927
+ }
928
+ const code = url.searchParams.get("code");
929
+ const returnedState = url.searchParams.get("state");
930
+ if (!code) {
931
+ throw new CLIError("AUTH_EXPIRED", "Missing authorization code in callback URL");
932
+ }
933
+ if (returnedState !== state) {
934
+ throw new CLIError(
935
+ "AUTH_EXPIRED",
936
+ "State mismatch. Please try again with a fresh login."
937
+ );
938
+ }
939
+ console.log(chalk2.gray("Exchanging authorization code for tokens..."));
940
+ const tokenUrl = `${config2.apiUrl}/oauth/token`;
941
+ const formData = new URLSearchParams({
942
+ grant_type: "authorization_code",
943
+ code,
944
+ redirect_uri: redirectUri,
945
+ client_id: OAUTH_CLIENT_ID,
946
+ code_verifier: verifier
947
+ });
948
+ const response = await fetch(tokenUrl, {
949
+ method: "POST",
950
+ headers: {
951
+ "Content-Type": "application/x-www-form-urlencoded"
952
+ },
953
+ body: formData.toString()
954
+ });
955
+ if (!response.ok) {
956
+ const error = await response.json().catch(() => ({}));
957
+ throw new CLIError(
958
+ "AUTH_EXPIRED",
959
+ error.error_description || error.error || "Failed to exchange authorization code"
960
+ );
961
+ }
962
+ const tokens = await response.json();
963
+ await storeTokens(tokens.access_token, tokens.refresh_token);
964
+ if (tokens.project_id) {
965
+ saveConfig({ defaultProjectId: tokens.project_id });
966
+ console.log(chalk2.green("Successfully authenticated!"));
967
+ console.log(chalk2.gray(`Default project set to: ${tokens.project_id}`));
968
+ } else {
969
+ console.log(chalk2.green("Successfully authenticated!"));
970
+ }
971
+ }
972
+
973
+ // src/lib/format.ts
974
+ import chalk3 from "chalk";
975
+ function formatOutput(data, options, globalOptions) {
976
+ if (globalOptions.quiet) {
977
+ return "";
978
+ }
979
+ const format = globalOptions.format || "text";
980
+ if (format === "json") {
981
+ const jsonData = options.json ? options.json(data) : data;
982
+ return JSON.stringify(jsonData, null, 2);
983
+ }
984
+ return options.text(data);
985
+ }
986
+ function printOutput(data, options, globalOptions) {
987
+ const output = formatOutput(data, options, globalOptions);
988
+ if (output) {
989
+ console.log(output);
990
+ }
991
+ }
992
+ function formatTable(headers, rows, options = {}) {
993
+ const useColor = options.useColor !== false;
994
+ const widths = headers.map(
995
+ (h, i) => Math.max(
996
+ h.length,
997
+ ...rows.map((row) => (row[i] || "").length)
998
+ )
999
+ );
1000
+ const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
1001
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
1002
+ const dataRows = rows.map(
1003
+ (row) => row.map((cell, i) => (cell || "").padEnd(widths[i])).join(" ")
1004
+ ).join("\n");
1005
+ const colorizedHeader = useColor ? chalk3.bold(headerRow) : headerRow;
1006
+ const colorizedSeparator = useColor ? chalk3.gray(separator) : separator;
1007
+ return `${colorizedHeader}
1008
+ ${colorizedSeparator}
1009
+ ${dataRows}`;
1010
+ }
1011
+ function formatTree(items, indent = "") {
1012
+ const lines = [];
1013
+ items.forEach((item, index) => {
1014
+ const isLast = index === items.length - 1;
1015
+ const prefix = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1016
+ const childIndent = indent + (isLast ? " " : "\u2502 ");
1017
+ const name = item.isFolder ? `${item.name}/` : item.name;
1018
+ lines.push(`${indent}${prefix}${name}`);
1019
+ if (item.children && item.children.length > 0) {
1020
+ lines.push(
1021
+ formatTree(
1022
+ item.children,
1023
+ childIndent
1024
+ )
1025
+ );
1026
+ }
1027
+ });
1028
+ return lines.join("\n");
1029
+ }
1030
+ function formatBytes(bytes) {
1031
+ const units = ["B", "KB", "MB", "GB"];
1032
+ let size = bytes;
1033
+ let unitIndex = 0;
1034
+ while (size >= 1024 && unitIndex < units.length - 1) {
1035
+ size /= 1024;
1036
+ unitIndex++;
1037
+ }
1038
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
1039
+ }
1040
+ function formatRelativeTime(date) {
1041
+ const now = /* @__PURE__ */ new Date();
1042
+ const d = typeof date === "string" ? new Date(date) : date;
1043
+ const diffMs = now.getTime() - d.getTime();
1044
+ const diffSec = Math.floor(diffMs / 1e3);
1045
+ const diffMin = Math.floor(diffSec / 60);
1046
+ const diffHour = Math.floor(diffMin / 60);
1047
+ const diffDay = Math.floor(diffHour / 24);
1048
+ if (diffSec < 60) {
1049
+ return "just now";
1050
+ } else if (diffMin < 60) {
1051
+ return `${diffMin}m ago`;
1052
+ } else if (diffHour < 24) {
1053
+ return `${diffHour}h ago`;
1054
+ } else if (diffDay < 30) {
1055
+ return `${diffDay}d ago`;
1056
+ } else {
1057
+ return d.toLocaleDateString();
1058
+ }
1059
+ }
1060
+
1061
+ // src/commands/auth.ts
1062
+ function registerAuthCommands(program2, context2) {
1063
+ const auth = program2.command("auth").description("Authentication commands");
1064
+ auth.command("login").description("Authenticate with ContextLoop").option("--api-key <key>", "Login with an API key").option("--manual", "Use manual login flow (for environments without browser)").action(async (options) => {
1065
+ const globalOptions = program2.opts();
1066
+ try {
1067
+ if (options.apiKey) {
1068
+ const spinner = ora("Validating API key...").start();
1069
+ try {
1070
+ const { user } = await context2.apiClient.validateApiKey(options.apiKey);
1071
+ await storeApiKey(options.apiKey);
1072
+ spinner.succeed(`Logged in as ${chalk4.bold(user.name)} (${user.email})`);
1073
+ } catch (error) {
1074
+ spinner.fail("Login failed");
1075
+ throw error;
1076
+ }
1077
+ } else if (options.manual) {
1078
+ await manualLogin(context2.config, context2.logger);
1079
+ } else {
1080
+ await interactiveLogin(context2.config, context2.logger);
1081
+ }
1082
+ } catch (error) {
1083
+ console.error(formatError(error, globalOptions.format));
1084
+ process.exit(getExitCode(error));
1085
+ }
1086
+ });
1087
+ auth.command("logout").description("Clear stored credentials").action(async () => {
1088
+ const globalOptions = program2.opts();
1089
+ try {
1090
+ await clearCredentials();
1091
+ clearConfig(["defaultProjectId", "defaultProjectSlug"]);
1092
+ console.log(chalk4.green("Logged out successfully."));
1093
+ } catch (error) {
1094
+ console.error(formatError(error, globalOptions.format));
1095
+ process.exit(getExitCode(error));
1096
+ }
1097
+ });
1098
+ auth.command("whoami").description("Display current user information").action(async () => {
1099
+ const globalOptions = program2.opts();
1100
+ try {
1101
+ const creds = await getCredentials();
1102
+ if (!creds.apiKey && !creds.accessToken) {
1103
+ if (globalOptions.format === "json") {
1104
+ console.log(JSON.stringify({ loggedIn: false }));
1105
+ } else {
1106
+ console.log(chalk4.yellow("Not logged in."));
1107
+ console.log(
1108
+ chalk4.gray("Run 'contextloop auth login' to authenticate.")
1109
+ );
1110
+ }
1111
+ return;
1112
+ }
1113
+ const spinner = ora("Fetching user info...").start();
1114
+ try {
1115
+ const user = await context2.apiClient.getCurrentUser();
1116
+ spinner.stop();
1117
+ const defaultProjectId = context2.config.defaultProjectId;
1118
+ printOutput(
1119
+ user,
1120
+ {
1121
+ text: (u) => {
1122
+ const lines = [
1123
+ chalk4.bold(u.name),
1124
+ chalk4.gray(u.email),
1125
+ chalk4.gray(`ID: ${u.id}`)
1126
+ ];
1127
+ if (defaultProjectId) {
1128
+ lines.push("");
1129
+ lines.push(chalk4.gray(`Default project: ${defaultProjectId}`));
1130
+ }
1131
+ return lines.join("\n");
1132
+ },
1133
+ json: (u) => ({
1134
+ loggedIn: true,
1135
+ user: u,
1136
+ defaultProjectId: defaultProjectId || null
1137
+ })
1138
+ },
1139
+ globalOptions
1140
+ );
1141
+ } catch (error) {
1142
+ spinner.fail("Failed to fetch user info");
1143
+ throw error;
1144
+ }
1145
+ } catch (error) {
1146
+ console.error(formatError(error, globalOptions.format));
1147
+ process.exit(getExitCode(error));
1148
+ }
1149
+ });
1150
+ auth.command("refresh").description("Refresh authentication tokens").action(async () => {
1151
+ const globalOptions = program2.opts();
1152
+ try {
1153
+ const creds = await getCredentials();
1154
+ if (!creds.apiKey && !creds.accessToken) {
1155
+ throw new CLIError(
1156
+ "AUTH_EXPIRED",
1157
+ "Not logged in",
1158
+ "Run 'contextloop auth login' to authenticate"
1159
+ );
1160
+ }
1161
+ if (creds.apiKey) {
1162
+ const spinner = ora("Validating credentials...").start();
1163
+ try {
1164
+ await context2.apiClient.getCurrentUser();
1165
+ spinner.succeed("Credentials are valid.");
1166
+ } catch (error) {
1167
+ spinner.fail("Credentials are invalid");
1168
+ throw error;
1169
+ }
1170
+ return;
1171
+ }
1172
+ console.log(chalk4.yellow("Token refresh not yet implemented for OAuth."));
1173
+ console.log(chalk4.gray("Run 'contextloop auth login' to re-authenticate."));
1174
+ } catch (error) {
1175
+ console.error(formatError(error, globalOptions.format));
1176
+ process.exit(getExitCode(error));
1177
+ }
1178
+ });
1179
+ }
1180
+
1181
+ // src/commands/project.ts
1182
+ import chalk5 from "chalk";
1183
+ import ora2 from "ora";
1184
+ function registerProjectCommands(program2, context2) {
1185
+ const project = program2.command("project").description("Manage projects");
1186
+ project.command("list").description("List accessible projects").option("--page <number>", "Page number", "1").option("--page-size <number>", "Items per page", "20").action(async (options) => {
1187
+ const globalOptions = program2.opts();
1188
+ try {
1189
+ const spinner = ora2("Fetching projects...").start();
1190
+ const response = await context2.apiClient.listProjects({
1191
+ page: parseInt(options.page, 10),
1192
+ pageSize: parseInt(options.pageSize, 10)
1193
+ });
1194
+ spinner.stop();
1195
+ if (response.projects.length === 0) {
1196
+ console.log(chalk5.yellow("No projects found."));
1197
+ return;
1198
+ }
1199
+ printOutput(
1200
+ response,
1201
+ {
1202
+ text: (r) => {
1203
+ const config2 = loadConfig();
1204
+ const rows = r.projects.map((p) => {
1205
+ const isDefault = p.id === config2.defaultProjectId;
1206
+ const name = isDefault ? `${p.name} ${chalk5.cyan("(default)")}` : p.name;
1207
+ return [
1208
+ name,
1209
+ p.slug,
1210
+ p.role,
1211
+ String(p.documentCount),
1212
+ formatRelativeTime(p.updatedAt)
1213
+ ];
1214
+ });
1215
+ const table = formatTable(
1216
+ ["Name", "Slug", "Role", "Documents", "Updated"],
1217
+ rows
1218
+ );
1219
+ const pagination = r.pagination.totalPages > 1 ? chalk5.gray(
1220
+ `
1221
+ Page ${r.pagination.page} of ${r.pagination.totalPages} (${r.pagination.totalItems} total)`
1222
+ ) : "";
1223
+ return table + pagination;
1224
+ },
1225
+ json: (r) => r
1226
+ },
1227
+ globalOptions
1228
+ );
1229
+ } catch (error) {
1230
+ console.error(formatError(error, globalOptions.format));
1231
+ process.exit(getExitCode(error));
1232
+ }
1233
+ });
1234
+ project.command("info").description("Show project details").argument("[project-id]", "Project ID or slug (uses default if not specified)").action(async (projectId) => {
1235
+ const globalOptions = program2.opts();
1236
+ try {
1237
+ const id = projectId || globalOptions.project || context2.config.defaultProjectId;
1238
+ if (!id) {
1239
+ throw new CLIError(
1240
+ "PROJECT_NOT_SET",
1241
+ "No project specified",
1242
+ "Use 'contextloop project use <id>' to set a default project"
1243
+ );
1244
+ }
1245
+ const spinner = ora2("Fetching project info...").start();
1246
+ const projectInfo = await context2.apiClient.getProject(id);
1247
+ spinner.stop();
1248
+ printOutput(
1249
+ projectInfo,
1250
+ {
1251
+ text: (p) => [
1252
+ chalk5.bold(p.name),
1253
+ chalk5.gray(`Slug: ${p.slug}`),
1254
+ chalk5.gray(`ID: ${p.id}`),
1255
+ p.description ? `
1256
+ ${p.description}` : "",
1257
+ "",
1258
+ chalk5.gray(`Role: ${p.role}`),
1259
+ chalk5.gray(`Created: ${new Date(p.createdAt).toLocaleDateString()}`),
1260
+ chalk5.gray(`Updated: ${formatRelativeTime(p.updatedAt)}`)
1261
+ ].filter(Boolean).join("\n"),
1262
+ json: (p) => p
1263
+ },
1264
+ globalOptions
1265
+ );
1266
+ } catch (error) {
1267
+ console.error(formatError(error, globalOptions.format));
1268
+ process.exit(getExitCode(error));
1269
+ }
1270
+ });
1271
+ project.command("use").description("Set default project").argument("<project-id>", "Project ID or slug to use as default").action(async (projectId) => {
1272
+ const globalOptions = program2.opts();
1273
+ try {
1274
+ const spinner = ora2("Validating project...").start();
1275
+ const projectInfo = await context2.apiClient.getProject(projectId);
1276
+ saveConfig({
1277
+ defaultProjectId: projectInfo.id,
1278
+ defaultProjectSlug: projectInfo.slug
1279
+ });
1280
+ spinner.succeed(
1281
+ `Default project set to ${chalk5.bold(projectInfo.name)} (${projectInfo.slug})`
1282
+ );
1283
+ } catch (error) {
1284
+ console.error(formatError(error, globalOptions.format));
1285
+ process.exit(getExitCode(error));
1286
+ }
1287
+ });
1288
+ project.command("current").description("Show current default project").action(async () => {
1289
+ const globalOptions = program2.opts();
1290
+ try {
1291
+ const config2 = loadConfig();
1292
+ if (!config2.defaultProjectId) {
1293
+ if (globalOptions.format === "json") {
1294
+ console.log(JSON.stringify({ defaultProject: null }));
1295
+ } else {
1296
+ console.log(chalk5.yellow("No default project set."));
1297
+ console.log(
1298
+ chalk5.gray("Use 'contextloop project use <id>' to set one.")
1299
+ );
1300
+ }
1301
+ return;
1302
+ }
1303
+ const spinner = ora2("Fetching project info...").start();
1304
+ try {
1305
+ const projectInfo = await context2.apiClient.getProject(
1306
+ config2.defaultProjectId
1307
+ );
1308
+ spinner.stop();
1309
+ printOutput(
1310
+ projectInfo,
1311
+ {
1312
+ text: (p) => [
1313
+ `Current project: ${chalk5.bold(p.name)}`,
1314
+ chalk5.gray(`Slug: ${p.slug}`),
1315
+ chalk5.gray(`ID: ${p.id}`)
1316
+ ].join("\n"),
1317
+ json: (p) => ({ defaultProject: p })
1318
+ },
1319
+ globalOptions
1320
+ );
1321
+ } catch {
1322
+ spinner.stop();
1323
+ console.log(chalk5.yellow("Default project no longer accessible."));
1324
+ console.log(chalk5.gray(`ID: ${config2.defaultProjectId}`));
1325
+ console.log(
1326
+ chalk5.gray("Use 'contextloop project use <id>' to set a new default.")
1327
+ );
1328
+ }
1329
+ } catch (error) {
1330
+ console.error(formatError(error, globalOptions.format));
1331
+ process.exit(getExitCode(error));
1332
+ }
1333
+ });
1334
+ }
1335
+
1336
+ // src/commands/document.ts
1337
+ import chalk6 from "chalk";
1338
+ import ora3 from "ora";
1339
+ import fs2 from "fs";
1340
+ import path2 from "path";
1341
+ import { glob } from "glob";
1342
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1343
+ function getProjectId(globalOptions, context2) {
1344
+ const projectId = globalOptions.project || context2.config.defaultProjectId;
1345
+ if (!projectId) {
1346
+ throw new CLIError(
1347
+ "PROJECT_NOT_SET",
1348
+ "No project specified",
1349
+ "Use --project <id> or run 'contextloop project use <id>' to set a default"
1350
+ );
1351
+ }
1352
+ return projectId;
1353
+ }
1354
+ function isUUID(str) {
1355
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1356
+ str
1357
+ );
1358
+ }
1359
+ async function readContent(source) {
1360
+ if (source === "-") {
1361
+ return new Promise((resolve, reject) => {
1362
+ let content = "";
1363
+ process.stdin.setEncoding("utf8");
1364
+ process.stdin.on("readable", () => {
1365
+ let chunk;
1366
+ while ((chunk = process.stdin.read()) !== null) {
1367
+ content += chunk;
1368
+ }
1369
+ });
1370
+ process.stdin.on("end", () => {
1371
+ resolve({ content });
1372
+ });
1373
+ process.stdin.on("error", (err) => {
1374
+ reject(
1375
+ new CLIError("STDIN_ERROR", `Failed to read from stdin: ${err.message}`)
1376
+ );
1377
+ });
1378
+ });
1379
+ }
1380
+ const filePath = path2.resolve(source);
1381
+ if (!fs2.existsSync(filePath)) {
1382
+ throw new CLIError("FILE_NOT_FOUND", `File not found: ${source}`);
1383
+ }
1384
+ const stats = fs2.statSync(filePath);
1385
+ if (stats.size > MAX_FILE_SIZE) {
1386
+ throw new CLIError(
1387
+ "FILE_TOO_LARGE",
1388
+ `File size (${formatBytes(stats.size)}) exceeds maximum allowed (${formatBytes(MAX_FILE_SIZE)})`
1389
+ );
1390
+ }
1391
+ try {
1392
+ const content = fs2.readFileSync(filePath, "utf8");
1393
+ return { content, filename: path2.basename(filePath) };
1394
+ } catch (err) {
1395
+ throw new CLIError(
1396
+ "FILE_READ_ERROR",
1397
+ `Failed to read file: ${err instanceof Error ? err.message : "Unknown error"}`
1398
+ );
1399
+ }
1400
+ }
1401
+ function writeContent(filePath, content) {
1402
+ try {
1403
+ const dir = path2.dirname(filePath);
1404
+ if (!fs2.existsSync(dir)) {
1405
+ fs2.mkdirSync(dir, { recursive: true });
1406
+ }
1407
+ fs2.writeFileSync(filePath, content, "utf8");
1408
+ } catch (err) {
1409
+ throw new CLIError(
1410
+ "FILE_WRITE_ERROR",
1411
+ `Failed to write file: ${err instanceof Error ? err.message : "Unknown error"}`
1412
+ );
1413
+ }
1414
+ }
1415
+ function buildTree(documents) {
1416
+ const map = /* @__PURE__ */ new Map();
1417
+ for (const doc of documents) {
1418
+ const parentId = doc.parentId;
1419
+ if (!map.has(parentId)) {
1420
+ map.set(parentId, []);
1421
+ }
1422
+ map.get(parentId).push({
1423
+ name: doc.title,
1424
+ isFolder: doc.isFolder,
1425
+ id: doc.id,
1426
+ children: []
1427
+ });
1428
+ }
1429
+ function buildNode(node) {
1430
+ const children = map.get(node.id) || [];
1431
+ return {
1432
+ name: node.name,
1433
+ isFolder: node.isFolder,
1434
+ children: children.map(buildNode)
1435
+ };
1436
+ }
1437
+ const roots = map.get(null) || [];
1438
+ return roots.map(buildNode);
1439
+ }
1440
+ function registerDocumentCommands(program2, context2) {
1441
+ const document = program2.command("document").alias("doc").description("Manage documents");
1442
+ document.command("list").description("List documents in project").option("--tree", "Show as tree structure").option("--path <path>", "Filter by path prefix").option("--page <number>", "Page number", "1").option("--page-size <number>", "Items per page", "50").action(
1443
+ async (options) => {
1444
+ const globalOptions = program2.opts();
1445
+ try {
1446
+ const projectId = getProjectId(globalOptions, context2);
1447
+ const spinner = ora3("Fetching documents...").start();
1448
+ const response = await context2.apiClient.listDocuments(projectId, {
1449
+ tree: options.tree,
1450
+ path: options.path,
1451
+ page: parseInt(options.page, 10),
1452
+ pageSize: parseInt(options.pageSize, 10)
1453
+ });
1454
+ spinner.stop();
1455
+ if (response.documents.length === 0) {
1456
+ console.log(chalk6.yellow("No documents found."));
1457
+ return;
1458
+ }
1459
+ printOutput(
1460
+ response,
1461
+ {
1462
+ text: (r) => {
1463
+ if (options.tree) {
1464
+ const tree = buildTree(r.documents);
1465
+ return formatTree(tree);
1466
+ }
1467
+ const rows = r.documents.map((d) => [
1468
+ d.isFolder ? chalk6.blue(d.title + "/") : d.title,
1469
+ d.path,
1470
+ String(d.version),
1471
+ formatRelativeTime(d.updatedAt)
1472
+ ]);
1473
+ const table = formatTable(
1474
+ ["Title", "Path", "Version", "Updated"],
1475
+ rows
1476
+ );
1477
+ const pagination = r.pagination.totalPages > 1 ? chalk6.gray(
1478
+ `
1479
+ Page ${r.pagination.page} of ${r.pagination.totalPages} (${r.pagination.totalItems} total)`
1480
+ ) : "";
1481
+ return table + pagination;
1482
+ },
1483
+ json: (r) => r
1484
+ },
1485
+ globalOptions
1486
+ );
1487
+ } catch (error) {
1488
+ console.error(formatError(error, globalOptions.format));
1489
+ process.exit(getExitCode(error));
1490
+ }
1491
+ }
1492
+ );
1493
+ document.command("upload").description("Upload a document").argument("<file>", "File to upload (use - for stdin)").option("--path <path>", "Remote path for the document").option("--title <title>", "Document title").option("--update", "Update if document exists at path").option("--dry-run", "Preview without uploading").action(
1494
+ async (file, options) => {
1495
+ const globalOptions = program2.opts();
1496
+ try {
1497
+ const projectId = getProjectId(globalOptions, context2);
1498
+ if (file.includes("*")) {
1499
+ await handleBatchUpload(
1500
+ file,
1501
+ projectId,
1502
+ options,
1503
+ globalOptions,
1504
+ context2
1505
+ );
1506
+ return;
1507
+ }
1508
+ const { content, filename } = await readContent(file);
1509
+ let title = options.title;
1510
+ if (!title && options.path) {
1511
+ title = path2.basename(options.path);
1512
+ }
1513
+ if (!title && filename) {
1514
+ title = filename;
1515
+ }
1516
+ if (!title && file === "-") {
1517
+ throw new CLIError(
1518
+ "VALIDATION_ERROR",
1519
+ "Title is required when uploading from stdin",
1520
+ "Use --title or --path to specify the document title"
1521
+ );
1522
+ }
1523
+ if (options.dryRun) {
1524
+ console.log(chalk6.cyan("Dry run - would upload:"));
1525
+ console.log(` Title: ${title}`);
1526
+ console.log(` Path: ${options.path || "(auto)"}`);
1527
+ console.log(` Content length: ${content.length} characters`);
1528
+ return;
1529
+ }
1530
+ const spinner = ora3(`Uploading ${title}...`).start();
1531
+ const doc = await context2.apiClient.createDocument(projectId, {
1532
+ title,
1533
+ content,
1534
+ path: options.path
1535
+ });
1536
+ spinner.succeed(`Uploaded: ${chalk6.bold(doc.title)} (${doc.path})`);
1537
+ if (globalOptions.format === "json") {
1538
+ console.log(JSON.stringify(doc, null, 2));
1539
+ }
1540
+ } catch (error) {
1541
+ console.error(formatError(error, globalOptions.format));
1542
+ process.exit(getExitCode(error));
1543
+ }
1544
+ }
1545
+ );
1546
+ document.command("download").description("Download a document").argument("<document-id>", "Document ID").option("-o, --output <file>", "Output file (stdout if not specified)").option("--path <path>", "Download by path instead of ID").option("--version <number>", "Download specific version").action(
1547
+ async (documentId, options) => {
1548
+ const globalOptions = program2.opts();
1549
+ try {
1550
+ const projectId = getProjectId(globalOptions, context2);
1551
+ let doc;
1552
+ const spinner = ora3("Downloading...").start();
1553
+ if (options.path) {
1554
+ doc = await context2.apiClient.getDocumentByPath(
1555
+ projectId,
1556
+ options.path
1557
+ );
1558
+ } else if (isUUID(documentId)) {
1559
+ doc = await context2.apiClient.getDocument(projectId, documentId);
1560
+ } else {
1561
+ throw new CLIError(
1562
+ "VALIDATION_ERROR",
1563
+ "Invalid document identifier. Provide a UUID or use --path"
1564
+ );
1565
+ }
1566
+ spinner.stop();
1567
+ if (options.output) {
1568
+ writeContent(options.output, doc.content);
1569
+ console.log(chalk6.green(`Saved to ${options.output}`));
1570
+ } else {
1571
+ process.stdout.write(doc.content);
1572
+ }
1573
+ } catch (error) {
1574
+ console.error(formatError(error, globalOptions.format));
1575
+ process.exit(getExitCode(error));
1576
+ }
1577
+ }
1578
+ );
1579
+ document.command("update").description("Update an existing document").argument("<document-id>", "Document ID").argument("<file>", "File with new content (use - for stdin)").option("--no-version", "Do not create a new version").action(
1580
+ async (documentId, file, options) => {
1581
+ const globalOptions = program2.opts();
1582
+ try {
1583
+ const projectId = getProjectId(globalOptions, context2);
1584
+ if (!isUUID(documentId)) {
1585
+ throw new CLIError(
1586
+ "VALIDATION_ERROR",
1587
+ "Invalid document ID. Must be a UUID."
1588
+ );
1589
+ }
1590
+ const { content } = await readContent(file);
1591
+ const spinner = ora3("Updating document...").start();
1592
+ const doc = await context2.apiClient.updateDocument(
1593
+ projectId,
1594
+ documentId,
1595
+ {
1596
+ content,
1597
+ createVersion: options.version !== false
1598
+ }
1599
+ );
1600
+ spinner.succeed(
1601
+ `Updated ${chalk6.bold(doc.title)} to version ${doc.version}`
1602
+ );
1603
+ if (globalOptions.format === "json") {
1604
+ console.log(JSON.stringify(doc, null, 2));
1605
+ }
1606
+ } catch (error) {
1607
+ console.error(formatError(error, globalOptions.format));
1608
+ process.exit(getExitCode(error));
1609
+ }
1610
+ }
1611
+ );
1612
+ document.command("delete").description("Delete a document").argument("<document-id>", "Document ID").option("-f, --force", "Skip confirmation").action(async (documentId, options) => {
1613
+ const globalOptions = program2.opts();
1614
+ try {
1615
+ const projectId = getProjectId(globalOptions, context2);
1616
+ if (!isUUID(documentId)) {
1617
+ throw new CLIError(
1618
+ "VALIDATION_ERROR",
1619
+ "Invalid document ID. Must be a UUID."
1620
+ );
1621
+ }
1622
+ if (!options.force) {
1623
+ const doc = await context2.apiClient.getDocument(projectId, documentId);
1624
+ const readline = await import("readline");
1625
+ const rl = readline.createInterface({
1626
+ input: process.stdin,
1627
+ output: process.stdout
1628
+ });
1629
+ const answer = await new Promise((resolve) => {
1630
+ rl.question(
1631
+ `Delete "${doc.title}"? This cannot be undone. (y/N) `,
1632
+ (answer2) => {
1633
+ rl.close();
1634
+ resolve(answer2.trim().toLowerCase());
1635
+ }
1636
+ );
1637
+ });
1638
+ if (answer !== "y" && answer !== "yes") {
1639
+ console.log(chalk6.gray("Cancelled."));
1640
+ return;
1641
+ }
1642
+ }
1643
+ const spinner = ora3("Deleting document...").start();
1644
+ await context2.apiClient.deleteDocument(projectId, documentId);
1645
+ spinner.succeed("Document deleted.");
1646
+ } catch (error) {
1647
+ console.error(formatError(error, globalOptions.format));
1648
+ process.exit(getExitCode(error));
1649
+ }
1650
+ });
1651
+ document.command("info").description("Show document metadata").argument("<document-id>", "Document ID").action(async (documentId) => {
1652
+ const globalOptions = program2.opts();
1653
+ try {
1654
+ const projectId = getProjectId(globalOptions, context2);
1655
+ if (!isUUID(documentId)) {
1656
+ throw new CLIError(
1657
+ "VALIDATION_ERROR",
1658
+ "Invalid document ID. Must be a UUID."
1659
+ );
1660
+ }
1661
+ const spinner = ora3("Fetching document info...").start();
1662
+ const doc = await context2.apiClient.getDocument(projectId, documentId);
1663
+ spinner.stop();
1664
+ printOutput(
1665
+ doc,
1666
+ {
1667
+ text: (d) => [
1668
+ chalk6.bold(d.title),
1669
+ chalk6.gray(`ID: ${d.id}`),
1670
+ chalk6.gray(`Path: ${d.path}`),
1671
+ chalk6.gray(`Version: ${d.version}`),
1672
+ chalk6.gray(`Type: ${d.isFolder ? "Folder" : "Document"}`),
1673
+ "",
1674
+ chalk6.gray(
1675
+ `Created: ${new Date(d.createdAt).toLocaleString()}`
1676
+ ),
1677
+ chalk6.gray(`Updated: ${formatRelativeTime(d.updatedAt)}`),
1678
+ chalk6.gray(`Size: ${formatBytes(d.content.length)}`)
1679
+ ].join("\n"),
1680
+ json: (d) => d
1681
+ },
1682
+ globalOptions
1683
+ );
1684
+ } catch (error) {
1685
+ console.error(formatError(error, globalOptions.format));
1686
+ process.exit(getExitCode(error));
1687
+ }
1688
+ });
1689
+ document.command("versions").description("List document versions").argument("<document-id>", "Document ID").action(async (documentId) => {
1690
+ const globalOptions = program2.opts();
1691
+ try {
1692
+ const projectId = getProjectId(globalOptions, context2);
1693
+ if (!isUUID(documentId)) {
1694
+ throw new CLIError(
1695
+ "VALIDATION_ERROR",
1696
+ "Invalid document ID. Must be a UUID."
1697
+ );
1698
+ }
1699
+ const spinner = ora3("Fetching versions...").start();
1700
+ const response = await context2.apiClient.listDocumentVersions(
1701
+ projectId,
1702
+ documentId
1703
+ );
1704
+ spinner.stop();
1705
+ if (response.versions.length === 0) {
1706
+ console.log(chalk6.yellow("No versions found."));
1707
+ return;
1708
+ }
1709
+ printOutput(
1710
+ response,
1711
+ {
1712
+ text: (r) => {
1713
+ const rows = r.versions.map((v) => [
1714
+ String(v.version),
1715
+ v.changeDescription || "-",
1716
+ v.createdBy,
1717
+ formatRelativeTime(v.createdAt)
1718
+ ]);
1719
+ return formatTable(
1720
+ ["Version", "Description", "Author", "Created"],
1721
+ rows
1722
+ );
1723
+ },
1724
+ json: (r) => r
1725
+ },
1726
+ globalOptions
1727
+ );
1728
+ } catch (error) {
1729
+ console.error(formatError(error, globalOptions.format));
1730
+ process.exit(getExitCode(error));
1731
+ }
1732
+ });
1733
+ }
1734
+ async function handleBatchUpload(pattern, projectId, options, globalOptions, context2) {
1735
+ const basePath = options.path || ".";
1736
+ const files = await glob(pattern, {
1737
+ nodir: true,
1738
+ ignore: ["node_modules/**", ".git/**"]
1739
+ });
1740
+ if (files.length === 0) {
1741
+ console.log(chalk6.yellow("No files matched the pattern."));
1742
+ return;
1743
+ }
1744
+ console.log(`Found ${files.length} file(s) to upload.`);
1745
+ if (options.dryRun) {
1746
+ console.log(chalk6.cyan("\nDry run - would upload:"));
1747
+ for (const file of files) {
1748
+ const remotePath = path2.join("/", path2.relative(basePath, file));
1749
+ console.log(` ${file} -> ${remotePath}`);
1750
+ }
1751
+ return;
1752
+ }
1753
+ let successCount = 0;
1754
+ let errorCount = 0;
1755
+ for (const file of files) {
1756
+ const remotePath = path2.join("/", path2.relative(basePath, file));
1757
+ const title = path2.basename(file);
1758
+ try {
1759
+ const content = fs2.readFileSync(file, "utf8");
1760
+ await context2.apiClient.createDocument(projectId, {
1761
+ title,
1762
+ content,
1763
+ path: remotePath
1764
+ });
1765
+ console.log(chalk6.green(`\u2713 ${file} -> ${remotePath}`));
1766
+ successCount++;
1767
+ } catch (error) {
1768
+ console.log(
1769
+ chalk6.red(
1770
+ `\u2717 ${file}: ${error instanceof Error ? error.message : "Unknown error"}`
1771
+ )
1772
+ );
1773
+ errorCount++;
1774
+ }
1775
+ }
1776
+ console.log();
1777
+ console.log(
1778
+ `Uploaded: ${successCount}, Failed: ${errorCount}, Total: ${files.length}`
1779
+ );
1780
+ }
1781
+
1782
+ // src/commands/context.ts
1783
+ import chalk7 from "chalk";
1784
+ import ora4 from "ora";
1785
+ import fs3 from "fs";
1786
+ import path3 from "path";
1787
+ var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
1788
+ function getProjectId2(globalOptions, context2) {
1789
+ const projectId = globalOptions.project || context2.config.defaultProjectId;
1790
+ if (!projectId) {
1791
+ throw new CLIError(
1792
+ "PROJECT_NOT_SET",
1793
+ "No project specified",
1794
+ "Use --project <id> or run 'contextloop project use <id>' to set a default"
1795
+ );
1796
+ }
1797
+ return projectId;
1798
+ }
1799
+ function isUUID2(str) {
1800
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1801
+ str
1802
+ );
1803
+ }
1804
+ async function readContent2(source) {
1805
+ if (source === "-") {
1806
+ return new Promise((resolve, reject) => {
1807
+ let content = "";
1808
+ process.stdin.setEncoding("utf8");
1809
+ process.stdin.on("readable", () => {
1810
+ let chunk;
1811
+ while ((chunk = process.stdin.read()) !== null) {
1812
+ content += chunk;
1813
+ }
1814
+ });
1815
+ process.stdin.on("end", () => {
1816
+ resolve({ content });
1817
+ });
1818
+ process.stdin.on("error", (err) => {
1819
+ reject(
1820
+ new CLIError("STDIN_ERROR", `Failed to read from stdin: ${err.message}`)
1821
+ );
1822
+ });
1823
+ });
1824
+ }
1825
+ const filePath = path3.resolve(source);
1826
+ if (!fs3.existsSync(filePath)) {
1827
+ throw new CLIError("FILE_NOT_FOUND", `File not found: ${source}`);
1828
+ }
1829
+ const stats = fs3.statSync(filePath);
1830
+ if (stats.size > MAX_FILE_SIZE2) {
1831
+ throw new CLIError(
1832
+ "FILE_TOO_LARGE",
1833
+ `File size (${formatBytes(stats.size)}) exceeds maximum allowed (${formatBytes(MAX_FILE_SIZE2)})`
1834
+ );
1835
+ }
1836
+ try {
1837
+ const content = fs3.readFileSync(filePath, "utf8");
1838
+ return { content, filename: path3.basename(filePath) };
1839
+ } catch (err) {
1840
+ throw new CLIError(
1841
+ "FILE_READ_ERROR",
1842
+ `Failed to read file: ${err instanceof Error ? err.message : "Unknown error"}`
1843
+ );
1844
+ }
1845
+ }
1846
+ function registerContextCommands(program2, context2) {
1847
+ const ctx = program2.command("context").alias("ctx").description("Manage context items");
1848
+ ctx.command("list").description("List context items").option("--page <number>", "Page number", "1").option("--page-size <number>", "Items per page", "50").action(async (options) => {
1849
+ const globalOptions = program2.opts();
1850
+ try {
1851
+ const projectId = getProjectId2(globalOptions, context2);
1852
+ const spinner = ora4("Fetching context items...").start();
1853
+ const response = await context2.apiClient.listContext(projectId, {
1854
+ page: parseInt(options.page, 10),
1855
+ pageSize: parseInt(options.pageSize, 10)
1856
+ });
1857
+ spinner.stop();
1858
+ if (response.items.length === 0) {
1859
+ console.log(chalk7.yellow("No context items found."));
1860
+ return;
1861
+ }
1862
+ printOutput(
1863
+ response,
1864
+ {
1865
+ text: (r) => {
1866
+ const rows = r.items.map((item) => [
1867
+ item.isFolder ? chalk7.blue(item.title + "/") : item.title,
1868
+ item.path,
1869
+ item.isEditable ? "Yes" : "No",
1870
+ formatRelativeTime(item.updatedAt)
1871
+ ]);
1872
+ const table = formatTable(
1873
+ ["Title", "Path", "Editable", "Updated"],
1874
+ rows
1875
+ );
1876
+ const pagination = r.pagination.totalPages > 1 ? chalk7.gray(
1877
+ `
1878
+ Page ${r.pagination.page} of ${r.pagination.totalPages} (${r.pagination.totalItems} total)`
1879
+ ) : "";
1880
+ return table + pagination;
1881
+ },
1882
+ json: (r) => r
1883
+ },
1884
+ globalOptions
1885
+ );
1886
+ } catch (error) {
1887
+ console.error(formatError(error, globalOptions.format));
1888
+ process.exit(getExitCode(error));
1889
+ }
1890
+ });
1891
+ ctx.command("upload").description("Upload a context item").argument("<file>", "File to upload (use - for stdin)").option("--name <name>", "Context item name").option("--type <type>", "Content type (text, json, etc.)").action(
1892
+ async (file, options) => {
1893
+ const globalOptions = program2.opts();
1894
+ try {
1895
+ const projectId = getProjectId2(globalOptions, context2);
1896
+ const { content, filename } = await readContent2(file);
1897
+ let title = options.name;
1898
+ if (!title && filename) {
1899
+ title = filename;
1900
+ }
1901
+ if (!title && file === "-") {
1902
+ throw new CLIError(
1903
+ "VALIDATION_ERROR",
1904
+ "Name is required when uploading from stdin",
1905
+ "Use --name to specify the context item name"
1906
+ );
1907
+ }
1908
+ const spinner = ora4(`Uploading ${title}...`).start();
1909
+ const item = await context2.apiClient.createContext(projectId, {
1910
+ title,
1911
+ content
1912
+ });
1913
+ spinner.succeed(`Uploaded: ${chalk7.bold(item.title)} (${item.path})`);
1914
+ if (globalOptions.format === "json") {
1915
+ console.log(JSON.stringify(item, null, 2));
1916
+ }
1917
+ } catch (error) {
1918
+ console.error(formatError(error, globalOptions.format));
1919
+ process.exit(getExitCode(error));
1920
+ }
1921
+ }
1922
+ );
1923
+ ctx.command("download").description("Download a context item").argument("<context-id>", "Context item ID").option("-o, --output <file>", "Output file (stdout if not specified)").action(async (contextId, options) => {
1924
+ const globalOptions = program2.opts();
1925
+ try {
1926
+ const projectId = getProjectId2(globalOptions, context2);
1927
+ if (!isUUID2(contextId)) {
1928
+ throw new CLIError(
1929
+ "VALIDATION_ERROR",
1930
+ "Invalid context ID. Must be a UUID."
1931
+ );
1932
+ }
1933
+ const spinner = ora4("Downloading...").start();
1934
+ const item = await context2.apiClient.getContext(projectId, contextId);
1935
+ spinner.stop();
1936
+ if (options.output) {
1937
+ const dir = path3.dirname(options.output);
1938
+ if (!fs3.existsSync(dir)) {
1939
+ fs3.mkdirSync(dir, { recursive: true });
1940
+ }
1941
+ fs3.writeFileSync(options.output, item.content, "utf8");
1942
+ console.log(chalk7.green(`Saved to ${options.output}`));
1943
+ } else {
1944
+ process.stdout.write(item.content);
1945
+ }
1946
+ } catch (error) {
1947
+ console.error(formatError(error, globalOptions.format));
1948
+ process.exit(getExitCode(error));
1949
+ }
1950
+ });
1951
+ ctx.command("delete").description("Delete a context item").argument("<context-id>", "Context item ID").option("-f, --force", "Skip confirmation").action(async (contextId, options) => {
1952
+ const globalOptions = program2.opts();
1953
+ try {
1954
+ const projectId = getProjectId2(globalOptions, context2);
1955
+ if (!isUUID2(contextId)) {
1956
+ throw new CLIError(
1957
+ "VALIDATION_ERROR",
1958
+ "Invalid context ID. Must be a UUID."
1959
+ );
1960
+ }
1961
+ if (!options.force) {
1962
+ const item = await context2.apiClient.getContext(projectId, contextId);
1963
+ const readline = await import("readline");
1964
+ const rl = readline.createInterface({
1965
+ input: process.stdin,
1966
+ output: process.stdout
1967
+ });
1968
+ const answer = await new Promise((resolve) => {
1969
+ rl.question(
1970
+ `Delete "${item.title}"? This cannot be undone. (y/N) `,
1971
+ (answer2) => {
1972
+ rl.close();
1973
+ resolve(answer2.trim().toLowerCase());
1974
+ }
1975
+ );
1976
+ });
1977
+ if (answer !== "y" && answer !== "yes") {
1978
+ console.log(chalk7.gray("Cancelled."));
1979
+ return;
1980
+ }
1981
+ }
1982
+ const spinner = ora4("Deleting context item...").start();
1983
+ await context2.apiClient.deleteContext(projectId, contextId);
1984
+ spinner.succeed("Context item deleted.");
1985
+ } catch (error) {
1986
+ console.error(formatError(error, globalOptions.format));
1987
+ process.exit(getExitCode(error));
1988
+ }
1989
+ });
1990
+ }
1991
+
1992
+ // src/commands/comment.ts
1993
+ import chalk8 from "chalk";
1994
+ import ora5 from "ora";
1995
+ function getProjectId3(globalOptions, context2) {
1996
+ const projectId = globalOptions.project || context2.config.defaultProjectId;
1997
+ if (!projectId) {
1998
+ throw new CLIError(
1999
+ "PROJECT_NOT_SET",
2000
+ "No project specified",
2001
+ "Use --project <id> or run 'contextloop project use <id>' to set a default"
2002
+ );
2003
+ }
2004
+ return projectId;
2005
+ }
2006
+ function isUUID3(str) {
2007
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
2008
+ str
2009
+ );
2010
+ }
2011
+ async function readBody(body) {
2012
+ if (body === "-") {
2013
+ return new Promise((resolve, reject) => {
2014
+ let content = "";
2015
+ process.stdin.setEncoding("utf8");
2016
+ process.stdin.on("readable", () => {
2017
+ let chunk;
2018
+ while ((chunk = process.stdin.read()) !== null) {
2019
+ content += chunk;
2020
+ }
2021
+ });
2022
+ process.stdin.on("end", () => {
2023
+ resolve(content.trim());
2024
+ });
2025
+ process.stdin.on("error", (err) => {
2026
+ reject(
2027
+ new CLIError("STDIN_ERROR", `Failed to read from stdin: ${err.message}`)
2028
+ );
2029
+ });
2030
+ });
2031
+ }
2032
+ return body;
2033
+ }
2034
+ function lineToOffset(content, lineNumber) {
2035
+ const lines = content.split("\n");
2036
+ let offset = 0;
2037
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
2038
+ offset += lines[i].length + 1;
2039
+ }
2040
+ return offset;
2041
+ }
2042
+ function formatComment(comment, indent = "") {
2043
+ const status = comment.resolved ? chalk8.green("[resolved]") : chalk8.yellow("[open]");
2044
+ const lines = [
2045
+ `${indent}${chalk8.bold(comment.author.name)} ${status}`,
2046
+ `${indent}${chalk8.gray(formatRelativeTime(comment.createdAt))}`
2047
+ ];
2048
+ if (comment.selectionText) {
2049
+ lines.push(`${indent}${chalk8.cyan(`"${comment.selectionText}"`)}`);
2050
+ }
2051
+ lines.push(`${indent}${comment.body}`);
2052
+ if (comment.replies.length > 0) {
2053
+ lines.push("");
2054
+ for (const reply of comment.replies) {
2055
+ lines.push(
2056
+ `${indent} ${chalk8.bold(reply.author.name)} ${chalk8.gray(formatRelativeTime(reply.createdAt))}`
2057
+ );
2058
+ lines.push(`${indent} ${reply.body}`);
2059
+ }
2060
+ }
2061
+ return lines.join("\n");
2062
+ }
2063
+ function registerCommentCommands(program2, context2) {
2064
+ const comment = program2.command("comment").description("Manage document comments");
2065
+ comment.command("list").description("List comments on a document").argument("<document-id>", "Document ID").option("--page <number>", "Page number", "1").option("--page-size <number>", "Items per page", "50").action(
2066
+ async (documentId, options) => {
2067
+ const globalOptions = program2.opts();
2068
+ try {
2069
+ const projectId = getProjectId3(globalOptions, context2);
2070
+ if (!isUUID3(documentId)) {
2071
+ throw new CLIError(
2072
+ "VALIDATION_ERROR",
2073
+ "Invalid document ID. Must be a UUID."
2074
+ );
2075
+ }
2076
+ const spinner = ora5("Fetching comments...").start();
2077
+ const response = await context2.apiClient.listComments(
2078
+ projectId,
2079
+ documentId,
2080
+ {
2081
+ page: parseInt(options.page, 10),
2082
+ pageSize: parseInt(options.pageSize, 10)
2083
+ }
2084
+ );
2085
+ spinner.stop();
2086
+ if (response.comments.length === 0) {
2087
+ console.log(chalk8.yellow("No comments found."));
2088
+ return;
2089
+ }
2090
+ printOutput(
2091
+ response,
2092
+ {
2093
+ text: (r) => {
2094
+ const formatted = r.comments.map((c) => formatComment(c)).join("\n\n" + chalk8.gray("\u2500".repeat(40)) + "\n\n");
2095
+ const pagination = r.pagination.totalPages > 1 ? chalk8.gray(
2096
+ `
2097
+ Page ${r.pagination.page} of ${r.pagination.totalPages} (${r.pagination.totalItems} total)`
2098
+ ) : "";
2099
+ return formatted + pagination;
2100
+ },
2101
+ json: (r) => r
2102
+ },
2103
+ globalOptions
2104
+ );
2105
+ } catch (error) {
2106
+ console.error(formatError(error, globalOptions.format));
2107
+ process.exit(getExitCode(error));
2108
+ }
2109
+ }
2110
+ );
2111
+ comment.command("add").description("Add a comment to a document").argument("<document-id>", "Document ID").option("--body <text>", "Comment body (use - for stdin)").option("--line <number>", "Line number to comment on").action(
2112
+ async (documentId, options) => {
2113
+ const globalOptions = program2.opts();
2114
+ try {
2115
+ const projectId = getProjectId3(globalOptions, context2);
2116
+ if (!isUUID3(documentId)) {
2117
+ throw new CLIError(
2118
+ "VALIDATION_ERROR",
2119
+ "Invalid document ID. Must be a UUID."
2120
+ );
2121
+ }
2122
+ if (!options.body) {
2123
+ throw new CLIError(
2124
+ "VALIDATION_ERROR",
2125
+ "Comment body is required",
2126
+ "Use --body <text> to specify the comment"
2127
+ );
2128
+ }
2129
+ const body = await readBody(options.body);
2130
+ if (!body.trim()) {
2131
+ throw new CLIError("VALIDATION_ERROR", "Comment body cannot be empty");
2132
+ }
2133
+ let selectionStart;
2134
+ let selectionEnd;
2135
+ if (options.line) {
2136
+ const lineNum = parseInt(options.line, 10);
2137
+ if (isNaN(lineNum) || lineNum < 1) {
2138
+ throw new CLIError(
2139
+ "VALIDATION_ERROR",
2140
+ "Line number must be a positive integer"
2141
+ );
2142
+ }
2143
+ const doc = await context2.apiClient.getDocument(
2144
+ projectId,
2145
+ documentId
2146
+ );
2147
+ selectionStart = lineToOffset(doc.content, lineNum);
2148
+ selectionEnd = selectionStart;
2149
+ }
2150
+ const spinner = ora5("Adding comment...").start();
2151
+ const createdComment = await context2.apiClient.createComment(
2152
+ projectId,
2153
+ documentId,
2154
+ {
2155
+ body,
2156
+ selectionStart,
2157
+ selectionEnd
2158
+ }
2159
+ );
2160
+ spinner.succeed("Comment added.");
2161
+ if (globalOptions.format === "json") {
2162
+ console.log(JSON.stringify(createdComment, null, 2));
2163
+ }
2164
+ } catch (error) {
2165
+ console.error(formatError(error, globalOptions.format));
2166
+ process.exit(getExitCode(error));
2167
+ }
2168
+ }
2169
+ );
2170
+ comment.command("reply").description("Reply to a comment").argument("<comment-id>", "Comment ID to reply to").option("--body <text>", "Reply body (use - for stdin)").action(async (commentId, options) => {
2171
+ const globalOptions = program2.opts();
2172
+ try {
2173
+ const projectId = getProjectId3(globalOptions, context2);
2174
+ if (!isUUID3(commentId)) {
2175
+ throw new CLIError(
2176
+ "VALIDATION_ERROR",
2177
+ "Invalid comment ID. Must be a UUID."
2178
+ );
2179
+ }
2180
+ if (!options.body) {
2181
+ throw new CLIError(
2182
+ "VALIDATION_ERROR",
2183
+ "Reply body is required",
2184
+ "Use --body <text> to specify the reply"
2185
+ );
2186
+ }
2187
+ const body = await readBody(options.body);
2188
+ if (!body.trim()) {
2189
+ throw new CLIError("VALIDATION_ERROR", "Reply body cannot be empty");
2190
+ }
2191
+ const spinner = ora5("Adding reply...").start();
2192
+ console.log(chalk8.yellow("Reply functionality requires API support for comment threads."));
2193
+ spinner.stop();
2194
+ } catch (error) {
2195
+ console.error(formatError(error, globalOptions.format));
2196
+ process.exit(getExitCode(error));
2197
+ }
2198
+ });
2199
+ comment.command("resolve").description("Resolve a comment").argument("<comment-id>", "Comment ID").action(async (commentId) => {
2200
+ const globalOptions = program2.opts();
2201
+ try {
2202
+ const projectId = getProjectId3(globalOptions, context2);
2203
+ if (!isUUID3(commentId)) {
2204
+ throw new CLIError(
2205
+ "VALIDATION_ERROR",
2206
+ "Invalid comment ID. Must be a UUID."
2207
+ );
2208
+ }
2209
+ const spinner = ora5("Resolving comment...").start();
2210
+ const result = await context2.apiClient.resolveComment(
2211
+ projectId,
2212
+ commentId
2213
+ );
2214
+ spinner.succeed("Comment resolved.");
2215
+ if (globalOptions.format === "json") {
2216
+ console.log(JSON.stringify(result, null, 2));
2217
+ }
2218
+ } catch (error) {
2219
+ console.error(formatError(error, globalOptions.format));
2220
+ process.exit(getExitCode(error));
2221
+ }
2222
+ });
2223
+ }
2224
+
2225
+ // src/index.ts
2226
+ var program = new Command();
2227
+ program.name("contextloop").description("CLI for ContextLoop - manage documents and context").version("0.1.0").option("-p, --project <id>", "Project ID or slug").option("-f, --format <format>", "Output format: text, json", "text").option("-q, --quiet", "Suppress non-essential output").option("-v, --verbose", "Enable verbose logging").option("--no-color", "Disable colored output");
2228
+ var config = loadConfig();
2229
+ var defaultLogger = createLogger({});
2230
+ var apiClient = createAPIClient(config, defaultLogger);
2231
+ var context = {
2232
+ config,
2233
+ apiClient,
2234
+ logger: defaultLogger
2235
+ };
2236
+ registerAuthCommands(program, context);
2237
+ registerProjectCommands(program, context);
2238
+ registerDocumentCommands(program, context);
2239
+ registerContextCommands(program, context);
2240
+ registerCommentCommands(program, context);
2241
+ async function main() {
2242
+ try {
2243
+ await program.parseAsync(process.argv);
2244
+ } catch (error) {
2245
+ const format = program.opts().format;
2246
+ console.error(formatError(error, format || "text"));
2247
+ if (process.env.DEBUG) {
2248
+ console.error(error);
2249
+ }
2250
+ process.exit(getExitCode(error));
2251
+ }
2252
+ }
2253
+ main();