teleportation-cli 1.0.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.
Files changed (54) hide show
  1. package/.claude/hooks/config-loader.mjs +93 -0
  2. package/.claude/hooks/heartbeat.mjs +331 -0
  3. package/.claude/hooks/notification.mjs +35 -0
  4. package/.claude/hooks/permission_request.mjs +307 -0
  5. package/.claude/hooks/post_tool_use.mjs +137 -0
  6. package/.claude/hooks/pre_tool_use.mjs +451 -0
  7. package/.claude/hooks/session-register.mjs +274 -0
  8. package/.claude/hooks/session_end.mjs +256 -0
  9. package/.claude/hooks/session_start.mjs +308 -0
  10. package/.claude/hooks/stop.mjs +277 -0
  11. package/.claude/hooks/user_prompt_submit.mjs +91 -0
  12. package/LICENSE +21 -0
  13. package/README.md +243 -0
  14. package/lib/auth/api-key.js +110 -0
  15. package/lib/auth/credentials.js +341 -0
  16. package/lib/backup/manager.js +461 -0
  17. package/lib/cli/daemon-commands.js +299 -0
  18. package/lib/cli/index.js +303 -0
  19. package/lib/cli/session-commands.js +294 -0
  20. package/lib/cli/snapshot-commands.js +223 -0
  21. package/lib/cli/worktree-commands.js +291 -0
  22. package/lib/config/manager.js +306 -0
  23. package/lib/daemon/lifecycle.js +336 -0
  24. package/lib/daemon/pid-manager.js +160 -0
  25. package/lib/daemon/teleportation-daemon.js +2009 -0
  26. package/lib/handoff/config.js +102 -0
  27. package/lib/handoff/example.js +152 -0
  28. package/lib/handoff/git-handoff.js +351 -0
  29. package/lib/handoff/handoff.js +277 -0
  30. package/lib/handoff/index.js +25 -0
  31. package/lib/handoff/session-state.js +238 -0
  32. package/lib/install/installer.js +555 -0
  33. package/lib/machine-coders/claude-code-adapter.js +329 -0
  34. package/lib/machine-coders/example.js +239 -0
  35. package/lib/machine-coders/gemini-cli-adapter.js +406 -0
  36. package/lib/machine-coders/index.js +103 -0
  37. package/lib/machine-coders/interface.js +168 -0
  38. package/lib/router/classifier.js +251 -0
  39. package/lib/router/example.js +92 -0
  40. package/lib/router/index.js +69 -0
  41. package/lib/router/mech-llms-client.js +277 -0
  42. package/lib/router/models.js +188 -0
  43. package/lib/router/router.js +382 -0
  44. package/lib/session/cleanup.js +100 -0
  45. package/lib/session/metadata.js +258 -0
  46. package/lib/session/mute-checker.js +114 -0
  47. package/lib/session-registry/manager.js +302 -0
  48. package/lib/snapshot/manager.js +390 -0
  49. package/lib/utils/errors.js +166 -0
  50. package/lib/utils/logger.js +148 -0
  51. package/lib/utils/retry.js +155 -0
  52. package/lib/worktree/manager.js +301 -0
  53. package/package.json +66 -0
  54. package/teleportation-cli.cjs +2987 -0
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Installation module for Teleportation
4
+ * Handles setting up hooks and settings at the PROJECT level (not global)
5
+ *
6
+ * Project-level installation:
7
+ * - Hooks live in PROJECT/.claude/hooks/ (source files, not copied)
8
+ * - Settings live in PROJECT/.claude/settings.json (with absolute paths)
9
+ * - Daemon lives in ~/.teleportation/daemon/ (shared across projects)
10
+ */
11
+
12
+ import { copyFile, mkdir, chmod, readFile, writeFile, stat, readdir } from 'fs/promises';
13
+ import { join, dirname, resolve } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { homedir } from 'os';
16
+ import { execSync } from 'child_process';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ const HOME_DIR = homedir();
22
+
23
+ // Protocol/config version - increment when hooks behavior changes significantly
24
+ // This helps identify outdated installations that may not send all required metadata
25
+ export const TELEPORTATION_VERSION = '1.1.0';
26
+ export const TELEPORTATION_PROTOCOL_VERSION = 2;
27
+
28
+ // Runtime getters to respect environment variables set after module load
29
+ function getTeleportationDir() {
30
+ return process.env.TELEPORTATION_DIR || join(__dirname, '..', '..');
31
+ }
32
+
33
+ function getProjectHooksDir() {
34
+ return process.env.TEST_HOOKS_DIR || join(getTeleportationDir(), '.claude', 'hooks');
35
+ }
36
+
37
+ function getProjectSettings() {
38
+ return process.env.TEST_SETTINGS || join(getTeleportationDir(), '.claude', 'settings.json');
39
+ }
40
+
41
+ // Claude Code reads settings from ~/.claude/settings.json (user-level)
42
+ function getUserSettings() {
43
+ return join(HOME_DIR, '.claude', 'settings.json');
44
+ }
45
+
46
+ /**
47
+ * Check if Node.js is installed and meets version requirements
48
+ */
49
+ export function checkNodeVersion() {
50
+ const nodeVersion = process.version;
51
+ const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
52
+
53
+ if (major < 20) {
54
+ return {
55
+ valid: false,
56
+ error: `Node.js 20+ required. Found: ${nodeVersion}`
57
+ };
58
+ }
59
+
60
+ return { valid: true, version: nodeVersion };
61
+ }
62
+
63
+ /**
64
+ * Check if Claude Code is installed
65
+ */
66
+ export function checkClaudeCode() {
67
+ try {
68
+ const claudePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
69
+ if (claudePath) {
70
+ return { valid: true, path: claudePath };
71
+ }
72
+ } catch (e) {
73
+ // Claude not found
74
+ }
75
+
76
+ return {
77
+ valid: false,
78
+ error: 'Claude Code not found in PATH. Please install Claude Code first.'
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Ensure required directories exist
84
+ */
85
+ export async function ensureDirectories() {
86
+ const projectDir = getTeleportationDir();
87
+ const hooksDir = getProjectHooksDir();
88
+ const settingsDir = dirname(getProjectSettings());
89
+
90
+ const dirs = [
91
+ settingsDir,
92
+ hooksDir,
93
+ join(HOME_DIR, '.teleportation'),
94
+ join(HOME_DIR, '.teleportation', 'daemon')
95
+ ];
96
+
97
+ for (const dir of dirs) {
98
+ try {
99
+ await mkdir(dir, { recursive: true });
100
+ } catch (e) {
101
+ if (e.code !== 'EEXIST') {
102
+ throw new Error(`Failed to create directory ${dir}: ${e.message}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ return dirs;
108
+ }
109
+
110
+ /**
111
+ * Verify hooks exist in project directory (no copying - they're source files)
112
+ */
113
+ export async function verifyHooks(sourceHooksDir) {
114
+ const hooks = [
115
+ 'pre_tool_use.mjs',
116
+ 'permission_request.mjs', // Handles remote approvals when user is away
117
+ 'post_tool_use.mjs', // Records tool executions to timeline
118
+ 'session_start.mjs',
119
+ 'session_end.mjs',
120
+ 'stop.mjs',
121
+ 'notification.mjs',
122
+ 'user_prompt_submit.mjs', // Handles /model command detection
123
+ 'config-loader.mjs',
124
+ 'session-register.mjs',
125
+ 'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
126
+ ];
127
+
128
+ const found = [];
129
+ const missing = [];
130
+
131
+ for (const hook of hooks) {
132
+ const hookPath = join(sourceHooksDir, hook);
133
+ try {
134
+ await stat(hookPath);
135
+ // Set executable permissions (755)
136
+ await chmod(hookPath, 0o755);
137
+ found.push(hook);
138
+ } catch (e) {
139
+ if (e.code === 'ENOENT') {
140
+ missing.push(hook);
141
+ }
142
+ }
143
+ }
144
+
145
+ return { found, missing };
146
+ }
147
+
148
+ /**
149
+ * Install hooks (verify they exist and set permissions)
150
+ * Returns structure compatible with test expectations
151
+ */
152
+ export async function installHooks(sourceHooksDir) {
153
+ const result = await verifyHooks(sourceHooksDir);
154
+ const destHooksDir = getProjectHooksDir();
155
+ const copyFailed = [];
156
+
157
+ // Copy hooks from source to destination if they're different
158
+ if (sourceHooksDir !== destHooksDir) {
159
+ for (const hook of result.found) {
160
+ const sourcePath = join(sourceHooksDir, hook);
161
+ const destPath = join(destHooksDir, hook);
162
+ try {
163
+ await copyFile(sourcePath, destPath);
164
+ await chmod(destPath, 0o755);
165
+ } catch (e) {
166
+ copyFailed.push({ file: hook, error: e.message });
167
+ }
168
+ }
169
+ }
170
+
171
+ return {
172
+ installed: result.found,
173
+ failed: result.missing.map(hook => ({ file: hook, error: 'File not found' })).concat(copyFailed)
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Copy daemon files to ~/.teleportation/daemon/
179
+ */
180
+ export async function installDaemon() {
181
+ const sourceDaemonDir = join(getTeleportationDir(), 'lib', 'daemon');
182
+ const destDaemonDir = join(HOME_DIR, '.teleportation', 'daemon');
183
+
184
+ const daemonFiles = [
185
+ 'teleportation-daemon.js',
186
+ 'pid-manager.js',
187
+ 'lifecycle.js'
188
+ ];
189
+
190
+ const installed = [];
191
+ const failed = [];
192
+
193
+ for (const file of daemonFiles) {
194
+ const src = join(sourceDaemonDir, file);
195
+ const dest = join(destDaemonDir, file);
196
+
197
+ try {
198
+ // Check if source exists
199
+ await stat(src);
200
+
201
+ // Copy file
202
+ await copyFile(src, dest);
203
+
204
+ // Set permissions (755 for daemon script, 644 for modules)
205
+ const perms = file === 'teleportation-daemon.js' ? 0o755 : 0o644;
206
+ await chmod(dest, perms);
207
+
208
+ installed.push(file);
209
+ } catch (e) {
210
+ if (e.code === 'ENOENT' && e.path === src) {
211
+ // Source file doesn't exist, skip
212
+ continue;
213
+ }
214
+ failed.push({ file, error: e.message });
215
+ }
216
+ }
217
+
218
+ return { installed, failed };
219
+ }
220
+
221
+ /**
222
+ * Copy daemon dependency modules to ~/.teleportation/
223
+ * This includes machine-coders and router
224
+ */
225
+ export async function installDaemonModules() {
226
+ const sourceLibDir = join(getTeleportationDir(), 'lib');
227
+ const destDir = join(HOME_DIR, '.teleportation');
228
+
229
+ const modules = [
230
+ {
231
+ name: 'machine-coders',
232
+ files: [
233
+ 'index.js',
234
+ 'interface.js',
235
+ 'claude-code-adapter.js',
236
+ 'gemini-cli-adapter.js'
237
+ ]
238
+ },
239
+ {
240
+ name: 'router',
241
+ files: [
242
+ 'index.js',
243
+ 'router.js',
244
+ 'classifier.js',
245
+ 'models.js',
246
+ 'mech-llms-client.js'
247
+ ]
248
+ }
249
+ ];
250
+
251
+ const installed = [];
252
+ const failed = [];
253
+
254
+ for (const mod of modules) {
255
+ const srcDir = join(sourceLibDir, mod.name);
256
+ const targetDir = join(destDir, mod.name);
257
+
258
+ try {
259
+ await mkdir(targetDir, { recursive: true });
260
+ } catch (e) {
261
+ if (e.code !== 'EEXIST') {
262
+ failed.push({ file: `${mod.name}/`, error: e.message });
263
+ continue;
264
+ }
265
+ }
266
+
267
+ for (const file of mod.files) {
268
+ const src = join(srcDir, file);
269
+ const dest = join(targetDir, file);
270
+ const displayName = `${mod.name}/${file}`;
271
+
272
+ try {
273
+ await stat(src);
274
+ await copyFile(src, dest);
275
+ await chmod(dest, 0o644);
276
+ installed.push(displayName);
277
+ } catch (e) {
278
+ if (e.code === 'ENOENT' && e.path === src) {
279
+ continue;
280
+ }
281
+ failed.push({ file: displayName, error: e.message });
282
+ }
283
+ }
284
+ }
285
+
286
+ return { installed, failed };
287
+ }
288
+
289
+ /**
290
+ * Copy lib files that hooks depend on to ~/.teleportation/lib/
291
+ * These include auth, session, and config modules
292
+ */
293
+ export async function installLibFiles() {
294
+ const sourceLibDir = join(getTeleportationDir(), 'lib');
295
+ const destLibDir = join(HOME_DIR, '.teleportation', 'lib');
296
+
297
+ // Define lib files to copy with their subdirectories
298
+ const libFiles = [
299
+ { subdir: 'auth', files: ['credentials.js', 'api-key.js'] },
300
+ { subdir: 'session', files: ['metadata.js'] },
301
+ { subdir: 'config', files: ['manager.js'] }
302
+ ];
303
+
304
+ const installed = [];
305
+ const failed = [];
306
+
307
+ for (const { subdir, files } of libFiles) {
308
+ const srcSubdir = join(sourceLibDir, subdir);
309
+ const destSubdir = join(destLibDir, subdir);
310
+
311
+ // Create destination subdirectory
312
+ try {
313
+ await mkdir(destSubdir, { recursive: true });
314
+ } catch (e) {
315
+ if (e.code !== 'EEXIST') {
316
+ failed.push({ file: `${subdir}/`, error: e.message });
317
+ continue;
318
+ }
319
+ }
320
+
321
+ for (const file of files) {
322
+ const src = join(srcSubdir, file);
323
+ const dest = join(destSubdir, file);
324
+ const displayName = `${subdir}/${file}`;
325
+
326
+ try {
327
+ // Check if source exists
328
+ await stat(src);
329
+
330
+ // Copy file
331
+ await copyFile(src, dest);
332
+
333
+ // Set permissions (644 for modules)
334
+ await chmod(dest, 0o644);
335
+
336
+ installed.push(displayName);
337
+ } catch (e) {
338
+ if (e.code === 'ENOENT' && e.path === src) {
339
+ // Source file doesn't exist, skip (not critical)
340
+ continue;
341
+ }
342
+ failed.push({ file: displayName, error: e.message });
343
+ }
344
+ }
345
+ }
346
+
347
+ return { installed, failed };
348
+ }
349
+
350
+ /**
351
+ * Write version file to ~/.teleportation/version.json
352
+ * This file is read by hooks to include version in session metadata
353
+ */
354
+ export async function writeVersionFile() {
355
+ const versionFile = join(HOME_DIR, '.teleportation', 'version.json');
356
+ const versionData = {
357
+ version: TELEPORTATION_VERSION,
358
+ protocol_version: TELEPORTATION_PROTOCOL_VERSION,
359
+ installed_at: new Date().toISOString(),
360
+ installed_timestamp: Date.now()
361
+ };
362
+
363
+ await writeFile(versionFile, JSON.stringify(versionData, null, 2));
364
+ return versionFile;
365
+ }
366
+
367
+ /**
368
+ * Create Claude Code settings.json at project level with absolute paths
369
+ */
370
+ export async function createSettings() {
371
+ const projectHooksDir = resolve(getProjectHooksDir());
372
+ const projectSettings = getProjectSettings();
373
+
374
+ // Ensure parent directory exists
375
+ await mkdir(dirname(projectSettings), { recursive: true });
376
+
377
+ const settings = {
378
+ hooks: {
379
+ PreToolUse: [{
380
+ matcher: ".*",
381
+ hooks: [{
382
+ type: "command",
383
+ command: `node ${join(projectHooksDir, 'pre_tool_use.mjs')}`
384
+ }]
385
+ }],
386
+ Stop: [{
387
+ matcher: ".*",
388
+ hooks: [{
389
+ type: "command",
390
+ command: `node ${join(projectHooksDir, 'stop.mjs')}`
391
+ }]
392
+ }],
393
+ SessionStart: [{
394
+ matcher: ".*",
395
+ hooks: [{
396
+ type: "command",
397
+ command: `node ${join(projectHooksDir, 'session_start.mjs')}`
398
+ }]
399
+ }],
400
+ SessionEnd: [{
401
+ matcher: ".*",
402
+ hooks: [{
403
+ type: "command",
404
+ command: `node ${join(projectHooksDir, 'session_end.mjs')}`
405
+ }]
406
+ }],
407
+ Notification: [{
408
+ matcher: ".*",
409
+ hooks: [{
410
+ type: "command",
411
+ command: `node ${join(projectHooksDir, 'notification.mjs')}`
412
+ }]
413
+ }],
414
+ UserPromptSubmit: [{
415
+ matcher: ".*",
416
+ hooks: [{
417
+ type: "command",
418
+ command: `node ${join(projectHooksDir, 'user_prompt_submit.mjs')}`
419
+ }]
420
+ }]
421
+ }
422
+ };
423
+
424
+ await writeFile(projectSettings, JSON.stringify(settings, null, 2));
425
+
426
+ // IMPORTANT: Also write to user-level settings (~/.claude/settings.json)
427
+ // Claude Code reads from this location, not from the project directory
428
+ const userSettings = getUserSettings();
429
+ await mkdir(dirname(userSettings), { recursive: true });
430
+ await writeFile(userSettings, JSON.stringify(settings, null, 2));
431
+
432
+ return { projectSettings, userSettings };
433
+ }
434
+
435
+ /**
436
+ * Verify installation
437
+ */
438
+ export async function verifyInstallation() {
439
+ const projectHooksDir = getProjectHooksDir();
440
+ const projectSettings = getProjectSettings();
441
+
442
+ const checks = {
443
+ directories: false,
444
+ hooks: false,
445
+ settings: false
446
+ };
447
+
448
+ // Check directories
449
+ try {
450
+ await stat(projectHooksDir);
451
+ checks.directories = true;
452
+ } catch (e) {
453
+ return { valid: false, checks, error: 'Directories not created' };
454
+ }
455
+
456
+ // Check hooks
457
+ try {
458
+ const hooks = await readdir(projectHooksDir);
459
+ const requiredHooks = ['pre_tool_use.mjs', 'config-loader.mjs'];
460
+ const foundHooks = requiredHooks.filter(h => hooks.includes(h));
461
+ checks.hooks = foundHooks.length === requiredHooks.length;
462
+ } catch (e) {
463
+ return { valid: false, checks, error: 'Cannot read hooks directory' };
464
+ }
465
+
466
+ // Check settings
467
+ try {
468
+ await stat(projectSettings);
469
+ const content = await readFile(projectSettings, 'utf8');
470
+ const settings = JSON.parse(content);
471
+ checks.settings = settings.hooks && Object.keys(settings.hooks).length > 0;
472
+ } catch (e) {
473
+ return { valid: false, checks, error: 'Settings file invalid or missing' };
474
+ }
475
+
476
+ const valid = Object.values(checks).every(v => v === true);
477
+ if (!valid) {
478
+ const failed = Object.entries(checks).filter(([_, v]) => !v).map(([k]) => k).join(', ');
479
+ return { valid, checks, error: `Failed checks: ${failed}` };
480
+ }
481
+ return { valid, checks };
482
+ }
483
+
484
+ /**
485
+ * Main installation function
486
+ *
487
+ * Project-level installation:
488
+ * - Hooks stay in PROJECT/.claude/hooks/ (source files, just verify they exist)
489
+ * - Settings created in PROJECT/.claude/settings.json with absolute paths
490
+ * - Daemon copied to ~/.teleportation/daemon/ (shared across projects)
491
+ */
492
+ export async function install(sourceHooksDir) {
493
+ // Pre-flight checks
494
+ const nodeCheck = checkNodeVersion();
495
+ if (!nodeCheck.valid) {
496
+ throw new Error(nodeCheck.error);
497
+ }
498
+
499
+ const claudeCheck = checkClaudeCode();
500
+ if (!claudeCheck.valid) {
501
+ throw new Error(claudeCheck.error);
502
+ }
503
+
504
+ // Create directories
505
+ await ensureDirectories();
506
+
507
+ // Install hooks (verify and copy to destination)
508
+ const hookResult = await installHooks(sourceHooksDir);
509
+ if (hookResult.failed.length > 0) {
510
+ throw new Error(`Failed to install hooks: ${hookResult.failed.map(f => f.file).join(', ')}`);
511
+ }
512
+
513
+ // Install daemon (still goes to ~/.teleportation/daemon/)
514
+ const daemonResult = await installDaemon();
515
+ if (daemonResult.failed.length > 0) {
516
+ throw new Error(`Failed to install daemon: ${daemonResult.failed.map(f => f.file).join(', ')}`);
517
+ }
518
+
519
+ // Install daemon modules (machine-coders, router)
520
+ const daemonModulesResult = await installDaemonModules();
521
+ if (daemonModulesResult.failed.length > 0) {
522
+ console.warn(`Warning: Some daemon modules failed to install: ${daemonModulesResult.failed.map(f => f.file).join(', ')}`);
523
+ }
524
+
525
+ // Install lib files that hooks depend on (auth, session, config modules)
526
+ const libResult = await installLibFiles();
527
+ // Lib files are not critical - just log failures but don't throw
528
+ if (libResult.failed.length > 0) {
529
+ console.warn(`Warning: Some lib files failed to install: ${libResult.failed.map(f => f.file).join(', ')}`);
530
+ }
531
+
532
+ // Create project-level settings with absolute paths
533
+ await createSettings();
534
+
535
+ // Write version file
536
+ await writeVersionFile();
537
+
538
+ // Verify
539
+ const verification = await verifyInstallation();
540
+ if (!verification.valid) {
541
+ throw new Error(`Installation verification failed: ${verification.error}`);
542
+ }
543
+
544
+ return {
545
+ success: true,
546
+ hooksInstalled: hookResult.installed.length,
547
+ daemonInstalled: daemonResult.installed.length,
548
+ libFilesInstalled: libResult.installed.length,
549
+ settingsFile: getProjectSettings(),
550
+ hooksDir: getProjectHooksDir(),
551
+ daemonDir: join(HOME_DIR, '.teleportation', 'daemon'),
552
+ libDir: join(HOME_DIR, '.teleportation', 'lib')
553
+ };
554
+ }
555
+