threadlog 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.
@@ -0,0 +1,50 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ import { redactSensitiveText } from "../redaction/redact.js";
4
+ export async function buildRedactedRawEventArrayFromJsonl(filePath) {
5
+ const events = [];
6
+ const reader = createInterface({
7
+ input: createReadStream(filePath, { encoding: "utf8" }),
8
+ crlfDelay: Infinity,
9
+ });
10
+ let lineIndex = 0;
11
+ for await (const rawLine of reader) {
12
+ const line = rawLine.trim();
13
+ if (!line) {
14
+ continue;
15
+ }
16
+ const parsed = parseJsonLine(line, lineIndex + 1);
17
+ events.push(redactJsonValue(parsed));
18
+ lineIndex += 1;
19
+ }
20
+ if (events.length === 0) {
21
+ throw new Error("session file does not contain any JSONL events");
22
+ }
23
+ return events;
24
+ }
25
+ function parseJsonLine(line, lineNumber) {
26
+ try {
27
+ return JSON.parse(line);
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : "invalid JSON";
31
+ throw new Error(`invalid JSONL at line ${lineNumber}: ${message}`);
32
+ }
33
+ }
34
+ function redactJsonValue(value) {
35
+ if (typeof value === "string") {
36
+ return redactSensitiveText(value);
37
+ }
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => redactJsonValue(item));
40
+ }
41
+ if (!value || typeof value !== "object") {
42
+ return value;
43
+ }
44
+ const input = value;
45
+ const output = {};
46
+ for (const [key, item] of Object.entries(input)) {
47
+ output[key] = redactJsonValue(item);
48
+ }
49
+ return output;
50
+ }
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import { gzipSync } from "node:zlib";
3
+ export function buildUploadArtifact(rawArtifact) {
4
+ const payloadBytes = Buffer.from(JSON.stringify(rawArtifact), "utf8");
5
+ const gzippedBytes = gzipSync(payloadBytes);
6
+ const sha256Hex = createHash("sha256").update(gzippedBytes).digest("hex");
7
+ return {
8
+ bytes: gzippedBytes,
9
+ sizeBytes: gzippedBytes.byteLength,
10
+ sha256Hex,
11
+ contentType: "application/json",
12
+ contentEncoding: "gzip",
13
+ };
14
+ }
@@ -0,0 +1,114 @@
1
+ export class UploadRequestError extends Error {
2
+ status;
3
+ code;
4
+ constructor(status, message, code) {
5
+ super(message);
6
+ this.name = "UploadRequestError";
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function createUploadClient(config) {
12
+ const authHeader = `Bearer ${config.token}`;
13
+ return {
14
+ async createAndUploadArtifact(input) {
15
+ const createRequest = {
16
+ contentType: input.artifact.contentType,
17
+ contentEncoding: input.artifact.contentEncoding,
18
+ sizeBytes: input.artifact.sizeBytes,
19
+ sha256Hex: input.artifact.sha256Hex,
20
+ };
21
+ const createResponse = await requestJson({
22
+ apiOrigin: config.apiOrigin,
23
+ path: "/v1/thread-uploads",
24
+ method: "POST",
25
+ authorization: authHeader,
26
+ body: createRequest,
27
+ });
28
+ await uploadToSignedUrl({
29
+ uploadUrl: createResponse.uploadUrl,
30
+ requiredHeaders: createResponse.requiredHeaders,
31
+ bytes: input.artifact.bytes,
32
+ });
33
+ const completeRequest = {
34
+ source: input.source,
35
+ title: input.title,
36
+ externalThreadId: input.externalThreadId,
37
+ capturedAt: input.capturedAt,
38
+ };
39
+ const completeResponse = await requestJson({
40
+ apiOrigin: config.apiOrigin,
41
+ path: `/v1/thread-uploads/${createResponse.uploadId}/complete`,
42
+ method: "POST",
43
+ authorization: authHeader,
44
+ body: completeRequest,
45
+ });
46
+ validateImportState(completeResponse.importState);
47
+ return completeResponse;
48
+ },
49
+ };
50
+ }
51
+ async function uploadToSignedUrl(params) {
52
+ const headers = new Headers();
53
+ for (const [name, value] of Object.entries(params.requiredHeaders)) {
54
+ headers.set(name, String(value));
55
+ }
56
+ const response = await fetch(params.uploadUrl, {
57
+ method: "PUT",
58
+ headers,
59
+ body: Buffer.from(params.bytes),
60
+ });
61
+ if (response.status !== 200 && response.status !== 204) {
62
+ throw new Error(`artifact upload failed with status ${response.status}`);
63
+ }
64
+ }
65
+ async function requestJson(input) {
66
+ const url = new URL(input.path, `${input.apiOrigin.replace(/\/$/, "")}/`);
67
+ const headers = new Headers({
68
+ authorization: input.authorization,
69
+ });
70
+ let body;
71
+ if (input.body !== undefined) {
72
+ headers.set("content-type", "application/json");
73
+ body = JSON.stringify(input.body);
74
+ }
75
+ const response = await fetch(url, {
76
+ method: input.method,
77
+ headers,
78
+ body,
79
+ });
80
+ const responseBody = await response.text();
81
+ if (!response.ok) {
82
+ let message = `request failed (${response.status})`;
83
+ let code;
84
+ const contentType = response.headers.get("content-type") ?? "";
85
+ if (contentType.includes("application/json") && responseBody.trim().length > 0) {
86
+ try {
87
+ const parsed = JSON.parse(responseBody);
88
+ if (typeof parsed.message === "string" && parsed.message.trim().length > 0) {
89
+ message = parsed.message.trim();
90
+ }
91
+ if (typeof parsed.code === "string" && parsed.code.trim().length > 0) {
92
+ code = parsed.code.trim();
93
+ }
94
+ }
95
+ catch {
96
+ message = `${message}: ${responseBody}`;
97
+ }
98
+ }
99
+ else if (responseBody.trim().length > 0) {
100
+ message = `${message}: ${responseBody}`;
101
+ }
102
+ throw new UploadRequestError(response.status, message, code);
103
+ }
104
+ if (!responseBody) {
105
+ return undefined;
106
+ }
107
+ return JSON.parse(responseBody);
108
+ }
109
+ function validateImportState(state) {
110
+ const allowedStates = ["queued", "processing", "ready", "failed"];
111
+ if (!allowedStates.includes(state)) {
112
+ throw new Error(`unexpected import state: ${state}`);
113
+ }
114
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "threadlog",
3
+ "version": "0.1.0",
4
+ "description": "CLI for uploading local Codex and Claude sessions to Threadlog.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "bin": {
8
+ "threadlog": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "homepage": "https://www.threadlog.dev",
16
+ "bugs": {
17
+ "url": "https://github.com/SerCeMan/threadlog/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/SerCeMan/threadlog.git"
22
+ },
23
+ "keywords": [
24
+ "threadlog",
25
+ "cli",
26
+ "codex",
27
+ "claude",
28
+ "threads",
29
+ "developer-tools"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "clean": "rm -rf dist",
39
+ "build": "npm run clean && tsc --project tsconfig.build.json",
40
+ "lint": "pnpm run typecheck",
41
+ "prepack": "npm run build",
42
+ "typecheck": "tsc --project tsconfig.json --noEmit",
43
+ "test": "vitest run"
44
+ },
45
+ "dependencies": {
46
+ "@inquirer/prompts": "^7.10.1",
47
+ "commander": "^14.0.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.4.0",
51
+ "typescript": "^5.9.2",
52
+ "vitest": "^3.2.4"
53
+ }
54
+ }