@deltrace/cli 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.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # compression-server
2
+
3
+ The chunking engine, AI proxy, and user API for Deltrace. Parses source files with tree-sitter, stores semantic code chunks in RDS via S3, and injects compressed codebase context into every Claude or Codex request.
4
+
5
+ ---
6
+
7
+ ## How it works
8
+
9
+ Before every prompt, a `UserPromptSubmit` hook runs `sync.ts`. It walks the current working directory, hashes each file, and POSTs changed files to the **chunker**. The chunker parses each file with tree-sitter into semantic `CodeChunk[]` objects and stores them in RDS (keyed by content hash — unchanged files are skipped). When a Claude Code or Codex request arrives at the **proxy**, it creates a job in the `jobs` table, pushes it to SQS, and a **Minions inference worker** (Qwen2.5-Coder-14B on a GPU box) pulls the message, runs the MinionS protocol against all workspace chunks, and writes a compressed codebase summary back. The proxy polls the row, injects the summary as `<codebase_context>` into the system prompt, and forwards to `api.anthropic.com` or `api.openai.com` with the user's own API key. If the worker doesn't finish in time, the proxy degrades gracefully and forwards without context.
10
+
11
+ ---
12
+
13
+ ## Processes
14
+
15
+ Three PM2 processes, each on its own port. nginx terminates TLS on 443 and routes by path prefix — ports 3000/3001/3002 are never exposed to the internet.
16
+
17
+ | Process | Port | Handles |
18
+ |---|---|---|
19
+ | `proxy` | 3000 | `/ws/:id/v1/*` — AI request forwarding, pure I/O |
20
+ | `chunker` | 3001 | `/upload`, `/auth/activate` — file ingestion, tree-sitter |
21
+ | `api` | 3002 | `/api/*` — user auth, API keys, repos, usage stats |
22
+
23
+ ---
24
+
25
+ ## Codebase structure
26
+
27
+ ```
28
+ src/
29
+ ├── proxy.ts — Port 3000. AI proxy routes.
30
+ ├── chunker-server.ts — Port 3001. File ingestion.
31
+ ├── api-server.ts — Port 3002. User-facing API server.
32
+ ├── userApi.ts — Route handlers for api-server (me, keys, repos, usage).
33
+ ├── store.ts — All PostgreSQL access. Fetches DB credentials from AWS
34
+ │ Secrets Manager at startup — no DATABASE_URL needed in env.
35
+ ├── session.ts — Redis session store. Falls back to in-memory Map in dev.
36
+ ├── s3.ts — Chunk blob storage helpers.
37
+ ├── sqs.ts — SQS helpers for Minions job dispatch.
38
+ ├── chunker.ts — AST-based semantic chunking via tree-sitter.
39
+ ├── treeSitter.ts — WASM tree-sitter init and language loading.
40
+ ├── languageMap.ts — File extension → language + WASM mapping.
41
+ ├── tokenizer.ts — Token estimation and overlap splitting.
42
+ ├── types.ts — Shared types (CodeChunk, CompressedChunk, etc.)
43
+ ├── sync.ts — Pre-prompt hook: walks cwd, uploads changed files.
44
+ ├── login.ts — OAuth setup for Claude Code.
45
+ └── login-codex.ts — OAuth setup for Codex.
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Infrastructure
51
+
52
+ | Component | Details |
53
+ |---|---|
54
+ | EC2 | t2.micro — upgrade to t3.medium recommended |
55
+ | Database | RDS PostgreSQL — credentials auto-rotated every 7 days via Secrets Manager |
56
+ | Credentials | EC2 IAM role (`compression-server-ec2-ssm-role`) — no API keys in env |
57
+ | Session state | ElastiCache Redis (in-memory fallback for dev) |
58
+ | Chunk storage | S3 `deltrace-chunks` bucket |
59
+ | Job queue | SQS `compression-jobs-queue` |
60
+ | TLS | nginx + Let's Encrypt |
61
+ | Process manager | PM2 |
62
+
63
+ ---
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ npm run dev # run with ts-node
69
+ npm run build # compile TypeScript → dist/
70
+ npm start # run compiled output
71
+
72
+ npm run compressor:claude # OAuth setup for Claude Code
73
+ npm run compressor:codex # OAuth setup for Codex
74
+ npm run teardown:claude # undo Claude Code setup
75
+ npm run teardown:codex # undo Codex setup
76
+ npm run sync # manually upload cwd to server
77
+ ```
78
+
79
+ ## Deploy (EC2)
80
+
81
+ ```bash
82
+ git pull && npm run build && pm2 restart all
83
+ ```
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const http = __importStar(require("http"));
41
+ const crypto = __importStar(require("crypto"));
42
+ const child_process_1 = require("child_process");
43
+ const CLOUD_URL = (process.env.DELTRACE_CLOUD ?? 'http://localhost:3000').replace(/\/$/, '');
44
+ const SYNC_SERVER = (process.env.SYNC_SERVER ?? CLOUD_URL).replace(/\/$/, '');
45
+ const CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
46
+ const HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
47
+ const SERVER_DIR = path.resolve(__dirname, '..');
48
+ function openBrowser(url) {
49
+ if (process.platform === 'win32') {
50
+ (0, child_process_1.execFile)('cmd.exe', ['/c', 'start', '', url]);
51
+ }
52
+ else {
53
+ (0, child_process_1.execFile)(process.platform === 'darwin' ? 'open' : 'xdg-open', [url]);
54
+ }
55
+ }
56
+ function findFreePort() {
57
+ return new Promise((resolve, reject) => {
58
+ const srv = http.createServer();
59
+ srv.listen(0, '127.0.0.1', () => {
60
+ const port = srv.address().port;
61
+ srv.close(() => resolve(port));
62
+ });
63
+ srv.on('error', reject);
64
+ });
65
+ }
66
+ function successPage() {
67
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
68
+ *{margin:0;padding:0;box-sizing:border-box}
69
+ body{font-family:monospace;display:flex;align-items:center;justify-content:center;
70
+ min-height:100vh;background:#0f1117;color:#4ec9b0}
71
+ .card{text-align:center;padding:2rem}
72
+ .check{font-size:2.5rem;margin-bottom:1rem}
73
+ p{font-size:.875rem;color:#64748b;margin-top:.5rem}
74
+ </style></head><body>
75
+ <div class="card">
76
+ <div class="check">✓</div>
77
+ <div>Authenticated successfully</div>
78
+ <p>You can close this window and return to the terminal.</p>
79
+ </div>
80
+ <script>setTimeout(()=>window.close(),1000)</script>
81
+ </body></html>`;
82
+ }
83
+ function deniedPage() {
84
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
85
+ *{margin:0;padding:0;box-sizing:border-box}
86
+ body{font-family:monospace;display:flex;align-items:center;justify-content:center;
87
+ min-height:100vh;background:#0f1117;color:#f87171}
88
+ .card{text-align:center;padding:2rem}
89
+ .icon{font-size:2.5rem;margin-bottom:1rem}
90
+ p{font-size:.875rem;color:#64748b;margin-top:.5rem}
91
+ </style></head><body>
92
+ <div class="card">
93
+ <div class="icon">✕</div>
94
+ <div>Authorization denied</div>
95
+ <p>You can close this window.</p>
96
+ </div>
97
+ <script>setTimeout(()=>window.close(),1000)</script>
98
+ </body></html>`;
99
+ }
100
+ function parseToml(content) {
101
+ const doc = { topLevel: {}, sections: {}, sectionOrder: [] };
102
+ let currentSection = null;
103
+ for (const line of content.split('\n')) {
104
+ const trimmed = line.trim();
105
+ if (!trimmed || trimmed.startsWith('#'))
106
+ continue;
107
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
108
+ if (sectionMatch) {
109
+ currentSection = sectionMatch[1];
110
+ if (!doc.sections[currentSection]) {
111
+ doc.sections[currentSection] = {};
112
+ doc.sectionOrder.push(currentSection);
113
+ }
114
+ continue;
115
+ }
116
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*"([^"]*)"$/);
117
+ if (kvMatch) {
118
+ if (currentSection) {
119
+ doc.sections[currentSection][kvMatch[1]] = kvMatch[2];
120
+ }
121
+ else {
122
+ doc.topLevel[kvMatch[1]] = kvMatch[2];
123
+ }
124
+ }
125
+ }
126
+ return doc;
127
+ }
128
+ function tomlVal(v) {
129
+ return (v === 'true' || v === 'false') ? v : `"${v}"`;
130
+ }
131
+ function writeToml(doc) {
132
+ const lines = [];
133
+ for (const [key, value] of Object.entries(doc.topLevel)) {
134
+ lines.push(`${key} = ${tomlVal(value)}`);
135
+ }
136
+ for (const section of doc.sectionOrder) {
137
+ const entries = Object.entries(doc.sections[section]);
138
+ if (entries.length === 0)
139
+ continue;
140
+ lines.push('');
141
+ lines.push(`[${section}]`);
142
+ for (const [key, value] of entries) {
143
+ lines.push(`${key} = ${tomlVal(value)}`);
144
+ }
145
+ }
146
+ return lines.join('\n') + '\n';
147
+ }
148
+ function upsertCodexHook(command) {
149
+ let hooksFile = {};
150
+ try {
151
+ hooksFile = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
152
+ }
153
+ catch { }
154
+ const hooks = hooksFile.hooks ?? {};
155
+ const existing = hooks.UserPromptSubmit ?? [];
156
+ const filtered = existing.filter((group) => !group.hooks?.some((hook) => hook.command?.includes('deltrace-sync') || hook.command?.includes('compression-server/src/sync.ts') || hook.command === command));
157
+ hooks.UserPromptSubmit = [
158
+ ...filtered,
159
+ {
160
+ hooks: [
161
+ {
162
+ type: 'command',
163
+ command,
164
+ statusMessage: '[deltrace] syncing codebase',
165
+ timeout: 600,
166
+ },
167
+ ],
168
+ },
169
+ ];
170
+ hooksFile.hooks = hooks;
171
+ fs.writeFileSync(HOOKS_PATH, JSON.stringify(hooksFile, null, 2) + '\n');
172
+ }
173
+ async function main() {
174
+ const state = crypto.randomBytes(16).toString('hex');
175
+ const port = await findFreePort();
176
+ let resolveAuth;
177
+ let rejectAuth;
178
+ const authPromise = new Promise((res, rej) => {
179
+ resolveAuth = res;
180
+ rejectAuth = rej;
181
+ });
182
+ const server = http.createServer((req, res) => {
183
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
184
+ if (url.pathname !== '/callback') {
185
+ res.writeHead(404);
186
+ res.end();
187
+ return;
188
+ }
189
+ const receivedState = url.searchParams.get('state');
190
+ if (receivedState !== state) {
191
+ res.writeHead(400, { 'Content-Type': 'text/html' });
192
+ res.end('<html><body style="font-family:monospace;color:#f87171;background:#0f1117;display:flex;align-items:center;justify-content:center;min-height:100vh">Invalid state. Close this window.</body></html>');
193
+ return;
194
+ }
195
+ const errorParam = url.searchParams.get('error');
196
+ if (errorParam) {
197
+ res.writeHead(200, { 'Content-Type': 'text/html' });
198
+ res.end(deniedPage());
199
+ rejectAuth(new Error('Authorization denied'));
200
+ return;
201
+ }
202
+ const key = url.searchParams.get('key');
203
+ const workspaceId = url.searchParams.get('workspaceId');
204
+ if (!key || !workspaceId) {
205
+ res.writeHead(400);
206
+ res.end();
207
+ rejectAuth(new Error('Missing key or workspaceId in callback'));
208
+ return;
209
+ }
210
+ res.writeHead(200, { 'Content-Type': 'text/html' });
211
+ res.end(successPage());
212
+ resolveAuth({ key, workspaceId });
213
+ });
214
+ server.listen(port, '127.0.0.1');
215
+ const authUrl = `${CLOUD_URL}/cli-auth?state=${state}&port=${port}`;
216
+ console.log('Opening browser to authenticate…');
217
+ console.log(`\nIf the browser doesn't open, visit:\n ${authUrl}\n`);
218
+ openBrowser(authUrl);
219
+ const TIMEOUT_MS = 5 * 60 * 1000;
220
+ const timer = setTimeout(() => {
221
+ server.close();
222
+ rejectAuth(new Error('Authentication timed out after 5 minutes'));
223
+ }, TIMEOUT_MS);
224
+ let key;
225
+ let workspaceId;
226
+ try {
227
+ ({ key, workspaceId } = await authPromise);
228
+ }
229
+ finally {
230
+ clearTimeout(timer);
231
+ server.close();
232
+ }
233
+ // Write custom provider + sync hook to ~/.codex/config.toml
234
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
235
+ let doc = { topLevel: {}, sections: {}, sectionOrder: [] };
236
+ try {
237
+ doc = parseToml(fs.readFileSync(CONFIG_PATH, 'utf8'));
238
+ }
239
+ catch { }
240
+ // Remove stale [env] section written by older versions of this script
241
+ if (doc.sections['env']) {
242
+ for (const k of ['OPENAI_BASE_URL', 'DELTRACE_API_KEY', 'DELTRACE_CLOUD', 'DELTRACE_WORKSPACE_ID', 'COMPRESSION_SERVER']) {
243
+ delete doc.sections['env'][k];
244
+ }
245
+ if (Object.keys(doc.sections['env']).length === 0) {
246
+ delete doc.sections['env'];
247
+ doc.sectionOrder = doc.sectionOrder.filter(s => s !== 'env');
248
+ }
249
+ }
250
+ // Custom provider — forces HTTP (no WebSocket) and routes through our proxy
251
+ doc.topLevel['model_provider'] = 'deltrace';
252
+ const providerKey = 'model_providers.deltrace';
253
+ if (!doc.sections[providerKey]) {
254
+ doc.sections[providerKey] = {};
255
+ doc.sectionOrder.push(providerKey);
256
+ }
257
+ doc.sections[providerKey]['name'] = 'deltrace';
258
+ doc.sections[providerKey]['base_url'] = `${SYNC_SERVER}/ws/${workspaceId}/v1/`;
259
+ doc.sections[providerKey]['requires_openai_auth'] = 'true';
260
+ doc.sections[providerKey]['supports_websockets'] = 'false';
261
+ if (!doc.sections['features']) {
262
+ doc.sections['features'] = {};
263
+ doc.sectionOrder.push('features');
264
+ }
265
+ doc.sections['features']['codex_hooks'] = 'true';
266
+ // Sync hook — uploads codebase before each prompt (env vars inlined so no shell config needed)
267
+ const hookCommand = `DELTRACE_API_KEY=${key} COMPRESSION_SERVER=${SYNC_SERVER} deltrace-sync`;
268
+ upsertCodexHook(hookCommand);
269
+ // Legacy hook key for older Codex builds.
270
+ if (!doc.sections['shell_hooks']) {
271
+ doc.sections['shell_hooks'] = {};
272
+ doc.sectionOrder.push('shell_hooks');
273
+ }
274
+ doc.sections['shell_hooks']['exec_before_each_prompt'] = hookCommand;
275
+ fs.writeFileSync(CONFIG_PATH, writeToml(doc));
276
+ console.log(`✓ workspace: ${workspaceId}`);
277
+ console.log(`✓ provider → deltrace`);
278
+ console.log(`✓ supports_websockets = false (forces HTTP POST)`);
279
+ console.log(`✓ sync hook added to ${HOOKS_PATH}`);
280
+ console.log(`✓ saved to ${CONFIG_PATH}`);
281
+ }
282
+ main().catch((err) => { console.error('login error:', err.message); process.exit(1); });
package/dist/login.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const http = __importStar(require("http"));
41
+ const crypto = __importStar(require("crypto"));
42
+ const child_process_1 = require("child_process");
43
+ const CLOUD_URL = (process.env.DELTRACE_CLOUD ?? 'http://localhost:3000').replace(/\/$/, '');
44
+ const SYNC_SERVER = (process.env.SYNC_SERVER ?? CLOUD_URL).replace(/\/$/, '');
45
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
46
+ const SERVER_DIR = path.resolve(__dirname, '..');
47
+ const DEFAULT_SONNET_MODEL = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';
48
+ function openBrowser(url) {
49
+ if (process.platform === 'win32') {
50
+ (0, child_process_1.execFile)('cmd.exe', ['/c', 'start', '', url]);
51
+ }
52
+ else {
53
+ (0, child_process_1.execFile)(process.platform === 'darwin' ? 'open' : 'xdg-open', [url]);
54
+ }
55
+ }
56
+ function findFreePort() {
57
+ return new Promise((resolve, reject) => {
58
+ const srv = http.createServer();
59
+ srv.listen(0, '127.0.0.1', () => {
60
+ const port = srv.address().port;
61
+ srv.close(() => resolve(port));
62
+ });
63
+ srv.on('error', reject);
64
+ });
65
+ }
66
+ function successPage() {
67
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
68
+ *{margin:0;padding:0;box-sizing:border-box}
69
+ body{font-family:monospace;display:flex;align-items:center;justify-content:center;
70
+ min-height:100vh;background:#0f1117;color:#4ec9b0}
71
+ .card{text-align:center;padding:2rem}
72
+ .check{font-size:2.5rem;margin-bottom:1rem}
73
+ p{font-size:.875rem;color:#64748b;margin-top:.5rem}
74
+ </style></head><body>
75
+ <div class="card">
76
+ <div class="check">✓</div>
77
+ <div>Authenticated successfully</div>
78
+ <p>You can close this window and return to the terminal.</p>
79
+ </div>
80
+ <script>setTimeout(()=>window.close(),1000)</script>
81
+ </body></html>`;
82
+ }
83
+ function deniedPage() {
84
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
85
+ *{margin:0;padding:0;box-sizing:border-box}
86
+ body{font-family:monospace;display:flex;align-items:center;justify-content:center;
87
+ min-height:100vh;background:#0f1117;color:#f87171}
88
+ .card{text-align:center;padding:2rem}
89
+ .icon{font-size:2.5rem;margin-bottom:1rem}
90
+ p{font-size:.875rem;color:#64748b;margin-top:.5rem}
91
+ </style></head><body>
92
+ <div class="card">
93
+ <div class="icon">✕</div>
94
+ <div>Authorization denied</div>
95
+ <p>You can close this window.</p>
96
+ </div>
97
+ <script>setTimeout(()=>window.close(),1000)</script>
98
+ </body></html>`;
99
+ }
100
+ async function main() {
101
+ const state = crypto.randomBytes(16).toString('hex');
102
+ const port = await findFreePort();
103
+ let resolveAuth;
104
+ let rejectAuth;
105
+ const authPromise = new Promise((res, rej) => {
106
+ resolveAuth = res;
107
+ rejectAuth = rej;
108
+ });
109
+ const server = http.createServer((req, res) => {
110
+ const url = new URL(req.url, `http://127.0.0.1:${port}`);
111
+ if (url.pathname !== '/callback') {
112
+ res.writeHead(404);
113
+ res.end();
114
+ return;
115
+ }
116
+ const receivedState = url.searchParams.get('state');
117
+ if (receivedState !== state) {
118
+ res.writeHead(400, { 'Content-Type': 'text/html' });
119
+ res.end('<html><body style="font-family:monospace;color:#f87171;background:#0f1117;display:flex;align-items:center;justify-content:center;min-height:100vh">Invalid state. Close this window.</body></html>');
120
+ return;
121
+ }
122
+ const errorParam = url.searchParams.get('error');
123
+ if (errorParam) {
124
+ res.writeHead(200, { 'Content-Type': 'text/html' });
125
+ res.end(deniedPage());
126
+ rejectAuth(new Error('Authorization denied'));
127
+ return;
128
+ }
129
+ const key = url.searchParams.get('key');
130
+ const workspaceId = url.searchParams.get('workspaceId');
131
+ if (!key || !workspaceId) {
132
+ res.writeHead(400);
133
+ res.end();
134
+ rejectAuth(new Error('Missing key or workspaceId in callback'));
135
+ return;
136
+ }
137
+ res.writeHead(200, { 'Content-Type': 'text/html' });
138
+ res.end(successPage());
139
+ resolveAuth({ key, workspaceId });
140
+ });
141
+ server.listen(port, '127.0.0.1');
142
+ const authUrl = `${CLOUD_URL}/cli-auth?state=${state}&port=${port}`;
143
+ console.log('Opening browser to authenticate…');
144
+ console.log(`\nIf the browser doesn't open, visit:\n ${authUrl}\n`);
145
+ openBrowser(authUrl);
146
+ const TIMEOUT_MS = 5 * 60 * 1000;
147
+ const timer = setTimeout(() => {
148
+ server.close();
149
+ rejectAuth(new Error('Authentication timed out after 5 minutes'));
150
+ }, TIMEOUT_MS);
151
+ let key;
152
+ let workspaceId;
153
+ try {
154
+ ({ key, workspaceId } = await authPromise);
155
+ }
156
+ finally {
157
+ clearTimeout(timer);
158
+ server.close();
159
+ }
160
+ fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
161
+ let settings = {};
162
+ try {
163
+ settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
164
+ }
165
+ catch { }
166
+ const env = (settings.env ?? {});
167
+ env.DELTRACE_API_KEY = key;
168
+ env.DELTRACE_CLOUD = CLOUD_URL;
169
+ env.DELTRACE_WORKSPACE_ID = workspaceId;
170
+ env.COMPRESSION_SERVER = SYNC_SERVER;
171
+ env.ANTHROPIC_BASE_URL = `${SYNC_SERVER}/ws/${workspaceId}`;
172
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = DEFAULT_SONNET_MODEL;
173
+ settings.env = env;
174
+ const hookCommand = `deltrace-sync`;
175
+ const hooks = (settings.hooks ?? {});
176
+ const existing = hooks['UserPromptSubmit'] ?? [];
177
+ const alreadyAdded = existing.some(h => h.hooks?.some(hh => hh.command?.includes('deltrace-sync') || hh.command?.includes('compression-server')));
178
+ if (!alreadyAdded) {
179
+ hooks['UserPromptSubmit'] = [...existing, { matcher: '', hooks: [{ type: 'command', command: hookCommand }] }];
180
+ }
181
+ settings.hooks = hooks;
182
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
183
+ console.log(`✓ workspace: ${workspaceId}`);
184
+ console.log(`✓ proxy configured`);
185
+ console.log(`✓ sonnet alias → ${DEFAULT_SONNET_MODEL}`);
186
+ console.log(`✓ sync hook added`);
187
+ console.log(`✓ saved to ${SETTINGS_PATH}`);
188
+ }
189
+ main().catch((err) => { console.error('login error:', err.message); process.exit(1); });
package/dist/sync.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const https = __importStar(require("https"));
40
+ const http = __importStar(require("http"));
41
+ const IGNORE_DIRS = new Set([
42
+ // JS/TS
43
+ 'node_modules', '.npm', '.yarn', '.pnp',
44
+ // Python
45
+ 'venv', '.venv', 'env', '__pycache__', '.mypy_cache', '.pytest_cache', '.ruff_cache', 'htmlcov',
46
+ // Go
47
+ 'vendor',
48
+ // Rust
49
+ 'target',
50
+ // Java/Kotlin/Gradle/Maven
51
+ '.gradle', '.m2',
52
+ // Ruby
53
+ '.bundle',
54
+ // Elixir/Erlang
55
+ '_build', 'deps',
56
+ // Dart/Flutter
57
+ '.dart_tool', '.pub-cache',
58
+ // Haskell
59
+ '.stack-work', 'dist-newstyle',
60
+ // iOS/macOS
61
+ 'Pods', 'Carthage', 'DerivedData',
62
+ // .NET
63
+ 'obj', '.nuget', 'packages',
64
+ // Build outputs
65
+ 'dist', 'build', 'out', '.next', '.turbo', '.nuxt', '.output', '.svelte-kit', '.vercel', '.netlify', 'storybook-static', '.docusaurus',
66
+ // Test coverage
67
+ 'coverage', '.nyc_output',
68
+ // Cache / temp
69
+ '.cache', '.temp', '.tmp', 'tmp', 'temp',
70
+ // Logs
71
+ 'logs', 'log',
72
+ // VCS
73
+ '.git', '.hg', '.svn',
74
+ ]);
75
+ const MAX_FILE_BYTES = 300_000;
76
+ const SUPPORTED_EXTS = new Set([
77
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
78
+ '.py', '.go', '.rs', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.h', '.c',
79
+ ]);
80
+ function walkDir(dir) {
81
+ const results = [];
82
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
+ if (IGNORE_DIRS.has(entry.name))
84
+ continue;
85
+ const full = path.join(dir, entry.name);
86
+ if (entry.isDirectory())
87
+ results.push(...walkDir(full));
88
+ else if (entry.isFile() && SUPPORTED_EXTS.has(path.extname(entry.name).toLowerCase()))
89
+ results.push(full);
90
+ }
91
+ return results;
92
+ }
93
+ function post(url, body, headers) {
94
+ return new Promise((resolve, reject) => {
95
+ const u = new URL(url);
96
+ const lib = u.protocol === 'https:' ? https : http;
97
+ const req = lib.request(u, {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json', 'content-length': String(Buffer.byteLength(body)), ...headers },
100
+ }, (res) => {
101
+ const chunks = [];
102
+ res.on('data', (c) => chunks.push(c));
103
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, data: Buffer.concat(chunks).toString('utf8') }));
104
+ });
105
+ req.on('error', reject);
106
+ req.write(body);
107
+ req.end();
108
+ });
109
+ }
110
+ function checkForUpdates() {
111
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
112
+ const current = require('../package.json').version;
113
+ const req = https.request({ hostname: 'registry.npmjs.org', path: '/@deltrace/cli/latest', method: 'GET',
114
+ headers: { 'Accept': 'application/json', 'User-Agent': 'deltrace-sync' } }, (res) => {
115
+ const chunks = [];
116
+ res.on('data', (c) => chunks.push(c));
117
+ res.on('end', () => {
118
+ try {
119
+ const { version: latest } = JSON.parse(Buffer.concat(chunks).toString('utf8'));
120
+ if (latest && latest !== current) {
121
+ process.stderr.write(`[deltrace] update available: ${current} → ${latest} — run npm install -g @deltrace/cli\n`);
122
+ }
123
+ }
124
+ catch { }
125
+ });
126
+ });
127
+ req.on('error', () => { });
128
+ req.end();
129
+ }
130
+ async function main() {
131
+ checkForUpdates();
132
+ const serverUrl = process.env.COMPRESSION_SERVER ?? 'http://localhost:8080';
133
+ const apiKey = process.env.DELTRACE_API_KEY;
134
+ if (!apiKey) {
135
+ process.stderr.write('[deltrace] DELTRACE_API_KEY not set — run `deltrace login` to authenticate\n');
136
+ process.exit(1);
137
+ }
138
+ const cwd = process.cwd();
139
+ const repo = path.basename(cwd);
140
+ const filePaths = walkDir(cwd);
141
+ if (filePaths.length === 0) {
142
+ process.stderr.write(`[deltrace] ${repo} — no source files found\n`);
143
+ process.exit(0);
144
+ }
145
+ const files = {};
146
+ let skipped = 0;
147
+ for (const absPath of filePaths) {
148
+ if (fs.statSync(absPath).size > MAX_FILE_BYTES) {
149
+ skipped++;
150
+ continue;
151
+ }
152
+ files[path.relative(cwd, absPath)] = fs.readFileSync(absPath, 'utf8');
153
+ }
154
+ const result = await post(`${serverUrl}/upload`, JSON.stringify({ repo, files }), { 'authorization': `Bearer ${apiKey}` });
155
+ if (result.status === 401) {
156
+ process.stderr.write('[deltrace] unauthorized — run `deltrace login` to re-authenticate\n');
157
+ process.exit(1);
158
+ }
159
+ if (result.status !== 200) {
160
+ process.stderr.write(`[deltrace] upload failed (${result.status}) — ${result.data}\n`);
161
+ process.exit(1);
162
+ }
163
+ const { stored, cached } = JSON.parse(result.data);
164
+ const total = stored + cached;
165
+ const changeNote = stored > 0 ? `${stored} updated` : 'no changes';
166
+ process.stderr.write(`[deltrace] ${repo} — ${total} files indexed (${changeNote})${skipped ? `, ${skipped} skipped` : ''}\n`);
167
+ await post(`${serverUrl}/auth/activate`, '{}', { 'authorization': `Bearer ${apiKey}` });
168
+ }
169
+ main().catch((err) => { process.stderr.write(`[deltrace] error: ${err.message}\n`); process.exit(1); });
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
41
+ const HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
42
+ function parseToml(content) {
43
+ const doc = { topLevel: {}, sections: {}, sectionOrder: [] };
44
+ let currentSection = null;
45
+ for (const line of content.split('\n')) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed || trimmed.startsWith('#'))
48
+ continue;
49
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
50
+ if (sectionMatch) {
51
+ currentSection = sectionMatch[1];
52
+ if (!doc.sections[currentSection]) {
53
+ doc.sections[currentSection] = {};
54
+ doc.sectionOrder.push(currentSection);
55
+ }
56
+ continue;
57
+ }
58
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*"([^"]*)"$/);
59
+ if (kvMatch) {
60
+ if (currentSection) {
61
+ doc.sections[currentSection][kvMatch[1]] = kvMatch[2];
62
+ }
63
+ else {
64
+ doc.topLevel[kvMatch[1]] = kvMatch[2];
65
+ }
66
+ }
67
+ }
68
+ return doc;
69
+ }
70
+ function tomlVal(v) {
71
+ return (v === 'true' || v === 'false') ? v : `"${v}"`;
72
+ }
73
+ function writeToml(doc) {
74
+ const lines = [];
75
+ for (const [key, value] of Object.entries(doc.topLevel)) {
76
+ lines.push(`${key} = ${tomlVal(value)}`);
77
+ }
78
+ for (const section of doc.sectionOrder) {
79
+ const entries = Object.entries(doc.sections[section]);
80
+ if (entries.length === 0)
81
+ continue;
82
+ lines.push('');
83
+ lines.push(`[${section}]`);
84
+ for (const [key, value] of entries) {
85
+ lines.push(`${key} = ${tomlVal(value)}`);
86
+ }
87
+ }
88
+ return lines.join('\n') + '\n';
89
+ }
90
+ // Remove deltrace block from ~/.zshenv
91
+ const ZSHENV_PATH = path.join(os.homedir(), '.zshenv');
92
+ const BLOCK_START = '# BEGIN deltrace-codex';
93
+ const BLOCK_END = '# END deltrace-codex';
94
+ const blockRe = new RegExp(`${BLOCK_START}[\\s\\S]*?${BLOCK_END}\n?`, 'm');
95
+ try {
96
+ let zshenv = fs.readFileSync(ZSHENV_PATH, 'utf8');
97
+ if (blockRe.test(zshenv)) {
98
+ zshenv = zshenv.replace(blockRe, '');
99
+ fs.writeFileSync(ZSHENV_PATH, zshenv);
100
+ console.log(`✓ Removed env vars from ${ZSHENV_PATH}`);
101
+ }
102
+ }
103
+ catch { }
104
+ // Clean up config.toml — remove provider, model_provider, and hook
105
+ try {
106
+ const doc = parseToml(fs.readFileSync(CONFIG_PATH, 'utf8'));
107
+ delete doc.topLevel['model_provider'];
108
+ delete doc.sections['model_providers.deltrace'];
109
+ doc.sectionOrder = doc.sectionOrder.filter(s => s !== 'model_providers.deltrace');
110
+ if (doc.sections['shell_hooks']) {
111
+ delete doc.sections['shell_hooks']['exec_before_each_prompt'];
112
+ }
113
+ fs.writeFileSync(CONFIG_PATH, writeToml(doc));
114
+ console.log(`✓ Removed provider and hook from ${CONFIG_PATH}`);
115
+ }
116
+ catch {
117
+ console.log('No Codex config found — nothing to remove from config.toml.');
118
+ }
119
+ try {
120
+ const hooksFile = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
121
+ const hooks = hooksFile.hooks ?? {};
122
+ const userPromptHooks = hooks.UserPromptSubmit ?? [];
123
+ hooks.UserPromptSubmit = userPromptHooks
124
+ .map((group) => ({
125
+ ...group,
126
+ hooks: group.hooks.filter((hook) => !hook.command?.includes('deltrace-sync') && !hook.command?.includes('compression-server/src/sync.ts')),
127
+ }))
128
+ .filter((group) => group.hooks.length > 0);
129
+ if (hooks.UserPromptSubmit.length === 0)
130
+ delete hooks.UserPromptSubmit;
131
+ hooksFile.hooks = hooks;
132
+ fs.writeFileSync(HOOKS_PATH, JSON.stringify(hooksFile, null, 2) + '\n');
133
+ console.log(`✓ Removed sync hook from ${HOOKS_PATH}`);
134
+ }
135
+ catch { }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
41
+ function readSettings() {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ const settings = readSettings();
50
+ const env = settings.env;
51
+ if (env) {
52
+ for (const key of ['ANTHROPIC_BASE_URL', 'DELTRACE_API_KEY', 'DELTRACE_CLOUD', 'DELTRACE_WORKSPACE_ID', 'COMPRESSION_SERVER', 'COMPRESSION_WORKSPACE_ID']) {
53
+ delete env[key];
54
+ }
55
+ if (Object.keys(env).length === 0)
56
+ delete settings.env;
57
+ }
58
+ const hooks = settings.hooks;
59
+ if (hooks?.['UserPromptSubmit']) {
60
+ hooks['UserPromptSubmit'] = hooks['UserPromptSubmit'].filter(h => !h.hooks?.some(hh => hh.command?.includes('deltrace-sync') || hh.command?.includes('compression-server')));
61
+ if (hooks['UserPromptSubmit'].length === 0)
62
+ delete hooks['UserPromptSubmit'];
63
+ if (Object.keys(hooks).length === 0)
64
+ delete settings.hooks;
65
+ }
66
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
67
+ console.log(`✓ Removed ANTHROPIC_BASE_URL and hook from ${SETTINGS_PATH}`);
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@deltrace/cli",
3
+ "version": "0.1.0",
4
+ "description": "Codebase context compression for AI coding agents",
5
+ "files": [
6
+ "dist/login.js",
7
+ "dist/login-codex.js",
8
+ "dist/sync.js",
9
+ "dist/teardown.js",
10
+ "dist/teardown-codex.js"
11
+ ],
12
+ "bin": {
13
+ "deltrace": "dist/login.js",
14
+ "deltrace-sync": "dist/sync.js",
15
+ "deltrace-teardown": "dist/teardown.js"
16
+ },
17
+ "scripts": {
18
+ "dev": "ts-node src/index.ts",
19
+ "build": "tsc",
20
+ "start": "node dist/index.js",
21
+ "prepublishOnly": "npm run build",
22
+ "sync": "ts-node src/sync.ts",
23
+ "login": "DELTRACE_CLOUD=https://deltrace.com/ SYNC_SERVER=https://proxy.deltrace.com ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6 ts-node src/login.ts",
24
+ "compressor:claude": "DELTRACE_CLOUD=https://deltrace.com/ SYNC_SERVER=https://proxy.deltrace.com ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6 ts-node src/login.ts",
25
+ "compressor:codex": "DELTRACE_CLOUD=https://deltrace.com/ SYNC_SERVER=https://proxy.deltrace.com ts-node src/login-codex.ts",
26
+ "setup": "ts-node src/setup.ts",
27
+ "teardown": "ts-node src/teardown.ts && ts-node src/teardown-codex.ts",
28
+ "teardown:claude": "ts-node src/teardown.ts",
29
+ "teardown:codex": "ts-node src/teardown-codex.ts"
30
+ },
31
+ "dependencies": {
32
+ "@aws-sdk/client-s3": "^3.1042.0",
33
+ "@aws-sdk/client-secrets-manager": "^3.1043.0",
34
+ "@aws-sdk/client-sqs": "^3.0.0",
35
+ "aws-jwt-verify": "^5.1.1",
36
+ "ioredis": "^5.3.0",
37
+ "pg": "^8.13.0",
38
+ "tree-sitter-wasms": "^0.1.7",
39
+ "web-tree-sitter": "^0.22.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "@types/pg": "^8.11.0",
44
+ "ts-node": "^10.9.0",
45
+ "typescript": "^5.3.0"
46
+ }
47
+ }