teleportation-cli 1.1.5 → 1.2.1

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 (42) hide show
  1. package/.claude/hooks/permission_request.mjs +326 -59
  2. package/.claude/hooks/post_tool_use.mjs +90 -0
  3. package/.claude/hooks/pre_tool_use.mjs +212 -293
  4. package/.claude/hooks/session-register.mjs +89 -104
  5. package/.claude/hooks/session_end.mjs +41 -42
  6. package/.claude/hooks/session_start.mjs +45 -60
  7. package/.claude/hooks/stop.mjs +752 -99
  8. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  9. package/lib/cli/daemon-commands.js +1 -1
  10. package/lib/cli/teleport-commands.js +469 -0
  11. package/lib/daemon/daemon-v2.js +104 -0
  12. package/lib/daemon/lifecycle.js +56 -171
  13. package/lib/daemon/services/index.js +3 -0
  14. package/lib/daemon/services/polling-service.js +173 -0
  15. package/lib/daemon/services/queue-service.js +318 -0
  16. package/lib/daemon/services/session-service.js +115 -0
  17. package/lib/daemon/state.js +35 -0
  18. package/lib/daemon/task-executor-v2.js +413 -0
  19. package/lib/daemon/task-executor.js +270 -96
  20. package/lib/daemon/teleportation-daemon.js +709 -126
  21. package/lib/daemon/timeline-analyzer.js +215 -0
  22. package/lib/daemon/transcript-ingestion.js +696 -0
  23. package/lib/daemon/utils.js +91 -0
  24. package/lib/install/installer.js +184 -20
  25. package/lib/install/uhr-installer.js +136 -0
  26. package/lib/remote/providers/base-provider.js +46 -0
  27. package/lib/remote/providers/daytona-provider.js +58 -0
  28. package/lib/remote/providers/provider-factory.js +90 -19
  29. package/lib/remote/providers/sprites-provider.js +711 -0
  30. package/lib/teleport/exporters/claude-exporter.js +302 -0
  31. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  32. package/lib/teleport/exporters/index.js +93 -0
  33. package/lib/teleport/exporters/interface.js +153 -0
  34. package/lib/teleport/fork-tracker.js +415 -0
  35. package/lib/teleport/git-committer.js +337 -0
  36. package/lib/teleport/index.js +48 -0
  37. package/lib/teleport/manager.js +620 -0
  38. package/lib/teleport/session-capture.js +282 -0
  39. package/package.json +6 -2
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. package/lib/daemon/pid-manager.js +0 -183
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Session Capture
3
+ *
4
+ * Captures the current state of a local coding session for teleportation.
5
+ * Captures git state, uncommitted changes, and session metadata.
6
+ *
7
+ * @module lib/teleport/session-capture
8
+ */
9
+
10
+ import { exec } from 'child_process';
11
+ import { promisify } from 'util';
12
+ import { readFile, stat } from 'fs/promises';
13
+ import { join, basename } from 'path';
14
+ import { homedir } from 'os';
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ /**
19
+ * Session state structure
20
+ * @typedef {Object} SessionState
21
+ * @property {string} sessionId - Teleportation session ID
22
+ * @property {GitState} git - Git repository state
23
+ * @property {string} [patch] - Base64-encoded uncommitted changes
24
+ * @property {string} cwd - Current working directory
25
+ * @property {Object} meta - Session metadata
26
+ */
27
+
28
+ /**
29
+ * Git state structure
30
+ * @typedef {Object} GitState
31
+ * @property {string} repoUrl - Git remote URL (origin)
32
+ * @property {string} branch - Current branch name
33
+ * @property {string} commitSha - Current commit SHA
34
+ * @property {boolean} isDirty - Has uncommitted changes
35
+ * @property {boolean} hasUntracked - Has untracked files
36
+ * @property {string[]} stagedFiles - List of staged files
37
+ * @property {string[]} modifiedFiles - List of modified files
38
+ */
39
+
40
+ /**
41
+ * Capture the current session state for teleportation.
42
+ *
43
+ * @param {string} sessionId - Session identifier
44
+ * @param {Object} [options={}] - Capture options
45
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
46
+ * @param {boolean} [options.includeUntracked=false] - Include untracked files in patch
47
+ * @param {string} [options.claudeSessionId] - Claude session ID for transcript lookup
48
+ * @returns {Promise<SessionState>} Captured session state
49
+ * @throws {Error} If not in a git repository or capture fails
50
+ */
51
+ export async function captureSessionState(sessionId, options = {}) {
52
+ const cwd = options.cwd || process.cwd();
53
+
54
+ // Verify we're in a git repository
55
+ const isGitRepo = await isGitRepository(cwd);
56
+ if (!isGitRepo) {
57
+ throw new Error(`Not a git repository: ${cwd}`);
58
+ }
59
+
60
+ // Capture git state
61
+ const git = await captureGitState(cwd);
62
+
63
+ // Capture uncommitted changes as a patch
64
+ let patch = null;
65
+ if (git.isDirty || (options.includeUntracked && git.hasUntracked)) {
66
+ patch = await captureUncommittedChanges(cwd, options.includeUntracked);
67
+ }
68
+
69
+ // Build session state
70
+ const state = {
71
+ sessionId,
72
+ git,
73
+ patch,
74
+ cwd,
75
+ meta: {
76
+ capturedAt: new Date().toISOString(),
77
+ hostname: process.env.HOSTNAME || 'unknown',
78
+ platform: process.platform,
79
+ nodeVersion: process.version,
80
+ },
81
+ };
82
+
83
+ // Add Claude session info if available
84
+ if (options.claudeSessionId) {
85
+ state.meta.claudeSessionId = options.claudeSessionId;
86
+ }
87
+
88
+ return state;
89
+ }
90
+
91
+ /**
92
+ * Check if a directory is a git repository.
93
+ *
94
+ * @param {string} cwd - Directory to check
95
+ * @returns {Promise<boolean>} True if git repository
96
+ */
97
+ export async function isGitRepository(cwd) {
98
+ try {
99
+ await execAsync('git rev-parse --is-inside-work-tree', { cwd });
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Capture git repository state.
108
+ *
109
+ * @param {string} cwd - Repository directory
110
+ * @returns {Promise<GitState>} Git state
111
+ * @throws {Error} If git commands fail
112
+ */
113
+ export async function captureGitState(cwd) {
114
+ // Get remote URL
115
+ let repoUrl;
116
+ try {
117
+ const { stdout } = await execAsync('git remote get-url origin', { cwd });
118
+ repoUrl = stdout.trim();
119
+ } catch {
120
+ throw new Error('No git remote "origin" configured');
121
+ }
122
+
123
+ // Get current branch
124
+ let branch;
125
+ try {
126
+ const { stdout } = await execAsync('git symbolic-ref --short HEAD', { cwd });
127
+ branch = stdout.trim();
128
+ } catch {
129
+ // Might be in detached HEAD state
130
+ try {
131
+ const { stdout } = await execAsync('git rev-parse --short HEAD', { cwd });
132
+ throw new Error(`Detached HEAD state at ${stdout.trim()}. Please checkout a branch before teleporting.`);
133
+ } catch (e) {
134
+ if (e.message.includes('Detached HEAD')) throw e;
135
+ throw new Error('Unable to determine current branch');
136
+ }
137
+ }
138
+
139
+ // Get current commit SHA
140
+ const { stdout: commitSha } = await execAsync('git rev-parse HEAD', { cwd });
141
+
142
+ // Check for uncommitted changes
143
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd });
144
+ const statusLines = statusOutput.trim().split('\n').filter(Boolean);
145
+
146
+ const stagedFiles = [];
147
+ const modifiedFiles = [];
148
+ const untrackedFiles = [];
149
+
150
+ for (const line of statusLines) {
151
+ const index = line[0];
152
+ const workTree = line[1];
153
+ const file = line.slice(3);
154
+
155
+ if (index !== ' ' && index !== '?') {
156
+ stagedFiles.push(file);
157
+ }
158
+ if (workTree === 'M' || workTree === 'D') {
159
+ modifiedFiles.push(file);
160
+ }
161
+ if (index === '?' && workTree === '?') {
162
+ untrackedFiles.push(file);
163
+ }
164
+ }
165
+
166
+ return {
167
+ repoUrl,
168
+ branch,
169
+ commitSha: commitSha.trim(),
170
+ isDirty: stagedFiles.length > 0 || modifiedFiles.length > 0,
171
+ hasUntracked: untrackedFiles.length > 0,
172
+ stagedFiles,
173
+ modifiedFiles,
174
+ untrackedFiles,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Capture uncommitted changes as a base64-encoded patch.
180
+ *
181
+ * @param {string} cwd - Repository directory
182
+ * @param {boolean} [includeUntracked=false] - Include untracked files
183
+ * @returns {Promise<string|null>} Base64-encoded patch or null if no changes
184
+ */
185
+ export async function captureUncommittedChanges(cwd, includeUntracked = false) {
186
+ let patch = '';
187
+
188
+ // Get staged changes
189
+ try {
190
+ const { stdout: staged } = await execAsync('git diff --cached', { cwd });
191
+ patch += staged;
192
+ } catch {
193
+ // No staged changes
194
+ }
195
+
196
+ // Get unstaged changes
197
+ try {
198
+ const { stdout: unstaged } = await execAsync('git diff', { cwd });
199
+ patch += unstaged;
200
+ } catch {
201
+ // No unstaged changes
202
+ }
203
+
204
+ // Optionally include untracked files
205
+ if (includeUntracked) {
206
+ try {
207
+ // Get list of untracked files
208
+ const { stdout: untrackedList } = await execAsync(
209
+ 'git ls-files --others --exclude-standard',
210
+ { cwd }
211
+ );
212
+ const untrackedFiles = untrackedList.trim().split('\n').filter(Boolean);
213
+
214
+ // Create patches for untracked files (as new file diffs)
215
+ for (const file of untrackedFiles) {
216
+ try {
217
+ const content = await readFile(join(cwd, file), 'utf-8');
218
+ const lines = content.split('\n');
219
+ patch += `diff --git a/${file} b/${file}\n`;
220
+ patch += `new file mode 100644\n`;
221
+ patch += `--- /dev/null\n`;
222
+ patch += `+++ b/${file}\n`;
223
+ patch += `@@ -0,0 +1,${lines.length} @@\n`;
224
+ patch += lines.map(l => `+${l}`).join('\n') + '\n';
225
+ } catch {
226
+ // Skip files that can't be read
227
+ }
228
+ }
229
+ } catch {
230
+ // No untracked files
231
+ }
232
+ }
233
+
234
+ if (!patch.trim()) {
235
+ return null;
236
+ }
237
+
238
+ // Encode as base64 for safe transport
239
+ return Buffer.from(patch).toString('base64');
240
+ }
241
+
242
+ /**
243
+ * Find Claude session transcript path.
244
+ *
245
+ * @param {string} claudeSessionId - Claude session ID (UUID)
246
+ * @param {string} [projectSlug] - Project slug (defaults to cwd-based slug)
247
+ * @returns {Promise<string|null>} Path to transcript file or null if not found
248
+ */
249
+ export async function findClaudeTranscript(claudeSessionId, projectSlug) {
250
+ const home = homedir();
251
+ const claudeDir = join(home, '.claude', 'projects');
252
+
253
+ // If project slug provided, check that directly
254
+ if (projectSlug) {
255
+ const transcriptPath = join(claudeDir, projectSlug, `${claudeSessionId}.jsonl`);
256
+ try {
257
+ await stat(transcriptPath);
258
+ return transcriptPath;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ // Otherwise, try to find by session ID across all projects
265
+ // This is a fallback - we'd need to search directories
266
+ return null;
267
+ }
268
+
269
+ /**
270
+ * Get the project slug for the current directory.
271
+ * Claude Code uses hyphenated paths like "-Users-kefentse-dev-project".
272
+ *
273
+ * @param {string} cwd - Current working directory
274
+ * @returns {string} Project slug
275
+ */
276
+ export function getProjectSlug(cwd) {
277
+ // Claude Code converts path to slug by replacing / with -
278
+ // e.g., /Users/kefentse/dev/project -> -Users-kefentse-dev-project
279
+ return cwd.replace(/\//g, '-');
280
+ }
281
+
282
+ export default captureSessionState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teleportation-cli",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "description": "Remote approval system for Claude Code - approve AI coding changes from your phone",
5
5
  "type": "module",
6
6
  "main": "teleportation-cli.cjs",
@@ -38,6 +38,8 @@
38
38
  "test:e2e": "bun --bun vitest run tests/e2e",
39
39
  "test:unit": "bun --bun vitest run tests/unit",
40
40
  "test:integration": "bun --bun vitest run tests/integration",
41
+ "test:local": "RELAY_URL=http://localhost:3030 bun --bun vitest run tests/integration",
42
+ "test:prod": "RELAY_URL=https://api.teleportation.dev bun --bun vitest run tests/integration",
41
43
  "test:production": "bun --bun vitest run tests/production",
42
44
  "dev:relay": "cd relay && bun run dev",
43
45
  "dev:mobile": "cd mobile-ui && bun run dev",
@@ -64,6 +66,8 @@
64
66
  "vitest": "^4.0.9"
65
67
  },
66
68
  "dependencies": {
67
- "dotenv": "^17.2.3"
69
+ "@derivativelabs/agent-process": "github:dundas/agent-process",
70
+ "dotenv": "^17.2.3",
71
+ "uhr": "github:dundas/uhr"
68
72
  }
69
73
  }