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,277 @@
1
+ /**
2
+ * Main Handoff Operations
3
+ *
4
+ * High-level functions for handoff and sync operations.
5
+ * Coordinates git operations, session state, and cloud provider.
6
+ */
7
+
8
+ import { GitHandoff } from './git-handoff.js';
9
+ import { SessionState, loadSessionState, saveSessionState, getOrCreateSessionState } from './session-state.js';
10
+ import { getHandoffConfig } from './config.js';
11
+
12
+ /**
13
+ * Perform a handoff from local to cloud
14
+ *
15
+ * @param {Object} options
16
+ * @param {string} options.projectPath - Path to the project
17
+ * @param {string} options.sessionId - Teleportation session ID
18
+ * @param {string} [options.message] - Optional message describing what was being worked on
19
+ * @param {boolean} [options.skipCommit] - Skip committing (if already committed)
20
+ * @param {boolean} [options.skipPush] - Skip pushing (dry run)
21
+ * @param {Function} [options.onProgress] - Progress callback
22
+ * @returns {Promise<{success: boolean, commit?: string, branch?: string, error?: string}>}
23
+ */
24
+ export async function handoff(options) {
25
+ const {
26
+ projectPath,
27
+ sessionId,
28
+ message = '',
29
+ skipCommit = false,
30
+ skipPush = false,
31
+ onProgress = () => {},
32
+ } = options;
33
+
34
+ const config = getHandoffConfig(projectPath);
35
+
36
+ if (!config.enabled) {
37
+ return { success: false, error: 'Handoff is disabled in configuration' };
38
+ }
39
+
40
+ onProgress({ step: 'init', message: 'Initializing handoff...' });
41
+
42
+ // Initialize git handoff
43
+ const git = new GitHandoff({
44
+ repoPath: projectPath,
45
+ sessionId,
46
+ branchPrefix: config.wip_branch_prefix,
47
+ verbose: true,
48
+ });
49
+
50
+ // Load or create session state
51
+ const state = getOrCreateSessionState(projectPath, sessionId);
52
+
53
+ try {
54
+ // Check for remote URL
55
+ const remoteUrl = await git.getRemoteUrl();
56
+ if (!remoteUrl) {
57
+ return { success: false, error: 'No git remote configured. Cannot handoff.' };
58
+ }
59
+
60
+ let commit = await git.getCurrentCommit();
61
+ let files = [];
62
+
63
+ // Commit WIP changes if needed
64
+ if (!skipCommit && config.commit_uncommitted) {
65
+ onProgress({ step: 'commit', message: 'Committing work in progress...' });
66
+
67
+ const hasChanges = await git.hasUncommittedChanges();
68
+ if (hasChanges) {
69
+ const result = await git.commitWip(message);
70
+ commit = result.commit;
71
+ files = result.files;
72
+ onProgress({ step: 'commit', message: `Committed ${files.length} files` });
73
+ } else {
74
+ onProgress({ step: 'commit', message: 'No uncommitted changes' });
75
+ }
76
+ }
77
+
78
+ // Push to WIP branch
79
+ let branch = git.wipBranch;
80
+ if (!skipPush) {
81
+ onProgress({ step: 'push', message: `Pushing to ${branch}...` });
82
+ const pushResult = await git.pushToWipBranch();
83
+ branch = pushResult.branch;
84
+ commit = pushResult.commit;
85
+ onProgress({ step: 'push', message: 'Pushed successfully' });
86
+ }
87
+
88
+ // Update session state
89
+ state.recordHandoff({
90
+ type: 'local_to_cloud',
91
+ commit,
92
+ branch,
93
+ message,
94
+ files,
95
+ });
96
+ saveSessionState(projectPath, state);
97
+
98
+ onProgress({ step: 'done', message: 'Handoff complete' });
99
+
100
+ return {
101
+ success: true,
102
+ commit,
103
+ branch,
104
+ files,
105
+ sessionId,
106
+ };
107
+ } catch (error) {
108
+ return {
109
+ success: false,
110
+ error: error.message,
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Sync changes from cloud back to local
117
+ *
118
+ * @param {Object} options
119
+ * @param {string} options.projectPath - Path to the project
120
+ * @param {string} options.sessionId - Teleportation session ID
121
+ * @param {string} [options.strategy='merge'] - 'merge' or 'rebase'
122
+ * @param {boolean} [options.force] - Force sync even if conflicts
123
+ * @param {Function} [options.onProgress] - Progress callback
124
+ * @returns {Promise<{success: boolean, commit?: string, files?: string[], conflicts?: string[], error?: string}>}
125
+ */
126
+ export async function sync(options) {
127
+ const {
128
+ projectPath,
129
+ sessionId,
130
+ strategy = 'merge',
131
+ force = false,
132
+ onProgress = () => {},
133
+ } = options;
134
+
135
+ const config = getHandoffConfig(projectPath);
136
+
137
+ onProgress({ step: 'init', message: 'Checking for remote changes...' });
138
+
139
+ // Initialize git handoff
140
+ const git = new GitHandoff({
141
+ repoPath: projectPath,
142
+ sessionId,
143
+ branchPrefix: config.wip_branch_prefix,
144
+ verbose: true,
145
+ });
146
+
147
+ // Load session state
148
+ const state = loadSessionState(projectPath);
149
+
150
+ try {
151
+ // Check if there are remote changes
152
+ const { hasChanges, localCommit, remoteCommit } = await git.checkRemoteChanges();
153
+
154
+ if (!hasChanges) {
155
+ onProgress({ step: 'done', message: 'Already up to date' });
156
+ return {
157
+ success: true,
158
+ commit: localCommit,
159
+ files: [],
160
+ message: 'Already up to date',
161
+ };
162
+ }
163
+
164
+ // Get diff summary
165
+ const diff = await git.getDiffSummary();
166
+ onProgress({
167
+ step: 'diff',
168
+ message: `${diff.behind} commits behind, ${diff.files.length} files changed`
169
+ });
170
+
171
+ // Perform sync
172
+ onProgress({ step: 'sync', message: `Syncing with ${strategy}...` });
173
+ const result = await git.syncFromRemote({ strategy, stashLocal: true });
174
+
175
+ if (!result.success && result.conflicts.length > 0) {
176
+ if (!force) {
177
+ return {
178
+ success: false,
179
+ conflicts: result.conflicts,
180
+ error: `Merge conflicts in: ${result.conflicts.join(', ')}`,
181
+ };
182
+ }
183
+ // Force mode: accept theirs
184
+ onProgress({ step: 'resolve', message: 'Force resolving conflicts...' });
185
+ // TODO: Implement force resolution
186
+ }
187
+
188
+ // Update session state
189
+ if (state) {
190
+ state.recordHandoff({
191
+ type: 'cloud_to_local',
192
+ commit: result.commit,
193
+ files: diff.files,
194
+ });
195
+ saveSessionState(projectPath, state);
196
+ }
197
+
198
+ onProgress({ step: 'done', message: 'Sync complete' });
199
+
200
+ return {
201
+ success: true,
202
+ commit: result.commit,
203
+ files: diff.files,
204
+ previousCommit: localCommit,
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ success: false,
209
+ error: error.message,
210
+ };
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Get the current handoff status
216
+ *
217
+ * @param {Object} options
218
+ * @param {string} options.projectPath - Path to the project
219
+ * @param {string} options.sessionId - Teleportation session ID
220
+ * @returns {Promise<{location: string, hasRemoteChanges: boolean, localCommit: string, remoteCommit?: string, diff?: Object}>}
221
+ */
222
+ export async function getHandoffStatus(options) {
223
+ const { projectPath, sessionId } = options;
224
+ const config = getHandoffConfig(projectPath);
225
+
226
+ const git = new GitHandoff({
227
+ repoPath: projectPath,
228
+ sessionId,
229
+ branchPrefix: config.wip_branch_prefix,
230
+ });
231
+
232
+ const state = loadSessionState(projectPath);
233
+
234
+ const localCommit = await git.getCurrentCommit();
235
+ const currentBranch = await git.getCurrentBranch();
236
+ const hasUncommitted = await git.hasUncommittedChanges();
237
+
238
+ // Check remote
239
+ const { hasChanges, remoteCommit } = await git.checkRemoteChanges();
240
+
241
+ let diff = null;
242
+ if (hasChanges) {
243
+ diff = await git.getDiffSummary();
244
+ }
245
+
246
+ return {
247
+ location: state?.location || 'local',
248
+ sessionId: state?.sessionId || sessionId,
249
+ currentBranch,
250
+ wipBranch: git.wipBranch,
251
+ localCommit,
252
+ remoteCommit,
253
+ hasUncommittedChanges: hasUncommitted,
254
+ hasRemoteChanges: hasChanges,
255
+ diff,
256
+ lastHandoff: state?.handoffHistory?.slice(-1)[0] || null,
257
+ cloudContainer: state?.cloudContainer || null,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Check if a handoff is currently in progress
263
+ *
264
+ * @param {string} projectPath
265
+ * @returns {boolean}
266
+ */
267
+ export function isHandoffInProgress(projectPath) {
268
+ const state = loadSessionState(projectPath);
269
+ return state?.location === 'syncing';
270
+ }
271
+
272
+ export default {
273
+ handoff,
274
+ sync,
275
+ getHandoffStatus,
276
+ isHandoffInProgress,
277
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Teleportation Handoff Module
3
+ *
4
+ * Handles seamless handoff between local and cloud dev environments.
5
+ *
6
+ * Key features:
7
+ * - Auto-commit WIP changes to teleport/wip-<session> branch
8
+ * - Push to remote for cloud pickup
9
+ * - Sync changes back from cloud
10
+ * - Session state persistence in .teleportation/ directory
11
+ *
12
+ * @module lib/handoff
13
+ */
14
+
15
+ export { GitHandoff, createGitHandoff } from './git-handoff.js';
16
+ export { SessionState, loadSessionState, saveSessionState } from './session-state.js';
17
+ export {
18
+ handoff,
19
+ sync,
20
+ getHandoffStatus,
21
+ isHandoffInProgress
22
+ } from './handoff.js';
23
+
24
+ // Re-export config helpers
25
+ export { getHandoffConfig, DEFAULT_HANDOFF_CONFIG } from './config.js';
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Session State Management
3
+ *
4
+ * Persists session state to .teleportation/ directory so it can be
5
+ * synced between local and cloud environments.
6
+ *
7
+ * State includes:
8
+ * - Session metadata (ID, timestamps, location)
9
+ * - Handoff history
10
+ * - Task progress
11
+ * - Conversation context (optional)
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
15
+ import { join } from 'path';
16
+
17
+ /**
18
+ * Session state structure
19
+ */
20
+ export class SessionState {
21
+ constructor(data = {}) {
22
+ // Core identifiers
23
+ this.sessionId = data.sessionId || null;
24
+ this.projectPath = data.projectPath || null;
25
+
26
+ // Location tracking
27
+ this.location = data.location || 'local'; // 'local' | 'cloud' | 'syncing'
28
+ this.lastLocalActivity = data.lastLocalActivity || null;
29
+ this.lastCloudActivity = data.lastCloudActivity || null;
30
+
31
+ // Handoff tracking
32
+ this.handoffHistory = data.handoffHistory || [];
33
+ this.lastHandoffCommit = data.lastHandoffCommit || null;
34
+ this.lastSyncCommit = data.lastSyncCommit || null;
35
+
36
+ // Cloud container info (when in cloud)
37
+ this.cloudContainer = data.cloudContainer || null;
38
+
39
+ // Task/workflow state
40
+ this.currentTask = data.currentTask || null;
41
+ this.taskProgress = data.taskProgress || {};
42
+
43
+ // Timestamps
44
+ this.createdAt = data.createdAt || new Date().toISOString();
45
+ this.updatedAt = data.updatedAt || new Date().toISOString();
46
+ }
47
+
48
+ /**
49
+ * Record a handoff event
50
+ * @param {Object} event
51
+ * @param {string} event.type - 'local_to_cloud' | 'cloud_to_local'
52
+ * @param {string} event.commit - Git commit hash
53
+ * @param {string} [event.message] - Optional message
54
+ */
55
+ recordHandoff(event) {
56
+ this.handoffHistory.push({
57
+ ...event,
58
+ timestamp: new Date().toISOString(),
59
+ });
60
+
61
+ if (event.type === 'local_to_cloud') {
62
+ this.lastHandoffCommit = event.commit;
63
+ this.location = 'cloud';
64
+ this.lastLocalActivity = new Date().toISOString();
65
+ } else if (event.type === 'cloud_to_local') {
66
+ this.lastSyncCommit = event.commit;
67
+ this.location = 'local';
68
+ this.lastCloudActivity = new Date().toISOString();
69
+ }
70
+
71
+ this.updatedAt = new Date().toISOString();
72
+ }
73
+
74
+ /**
75
+ * Update location
76
+ * @param {'local'|'cloud'|'syncing'} location
77
+ */
78
+ setLocation(location) {
79
+ this.location = location;
80
+ if (location === 'local') {
81
+ this.lastLocalActivity = new Date().toISOString();
82
+ } else if (location === 'cloud') {
83
+ this.lastCloudActivity = new Date().toISOString();
84
+ }
85
+ this.updatedAt = new Date().toISOString();
86
+ }
87
+
88
+ /**
89
+ * Update cloud container info
90
+ * @param {Object|null} container
91
+ */
92
+ setCloudContainer(container) {
93
+ this.cloudContainer = container;
94
+ this.updatedAt = new Date().toISOString();
95
+ }
96
+
97
+ /**
98
+ * Update current task
99
+ * @param {Object|null} task
100
+ */
101
+ setCurrentTask(task) {
102
+ this.currentTask = task;
103
+ this.updatedAt = new Date().toISOString();
104
+ }
105
+
106
+ /**
107
+ * Update task progress
108
+ * @param {string} taskId
109
+ * @param {Object} progress
110
+ */
111
+ updateTaskProgress(taskId, progress) {
112
+ this.taskProgress[taskId] = {
113
+ ...this.taskProgress[taskId],
114
+ ...progress,
115
+ updatedAt: new Date().toISOString(),
116
+ };
117
+ this.updatedAt = new Date().toISOString();
118
+ }
119
+
120
+ /**
121
+ * Serialize to JSON
122
+ * @returns {Object}
123
+ */
124
+ toJSON() {
125
+ return {
126
+ sessionId: this.sessionId,
127
+ projectPath: this.projectPath,
128
+ location: this.location,
129
+ lastLocalActivity: this.lastLocalActivity,
130
+ lastCloudActivity: this.lastCloudActivity,
131
+ handoffHistory: this.handoffHistory,
132
+ lastHandoffCommit: this.lastHandoffCommit,
133
+ lastSyncCommit: this.lastSyncCommit,
134
+ cloudContainer: this.cloudContainer,
135
+ currentTask: this.currentTask,
136
+ taskProgress: this.taskProgress,
137
+ createdAt: this.createdAt,
138
+ updatedAt: this.updatedAt,
139
+ };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get the .teleportation directory path for a project
145
+ * @param {string} projectPath
146
+ * @returns {string}
147
+ */
148
+ function getTeleportationDir(projectPath) {
149
+ return join(projectPath, '.teleportation');
150
+ }
151
+
152
+ /**
153
+ * Get the session state file path
154
+ * @param {string} projectPath
155
+ * @returns {string}
156
+ */
157
+ function getSessionStatePath(projectPath) {
158
+ return join(getTeleportationDir(projectPath), 'session.json');
159
+ }
160
+
161
+ /**
162
+ * Ensure .teleportation directory exists
163
+ * @param {string} projectPath
164
+ */
165
+ function ensureTeleportationDir(projectPath) {
166
+ const dir = getTeleportationDir(projectPath);
167
+ if (!existsSync(dir)) {
168
+ // Create with 700 permissions (owner only) for security
169
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Load session state from disk
175
+ * @param {string} projectPath - Project directory
176
+ * @returns {SessionState|null}
177
+ */
178
+ export function loadSessionState(projectPath) {
179
+ const statePath = getSessionStatePath(projectPath);
180
+
181
+ if (!existsSync(statePath)) {
182
+ return null;
183
+ }
184
+
185
+ try {
186
+ const data = JSON.parse(readFileSync(statePath, 'utf-8'));
187
+ return new SessionState(data);
188
+ } catch (error) {
189
+ console.error(`[session-state] Failed to load state: ${error.message}`);
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Save session state to disk
196
+ * @param {string} projectPath - Project directory
197
+ * @param {SessionState} state - State to save
198
+ */
199
+ export function saveSessionState(projectPath, state) {
200
+ ensureTeleportationDir(projectPath);
201
+ const statePath = getSessionStatePath(projectPath);
202
+
203
+ try {
204
+ // Write with 600 permissions (owner read/write only) for security
205
+ // Session state may contain sensitive info like cloud container credentials
206
+ writeFileSync(statePath, JSON.stringify(state.toJSON(), null, 2), { mode: 0o600 });
207
+ } catch (error) {
208
+ console.error(`[session-state] Failed to save state: ${error.message}`);
209
+ throw error;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Create or load session state
215
+ * @param {string} projectPath
216
+ * @param {string} sessionId
217
+ * @returns {SessionState}
218
+ */
219
+ export function getOrCreateSessionState(projectPath, sessionId) {
220
+ let state = loadSessionState(projectPath);
221
+
222
+ if (!state || state.sessionId !== sessionId) {
223
+ state = new SessionState({
224
+ sessionId,
225
+ projectPath,
226
+ });
227
+ saveSessionState(projectPath, state);
228
+ }
229
+
230
+ return state;
231
+ }
232
+
233
+ export default {
234
+ SessionState,
235
+ loadSessionState,
236
+ saveSessionState,
237
+ getOrCreateSessionState,
238
+ };