@threadbase-sh/streamer 1.15.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,23 @@
1
+ -- Projects table: canonical normalized project identity.
2
+ -- Project discovery is conversation-driven; rows are upserted as
3
+ -- conversations are scanned from disk.
4
+ CREATE TABLE IF NOT EXISTS projects (
5
+ id TEXT PRIMARY KEY,
6
+ path TEXT NOT NULL UNIQUE,
7
+ name TEXT,
8
+
9
+ -- Conversation-based indexing metadata.
10
+ last_conversation_id TEXT,
11
+ last_conversation_created_at TEXT,
12
+ last_indexed_at TEXT,
13
+
14
+ -- Project-level activity metadata.
15
+ latest_message_at TEXT,
16
+ latest_message_id TEXT,
17
+ message_count INTEGER DEFAULT 0,
18
+
19
+ created_at TEXT NOT NULL,
20
+ updated_at TEXT NOT NULL
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path);
@@ -0,0 +1,11 @@
1
+ -- Add project_id to conversation_meta. SQLite cannot ADD COLUMN with
2
+ -- a foreign key constraint after the fact, so we keep it as a plain TEXT
3
+ -- and enforce the relationship in code (see projects repository).
4
+ ALTER TABLE conversation_meta ADD COLUMN project_id TEXT;
5
+
6
+ CREATE INDEX IF NOT EXISTS idx_conversations_project_id
7
+ ON conversation_meta(project_id);
8
+
9
+ -- Sessions are stored in-memory by SessionStore, not in SQLite. We do not
10
+ -- add a sessions table here; ManagedSession.projectId is added to the
11
+ -- in-memory shape in src/types.ts and propagated through SessionStore.
@@ -0,0 +1,12 @@
1
+ -- Generic key/value cache metadata used to decide whether to refresh
2
+ -- the projects/conversations cache. Known keys:
3
+ -- last_conversation_id
4
+ -- last_conversation_created_at
5
+ -- projects_last_indexed_at
6
+ -- conversations_last_indexed_at
7
+ -- conversations_dirty
8
+ CREATE TABLE IF NOT EXISTS cache_metadata (
9
+ key TEXT PRIMARY KEY,
10
+ value TEXT NOT NULL,
11
+ updated_at TEXT NOT NULL
12
+ );
@@ -0,0 +1,4 @@
1
+ -- Identifies the origin of a conversation.
2
+ -- 'streamer' = spawned via tb-streamer PTY session.
3
+ -- NULL = discovered by scanner (user ran claude in a terminal, VS Code, etc.)
4
+ ALTER TABLE conversation_meta ADD COLUMN source TEXT;
@@ -0,0 +1,5 @@
1
+ -- Stat-based scan cache: skip full parseMeta on files whose size and mtime
2
+ -- haven't changed since the last scan. Both columns are NULL for rows written
3
+ -- before this migration; the next scan will backfill them on the first parse.
4
+ ALTER TABLE conversation_meta ADD COLUMN mtime_ms INTEGER;
5
+ ALTER TABLE conversation_meta ADD COLUMN file_size INTEGER;
@@ -0,0 +1,17 @@
1
+ CREATE TABLE IF NOT EXISTS managed_sessions (
2
+ id TEXT PRIMARY KEY,
3
+ conversation_id TEXT NOT NULL,
4
+ project_path TEXT NOT NULL,
5
+ project_name TEXT NOT NULL,
6
+ branch TEXT NOT NULL DEFAULT '',
7
+ status TEXT NOT NULL DEFAULT 'running',
8
+ started_at TIMESTAMPTZ NOT NULL,
9
+ completed_at TIMESTAMPTZ,
10
+ prompt_count INTEGER NOT NULL DEFAULT 0,
11
+ last_output TEXT NOT NULL DEFAULT '',
12
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
13
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
14
+ );
15
+
16
+ CREATE INDEX IF NOT EXISTS idx_managed_sessions_conversation_id
17
+ ON managed_sessions(conversation_id);
@@ -0,0 +1,11 @@
1
+ ALTER TABLE managed_sessions
2
+ ADD COLUMN IF NOT EXISTS session_name TEXT,
3
+ ADD COLUMN IF NOT EXISTS model TEXT,
4
+ ADD COLUMN IF NOT EXISTS account TEXT,
5
+ ADD COLUMN IF NOT EXISTS message_count INTEGER NOT NULL DEFAULT 0,
6
+ ADD COLUMN IF NOT EXISTS preview TEXT,
7
+ ADD COLUMN IF NOT EXISTS first_message_text TEXT,
8
+ ADD COLUMN IF NOT EXISTS first_message_at TIMESTAMPTZ,
9
+ ADD COLUMN IF NOT EXISTS last_message_text TEXT,
10
+ ADD COLUMN IF NOT EXISTS last_message_at TIMESTAMPTZ,
11
+ ADD COLUMN IF NOT EXISTS file_path TEXT;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE managed_sessions
2
+ ADD COLUMN IF NOT EXISTS instance_id TEXT;
3
+
4
+ CREATE INDEX IF NOT EXISTS idx_managed_sessions_instance_id
5
+ ON managed_sessions(instance_id);
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS session_uploads (
2
+ id TEXT PRIMARY KEY,
3
+ session_id TEXT NOT NULL,
4
+ instance_id TEXT,
5
+ file_path TEXT NOT NULL,
6
+ original_name TEXT NOT NULL,
7
+ mime_type TEXT NOT NULL,
8
+ size_bytes INTEGER NOT NULL,
9
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10
+ );
11
+
12
+ CREATE INDEX IF NOT EXISTS idx_session_uploads_session_id
13
+ ON session_uploads(session_id);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE managed_sessions
2
+ ADD COLUMN IF NOT EXISTS last_activity_at TIMESTAMPTZ;
@@ -0,0 +1,4 @@
1
+ -- Session state is no longer persisted to the database.
2
+ -- The JSONL file on disk is the single source of truth for session identity.
3
+ -- PTY state is ephemeral (in-process memory only).
4
+ DROP TABLE IF EXISTS managed_sessions CASCADE;
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/docker/seed-claude-config.ts
22
+ var seed_claude_config_exports = {};
23
+ __export(seed_claude_config_exports, {
24
+ TRUSTED_DIR: () => TRUSTED_DIR,
25
+ seedClaudeConfig: () => seedClaudeConfig
26
+ });
27
+ module.exports = __toCommonJS(seed_claude_config_exports);
28
+ var import_node_fs = require("fs");
29
+ var TRUSTED_DIR = "/data/.claude/projects";
30
+ function seedClaudeConfig(configPath, apiKey) {
31
+ let config = {};
32
+ try {
33
+ config = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
34
+ } catch (err) {
35
+ if (err.code !== "ENOENT") {
36
+ throw new Error(`refusing to overwrite ${configPath}: ${err.message}`);
37
+ }
38
+ }
39
+ config.hasCompletedOnboarding = true;
40
+ config.theme = config.theme || "dark";
41
+ config.hasTrustDialogAccepted = true;
42
+ config.projects = config.projects || {};
43
+ config.projects[TRUSTED_DIR] = Object.assign(
44
+ { allowedTools: [], hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true },
45
+ config.projects[TRUSTED_DIR] || {}
46
+ );
47
+ if (apiKey) {
48
+ const suffix = apiKey.slice(-20);
49
+ config.customApiKeyResponses = config.customApiKeyResponses || { approved: [], rejected: [] };
50
+ config.customApiKeyResponses.approved = config.customApiKeyResponses.approved || [];
51
+ if (!config.customApiKeyResponses.approved.includes(suffix)) {
52
+ config.customApiKeyResponses.approved.push(suffix);
53
+ }
54
+ }
55
+ (0, import_node_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2));
56
+ }
57
+ function main() {
58
+ const configPath = process.env.CLAUDE_CONFIG;
59
+ if (!configPath) {
60
+ console.error("[entrypoint] seed-claude-config: CLAUDE_CONFIG is not set");
61
+ process.exit(1);
62
+ }
63
+ try {
64
+ seedClaudeConfig(configPath, process.env.CLAUDE_API_KEY || "");
65
+ } catch (err) {
66
+ console.error(`[entrypoint] ${err.message}`);
67
+ process.exit(1);
68
+ }
69
+ }
70
+ if (require.main === module) {
71
+ main();
72
+ }
73
+ // Annotate the CommonJS export names for ESM import in node:
74
+ 0 && (module.exports = {
75
+ TRUSTED_DIR,
76
+ seedClaudeConfig
77
+ });
78
+ //# sourceMappingURL=seed-claude-config.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/docker/seed-claude-config.ts"],"sourcesContent":["// Seed $HOME/.claude.json so spawned interactive Claude sessions reach a usable\n// prompt instead of a blocking first-run dialog (the mobile app shows an empty\n// screen otherwise). On a fresh /data volume the CLI would show, in order:\n// onboarding/theme picker, workspace-trust dialog, and the \"custom API key\n// detected\" approval. Seeding these flags clears all three. (The fourth gate,\n// the Bypass Permissions warning, is avoided by launching with\n// `--permission-mode dontAsk` rather than `--dangerously-skip-permissions` —\n// see src/pty-manager.ts.)\n//\n// Compiled by tsup to dist/seed-claude-config.cjs; docker/entrypoint.sh runs it\n// with the plain `node` in the runtime image, before the streamer starts.\n// __tests__/seed-claude-config.test.ts exercises seedClaudeConfig() directly.\nimport { readFileSync, writeFileSync } from \"node:fs\";\n\n// Trust this workspace dir without the per-project trust dialog. Matches the\n// --browse-root the streamer serves in the Fly container.\nexport const TRUSTED_DIR = \"/data/.claude/projects\";\n\ninterface ProjectEntry {\n allowedTools: string[];\n hasTrustDialogAccepted: boolean;\n hasCompletedProjectOnboarding: boolean;\n}\n\ninterface ClaudeConfig {\n hasCompletedOnboarding?: boolean;\n theme?: string;\n hasTrustDialogAccepted?: boolean;\n projects?: Record<string, Partial<ProjectEntry>>;\n customApiKeyResponses?: { approved?: string[]; rejected?: string[] };\n [key: string]: unknown;\n}\n\n// Read-modify-write merge into any existing config. Idempotent: re-runs (the\n// Fly volume persists .claude.json across restarts) preserve existing keys and\n// never duplicate an approved key suffix.\n//\n// Error handling is deliberate: a fresh volume (ENOENT) starts from empty, but\n// any OTHER read/parse error means the file EXISTS but is unreadable. The file\n// is the system of record (userID, approved keys, feature flags) — overwriting\n// it would silently truncate that state, so we throw rather than clobber.\nexport function seedClaudeConfig(configPath: string, apiKey: string): void {\n let config: ClaudeConfig = {};\n try {\n config = JSON.parse(readFileSync(configPath, \"utf8\"));\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw new Error(`refusing to overwrite ${configPath}: ${(err as Error).message}`);\n }\n // ENOENT — expected first boot. Fall through with an empty config.\n }\n\n config.hasCompletedOnboarding = true;\n config.theme = config.theme || \"dark\";\n config.hasTrustDialogAccepted = true;\n\n config.projects = config.projects || {};\n config.projects[TRUSTED_DIR] = Object.assign(\n { allowedTools: [], hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true },\n config.projects[TRUSTED_DIR] || {},\n );\n\n // The custom-API-key approval is keyed by the last 20 chars of the API key,\n // matching how the CLI records an accepted key. The streamer maps\n // CLAUDE_API_KEY → ANTHROPIC_API_KEY at spawn, so the CLI reads the same key.\n if (apiKey) {\n const suffix = apiKey.slice(-20);\n config.customApiKeyResponses = config.customApiKeyResponses || { approved: [], rejected: [] };\n config.customApiKeyResponses.approved = config.customApiKeyResponses.approved || [];\n if (!config.customApiKeyResponses.approved.includes(suffix)) {\n config.customApiKeyResponses.approved.push(suffix);\n }\n }\n\n writeFileSync(configPath, JSON.stringify(config, null, 2));\n}\n\n// CLI entry: `node dist/seed-claude-config.cjs`. Reads CLAUDE_CONFIG (path) and\n// CLAUDE_API_KEY from the environment. Any failure is logged with an\n// [entrypoint] prefix and exits non-zero so `set -euo pipefail` aborts boot\n// rather than letting the streamer start with unseeded config.\nfunction main(): void {\n const configPath = process.env.CLAUDE_CONFIG;\n if (!configPath) {\n console.error(\"[entrypoint] seed-claude-config: CLAUDE_CONFIG is not set\");\n process.exit(1);\n }\n try {\n seedClaudeConfig(configPath, process.env.CLAUDE_API_KEY || \"\");\n } catch (err) {\n console.error(`[entrypoint] ${(err as Error).message}`);\n process.exit(1);\n }\n}\n\n// tsup bundles this as CJS, so require.main === module is the right guard.\nif (require.main === module) {\n main();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,qBAA4C;AAIrC,IAAM,cAAc;AAyBpB,SAAS,iBAAiB,YAAoB,QAAsB;AACzE,MAAI,SAAuB,CAAC;AAC5B,MAAI;AACF,aAAS,KAAK,UAAM,6BAAa,YAAY,MAAM,CAAC;AAAA,EACtD,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM,IAAI,MAAM,yBAAyB,UAAU,KAAM,IAAc,OAAO,EAAE;AAAA,IAClF;AAAA,EAEF;AAEA,SAAO,yBAAyB;AAChC,SAAO,QAAQ,OAAO,SAAS;AAC/B,SAAO,yBAAyB;AAEhC,SAAO,WAAW,OAAO,YAAY,CAAC;AACtC,SAAO,SAAS,WAAW,IAAI,OAAO;AAAA,IACpC,EAAE,cAAc,CAAC,GAAG,wBAAwB,MAAM,+BAA+B,KAAK;AAAA,IACtF,OAAO,SAAS,WAAW,KAAK,CAAC;AAAA,EACnC;AAKA,MAAI,QAAQ;AACV,UAAM,SAAS,OAAO,MAAM,GAAG;AAC/B,WAAO,wBAAwB,OAAO,yBAAyB,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC,EAAE;AAC5F,WAAO,sBAAsB,WAAW,OAAO,sBAAsB,YAAY,CAAC;AAClF,QAAI,CAAC,OAAO,sBAAsB,SAAS,SAAS,MAAM,GAAG;AAC3D,aAAO,sBAAsB,SAAS,KAAK,MAAM;AAAA,IACnD;AAAA,EACF;AAEA,oCAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC3D;AAMA,SAAS,OAAa;AACpB,QAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,YAAY;AACf,YAAQ,MAAM,2DAA2D;AACzE,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI;AACF,qBAAiB,YAAY,QAAQ,IAAI,kBAAkB,EAAE;AAAA,EAC/D,SAAS,KAAK;AACZ,YAAQ,MAAM,gBAAiB,IAAc,OAAO,EAAE;AACtD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAGA,IAAI,QAAQ,SAAS,QAAQ;AAC3B,OAAK;AACP;","names":[]}
package/package.json ADDED
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "@threadbase-sh/streamer",
3
+ "version": "1.15.0",
4
+ "description": "PTY session management, WebSocket streaming, and REST API server for Claude Code conversations",
5
+ "license": "MIT",
6
+ "author": "Ronen Mars",
7
+ "homepage": "https://github.com/RonenMars/threadbase-streamer#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/RonenMars/threadbase-streamer.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/RonenMars/threadbase-streamer/issues"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "pty",
19
+ "websocket",
20
+ "terminal",
21
+ "streamer",
22
+ "threadbase"
23
+ ],
24
+ "type": "module",
25
+ "main": "dist/index.cjs",
26
+ "module": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "bin": {
29
+ "threadbase-streamer": "dist/cli.cjs"
30
+ },
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup && node -e \"const{cpSync}=require('fs');cpSync('src/db/migrations','dist/migrations',{recursive:true});cpSync('src/db/pg-migrations','dist/pg-migrations',{recursive:true})\"",
49
+ "dev": "tsup --watch",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "lint": "tsc --noEmit && npx biome check .",
53
+ "format": "npx biome format --write .",
54
+ "check": "npx biome check --write .",
55
+ "prepare": "patch-package",
56
+ "postinstall": "node -e \"try{require('fs').chmodSync(require('path').join(require.resolve('node-pty'),'..','..','prebuilds',process.platform+'-'+process.arch,'spawn-helper'),0o755)}catch{}\"",
57
+ "prepublishOnly": "npm run build",
58
+ "test:contracts": "vitest run __tests__/contracts/",
59
+ "test:e2e": "vitest run __tests__/e2e/",
60
+ "migrate": "tsx scripts/migrate.ts",
61
+ "migrate:projects": "tsx scripts/migrate-projects.ts",
62
+ "db:validate": "tsx scripts/validate-db.ts",
63
+ "update-schema": "tsx scripts/update-schema.ts",
64
+ "update-schema:mobile": "tsx scripts/update-schema.ts --mobile",
65
+ "update-schema:desktop": "tsx scripts/update-schema.ts --desktop",
66
+ "update-schema:shared": "tsx scripts/update-schema.ts --shared",
67
+ "deploy": "scripts/deploy.sh deploy",
68
+ "deploy:force": "scripts/deploy.sh deploy --force",
69
+ "deploy:rollback": "scripts/deploy.sh rollback",
70
+ "deploy:status": "scripts/deploy.sh status",
71
+ "deploy:healthcheck": "scripts/deploy.sh healthcheck",
72
+ "deploy:linux": "scripts/deploy-linux.sh deploy",
73
+ "deploy:linux:force": "scripts/deploy-linux.sh deploy --force",
74
+ "deploy:linux:rollback": "scripts/deploy-linux.sh rollback",
75
+ "deploy:linux:status": "scripts/deploy-linux.sh status",
76
+ "deploy:linux:healthcheck": "scripts/deploy-linux.sh healthcheck",
77
+ "deploy:windows": "pwsh scripts/deploy.ps1 deploy",
78
+ "deploy:windows:force": "pwsh scripts/deploy.ps1 deploy -Force",
79
+ "deploy:windows:rollback": "pwsh scripts/deploy.ps1 rollback",
80
+ "deploy:windows:status": "pwsh scripts/deploy.ps1 status",
81
+ "deploy:windows:healthcheck": "pwsh scripts/deploy.ps1 healthcheck",
82
+ "deploy:fly": "scripts/deploy-fly.sh",
83
+ "fly:secrets": "scripts/fly-secrets.sh",
84
+ "fly:secrets:list": "scripts/fly-secrets.sh --list && scripts/fly-secrets.sh --prod --list"
85
+ },
86
+ "dependencies": {
87
+ "@hono/node-server": "^1.19.14",
88
+ "@hono/node-ws": "^1.3.1",
89
+ "@temporalio/client": "^1.18.1",
90
+ "@threadbase-sh/agent-types": "^0.1.0",
91
+ "@threadbase-sh/scanner": "^0.7.2",
92
+ "@xterm/headless": "^6.0.0",
93
+ "better-sqlite3": "^12.9.0",
94
+ "chokidar": "^5.0.0",
95
+ "commander": "^12.0.0",
96
+ "date-fns": "^4.4.0",
97
+ "dotenv": "^17.4.2",
98
+ "heic-convert": "^2.1.0",
99
+ "hono": "^4.12.25",
100
+ "node-pty": "^1.1.0",
101
+ "pg": "^8.20.0",
102
+ "pino": "^10.3.1",
103
+ "pino-http": "^11.0.0",
104
+ "qrcode-terminal": "^0.12.0",
105
+ "semver": "^7.8.0",
106
+ "tar": "^7.5.16",
107
+ "tweetnacl": "^1.0.3",
108
+ "tweetnacl-util": "^0.15.1",
109
+ "ws": "^8.18.0",
110
+ "yaml": "^2.9.0",
111
+ "zod": "^4.4.3"
112
+ },
113
+ "devDependencies": {
114
+ "@biomejs/biome": "^2.4.12",
115
+ "@semantic-release/changelog": "^6.0.3",
116
+ "@semantic-release/exec": "^7.1.0",
117
+ "@semantic-release/git": "^10.0.1",
118
+ "@types/better-sqlite3": "^7.6.13",
119
+ "@types/heic-convert": "^2.1.1",
120
+ "@types/node": "^20.0.0",
121
+ "@types/pg": "^8.20.0",
122
+ "@types/qrcode-terminal": "^0.12.2",
123
+ "@types/semver": "^7.7.1",
124
+ "@types/tar": "^7.0.87",
125
+ "@types/ws": "^8.5.0",
126
+ "ajv": "^8.18.0",
127
+ "ajv-formats": "^3.0.1",
128
+ "conventional-changelog-conventionalcommits": "^9.3.1",
129
+ "patch-package": "^8.0.1",
130
+ "semantic-release": "^25.0.5",
131
+ "tsup": "^8.0.0",
132
+ "tsx": "^4.21.0",
133
+ "typescript": "^5.5.0",
134
+ "vitest": "^2.0.0"
135
+ }
136
+ }