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,133 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ const TITLE_WORD_LIMIT = 10;
4
+ const ELLIPSIS = "...";
5
+ const CONTEXT_SEGMENT_PATTERNS = [
6
+ /# AGENTS\.md instructions for [^\n]*\n\s*<INSTRUCTIONS>.*?<\/INSTRUCTIONS>/gis,
7
+ /<([a-z0-9_ -]*(?:context|instructions)[a-z0-9_ -]*)>.*?<\/\1>/gis,
8
+ ];
9
+ export async function readCodexPromptPreviewTitle(filePath) {
10
+ const reader = createInterface({
11
+ input: createReadStream(filePath, { encoding: "utf8" }),
12
+ crlfDelay: Infinity,
13
+ });
14
+ let fallbackText = null;
15
+ for await (const rawLine of reader) {
16
+ const line = rawLine.trim();
17
+ if (!line) {
18
+ continue;
19
+ }
20
+ const parsed = safeParseRecord(line);
21
+ if (!parsed) {
22
+ continue;
23
+ }
24
+ const primaryUserMessage = extractPrimaryUserMessage(parsed);
25
+ if (primaryUserMessage) {
26
+ return truncateForThreadTitle(primaryUserMessage);
27
+ }
28
+ if (!fallbackText) {
29
+ fallbackText = extractFallbackText(parsed);
30
+ }
31
+ }
32
+ return truncateForThreadTitle(fallbackText);
33
+ }
34
+ export function truncateForThreadTitle(sourceText) {
35
+ const normalized = sourceText?.replace(/\s+/g, " ").trim() ?? "";
36
+ if (!normalized) {
37
+ return null;
38
+ }
39
+ const words = normalized.split(" ").filter((word) => word.trim().length > 0);
40
+ if (words.length === 0) {
41
+ return null;
42
+ }
43
+ return `${words.slice(0, TITLE_WORD_LIMIT).join(" ")}${ELLIPSIS}`;
44
+ }
45
+ function extractPrimaryUserMessage(event) {
46
+ const payload = asRecord(event.payload);
47
+ const eventType = asString(event.type);
48
+ const payloadType = asString(payload?.type);
49
+ if (eventType === "event_msg" && payloadType === "user_message") {
50
+ return cleanMessageText(firstNonBlank(asString(payload?.message), asString(event.message), asString(payload?.text), asString(event.text)));
51
+ }
52
+ if (eventType === "response_item" && payloadType === "message" && normalizeRole(payload?.role) === "user") {
53
+ return cleanMessageText(firstNonBlank(extractText(payload?.content), asString(payload?.message), asString(payload?.text), extractText(payload?.text_elements)));
54
+ }
55
+ return null;
56
+ }
57
+ function cleanMessageText(rawText) {
58
+ const input = rawText?.trim() ?? "";
59
+ if (!input) {
60
+ return null;
61
+ }
62
+ let remaining = input;
63
+ for (const pattern of CONTEXT_SEGMENT_PATTERNS) {
64
+ remaining = remaining.replace(pattern, "\n\n");
65
+ }
66
+ const compact = remaining.replace(/\n{3,}/g, "\n\n").trim();
67
+ return compact || null;
68
+ }
69
+ function extractFallbackText(event) {
70
+ const payload = asRecord(event.payload);
71
+ return firstNonBlank(asString(event.message), asString(event.text), extractText(event.content), asString(payload?.message), asString(payload?.text), extractText(payload?.content), extractText(payload?.summary));
72
+ }
73
+ function extractText(value) {
74
+ if (typeof value === "string") {
75
+ const text = value.trim();
76
+ return text.length > 0 ? text : null;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ const joined = value
80
+ .map((item) => extractText(item))
81
+ .filter((text) => Boolean(text))
82
+ .join("\n")
83
+ .trim();
84
+ return joined || null;
85
+ }
86
+ if (!value || typeof value !== "object") {
87
+ return null;
88
+ }
89
+ const obj = value;
90
+ const joined = ["text", "input_text", "output_text", "message", "summary_text", "content"]
91
+ .map((key) => extractText(obj[key]))
92
+ .filter((text) => Boolean(text))
93
+ .join("\n")
94
+ .trim();
95
+ return joined || null;
96
+ }
97
+ function normalizeRole(value) {
98
+ if (typeof value !== "string") {
99
+ return null;
100
+ }
101
+ const normalized = value.trim().toLowerCase();
102
+ return normalized.length > 0 ? normalized : null;
103
+ }
104
+ function safeParseRecord(line) {
105
+ try {
106
+ const parsed = JSON.parse(line);
107
+ return asRecord(parsed);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ function asRecord(value) {
114
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
115
+ return null;
116
+ }
117
+ return value;
118
+ }
119
+ function asString(value) {
120
+ if (typeof value !== "string") {
121
+ return null;
122
+ }
123
+ const trimmed = value.trim();
124
+ return trimmed.length > 0 ? trimmed : null;
125
+ }
126
+ function firstNonBlank(...values) {
127
+ for (const value of values) {
128
+ if (value && value.trim().length > 0) {
129
+ return value.trim();
130
+ }
131
+ }
132
+ return null;
133
+ }
@@ -0,0 +1,26 @@
1
+ import { password } from "@inquirer/prompts";
2
+ import { defaultAppOrigin, defaultApiOrigin, loadConfig, saveConfig, } from "../config/config.js";
3
+ import { saveCredentials } from "../config/credentials.js";
4
+ export async function runLoginCommand(options) {
5
+ const existing = await loadConfig();
6
+ const token = await resolveToken(options.token);
7
+ await saveConfig({
8
+ apiOrigin: options.apiOrigin?.trim() || existing.apiOrigin || defaultApiOrigin(),
9
+ appOrigin: options.appOrigin?.trim() || existing.appOrigin || defaultAppOrigin(),
10
+ });
11
+ await saveCredentials({ token });
12
+ process.stdout.write("Login complete. Credentials saved to the system keyring.\n");
13
+ }
14
+ async function resolveToken(flagToken) {
15
+ if (flagToken && flagToken.trim()) {
16
+ return flagToken.trim();
17
+ }
18
+ const promptedToken = await password({
19
+ message: "Threadlog personal access token",
20
+ mask: "*",
21
+ validate(input) {
22
+ return input.trim().length > 0 || "Token is required";
23
+ },
24
+ });
25
+ return promptedToken.trim();
26
+ }
@@ -0,0 +1,9 @@
1
+ import { clearCredentials } from "../config/credentials.js";
2
+ export async function runLogoutCommand() {
3
+ const removed = await clearCredentials();
4
+ if (removed) {
5
+ process.stdout.write("Logged out. Stored token removed from the system keyring.\n");
6
+ return;
7
+ }
8
+ process.stdout.write("No stored token found.\n");
9
+ }
@@ -0,0 +1,4 @@
1
+ import { runShareSourceCommand } from "./shareSource.js";
2
+ export async function runShareClaudeCommand(options) {
3
+ await runShareSourceCommand("claude", options);
4
+ }
@@ -0,0 +1,4 @@
1
+ import { runShareSourceCommand } from "./shareSource.js";
2
+ export async function runShareCodexCommand(options) {
3
+ await runShareSourceCommand("codex", options);
4
+ }
@@ -0,0 +1,68 @@
1
+ import { select } from "@inquirer/prompts";
2
+ import path from "node:path";
3
+ import { sourceAdapterById } from "../agents/registry.js";
4
+ import { loadConfig } from "../config/config.js";
5
+ import { loadCredentials } from "../config/credentials.js";
6
+ import { formatBytes, formatIsoDate } from "../lib/format.js";
7
+ import { buildUploadArtifact } from "../upload/artifact.js";
8
+ import { UploadRequestError, createUploadClient } from "../upload/client.js";
9
+ export async function runShareSourceCommand(source, options) {
10
+ const adapter = sourceAdapterById(source);
11
+ const config = await loadConfig();
12
+ const credentials = await loadCredentials();
13
+ if (!credentials) {
14
+ throw new Error("You are not logged in. Run 'threadlog login' first.");
15
+ }
16
+ const mainFilePath = options.file
17
+ ? await adapter.resolveExplicitFilePath(options.file)
18
+ : await pickSourceSessionPath(adapter);
19
+ const uploadInput = await adapter.buildUploadInput(mainFilePath);
20
+ const artifact = buildUploadArtifact(uploadInput.artifact);
21
+ const uploadClient = createUploadClient({
22
+ apiOrigin: config.apiOrigin,
23
+ token: credentials.token,
24
+ });
25
+ const uploadResult = await runUploadWithFriendlyErrors(async () => uploadClient.createAndUploadArtifact({
26
+ artifact,
27
+ source,
28
+ title: uploadInput.title,
29
+ capturedAt: uploadInput.capturedAt,
30
+ }));
31
+ process.stdout.write(`${resolveThreadUrl(uploadResult.threadUrl, config.appOrigin)}\n`);
32
+ }
33
+ async function runUploadWithFriendlyErrors(run) {
34
+ try {
35
+ return await run();
36
+ }
37
+ catch (error) {
38
+ if (error instanceof UploadRequestError && error.code === "quota_exceeded") {
39
+ throw new Error(`${error.message} Open app settings and upgrade your organization plan: /settings/org`);
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ function resolveThreadUrl(threadUrl, appOrigin) {
45
+ return new URL(threadUrl, `${appOrigin.replace(/\/$/, "")}/`).toString();
46
+ }
47
+ async function pickSourceSessionPath(adapter) {
48
+ const candidates = await adapter.discoverSessions({ maxResults: 30 });
49
+ if (candidates.length === 0) {
50
+ throw new Error(`No ${adapter.label} session files found.`);
51
+ }
52
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
53
+ throw new Error("No interactive terminal detected. Use --file <path>.");
54
+ }
55
+ return select({
56
+ message: `Select a ${adapter.label} session file`,
57
+ choices: candidates.map((candidate) => {
58
+ const dateText = new Date(candidate.mtimeMs).toISOString();
59
+ const fallbackName = path.basename(candidate.mainFilePath);
60
+ return {
61
+ value: candidate.mainFilePath,
62
+ name: `${dateText} ${formatBytes(candidate.sizeBytes)} ${candidate.displayName || fallbackName}`,
63
+ description: `modified ${formatIsoDate(dateText)} ${candidate.description}`,
64
+ };
65
+ }),
66
+ pageSize: 15,
67
+ });
68
+ }
@@ -0,0 +1,151 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const DEPLOYMENT_ENV_NAME = "THREADLOG_DEPLOYMENT";
5
+ const ORIGIN_DEFAULTS = {
6
+ local: {
7
+ apiOrigin: "http://localhost:8080",
8
+ appOrigin: "http://localhost:3000",
9
+ },
10
+ prod: {
11
+ apiOrigin: "https://api.threadlog.dev",
12
+ appOrigin: "https://app.threadlog.dev",
13
+ },
14
+ };
15
+ export function deploymentTarget() {
16
+ const raw = process.env[DEPLOYMENT_ENV_NAME]?.trim().toLowerCase();
17
+ if (!raw) {
18
+ return "prod";
19
+ }
20
+ if (raw === "local" || raw === "prod") {
21
+ return raw;
22
+ }
23
+ throw new Error(`${DEPLOYMENT_ENV_NAME} must be one of: local, prod`);
24
+ }
25
+ export function defaultApiOrigin() {
26
+ return requiredEnvOrigin("THREADLOG_API_ORIGIN")
27
+ ?? ORIGIN_DEFAULTS[deploymentTarget()].apiOrigin;
28
+ }
29
+ export function defaultAppOrigin() {
30
+ return requiredEnvOrigin("THREADLOG_APP_ORIGIN")
31
+ ?? ORIGIN_DEFAULTS[deploymentTarget()].appOrigin;
32
+ }
33
+ export function configDir() {
34
+ const overridden = process.env.THREADLOG_CONFIG_DIR?.trim();
35
+ if (overridden) {
36
+ return path.resolve(overridden);
37
+ }
38
+ return path.join(os.homedir(), ".threadlog");
39
+ }
40
+ export function configFilePath() {
41
+ return path.join(configDir(), "config.json");
42
+ }
43
+ export async function loadConfig() {
44
+ const defaults = {
45
+ apiOrigin: defaultApiOrigin(),
46
+ appOrigin: defaultAppOrigin(),
47
+ };
48
+ const file = configFilePath();
49
+ const raw = await readFileIfExists(file);
50
+ if (!raw) {
51
+ return defaults;
52
+ }
53
+ const parsed = parseConfig(raw, file);
54
+ return {
55
+ apiOrigin: readStoredOrigin(file, parsed.apiOrigin, "apiOrigin") ?? defaults.apiOrigin,
56
+ appOrigin: readStoredOrigin(file, parsed.appOrigin, "appOrigin") ?? defaults.appOrigin,
57
+ };
58
+ }
59
+ export async function saveConfig(next) {
60
+ const file = configFilePath();
61
+ await mkdir(path.dirname(file), { recursive: true });
62
+ const normalized = {
63
+ apiOrigin: requiredOrigin(next.apiOrigin, "apiOrigin"),
64
+ appOrigin: requiredOrigin(next.appOrigin, "appOrigin"),
65
+ };
66
+ const payload = JSON.stringify(normalized, null, 2);
67
+ await writeFile(file, `${payload}\n`, {
68
+ encoding: "utf8",
69
+ mode: 0o600,
70
+ });
71
+ }
72
+ function parseConfig(raw, filePath) {
73
+ try {
74
+ const parsed = JSON.parse(raw);
75
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
76
+ throw new Error(`${filePath} must contain a JSON object`);
77
+ }
78
+ return parsed;
79
+ }
80
+ catch (error) {
81
+ if (error instanceof Error && error.message.includes(filePath)) {
82
+ throw error;
83
+ }
84
+ throw new Error(`${filePath} must contain valid JSON`);
85
+ }
86
+ }
87
+ function normalizeOrigin(raw) {
88
+ if (typeof raw !== "string") {
89
+ return null;
90
+ }
91
+ const trimmed = raw.trim();
92
+ if (trimmed.length === 0) {
93
+ return null;
94
+ }
95
+ try {
96
+ const url = new URL(trimmed);
97
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
98
+ return null;
99
+ }
100
+ if (url.pathname !== "" && url.pathname !== "/") {
101
+ return null;
102
+ }
103
+ if (url.search !== "" || url.hash !== "") {
104
+ return null;
105
+ }
106
+ return url.origin;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ function requiredOrigin(raw, fieldName) {
113
+ const normalized = normalizeOrigin(raw);
114
+ if (!normalized) {
115
+ throw new Error(`${fieldName} must be a valid absolute http(s) URL origin`);
116
+ }
117
+ return normalized;
118
+ }
119
+ function requiredEnvOrigin(envName) {
120
+ const raw = process.env[envName];
121
+ if (raw === undefined) {
122
+ return null;
123
+ }
124
+ if (raw.trim().length === 0) {
125
+ throw new Error(`${envName} must not be blank when set`);
126
+ }
127
+ return requiredOrigin(raw, envName);
128
+ }
129
+ function readStoredOrigin(filePath, value, fieldName) {
130
+ if (value === undefined) {
131
+ return null;
132
+ }
133
+ if (typeof value !== "string") {
134
+ throw new Error(`${filePath}: ${fieldName} must be a string`);
135
+ }
136
+ if (value.trim().length === 0) {
137
+ throw new Error(`${filePath}: ${fieldName} must not be blank`);
138
+ }
139
+ return requiredOrigin(value, `${filePath}: ${fieldName}`);
140
+ }
141
+ async function readFileIfExists(filePath) {
142
+ try {
143
+ return await readFile(filePath, "utf8");
144
+ }
145
+ catch (error) {
146
+ if (error.code === "ENOENT") {
147
+ return null;
148
+ }
149
+ throw error;
150
+ }
151
+ }
@@ -0,0 +1,180 @@
1
+ import { spawn } from "node:child_process";
2
+ const KEYRING_REQUIRED_MESSAGE = "A system keyring is required for persisted login on this machine.";
3
+ const KEYRING_GUIDANCE = "macOS requires the Keychain via 'security'; Linux requires Secret Service via 'secret-tool'.";
4
+ const KEYRING_SERVICE = "threadlog";
5
+ const KEYRING_ACCOUNT = "personal-access-token";
6
+ const KEYRING_LABEL = "Threadlog CLI token";
7
+ const MACOS_NOT_FOUND_EXIT_CODE = 44;
8
+ let commandRunner = runCommand;
9
+ let platformOverride = null;
10
+ export async function saveCredentials(next) {
11
+ const token = next.token.trim();
12
+ if (!token) {
13
+ throw new Error("token must not be empty");
14
+ }
15
+ const store = resolveSecureStore({ required: true });
16
+ if (!store) {
17
+ throw keyringUnavailableError();
18
+ }
19
+ await store.save(token);
20
+ }
21
+ export async function loadCredentials() {
22
+ const store = resolveSecureStore({ required: false });
23
+ if (!store) {
24
+ return null;
25
+ }
26
+ const storedToken = await store.load();
27
+ if (storedToken) {
28
+ return { token: storedToken };
29
+ }
30
+ return null;
31
+ }
32
+ export async function clearCredentials() {
33
+ const store = resolveSecureStore({ required: false });
34
+ if (!store) {
35
+ return false;
36
+ }
37
+ return await store.clear();
38
+ }
39
+ export function installCredentialTestHooks(options) {
40
+ if (options.commandRunner) {
41
+ commandRunner = options.commandRunner;
42
+ }
43
+ if (options.platform) {
44
+ platformOverride = options.platform;
45
+ }
46
+ }
47
+ export function resetCredentialTestHooks() {
48
+ commandRunner = runCommand;
49
+ platformOverride = null;
50
+ }
51
+ function currentPlatform() {
52
+ return platformOverride ?? process.platform;
53
+ }
54
+ function resolveSecureStore(options) {
55
+ const platform = currentPlatform();
56
+ if (platform === "darwin") {
57
+ return createMacOsSecureStore();
58
+ }
59
+ if (platform === "linux") {
60
+ return createLinuxSecureStore();
61
+ }
62
+ if (!options.required) {
63
+ return null;
64
+ }
65
+ throw keyringUnavailableError();
66
+ }
67
+ function createMacOsSecureStore() {
68
+ return {
69
+ async load() {
70
+ const result = await runKeyringCommand("security", ["find-generic-password", "-a", KEYRING_ACCOUNT, "-s", KEYRING_SERVICE, "-w"], { notFoundExitCodes: [MACOS_NOT_FOUND_EXIT_CODE] });
71
+ if (result === null) {
72
+ return null;
73
+ }
74
+ const token = trimTrailingNewline(result.stdout);
75
+ return token.length > 0 ? token : null;
76
+ },
77
+ async save(token) {
78
+ await runKeyringCommand("security", [
79
+ "add-generic-password",
80
+ "-U",
81
+ "-a",
82
+ KEYRING_ACCOUNT,
83
+ "-s",
84
+ KEYRING_SERVICE,
85
+ "-l",
86
+ KEYRING_LABEL,
87
+ "-w",
88
+ token,
89
+ ]);
90
+ },
91
+ async clear() {
92
+ const result = await runKeyringCommand("security", ["delete-generic-password", "-a", KEYRING_ACCOUNT, "-s", KEYRING_SERVICE], { notFoundExitCodes: [MACOS_NOT_FOUND_EXIT_CODE] });
93
+ return result !== null;
94
+ },
95
+ };
96
+ }
97
+ function createLinuxSecureStore() {
98
+ const attributes = ["service", KEYRING_SERVICE, "account", KEYRING_ACCOUNT];
99
+ const loadToken = async () => {
100
+ const result = await runKeyringCommand("secret-tool", ["lookup", ...attributes], { notFoundExitCodes: [1] });
101
+ if (result === null) {
102
+ return null;
103
+ }
104
+ const token = trimTrailingNewline(result.stdout);
105
+ return token.length > 0 ? token : null;
106
+ };
107
+ return {
108
+ load: loadToken,
109
+ async save(token) {
110
+ await runKeyringCommand("secret-tool", ["store", `--label=${KEYRING_LABEL}`, ...attributes], { input: token });
111
+ },
112
+ async clear() {
113
+ const existing = await loadToken();
114
+ if (!existing) {
115
+ return false;
116
+ }
117
+ await runKeyringCommand("secret-tool", ["clear", ...attributes]);
118
+ return true;
119
+ },
120
+ };
121
+ }
122
+ async function runKeyringCommand(command, args, options = {}) {
123
+ try {
124
+ const result = await commandRunner(command, args, { input: options.input });
125
+ if (result.exitCode === 0) {
126
+ return result;
127
+ }
128
+ if (options.notFoundExitCodes?.includes(result.exitCode)) {
129
+ return null;
130
+ }
131
+ throw keyringAccessError(result.stderr || result.stdout || `${command} exited with status ${result.exitCode}`);
132
+ }
133
+ catch (error) {
134
+ if (isCommandUnavailable(error)) {
135
+ throw keyringUnavailableError();
136
+ }
137
+ throw error;
138
+ }
139
+ }
140
+ async function runCommand(command, args, options = {}) {
141
+ return await new Promise((resolve, reject) => {
142
+ const child = spawn(command, args, {
143
+ stdio: "pipe",
144
+ });
145
+ let stdout = "";
146
+ let stderr = "";
147
+ child.stdout.setEncoding("utf8");
148
+ child.stdout.on("data", (chunk) => {
149
+ stdout += chunk;
150
+ });
151
+ child.stderr.setEncoding("utf8");
152
+ child.stderr.on("data", (chunk) => {
153
+ stderr += chunk;
154
+ });
155
+ child.on("error", reject);
156
+ child.on("close", (exitCode) => {
157
+ resolve({
158
+ exitCode: exitCode ?? 1,
159
+ stdout,
160
+ stderr,
161
+ });
162
+ });
163
+ child.stdin.end(options.input);
164
+ });
165
+ }
166
+ function trimTrailingNewline(value) {
167
+ return value.replace(/\r?\n$/, "");
168
+ }
169
+ function keyringUnavailableError() {
170
+ return new Error(`${KEYRING_REQUIRED_MESSAGE} ${KEYRING_GUIDANCE}`);
171
+ }
172
+ function keyringAccessError(detail) {
173
+ return new Error(`Failed to access the system keyring: ${detail.trim()}`);
174
+ }
175
+ function isCommandUnavailable(error) {
176
+ return typeof error === "object"
177
+ && error !== null
178
+ && "code" in error
179
+ && error.code === "ENOENT";
180
+ }
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runLoginCommand } from "./commands/login.js";
4
+ import { runLogoutCommand } from "./commands/logout.js";
5
+ import { runShareClaudeCommand } from "./commands/shareClaude.js";
6
+ import { runShareCodexCommand } from "./commands/shareCodex.js";
7
+ export async function run(argv) {
8
+ const program = new Command();
9
+ program
10
+ .name("threadlog")
11
+ .description("Threadlog CLI")
12
+ .showHelpAfterError();
13
+ program
14
+ .command("login")
15
+ .description("Store a personal access token for CLI uploads")
16
+ .option("--token <token>", "Personal access token (PAT)")
17
+ .option("--api-origin <origin>", "API base URL (default: prod=https://api.threadlog.dev, local=http://localhost:8080)")
18
+ .option("--app-origin <origin>", "App base URL for printed thread links (default: prod=https://app.threadlog.dev, local=http://localhost:3000)")
19
+ .action(async (options) => {
20
+ await runLoginCommand({
21
+ token: options.token,
22
+ apiOrigin: options.apiOrigin,
23
+ appOrigin: options.appOrigin,
24
+ });
25
+ });
26
+ program
27
+ .command("logout")
28
+ .description("Remove stored credentials")
29
+ .action(async () => {
30
+ await runLogoutCommand();
31
+ });
32
+ const share = program
33
+ .command("share")
34
+ .description("Share local agent sessions to Threadlog");
35
+ share
36
+ .command("codex")
37
+ .description("Discover and upload a local Codex rollout")
38
+ .option("--file <path>", "Explicit rollout JSONL file path")
39
+ .action(async (options) => {
40
+ await runShareCodexCommand({ file: options.file });
41
+ });
42
+ share
43
+ .command("claude")
44
+ .description("Discover and upload a local Claude session")
45
+ .option("--file <path>", "Explicit Claude session JSONL file path")
46
+ .action(async (options) => {
47
+ await runShareClaudeCommand({ file: options.file });
48
+ });
49
+ await program.parseAsync(argv);
50
+ }
51
+ async function main() {
52
+ try {
53
+ await run(process.argv);
54
+ }
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ process.stderr.write(`threadlog: ${message}\n`);
58
+ if (process.env.THREADLOG_DEBUG === "1" && error instanceof Error && error.stack) {
59
+ process.stderr.write(`${error.stack}\n`);
60
+ }
61
+ process.exitCode = 1;
62
+ }
63
+ }
64
+ await main();
@@ -0,0 +1,23 @@
1
+ export function formatBytes(sizeBytes) {
2
+ if (sizeBytes < 1024) {
3
+ return `${sizeBytes} B`;
4
+ }
5
+ const units = ["KB", "MB", "GB", "TB"];
6
+ let value = sizeBytes / 1024;
7
+ let unitIndex = 0;
8
+ while (value >= 1024 && unitIndex < units.length - 1) {
9
+ value /= 1024;
10
+ unitIndex += 1;
11
+ }
12
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
13
+ }
14
+ export function formatIsoDate(raw) {
15
+ if (!raw) {
16
+ return "n/a";
17
+ }
18
+ const date = new Date(raw);
19
+ if (Number.isNaN(date.getTime())) {
20
+ return raw;
21
+ }
22
+ return date.toISOString();
23
+ }
@@ -0,0 +1,19 @@
1
+ const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi;
2
+ const JWT_PATTERN = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
3
+ const SECRET_ASSIGNMENT_PATTERN = /\b([A-Z0-9_]*(?:API_KEY|TOKEN|SECRET)[A-Z0-9_]*)\b\s*([:=])\s*(["'])?([^"'\s,}\]]+)(\3)?/g;
4
+ const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN(?: [A-Z0-9-]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9-]+)? PRIVATE KEY-----/g;
5
+ export function redactSensitiveText(input) {
6
+ if (!input) {
7
+ return input;
8
+ }
9
+ return input
10
+ .replace(PEM_PRIVATE_KEY_PATTERN, "[REDACTED_PRIVATE_KEY]")
11
+ .replace(BEARER_PATTERN, "Bearer [REDACTED_TOKEN]")
12
+ .replace(JWT_PATTERN, "[REDACTED_JWT]")
13
+ .replace(SECRET_ASSIGNMENT_PATTERN, (_match, key, sep, quote) => {
14
+ if (quote) {
15
+ return `${key}${sep}${quote}[REDACTED_SECRET]${quote}`;
16
+ }
17
+ return `${key}${sep}[REDACTED_SECRET]`;
18
+ });
19
+ }