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,291 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Commands for Worktree Management
4
+ */
5
+
6
+ import {
7
+ createWorktree,
8
+ removeWorktree,
9
+ listWorktrees,
10
+ listSessionWorktrees,
11
+ getWorktreeInfo,
12
+ pruneWorktrees,
13
+ getCurrentSessionId,
14
+ isInWorktree
15
+ } from '../worktree/manager.js';
16
+ import {
17
+ registerSession,
18
+ unregisterSession,
19
+ getSession,
20
+ getActiveSessionsInRepo
21
+ } from '../session-registry/manager.js';
22
+ import { createSnapshot, SnapshotType } from '../snapshot/manager.js';
23
+
24
+ /**
25
+ * Create a new worktree for a session
26
+ */
27
+ export async function commandWorktreeCreate(args) {
28
+ const { sessionId, branch, agent = 'claude-code', base = 'main' } = args;
29
+
30
+ if (!sessionId) {
31
+ throw new Error('Session ID is required (--session-id or -s)');
32
+ }
33
+
34
+ if (!branch) {
35
+ throw new Error('Branch name is required (--branch or -b)');
36
+ }
37
+
38
+ try {
39
+ console.log(`Creating worktree for session: ${sessionId}`);
40
+ console.log(`Branch: ${branch}`);
41
+ console.log(`Base: ${base}`);
42
+
43
+ // Create the worktree
44
+ const worktree = await createWorktree(sessionId, branch, base);
45
+ console.log(`✓ Worktree created at: ${worktree.path}`);
46
+
47
+ // Register the session
48
+ await registerSession(sessionId, agent, worktree.path, branch);
49
+ console.log(`✓ Session registered`);
50
+
51
+ // Create baseline snapshot
52
+ const snapshot = await createSnapshot(
53
+ sessionId,
54
+ SnapshotType.BASELINE,
55
+ 'Initial baseline snapshot'
56
+ );
57
+ console.log(`✓ Baseline snapshot created: ${snapshot.id}`);
58
+
59
+ console.log(`\nTo switch to this worktree:`);
60
+ console.log(` cd ${worktree.path}`);
61
+
62
+ return worktree;
63
+ } catch (error) {
64
+ console.error(`Failed to create worktree: ${error.message}`);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * List all worktrees
71
+ */
72
+ export async function commandWorktreeList() {
73
+ try {
74
+ const sessionWorktrees = await listSessionWorktrees();
75
+
76
+ if (sessionWorktrees.length === 0) {
77
+ console.log('No session worktrees found.');
78
+ return;
79
+ }
80
+
81
+ console.log('\nSession Worktrees:\n');
82
+ console.log('SESSION ID'.padEnd(30), 'BRANCH'.padEnd(30), 'PATH');
83
+ console.log('-'.repeat(90));
84
+
85
+ for (const wt of sessionWorktrees) {
86
+ console.log(
87
+ wt.sessionId.padEnd(30),
88
+ wt.branch.padEnd(30),
89
+ wt.path
90
+ );
91
+ }
92
+
93
+ // Show current worktree if we're in one
94
+ const currentSessionId = getCurrentSessionId();
95
+ if (currentSessionId) {
96
+ console.log(`\n→ Current session: ${currentSessionId}`);
97
+ }
98
+
99
+ return sessionWorktrees;
100
+ } catch (error) {
101
+ console.error(`Failed to list worktrees: ${error.message}`);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Remove a worktree
108
+ */
109
+ export async function commandWorktreeRemove(args) {
110
+ const { sessionId, force = false, keepSnapshot = true } = args;
111
+
112
+ if (!sessionId) {
113
+ throw new Error('Session ID is required (--session-id or -s)');
114
+ }
115
+
116
+ try {
117
+ const sessionWorktrees = await listSessionWorktrees();
118
+ const worktree = sessionWorktrees.find(wt => wt.sessionId === sessionId);
119
+
120
+ if (!worktree) {
121
+ throw new Error(`No worktree found for session: ${sessionId}`);
122
+ }
123
+
124
+ // Create pre-destroy snapshot if requested
125
+ if (keepSnapshot) {
126
+ console.log('Creating pre-destroy snapshot...');
127
+ const snapshot = await createSnapshot(
128
+ sessionId,
129
+ SnapshotType.PRE_DESTROY,
130
+ 'Snapshot before worktree destruction'
131
+ );
132
+ console.log(`✓ Snapshot created: ${snapshot.id}`);
133
+ }
134
+
135
+ // Remove the worktree
136
+ console.log(`Removing worktree: ${worktree.path}`);
137
+ await removeWorktree(worktree.path, force);
138
+ console.log('✓ Worktree removed');
139
+
140
+ // Unregister the session
141
+ await unregisterSession(sessionId);
142
+ console.log('✓ Session unregistered');
143
+
144
+ return true;
145
+ } catch (error) {
146
+ console.error(`Failed to remove worktree: ${error.message}`);
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Show worktree information
153
+ */
154
+ export async function commandWorktreeInfo(args) {
155
+ const { sessionId } = args;
156
+
157
+ try {
158
+ let targetSessionId = sessionId;
159
+
160
+ // If no session ID provided, try to get current
161
+ if (!targetSessionId) {
162
+ targetSessionId = getCurrentSessionId();
163
+ if (!targetSessionId) {
164
+ throw new Error('Not in a worktree. Specify --session-id to get info for a specific session.');
165
+ }
166
+ }
167
+
168
+ // Get worktree info
169
+ const sessionWorktrees = await listSessionWorktrees();
170
+ const worktree = sessionWorktrees.find(wt => wt.sessionId === targetSessionId);
171
+
172
+ if (!worktree) {
173
+ throw new Error(`No worktree found for session: ${targetSessionId}`);
174
+ }
175
+
176
+ // Get session info
177
+ const session = await getSession(targetSessionId);
178
+
179
+ console.log('\nWorktree Information:\n');
180
+ console.log(`Session ID: ${targetSessionId}`);
181
+ console.log(`Agent: ${session?.agent || 'unknown'}`);
182
+ console.log(`Branch: ${worktree.branch}`);
183
+ console.log(`Path: ${worktree.path}`);
184
+ console.log(`Commit: ${worktree.commitHash}`);
185
+ console.log(`Status: ${session?.status || 'unknown'}`);
186
+
187
+ if (session) {
188
+ console.log(`Started: ${new Date(session.startedAt).toLocaleString()}`);
189
+ console.log(`Last Active: ${new Date(session.lastActiveAt).toLocaleString()}`);
190
+
191
+ if (session.modifiedFiles.length > 0) {
192
+ console.log(`\nModified Files (${session.modifiedFiles.length}):`);
193
+ session.modifiedFiles.slice(0, 10).forEach(file => {
194
+ console.log(` - ${file}`);
195
+ });
196
+ if (session.modifiedFiles.length > 10) {
197
+ console.log(` ... and ${session.modifiedFiles.length - 10} more`);
198
+ }
199
+ }
200
+ }
201
+
202
+ return { worktree, session };
203
+ } catch (error) {
204
+ console.error(`Failed to get worktree info: ${error.message}`);
205
+ throw error;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Merge worktree back to main
211
+ */
212
+ export async function commandWorktreeMerge(args) {
213
+ const { sessionId, target = 'main', deleteAfter = false } = args;
214
+
215
+ if (!sessionId) {
216
+ throw new Error('Session ID is required (--session-id or -s)');
217
+ }
218
+
219
+ try {
220
+ const sessionWorktrees = await listSessionWorktrees();
221
+ const worktree = sessionWorktrees.find(wt => wt.sessionId === sessionId);
222
+
223
+ if (!worktree) {
224
+ throw new Error(`No worktree found for session: ${sessionId}`);
225
+ }
226
+
227
+ console.log(`Merging ${worktree.branch} into ${target}...`);
228
+ console.log('This will:');
229
+ console.log(` 1. Switch to ${target}`);
230
+ console.log(` 2. Pull latest changes`);
231
+ console.log(` 3. Merge ${worktree.branch}`);
232
+ console.log(` 4. Push to remote`);
233
+
234
+ if (deleteAfter) {
235
+ console.log(` 5. Delete worktree for ${sessionId}`);
236
+ }
237
+
238
+ console.log('\nThis operation is not yet implemented.');
239
+ console.log('Please merge manually using git commands.');
240
+
241
+ return false;
242
+ } catch (error) {
243
+ console.error(`Failed to merge worktree: ${error.message}`);
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Prune stale worktrees
250
+ */
251
+ export async function commandWorktreePrune() {
252
+ try {
253
+ console.log('Pruning stale worktree references...');
254
+ pruneWorktrees();
255
+ console.log('✓ Worktrees pruned');
256
+
257
+ return true;
258
+ } catch (error) {
259
+ console.error(`Failed to prune worktrees: ${error.message}`);
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Switch to a worktree
266
+ */
267
+ export async function commandWorktreeUse(args) {
268
+ const { sessionId } = args;
269
+
270
+ if (!sessionId) {
271
+ throw new Error('Session ID is required (--session-id or -s)');
272
+ }
273
+
274
+ try {
275
+ const sessionWorktrees = await listSessionWorktrees();
276
+ const worktree = sessionWorktrees.find(wt => wt.sessionId === sessionId);
277
+
278
+ if (!worktree) {
279
+ throw new Error(`No worktree found for session: ${sessionId}`);
280
+ }
281
+
282
+ console.log(`\nTo switch to this worktree, run:`);
283
+ console.log(` cd ${worktree.path}`);
284
+ console.log(`\nOr use your shell's cd command directly.`);
285
+
286
+ return worktree;
287
+ } catch (error) {
288
+ console.error(`Failed to switch worktree: ${error.message}`);
289
+ throw error;
290
+ }
291
+ }
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Configuration file management
4
+ * Handles reading/writing ~/.teleportation/config.json
5
+ */
6
+
7
+ import { readFile, writeFile, mkdir, stat } from 'fs/promises';
8
+ import { join, dirname } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ const DEFAULT_CONFIG_PATH = join(homedir(), '.teleportation', 'config.json');
12
+
13
+ // Default configuration
14
+ const DEFAULT_CONFIG = {
15
+ relay: {
16
+ url: 'https://api.teleportation.dev',
17
+ timeout: 30000
18
+ },
19
+ hooks: {
20
+ autoUpdate: true,
21
+ updateCheckInterval: 86400000 // 24 hours
22
+ },
23
+ session: {
24
+ timeout: 3600000, // 1 hour
25
+ muteTimeout: 300000, // 5 minutes
26
+ heartbeat: {
27
+ enabled: true,
28
+ interval: 120000, // 2 minutes between heartbeats
29
+ timeout: 300000, // 5 minutes without heartbeat = session dead
30
+ startDelay: 5000, // Wait 5 seconds after session start before first heartbeat
31
+ maxFailures: 3 // Stop heartbeat after 3 consecutive failures
32
+ }
33
+ },
34
+ notifications: {
35
+ enabled: true,
36
+ sound: false
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Validate configuration structure and values
42
+ */
43
+ function validateConfig(config) {
44
+ const errors = [];
45
+ const warnings = [];
46
+
47
+ // Validate relay URL format
48
+ if (config.relay?.url) {
49
+ try {
50
+ const url = new URL(config.relay.url);
51
+ if (!['http:', 'https:'].includes(url.protocol)) {
52
+ errors.push('Relay URL must use http:// or https:// protocol');
53
+ }
54
+ } catch (e) {
55
+ errors.push(`Invalid relay URL format: ${config.relay.url}`);
56
+ }
57
+ }
58
+
59
+ // Validate timeout values
60
+ if (config.relay?.timeout !== undefined) {
61
+ if (typeof config.relay.timeout !== 'number' || config.relay.timeout < 1000) {
62
+ errors.push('Relay timeout must be a number >= 1000ms');
63
+ }
64
+ if (config.relay.timeout > 300000) {
65
+ warnings.push('Relay timeout is very high (>5 minutes), this may cause slow responses');
66
+ }
67
+ }
68
+
69
+ // Validate session timeout
70
+ if (config.session?.timeout !== undefined) {
71
+ if (typeof config.session.timeout !== 'number' || config.session.timeout < 60000) {
72
+ errors.push('Session timeout must be a number >= 60000ms (1 minute)');
73
+ }
74
+ }
75
+
76
+ // Validate boolean values
77
+ const booleanFields = [
78
+ 'hooks.autoUpdate',
79
+ 'notifications.enabled',
80
+ 'notifications.sound'
81
+ ];
82
+
83
+ booleanFields.forEach(field => {
84
+ const value = getNestedValue(config, field);
85
+ if (value !== undefined && typeof value !== 'boolean') {
86
+ errors.push(`${field} must be a boolean (true/false)`);
87
+ }
88
+ });
89
+
90
+ return { errors, warnings };
91
+ }
92
+
93
+ /**
94
+ * Get nested value from object using dot notation
95
+ */
96
+ function getNestedValue(obj, path) {
97
+ return path.split('.').reduce((current, key) => current?.[key], obj);
98
+ }
99
+
100
+ /**
101
+ * Auto-fix common configuration issues
102
+ */
103
+ function autoFixConfig(config) {
104
+ const fixed = { ...config };
105
+
106
+ // Ensure relay URL has trailing slash removed
107
+ if (fixed.relay?.url && fixed.relay.url.endsWith('/')) {
108
+ fixed.relay.url = fixed.relay.url.slice(0, -1);
109
+ }
110
+
111
+ // Ensure timeouts are within reasonable bounds
112
+ if (fixed.relay?.timeout) {
113
+ fixed.relay.timeout = Math.max(1000, Math.min(300000, fixed.relay.timeout));
114
+ }
115
+
116
+ if (fixed.session?.timeout) {
117
+ fixed.session.timeout = Math.max(60000, Math.min(86400000, fixed.session.timeout));
118
+ }
119
+
120
+ return fixed;
121
+ }
122
+
123
+ /**
124
+ * Load configuration from JSON file with validation
125
+ */
126
+ async function loadConfig() {
127
+ try {
128
+ const content = await readFile(DEFAULT_CONFIG_PATH, 'utf8');
129
+ const config = JSON.parse(content);
130
+
131
+ // Auto-fix common issues
132
+ const fixedConfig = autoFixConfig(config);
133
+
134
+ // Validate configuration
135
+ const { errors, warnings } = validateConfig(fixedConfig);
136
+
137
+ if (errors.length > 0) {
138
+ throw new Error(`Configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}\n\nPlease fix these issues or delete the config file to use defaults.`);
139
+ }
140
+
141
+ if (warnings.length > 0) {
142
+ // Log warnings but don't fail
143
+ console.warn('Configuration warnings:');
144
+ warnings.forEach(w => console.warn(` - ${w}`));
145
+ }
146
+
147
+ return fixedConfig;
148
+ } catch (e) {
149
+ if (e.code === 'ENOENT') {
150
+ // Config doesn't exist, return defaults
151
+ return DEFAULT_CONFIG;
152
+ }
153
+ if (e instanceof SyntaxError) {
154
+ throw new Error(`Failed to parse config file (invalid JSON): ${e.message}\n\nPlease check the JSON syntax in ${DEFAULT_CONFIG_PATH} or delete it to use defaults.`);
155
+ }
156
+ throw new Error(`Failed to load config: ${e.message}`);
157
+ }
158
+ }
159
+
160
+
161
+ /**
162
+ * Save configuration
163
+ */
164
+ async function saveConfig(config) {
165
+ await mkdir(dirname(DEFAULT_CONFIG_PATH), { recursive: true });
166
+
167
+ // Merge with defaults
168
+ const merged = deepMerge(DEFAULT_CONFIG, config);
169
+
170
+ // Save as JSON for now (easier to parse)
171
+ await writeFile(
172
+ DEFAULT_CONFIG_PATH,
173
+ JSON.stringify(merged, null, 2),
174
+ { mode: 0o600 }
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Deep merge objects
180
+ */
181
+ function deepMerge(target, source) {
182
+ const output = { ...target };
183
+
184
+ if (isObject(target) && isObject(source)) {
185
+ Object.keys(source).forEach(key => {
186
+ if (isObject(source[key])) {
187
+ if (!(key in target)) {
188
+ Object.assign(output, { [key]: source[key] });
189
+ } else {
190
+ output[key] = deepMerge(target[key], source[key]);
191
+ }
192
+ } else {
193
+ Object.assign(output, { [key]: source[key] });
194
+ }
195
+ });
196
+ }
197
+
198
+ return output;
199
+ }
200
+
201
+ function isObject(item) {
202
+ return item && typeof item === 'object' && !Array.isArray(item);
203
+ }
204
+
205
+ /**
206
+ * Get a specific config value by dot-notation path
207
+ */
208
+ async function getConfigValue(path) {
209
+ const config = await loadConfig();
210
+ const parts = path.split('.');
211
+ let value = config;
212
+
213
+ for (const part of parts) {
214
+ if (value && typeof value === 'object' && part in value) {
215
+ value = value[part];
216
+ } else {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ return value;
222
+ }
223
+
224
+ /**
225
+ * Set a config value by dot-notation path
226
+ */
227
+ async function setConfigValue(path, value) {
228
+ // Validate path doesn't contain dangerous properties (prototype pollution protection)
229
+ if (path.includes('__proto__') || path.includes('constructor') || path.includes('prototype')) {
230
+ throw new Error('Invalid config path: cannot contain __proto__, constructor, or prototype');
231
+ }
232
+
233
+ // Validate path format (only alphanumeric, dots, underscores, hyphens)
234
+ if (!/^[a-zA-Z0-9._-]+$/.test(path)) {
235
+ throw new Error('Invalid config path format: only alphanumeric characters, dots, underscores, and hyphens allowed');
236
+ }
237
+
238
+ const config = await loadConfig();
239
+ const parts = path.split('.');
240
+ const lastPart = parts.pop();
241
+ let current = config;
242
+
243
+ // Navigate/create nested objects
244
+ for (const part of parts) {
245
+ // Additional validation for each part
246
+ if (part.includes('__proto__') || part.includes('constructor') || part.includes('prototype')) {
247
+ throw new Error(`Invalid config path part: ${part}`);
248
+ }
249
+
250
+ if (!current[part] || typeof current[part] !== 'object') {
251
+ current[part] = {};
252
+ }
253
+ current = current[part];
254
+ }
255
+
256
+ // Set the value
257
+ current[lastPart] = value;
258
+
259
+ await saveConfig(config);
260
+ }
261
+
262
+ /**
263
+ * Check if config file exists
264
+ */
265
+ async function configExists() {
266
+ try {
267
+ await stat(DEFAULT_CONFIG_PATH);
268
+ return true;
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Validate configuration without loading
276
+ */
277
+ async function validateConfigFile() {
278
+ try {
279
+ const config = await loadConfig();
280
+ const validation = validateConfig(config);
281
+ return {
282
+ valid: validation.errors.length === 0,
283
+ errors: validation.errors,
284
+ warnings: validation.warnings,
285
+ config
286
+ };
287
+ } catch (error) {
288
+ return {
289
+ valid: false,
290
+ errors: [error.message],
291
+ warnings: [],
292
+ config: null
293
+ };
294
+ }
295
+ }
296
+
297
+ export {
298
+ loadConfig,
299
+ saveConfig,
300
+ getConfigValue,
301
+ setConfigValue,
302
+ configExists,
303
+ validateConfigFile,
304
+ DEFAULT_CONFIG_PATH
305
+ };
306
+