codex-link 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +996 -0
- package/dist/index.js +18 -18
- package/package.json +2 -2
package/dist/bin.js
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
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
|
+
function createProgram() {
|
|
975
|
+
const program = new Command();
|
|
976
|
+
program.name("cdxl").description("Publish and track Codex chats").showHelpAfterError().showSuggestionAfterError();
|
|
977
|
+
program.command("share").description("Create a public share for one Codex session and print its URL.").argument("<sessionId>", "Codex session ID to publish").option("--api-url <url>", "Override the CodexLink API base URL").action(async (sessionId, options) => {
|
|
978
|
+
await runShare(sessionId, options.apiUrl);
|
|
979
|
+
});
|
|
980
|
+
program.command("track").description("Keep one session synced to the same public link as new messages arrive.").argument("<sessionId>", "Codex session ID to track continuously").option("--api-url <url>", "Override the CodexLink API base URL").action(async (sessionId, options) => {
|
|
981
|
+
await runTrack(sessionId, options.apiUrl);
|
|
982
|
+
});
|
|
983
|
+
program.command("monitor").description("Watch for new local Codex sessions and offer to start tracking them.").option("--api-url <url>", "Override the CodexLink API base URL").action(async (options) => {
|
|
984
|
+
await runMonitor(options.apiUrl);
|
|
985
|
+
});
|
|
986
|
+
program.command("unshare").description("Revoke a public share by share ID or by a locally tracked session ID.").argument("<shareOrSessionId>", "Share ID or tracked session ID to revoke").option("--api-url <url>", "Override the CodexLink API base URL").action(async (target, options) => {
|
|
987
|
+
await runUnshare(target, options.apiUrl);
|
|
988
|
+
});
|
|
989
|
+
return program;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/bin.ts
|
|
993
|
+
createProgram().parseAsync(process.argv).catch((error) => {
|
|
994
|
+
console.error(error);
|
|
995
|
+
process.exitCode = 1;
|
|
996
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -971,25 +971,25 @@ async function runUnshare(target, apiUrl) {
|
|
|
971
971
|
await revokeTrackedSession(target, context);
|
|
972
972
|
context.logger.log(`Revoked ${target}`);
|
|
973
973
|
}
|
|
974
|
-
|
|
975
|
-
program
|
|
976
|
-
program.
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
});
|
|
974
|
+
function createProgram() {
|
|
975
|
+
const program = new Command();
|
|
976
|
+
program.name("cdxl").description("Publish and track Codex chats").showHelpAfterError().showSuggestionAfterError();
|
|
977
|
+
program.command("share").description("Create a public share for one Codex session and print its URL.").argument("<sessionId>", "Codex session ID to publish").option("--api-url <url>", "Override the CodexLink API base URL").action(async (sessionId, options) => {
|
|
978
|
+
await runShare(sessionId, options.apiUrl);
|
|
979
|
+
});
|
|
980
|
+
program.command("track").description("Keep one session synced to the same public link as new messages arrive.").argument("<sessionId>", "Codex session ID to track continuously").option("--api-url <url>", "Override the CodexLink API base URL").action(async (sessionId, options) => {
|
|
981
|
+
await runTrack(sessionId, options.apiUrl);
|
|
982
|
+
});
|
|
983
|
+
program.command("monitor").description("Watch for new local Codex sessions and offer to start tracking them.").option("--api-url <url>", "Override the CodexLink API base URL").action(async (options) => {
|
|
984
|
+
await runMonitor(options.apiUrl);
|
|
985
|
+
});
|
|
986
|
+
program.command("unshare").description("Revoke a public share by share ID or by a locally tracked session ID.").argument("<shareOrSessionId>", "Share ID or tracked session ID to revoke").option("--api-url <url>", "Override the CodexLink API base URL").action(async (target, options) => {
|
|
987
|
+
await runUnshare(target, options.apiUrl);
|
|
988
|
+
});
|
|
989
|
+
return program;
|
|
990
|
+
}
|
|
992
991
|
export {
|
|
992
|
+
createProgram,
|
|
993
993
|
runMonitor,
|
|
994
994
|
runShare,
|
|
995
995
|
runTrack,
|