codex-link 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.
Files changed (2) hide show
  1. package/dist/index.js +997 -0
  2. package/package.json +18 -0
package/dist/index.js ADDED
@@ -0,0 +1,997 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { watch as watch2 } from "fs";
5
+ import { join as join4 } from "path";
6
+ import { Command } from "commander";
7
+
8
+ // ../shared/src/env.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { dirname, join, parse, resolve } from "path";
12
+ var envLoaded = false;
13
+ function findEnvDirectory(startDir) {
14
+ let currentDir = resolve(startDir);
15
+ const rootDir = parse(currentDir).root;
16
+ while (true) {
17
+ if (existsSync(join(currentDir, ".env")) || existsSync(join(currentDir, ".env.local"))) {
18
+ return currentDir;
19
+ }
20
+ if (currentDir === rootDir) {
21
+ return null;
22
+ }
23
+ currentDir = dirname(currentDir);
24
+ }
25
+ }
26
+ function decodeQuotedValue(input) {
27
+ if (input.startsWith('"') && input.endsWith('"')) {
28
+ return input.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
29
+ }
30
+ if (input.startsWith("'") && input.endsWith("'")) {
31
+ return input.slice(1, -1);
32
+ }
33
+ const commentIndex = input.search(/\s#/);
34
+ return (commentIndex >= 0 ? input.slice(0, commentIndex) : input).trim();
35
+ }
36
+ function parseEnvFile(path) {
37
+ const content = readFileSync(path, "utf8");
38
+ const entries = {};
39
+ for (const rawLine of content.split(/\r?\n/)) {
40
+ const trimmedLine = rawLine.trim();
41
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
42
+ continue;
43
+ }
44
+ const line = trimmedLine.startsWith("export ") ? trimmedLine.slice("export ".length).trim() : trimmedLine;
45
+ const separatorIndex = line.indexOf("=");
46
+ if (separatorIndex < 0) {
47
+ continue;
48
+ }
49
+ const key = line.slice(0, separatorIndex).trim();
50
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
51
+ continue;
52
+ }
53
+ const rawValue = line.slice(separatorIndex + 1).trim();
54
+ entries[key] = decodeQuotedValue(rawValue);
55
+ }
56
+ return entries;
57
+ }
58
+ function loadEnvFile(path, override = false) {
59
+ if (!existsSync(path)) {
60
+ return;
61
+ }
62
+ const parsed = parseEnvFile(path);
63
+ for (const [key, value] of Object.entries(parsed)) {
64
+ if (!override && process.env[key] !== void 0) {
65
+ continue;
66
+ }
67
+ process.env[key] = value;
68
+ }
69
+ }
70
+ function loadCodexLinkEnv() {
71
+ if (envLoaded) {
72
+ return;
73
+ }
74
+ const candidates = [
75
+ process.env["INIT_CWD"],
76
+ process.cwd()
77
+ ].filter((value) => Boolean(value));
78
+ for (const candidate of candidates) {
79
+ const envDir = findEnvDirectory(candidate);
80
+ if (!envDir) {
81
+ continue;
82
+ }
83
+ loadEnvFile(join(envDir, ".env"));
84
+ loadEnvFile(join(envDir, ".env.local"), true);
85
+ envLoaded = true;
86
+ return;
87
+ }
88
+ envLoaded = true;
89
+ }
90
+ function expandHomePath(value) {
91
+ if (!value) {
92
+ return value;
93
+ }
94
+ if (value === "~") {
95
+ return homedir();
96
+ }
97
+ if (value.startsWith("~/")) {
98
+ return join(homedir(), value.slice(2));
99
+ }
100
+ return value;
101
+ }
102
+
103
+ // ../shared/src/parser.ts
104
+ import { createHash } from "crypto";
105
+ import { gzipSync } from "zlib";
106
+
107
+ // ../shared/src/redaction.ts
108
+ var ABSOLUTE_PATH_PATTERNS = [
109
+ /(?:\/Users\/|\/home\/|\/var\/|\/tmp\/|\/private\/|\/opt\/|\/Applications\/)[^\s'"`]+/g,
110
+ /[A-Za-z]:\\[^\s'"`]+/g
111
+ ];
112
+ var SECRET_PATTERNS = [
113
+ /\b(?:sk|rk|pk)_[A-Za-z0-9_-]{16,}\b/g,
114
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g,
115
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
116
+ /\bsess_[A-Za-z0-9]{16,}\b/g,
117
+ /\b(?:token|secret|password|authorization|api[_-]?key)\b\s*[:=]\s*["']?[^\s"']+/gi,
118
+ /\b(?:Bearer)\s+[A-Za-z0-9._-]{12,}/gi
119
+ ];
120
+ var SIGNED_URL_PATTERN = /https?:\/\/[^\s'"`]+(?:X-Amz-Signature|Signature=|sig=|token=)[^\s'"`]*/gi;
121
+ var TOOL_OUTPUT_LIMIT = 4e3;
122
+ function replaceAbsolutePaths(input) {
123
+ return ABSOLUTE_PATH_PATTERNS.reduce(
124
+ (text, pattern) => text.replace(pattern, "<redacted-path>"),
125
+ input
126
+ );
127
+ }
128
+ function replaceSecrets(input) {
129
+ return SECRET_PATTERNS.reduce(
130
+ (text, pattern) => text.replace(pattern, "<redacted-secret>"),
131
+ input
132
+ );
133
+ }
134
+ function replaceSignedUrls(input) {
135
+ return input.replace(SIGNED_URL_PATTERN, "<redacted-signed-url>");
136
+ }
137
+ function redactText(input) {
138
+ return replaceSecrets(replaceSignedUrls(replaceAbsolutePaths(input)));
139
+ }
140
+ function truncateText(input, maxLength = TOOL_OUTPUT_LIMIT) {
141
+ if (input.length <= maxLength) {
142
+ return { text: input, truncated: false };
143
+ }
144
+ const head = input.slice(0, Math.floor(maxLength * 0.75));
145
+ const tail = input.slice(-Math.floor(maxLength * 0.15));
146
+ return {
147
+ text: `${head}
148
+
149
+ \u2026 output truncated \u2026
150
+
151
+ ${tail}`,
152
+ truncated: true
153
+ };
154
+ }
155
+ function sanitizeToolOutput(input) {
156
+ return truncateText(redactText(input));
157
+ }
158
+ function shouldSkipUserMessage(input) {
159
+ const trimmed = input.trimStart();
160
+ return trimmed.startsWith("# AGENTS.md instructions") || trimmed.startsWith("<environment_context>") || trimmed.startsWith("<permissions instructions>");
161
+ }
162
+
163
+ // ../shared/src/parser.ts
164
+ function normalizeTextForDedup(text) {
165
+ return text.replace(/\s+/g, " ").trim();
166
+ }
167
+ function normalizeTextSegment(text) {
168
+ const trimmed = text.trim();
169
+ if (!trimmed) {
170
+ return "";
171
+ }
172
+ if (/^<image\b[^>]*>$/i.test(trimmed) || /^<\/image>$/i.test(trimmed)) {
173
+ return "";
174
+ }
175
+ return trimmed.replace(/\[Image #\d+\]/g, "").trim();
176
+ }
177
+ function sanitizeImageSrc(src) {
178
+ const trimmed = src.trim();
179
+ if (!trimmed) {
180
+ return null;
181
+ }
182
+ if (/^data:image\//i.test(trimmed)) {
183
+ return trimmed;
184
+ }
185
+ if (!/^https?:\/\//i.test(trimmed)) {
186
+ return null;
187
+ }
188
+ if (/[?&](?:sig|signature|token|x-amz-|expires|awsaccesskeyid)=/i.test(trimmed)) {
189
+ return null;
190
+ }
191
+ return trimmed;
192
+ }
193
+ function extractImageUrl(item) {
194
+ const directImageUrl = asString(item.image_url);
195
+ if (directImageUrl) {
196
+ return sanitizeImageSrc(directImageUrl);
197
+ }
198
+ const nestedImageUrl = item.image_url;
199
+ if (nestedImageUrl && typeof nestedImageUrl === "object") {
200
+ const candidate = asString(nestedImageUrl.url) ?? asString(nestedImageUrl.uri);
201
+ if (candidate) {
202
+ return sanitizeImageSrc(candidate);
203
+ }
204
+ }
205
+ const directUrl = asString(item.url);
206
+ if (directUrl) {
207
+ return sanitizeImageSrc(directUrl);
208
+ }
209
+ return null;
210
+ }
211
+ function extractMessageContent(content) {
212
+ if (!Array.isArray(content)) {
213
+ return {
214
+ images: [],
215
+ text: ""
216
+ };
217
+ }
218
+ const images = [];
219
+ const text = content.flatMap((item) => {
220
+ if (!item || typeof item !== "object") {
221
+ return [];
222
+ }
223
+ const typedItem = item;
224
+ const itemType = asString(typedItem.type);
225
+ const imageUrl = extractImageUrl(typedItem);
226
+ if (imageUrl && (itemType === "input_image" || itemType === "output_image" || itemType === "image" || itemType === "image_url")) {
227
+ images.push({
228
+ alt: `Image ${images.length + 1}`,
229
+ src: imageUrl
230
+ });
231
+ return [];
232
+ }
233
+ const itemText = typedItem.text;
234
+ if (typeof itemText !== "string") {
235
+ return [];
236
+ }
237
+ const normalizedText = normalizeTextSegment(itemText);
238
+ return normalizedText ? [normalizedText] : [];
239
+ }).join("\n\n").trim();
240
+ return {
241
+ images,
242
+ text
243
+ };
244
+ }
245
+ function asString(value) {
246
+ return typeof value === "string" && value.length > 0 ? value : null;
247
+ }
248
+ function createEntryId(prefix, counter) {
249
+ return `${prefix}-${counter}`;
250
+ }
251
+ function createStats(entries) {
252
+ return entries.reduce(
253
+ (stats, entry) => {
254
+ if (entry.kind === "message" && entry.role === "user") {
255
+ stats.userMessages += 1;
256
+ }
257
+ if (entry.kind === "message" && entry.role === "assistant") {
258
+ stats.assistantMessages += 1;
259
+ }
260
+ if (entry.kind === "tool_call") {
261
+ stats.toolCalls += 1;
262
+ }
263
+ if (entry.kind === "tool_output") {
264
+ stats.toolOutputs += 1;
265
+ }
266
+ return stats;
267
+ },
268
+ {
269
+ assistantMessages: 0,
270
+ toolCalls: 0,
271
+ toolOutputs: 0,
272
+ userMessages: 0
273
+ }
274
+ );
275
+ }
276
+ function areEquivalentImages(left, right) {
277
+ const leftImages = left ?? [];
278
+ const rightImages = right ?? [];
279
+ if (leftImages.length !== rightImages.length) {
280
+ return false;
281
+ }
282
+ return leftImages.every((image, index) => image.src === rightImages[index]?.src);
283
+ }
284
+ function canDeduplicateMessages(previousEntry, nextEntry) {
285
+ if (normalizeTextForDedup(previousEntry.text) !== normalizeTextForDedup(nextEntry.text)) {
286
+ return false;
287
+ }
288
+ const previousImageCount = previousEntry.images?.length ?? 0;
289
+ const nextImageCount = nextEntry.images?.length ?? 0;
290
+ return areEquivalentImages(previousEntry.images, nextEntry.images) || previousImageCount === 0 || nextImageCount === 0;
291
+ }
292
+ function scoreMessageEntry(entry) {
293
+ return (entry.images?.length ?? 0) * 1e3 + entry.text.length;
294
+ }
295
+ function mergeDuplicateMessageEntries(previousEntry, nextEntry) {
296
+ const preferred = scoreMessageEntry(nextEntry) > scoreMessageEntry(previousEntry) ? nextEntry : previousEntry;
297
+ return {
298
+ ...preferred,
299
+ id: previousEntry.id
300
+ };
301
+ }
302
+ function isCloseDuplicateTimestamp(left, right, maxDeltaMs = 5e3) {
303
+ const leftTime = Date.parse(left);
304
+ const rightTime = Date.parse(right);
305
+ if (Number.isNaN(leftTime) || Number.isNaN(rightTime)) {
306
+ return left === right;
307
+ }
308
+ return Math.abs(leftTime - rightTime) <= maxDeltaMs;
309
+ }
310
+ function pushEntry(state, nextEntry) {
311
+ const previousEntry = state.entries.at(-1);
312
+ if (!previousEntry) {
313
+ state.entries.push(nextEntry);
314
+ return;
315
+ }
316
+ if (nextEntry.kind === "commentary" && previousEntry.kind === "commentary" && normalizeTextForDedup(previousEntry.text) === normalizeTextForDedup(nextEntry.text) && isCloseDuplicateTimestamp(previousEntry.createdAt, nextEntry.createdAt)) {
317
+ return;
318
+ }
319
+ if (nextEntry.kind === "message" && previousEntry.kind === "message" && previousEntry.role === nextEntry.role && canDeduplicateMessages(previousEntry, nextEntry) && isCloseDuplicateTimestamp(previousEntry.createdAt, nextEntry.createdAt)) {
320
+ state.entries[state.entries.length - 1] = mergeDuplicateMessageEntries(
321
+ previousEntry,
322
+ nextEntry
323
+ );
324
+ return;
325
+ }
326
+ state.entries.push(nextEntry);
327
+ }
328
+ function pushCommentaryEntry(state, createdAt, text) {
329
+ pushEntry(state, {
330
+ createdAt,
331
+ id: createEntryId("commentary", ++state.commentaryCount),
332
+ kind: "commentary",
333
+ text
334
+ });
335
+ }
336
+ function pushMessageEntry(state, createdAt, role, text, images = []) {
337
+ pushEntry(state, {
338
+ createdAt,
339
+ id: createEntryId("message", ++state.messageCount),
340
+ images: images.length > 0 ? images : void 0,
341
+ kind: "message",
342
+ role,
343
+ text
344
+ });
345
+ }
346
+ function createExcerpt(entries) {
347
+ const firstMessage = entries.find((entry) => entry.kind === "message");
348
+ if (!firstMessage || firstMessage.kind !== "message") {
349
+ return "Shared Codex session";
350
+ }
351
+ return firstMessage.text.replace(/\s+/g, " ").slice(0, 180);
352
+ }
353
+ function parseJsonLine(line) {
354
+ try {
355
+ return JSON.parse(line);
356
+ } catch {
357
+ return null;
358
+ }
359
+ }
360
+ function parseRecordText(record) {
361
+ if (!record.payload) {
362
+ return null;
363
+ }
364
+ return (asString(record.payload.message) ? normalizeTextSegment(asString(record.payload.message) ?? "") : null) ?? (asString(record.payload.text) ? normalizeTextSegment(asString(record.payload.text) ?? "") : null) ?? extractMessageContent(record.payload.content).text;
365
+ }
366
+ function parseRecordImages(record) {
367
+ if (!record.payload) {
368
+ return [];
369
+ }
370
+ if (Array.isArray(record.payload.images)) {
371
+ return record.payload.images.flatMap((item, index) => {
372
+ if (typeof item !== "string") {
373
+ return [];
374
+ }
375
+ const src = sanitizeImageSrc(item);
376
+ if (!src) {
377
+ return [];
378
+ }
379
+ return [{
380
+ alt: `Image ${index + 1}`,
381
+ src
382
+ }];
383
+ });
384
+ }
385
+ return extractMessageContent(record.payload.content).images;
386
+ }
387
+ function parseSessionJsonl(rawText, options) {
388
+ const state = {
389
+ commentaryCount: 0,
390
+ entries: [],
391
+ messageCount: 0,
392
+ toolCallCount: 0,
393
+ toolOutputCount: 0
394
+ };
395
+ let createdAt = options.sourceUpdatedAt;
396
+ for (const line of rawText.split("\n")) {
397
+ if (!line.trim()) {
398
+ continue;
399
+ }
400
+ const record = parseJsonLine(line);
401
+ if (!record?.type) {
402
+ continue;
403
+ }
404
+ if (record.type === "session_meta" && record.timestamp) {
405
+ createdAt = record.timestamp;
406
+ continue;
407
+ }
408
+ if (record.type === "turn_context" || !record.payload) {
409
+ continue;
410
+ }
411
+ const payloadType = asString(record.payload.type);
412
+ const createdAtValue = record.timestamp ?? options.sourceUpdatedAt;
413
+ if (record.type === "event_msg") {
414
+ if (payloadType === "user_message" || payloadType === "agent_message") {
415
+ const role = payloadType === "user_message" ? "user" : "assistant";
416
+ const text = redactText(parseRecordText(record) ?? "");
417
+ const images = parseRecordImages(record);
418
+ if (!text && images.length === 0) {
419
+ continue;
420
+ }
421
+ if (role === "user" && shouldSkipUserMessage(text)) {
422
+ continue;
423
+ }
424
+ pushMessageEntry(state, createdAtValue, role, text, images);
425
+ continue;
426
+ }
427
+ if (payloadType === "agent_reasoning") {
428
+ const text = redactText(parseRecordText(record) ?? "");
429
+ if (!text) {
430
+ continue;
431
+ }
432
+ pushCommentaryEntry(state, createdAtValue, text);
433
+ }
434
+ continue;
435
+ }
436
+ if (record.type !== "response_item") {
437
+ continue;
438
+ }
439
+ if (payloadType === "message") {
440
+ const role = asString(record.payload.role);
441
+ if (role !== "user" && role !== "assistant") {
442
+ continue;
443
+ }
444
+ const parsedContent = extractMessageContent(record.payload.content);
445
+ const text = redactText(parsedContent.text);
446
+ if (!text && parsedContent.images.length === 0) {
447
+ continue;
448
+ }
449
+ if (role === "user" && shouldSkipUserMessage(text)) {
450
+ continue;
451
+ }
452
+ pushMessageEntry(state, createdAtValue, role, text, parsedContent.images);
453
+ continue;
454
+ }
455
+ if (payloadType === "function_call" || payloadType === "custom_tool_call") {
456
+ const toolName = asString(record.payload.name) ?? "tool";
457
+ const input = asString(record.payload.arguments) ?? asString(record.payload.input) ?? "{}";
458
+ state.entries.push({
459
+ createdAt: createdAtValue,
460
+ id: createEntryId("tool-call", ++state.toolCallCount),
461
+ input: redactText(input),
462
+ kind: "tool_call",
463
+ toolName
464
+ });
465
+ continue;
466
+ }
467
+ if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
468
+ const output = asString(record.payload.output) ?? asString(record.payload.text) ?? "";
469
+ if (!output) {
470
+ continue;
471
+ }
472
+ const toolName = asString(record.payload.name) ?? "tool";
473
+ const sanitized = sanitizeToolOutput(output);
474
+ state.entries.push({
475
+ createdAt: createdAtValue,
476
+ id: createEntryId("tool-output", ++state.toolOutputCount),
477
+ kind: "tool_output",
478
+ output: sanitized.text,
479
+ toolName,
480
+ truncated: sanitized.truncated
481
+ });
482
+ }
483
+ }
484
+ return {
485
+ createdAt,
486
+ entries: state.entries,
487
+ excerpt: createExcerpt(state.entries),
488
+ id: options.title,
489
+ sourceUpdatedAt: options.sourceUpdatedAt,
490
+ stats: createStats(state.entries),
491
+ title: options.title
492
+ };
493
+ }
494
+ function hashContent(input) {
495
+ return createHash("sha256").update(input).digest("hex");
496
+ }
497
+ function buildShareSnapshot(input) {
498
+ const renderPayload = parseSessionJsonl(input.rawText, {
499
+ sourceUpdatedAt: input.sourceUpdatedAt,
500
+ title: input.title
501
+ });
502
+ const rawJsonlGzip = gzipSync(input.rawText);
503
+ const contentHash = hashContent(input.rawText);
504
+ return {
505
+ contentHash,
506
+ rawJsonlGzip,
507
+ renderPayload: {
508
+ ...renderPayload,
509
+ id: input.sessionId
510
+ },
511
+ sourceSessionId: input.sessionId,
512
+ sourceUpdatedAt: input.sourceUpdatedAt,
513
+ stats: renderPayload.stats,
514
+ title: input.title
515
+ };
516
+ }
517
+
518
+ // ../shared/src/public-url.ts
519
+ function trimTrailingSlash(input) {
520
+ return input.endsWith("/") ? input.slice(0, -1) : input;
521
+ }
522
+ function getSharePath(shareId) {
523
+ return `/c/${shareId}`;
524
+ }
525
+ function buildPublicShareUrl(siteUrl, shareId) {
526
+ return `${trimTrailingSlash(siteUrl)}${getSharePath(shareId)}`;
527
+ }
528
+
529
+ // ../shared/src/session-resolution.ts
530
+ import { readFile, readdir, stat } from "fs/promises";
531
+ import { join as join2 } from "path";
532
+ import { homedir as homedir2 } from "os";
533
+ async function pathExists(path) {
534
+ try {
535
+ await stat(path);
536
+ return true;
537
+ } catch {
538
+ return false;
539
+ }
540
+ }
541
+ function getCodexHome(explicitCodexHome) {
542
+ return explicitCodexHome ?? process.env["CODEX_HOME"] ?? join2(homedir2(), ".codex");
543
+ }
544
+ function parseJsonLine2(line) {
545
+ try {
546
+ return JSON.parse(line);
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ async function readSessionIndex(explicitCodexHome) {
552
+ const codexHome = getCodexHome(explicitCodexHome);
553
+ const indexPath = join2(codexHome, "session_index.jsonl");
554
+ if (!await pathExists(indexPath)) {
555
+ return [];
556
+ }
557
+ const raw = await readFile(indexPath, "utf8");
558
+ return raw.split("\n").flatMap((line) => {
559
+ if (!line.trim()) {
560
+ return [];
561
+ }
562
+ const entry = parseJsonLine2(line);
563
+ return entry ? [entry] : [];
564
+ });
565
+ }
566
+ async function findSessionFileById(directory, sessionId) {
567
+ const entries = await readdir(directory, { withFileTypes: true });
568
+ for (const entry of entries) {
569
+ const fullPath = join2(directory, entry.name);
570
+ if (entry.isDirectory()) {
571
+ const nested = await findSessionFileById(fullPath, sessionId);
572
+ if (nested) {
573
+ return nested;
574
+ }
575
+ continue;
576
+ }
577
+ if (!entry.name.endsWith(".jsonl")) {
578
+ continue;
579
+ }
580
+ if (entry.name.includes(sessionId)) {
581
+ return fullPath;
582
+ }
583
+ const firstLine = (await readFile(fullPath, "utf8")).split("\n", 1)[0] ?? "";
584
+ if (firstLine.includes(sessionId)) {
585
+ return fullPath;
586
+ }
587
+ }
588
+ return null;
589
+ }
590
+ async function resolveSessionById(sessionId, explicitCodexHome) {
591
+ const codexHome = getCodexHome(explicitCodexHome);
592
+ const index = await readSessionIndex(codexHome);
593
+ const indexEntry = index.find((entry) => entry.id === sessionId);
594
+ const sessionsRoot = join2(codexHome, "sessions");
595
+ const filePath = await findSessionFileById(sessionsRoot, sessionId);
596
+ if (!filePath) {
597
+ throw new Error(`Could not find session file for ${sessionId}`);
598
+ }
599
+ const fileStat = await stat(filePath);
600
+ const rawText = await readFile(filePath, "utf8");
601
+ return {
602
+ filePath,
603
+ id: sessionId,
604
+ rawText,
605
+ sourceUpdatedAt: indexEntry?.updated_at ?? fileStat.mtime.toISOString(),
606
+ title: indexEntry?.thread_name ?? "Untitled Codex Session"
607
+ };
608
+ }
609
+
610
+ // src/tracker.ts
611
+ import { watch } from "fs";
612
+ import { execFile } from "child_process";
613
+ import { promisify } from "util";
614
+ import { platform as platform2 } from "os";
615
+
616
+ // src/api-client.ts
617
+ import { Buffer } from "buffer";
618
+ var ApiRequestError = class extends Error {
619
+ constructor(message, status, body) {
620
+ super(message);
621
+ this.status = status;
622
+ this.body = body;
623
+ this.name = "ApiRequestError";
624
+ }
625
+ status;
626
+ body;
627
+ };
628
+ function createFormData(snapshot) {
629
+ const formData = new FormData();
630
+ formData.set(
631
+ "metadata",
632
+ JSON.stringify({
633
+ contentHash: snapshot.contentHash,
634
+ renderPayload: snapshot.renderPayload,
635
+ sourceSessionId: snapshot.sourceSessionId,
636
+ sourceUpdatedAt: snapshot.sourceUpdatedAt,
637
+ stats: snapshot.stats,
638
+ title: snapshot.title
639
+ })
640
+ );
641
+ formData.set(
642
+ "sessionFile",
643
+ new File([Buffer.from(snapshot.rawJsonlGzip)], `${snapshot.sourceSessionId}.jsonl.gz`, {
644
+ type: "application/gzip"
645
+ })
646
+ );
647
+ return formData;
648
+ }
649
+ var CodexLinkApiClient = class {
650
+ constructor(baseUrl, fetchImpl) {
651
+ this.baseUrl = baseUrl;
652
+ this.fetchImpl = fetchImpl;
653
+ }
654
+ baseUrl;
655
+ fetchImpl;
656
+ async createShare(snapshot) {
657
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/shares`, {
658
+ body: createFormData(snapshot),
659
+ method: "POST"
660
+ });
661
+ if (!response.ok) {
662
+ const body = await response.text();
663
+ throw new ApiRequestError(`Failed to create share: ${body}`, response.status, body);
664
+ }
665
+ return await response.json();
666
+ }
667
+ async getShare(shareId) {
668
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/shares/${shareId}`, {
669
+ method: "GET"
670
+ });
671
+ if (!response.ok) {
672
+ const body = await response.text();
673
+ throw new ApiRequestError(`Failed to fetch share: ${body}`, response.status, body);
674
+ }
675
+ return await response.json();
676
+ }
677
+ async updateShare(shareId, manageToken, snapshot) {
678
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/shares/${shareId}`, {
679
+ body: createFormData(snapshot),
680
+ headers: {
681
+ authorization: `Bearer ${manageToken}`
682
+ },
683
+ method: "PUT"
684
+ });
685
+ if (!response.ok) {
686
+ const body = await response.text();
687
+ throw new ApiRequestError(`Failed to update share: ${body}`, response.status, body);
688
+ }
689
+ }
690
+ async revokeShare(shareId, manageToken) {
691
+ const response = await this.fetchImpl(`${this.baseUrl}/v1/shares/${shareId}`, {
692
+ headers: {
693
+ authorization: `Bearer ${manageToken}`
694
+ },
695
+ method: "DELETE"
696
+ });
697
+ if (!response.ok) {
698
+ const body = await response.text();
699
+ throw new ApiRequestError(`Failed to revoke share: ${body}`, response.status, body);
700
+ }
701
+ }
702
+ };
703
+
704
+ // src/state.ts
705
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
706
+ import { join as join3 } from "path";
707
+ import { homedir as homedir3, platform } from "os";
708
+ var EMPTY_STATE = {
709
+ seenSessionIds: [],
710
+ trackedSessions: {}
711
+ };
712
+ function getDefaultStateRoot() {
713
+ if (process.env["CODEXLINK_STATE_DIR"]) {
714
+ return expandHomePath(process.env["CODEXLINK_STATE_DIR"]) ?? process.env["CODEXLINK_STATE_DIR"];
715
+ }
716
+ if (platform() === "darwin") {
717
+ return join3(homedir3(), "Library", "Application Support", "CodexLink");
718
+ }
719
+ return join3(
720
+ process.env["XDG_STATE_HOME"] ?? join3(homedir3(), ".local", "state"),
721
+ "codexlink"
722
+ );
723
+ }
724
+ function getStatePath(stateRoot) {
725
+ return join3(stateRoot, "state.json");
726
+ }
727
+ async function loadAppState(stateRoot) {
728
+ try {
729
+ const raw = await readFile2(getStatePath(stateRoot), "utf8");
730
+ const parsed = JSON.parse(raw);
731
+ return { ...EMPTY_STATE, ...parsed };
732
+ } catch {
733
+ return EMPTY_STATE;
734
+ }
735
+ }
736
+ async function saveAppState(stateRoot, state) {
737
+ await mkdir(stateRoot, { recursive: true });
738
+ await writeFile(getStatePath(stateRoot), JSON.stringify(state, null, 2));
739
+ }
740
+
741
+ // src/tracker.ts
742
+ var execFileAsync = promisify(execFile);
743
+ var DEFAULT_API_BASE_URL = "https://api.codexl.ink";
744
+ function shouldRecreateShare(error) {
745
+ return error instanceof ApiRequestError && [401, 403, 404, 410].includes(error.status);
746
+ }
747
+ function createCliContext(overrides = {}) {
748
+ loadCodexLinkEnv();
749
+ return {
750
+ apiBaseUrl: overrides.apiBaseUrl ?? process.env["CODEXLINK_API_URL"] ?? process.env["API_BASE_URL"] ?? DEFAULT_API_BASE_URL,
751
+ codexHome: expandHomePath(overrides.codexHome) ?? expandHomePath(process.env["CODEX_HOME"]),
752
+ fetchImpl: overrides.fetchImpl ?? fetch,
753
+ logger: overrides.logger ?? console,
754
+ stateRoot: expandHomePath(overrides.stateRoot) ?? getDefaultStateRoot()
755
+ };
756
+ }
757
+ async function upsertTrackedSession(context, snapshot, resolvedFilePath) {
758
+ const state = await loadAppState(context.stateRoot);
759
+ const existing = state.trackedSessions[snapshot.sourceSessionId];
760
+ const client = new CodexLinkApiClient(context.apiBaseUrl, context.fetchImpl);
761
+ if (existing) {
762
+ try {
763
+ if (existing.lastContentHash !== snapshot.contentHash) {
764
+ await client.updateShare(existing.shareId, existing.manageToken, snapshot);
765
+ } else {
766
+ await client.getShare(existing.shareId);
767
+ }
768
+ } catch (error) {
769
+ if (!shouldRecreateShare(error)) {
770
+ throw error;
771
+ }
772
+ const created2 = await client.createShare(snapshot);
773
+ const recreatedTracked = {
774
+ filePath: resolvedFilePath,
775
+ lastContentHash: snapshot.contentHash,
776
+ manageToken: created2.manageToken,
777
+ sessionId: snapshot.sourceSessionId,
778
+ shareId: created2.shareId,
779
+ title: snapshot.title
780
+ };
781
+ state.trackedSessions[snapshot.sourceSessionId] = recreatedTracked;
782
+ await saveAppState(context.stateRoot, state);
783
+ return recreatedTracked;
784
+ }
785
+ const tracked2 = {
786
+ ...existing,
787
+ filePath: resolvedFilePath,
788
+ lastContentHash: snapshot.contentHash,
789
+ title: snapshot.title
790
+ };
791
+ state.trackedSessions[snapshot.sourceSessionId] = tracked2;
792
+ await saveAppState(context.stateRoot, state);
793
+ return tracked2;
794
+ }
795
+ const created = await client.createShare(snapshot);
796
+ const tracked = {
797
+ filePath: resolvedFilePath,
798
+ lastContentHash: snapshot.contentHash,
799
+ manageToken: created.manageToken,
800
+ sessionId: snapshot.sourceSessionId,
801
+ shareId: created.shareId,
802
+ title: snapshot.title
803
+ };
804
+ state.trackedSessions[snapshot.sourceSessionId] = tracked;
805
+ await saveAppState(context.stateRoot, state);
806
+ return tracked;
807
+ }
808
+ async function syncSession(sessionId, context) {
809
+ const resolved = await resolveSessionById(sessionId, context.codexHome);
810
+ const snapshot = buildShareSnapshot({
811
+ rawText: resolved.rawText,
812
+ sessionId: resolved.id,
813
+ sourceUpdatedAt: resolved.sourceUpdatedAt,
814
+ title: resolved.title
815
+ });
816
+ return upsertTrackedSession(context, snapshot, resolved.filePath);
817
+ }
818
+ var SessionTracker = class {
819
+ constructor(sessionId, context, debounceMs = 800) {
820
+ this.sessionId = sessionId;
821
+ this.context = context;
822
+ this.debounceMs = debounceMs;
823
+ }
824
+ sessionId;
825
+ context;
826
+ debounceMs;
827
+ debounceHandle = null;
828
+ filePath = "";
829
+ watcher = null;
830
+ async start() {
831
+ const tracked = await syncSession(this.sessionId, this.context);
832
+ this.filePath = tracked.filePath;
833
+ this.watcher = watch(this.filePath, () => {
834
+ this.scheduleSync();
835
+ });
836
+ }
837
+ scheduleSync() {
838
+ if (this.debounceHandle) {
839
+ clearTimeout(this.debounceHandle);
840
+ }
841
+ this.debounceHandle = setTimeout(() => {
842
+ void this.runSync().catch((error) => this.context.logger.error(error));
843
+ }, this.debounceMs);
844
+ }
845
+ async runSync() {
846
+ const state = await loadAppState(this.context.stateRoot);
847
+ const existing = state.trackedSessions[this.sessionId];
848
+ if (!existing) {
849
+ await syncSession(this.sessionId, this.context);
850
+ return;
851
+ }
852
+ const nextTracked = await syncSession(this.sessionId, this.context);
853
+ this.filePath = nextTracked.filePath;
854
+ }
855
+ async dispose() {
856
+ if (this.debounceHandle) {
857
+ clearTimeout(this.debounceHandle);
858
+ this.debounceHandle = null;
859
+ }
860
+ this.watcher?.close();
861
+ this.watcher = null;
862
+ }
863
+ };
864
+ async function revokeTrackedSession(target, context) {
865
+ const state = await loadAppState(context.stateRoot);
866
+ const tracked = state.trackedSessions[target] ?? Object.values(state.trackedSessions).find((entry) => entry.shareId === target);
867
+ if (!tracked) {
868
+ throw new Error(`No tracked session or share found for ${target}`);
869
+ }
870
+ const client = new CodexLinkApiClient(context.apiBaseUrl, context.fetchImpl);
871
+ await client.revokeShare(tracked.shareId, tracked.manageToken);
872
+ delete state.trackedSessions[tracked.sessionId];
873
+ await saveAppState(context.stateRoot, state);
874
+ }
875
+ async function defaultPromptHandler(input) {
876
+ if (platform2() !== "darwin") {
877
+ return false;
878
+ }
879
+ const script = `
880
+ set response to button returned of (display dialog "Track this Codex chat on codexl.ink?
881
+
882
+ ${input.title}
883
+ ${input.sessionId}" buttons {"Ignore", "Track"} default button "Track")
884
+ return response
885
+ `;
886
+ const { stdout } = await execFileAsync("osascript", ["-e", script]);
887
+ return stdout.trim() === "Track";
888
+ }
889
+ var MonitorService = class {
890
+ constructor(context, prompt = defaultPromptHandler) {
891
+ this.context = context;
892
+ this.prompt = prompt;
893
+ }
894
+ context;
895
+ prompt;
896
+ trackers = /* @__PURE__ */ new Map();
897
+ async handleSessionIndex(indexEntries) {
898
+ const state = await loadAppState(this.context.stateRoot);
899
+ const seen = new Set(state.seenSessionIds);
900
+ for (const entry of indexEntries) {
901
+ if (seen.has(entry.id)) {
902
+ continue;
903
+ }
904
+ seen.add(entry.id);
905
+ const accepted = await this.prompt({
906
+ sessionId: entry.id,
907
+ title: entry.thread_name ?? "Untitled Codex Session"
908
+ });
909
+ if (accepted) {
910
+ const tracker = new SessionTracker(entry.id, this.context);
911
+ await tracker.start();
912
+ this.trackers.set(entry.id, tracker);
913
+ }
914
+ }
915
+ const nextState = {
916
+ ...state,
917
+ seenSessionIds: [...seen]
918
+ };
919
+ await saveAppState(this.context.stateRoot, nextState);
920
+ }
921
+ async dispose() {
922
+ await Promise.all([...this.trackers.values()].map((tracker) => tracker.dispose()));
923
+ this.trackers.clear();
924
+ }
925
+ };
926
+
927
+ // src/index.ts
928
+ var DEFAULT_SITE_URL = "https://codexl.ink";
929
+ async function waitForExit() {
930
+ await new Promise((resolve2) => {
931
+ const handler = () => resolve2();
932
+ process.once("SIGINT", handler);
933
+ process.once("SIGTERM", handler);
934
+ });
935
+ }
936
+ async function runShare(sessionId, apiUrl) {
937
+ const context = createCliContext({ apiBaseUrl: apiUrl });
938
+ const tracked = await syncSession(sessionId, context);
939
+ const siteUrl = process.env["SITE_URL"] ?? DEFAULT_SITE_URL;
940
+ context.logger.log(`Share URL: ${buildPublicShareUrl(siteUrl, tracked.shareId)}`);
941
+ context.logger.log(`Manage token: ${tracked.manageToken}`);
942
+ }
943
+ async function runTrack(sessionId, apiUrl) {
944
+ const context = createCliContext({ apiBaseUrl: apiUrl });
945
+ const tracker = new SessionTracker(sessionId, context);
946
+ await tracker.start();
947
+ context.logger.log(`Tracking ${sessionId}`);
948
+ await waitForExit();
949
+ await tracker.dispose();
950
+ }
951
+ async function runMonitor(apiUrl) {
952
+ const context = createCliContext({ apiBaseUrl: apiUrl });
953
+ const service = new MonitorService(context);
954
+ const codexHome = context.codexHome ?? process.env["CODEX_HOME"];
955
+ const indexPath = join4(codexHome ?? join4(process.env["HOME"] ?? "", ".codex"), "session_index.jsonl");
956
+ const syncIndex = async () => {
957
+ const entries = await readSessionIndex(context.codexHome);
958
+ await service.handleSessionIndex(entries);
959
+ };
960
+ await syncIndex();
961
+ const watcher = watch2(indexPath, () => {
962
+ void syncIndex().catch((error) => context.logger.error(error));
963
+ });
964
+ context.logger.log(`Monitoring ${indexPath}`);
965
+ await waitForExit();
966
+ watcher.close();
967
+ await service.dispose();
968
+ }
969
+ async function runUnshare(target, apiUrl) {
970
+ const context = createCliContext({ apiBaseUrl: apiUrl });
971
+ await revokeTrackedSession(target, context);
972
+ context.logger.log(`Revoked ${target}`);
973
+ }
974
+ var program = new Command();
975
+ program.name("cdxl").description("Publish and track Codex chats");
976
+ program.command("share").argument("<sessionId>").option("--api-url <url>").action(async (sessionId, options) => {
977
+ await runShare(sessionId, options.apiUrl);
978
+ });
979
+ program.command("track").argument("<sessionId>").option("--api-url <url>").action(async (sessionId, options) => {
980
+ await runTrack(sessionId, options.apiUrl);
981
+ });
982
+ program.command("monitor").option("--api-url <url>").action(async (options) => {
983
+ await runMonitor(options.apiUrl);
984
+ });
985
+ program.command("unshare").argument("<shareOrSessionId>").option("--api-url <url>").action(async (target, options) => {
986
+ await runUnshare(target, options.apiUrl);
987
+ });
988
+ program.parseAsync(process.argv).catch((error) => {
989
+ console.error(error);
990
+ process.exitCode = 1;
991
+ });
992
+ export {
993
+ runMonitor,
994
+ runShare,
995
+ runTrack,
996
+ runUnshare
997
+ };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "codex-link",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "cdxl": "dist/index.js"
7
+ },
8
+ "description": "Publish and track Codex chats",
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "commander": "^14.0.1"
17
+ }
18
+ }