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.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +6 -2
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- 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
|
|
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
|
-
"
|
|
69
|
+
"@derivativelabs/agent-process": "github:dundas/agent-process",
|
|
70
|
+
"dotenv": "^17.2.3",
|
|
71
|
+
"uhr": "github:dundas/uhr"
|
|
68
72
|
}
|
|
69
73
|
}
|