teleportation-cli 1.0.0 → 1.0.2
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/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/stop.mjs +205 -1
- package/.claude/hooks/user_prompt_submit.mjs +111 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/daemon/teleportation-daemon.js +131 -41
- package/lib/install/installer.js +22 -7
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRCreator - GitHub Pull Request Creation Utility
|
|
3
|
+
*
|
|
4
|
+
* Handles PR creation for remote session results using GitHub CLI (gh).
|
|
5
|
+
* Generates properly formatted PR titles, descriptions with metadata,
|
|
6
|
+
* and includes timeline links and conflict warnings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PRCreator class
|
|
19
|
+
*/
|
|
20
|
+
export class PRCreator {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {string} options.repoPath - Path to git repository
|
|
24
|
+
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
25
|
+
*/
|
|
26
|
+
constructor(options) {
|
|
27
|
+
if (!options || !options.repoPath) {
|
|
28
|
+
throw new Error('repoPath is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.repoPath = options.repoPath;
|
|
32
|
+
this.verbose = options.verbose || false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execute gh CLI command (async, non-blocking)
|
|
37
|
+
* @private
|
|
38
|
+
* @param {string} command - gh command (without 'gh' prefix)
|
|
39
|
+
* @returns {Promise<string>} Command output
|
|
40
|
+
* @throws {Error} If command fails
|
|
41
|
+
*/
|
|
42
|
+
async _gh(command) {
|
|
43
|
+
const fullCommand = `gh ${command}`;
|
|
44
|
+
|
|
45
|
+
if (this.verbose) {
|
|
46
|
+
console.log(`[pr-creator] ${fullCommand}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const { stdout, stderr } = await execAsync(fullCommand, {
|
|
51
|
+
cwd: this.repoPath,
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
timeout: 30000, // 30 second timeout
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (stderr && this.verbose) {
|
|
57
|
+
console.warn(`[pr-creator] stderr: ${stderr}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return stdout.trim();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Handle specific gh CLI errors
|
|
63
|
+
if (error.code === 127 || error.message.includes('command not found')) {
|
|
64
|
+
throw new Error('GitHub CLI (gh) is not installed. Install from https://cli.github.com/');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error.message.includes('authentication') || error.message.includes('auth')) {
|
|
68
|
+
throw new Error('GitHub authentication required. Run: gh auth login');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error.message.includes('pull request not found')) {
|
|
72
|
+
const prNumber = command.match(/\d+/)?.[0];
|
|
73
|
+
throw new Error(`Pull request #${prNumber} not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate PR title
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} options
|
|
84
|
+
* @param {string} options.sessionId - Session ID
|
|
85
|
+
* @param {string} [options.task] - Task description
|
|
86
|
+
* @param {boolean} [options.hasConflicts] - Whether PR has conflicts
|
|
87
|
+
* @returns {string} PR title
|
|
88
|
+
*/
|
|
89
|
+
generatePRTitle(options) {
|
|
90
|
+
const { sessionId, task, hasConflicts } = options;
|
|
91
|
+
|
|
92
|
+
let title = `Remote Session Results: ${sessionId}`;
|
|
93
|
+
|
|
94
|
+
if (task) {
|
|
95
|
+
// Truncate task to keep title under 100 chars
|
|
96
|
+
const maxTaskLength = 50;
|
|
97
|
+
const truncatedTask = task.length > maxTaskLength
|
|
98
|
+
? task.substring(0, maxTaskLength) + '...'
|
|
99
|
+
: task;
|
|
100
|
+
|
|
101
|
+
title = `${truncatedTask} (${sessionId})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (hasConflicts) {
|
|
105
|
+
title = `⚠️ ${title} (Conflicts)`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ensure title doesn't exceed GitHub's limit
|
|
109
|
+
return title.substring(0, 100);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format duration in seconds to human-readable string
|
|
114
|
+
*
|
|
115
|
+
* @param {number} seconds - Duration in seconds
|
|
116
|
+
* @returns {string} Formatted duration (e.g., "1h 23m 45s")
|
|
117
|
+
*/
|
|
118
|
+
formatDuration(seconds) {
|
|
119
|
+
if (seconds === 0) return '0s';
|
|
120
|
+
|
|
121
|
+
const hours = Math.floor(seconds / 3600);
|
|
122
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
123
|
+
const secs = seconds % 60;
|
|
124
|
+
|
|
125
|
+
const parts = [];
|
|
126
|
+
|
|
127
|
+
if (hours > 0) {
|
|
128
|
+
parts.push(`${hours}h`);
|
|
129
|
+
// Always show minutes when hours are present
|
|
130
|
+
parts.push(`${minutes}m`);
|
|
131
|
+
} else if (minutes > 0) {
|
|
132
|
+
parts.push(`${minutes}m`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Only show seconds if non-zero OR if no other parts
|
|
136
|
+
if (secs > 0 || parts.length === 0) {
|
|
137
|
+
parts.push(`${secs}s`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return parts.join(' ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate PR body with metadata
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} options
|
|
147
|
+
* @param {string} options.task - Task description
|
|
148
|
+
* @param {string} options.sessionId - Session ID
|
|
149
|
+
* @param {string} [options.timelineUrl] - Timeline URL
|
|
150
|
+
* @param {Object} [options.conflicts] - Conflict information
|
|
151
|
+
* @param {boolean} [options.conflicts.hasConflicts] - Whether conflicts exist
|
|
152
|
+
* @param {string[]} [options.conflicts.conflictFiles] - List of conflicting files
|
|
153
|
+
* @param {Object} [options.metadata] - Session metadata
|
|
154
|
+
* @param {string} [options.metadata.provider] - Cloud provider used
|
|
155
|
+
* @param {number} [options.metadata.duration] - Session duration in seconds
|
|
156
|
+
* @param {string} [options.metadata.machineId] - Machine ID
|
|
157
|
+
* @returns {string} PR body in markdown format
|
|
158
|
+
*/
|
|
159
|
+
generatePRBody(options) {
|
|
160
|
+
const { task, sessionId, timelineUrl, conflicts, metadata = {} } = options;
|
|
161
|
+
|
|
162
|
+
const sections = [];
|
|
163
|
+
|
|
164
|
+
// Task description
|
|
165
|
+
sections.push('## Task\n');
|
|
166
|
+
sections.push(task || 'No task description provided');
|
|
167
|
+
sections.push('\n');
|
|
168
|
+
|
|
169
|
+
// Conflict warning
|
|
170
|
+
if (conflicts?.hasConflicts) {
|
|
171
|
+
sections.push('## ⚠️ Conflicts Detected\n');
|
|
172
|
+
sections.push('This PR contains merge conflicts that must be resolved manually:\n');
|
|
173
|
+
sections.push('');
|
|
174
|
+
|
|
175
|
+
if (conflicts.conflictFiles && conflicts.conflictFiles.length > 0) {
|
|
176
|
+
conflicts.conflictFiles.forEach((file) => {
|
|
177
|
+
sections.push(`- \`${file}\``);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sections.push('\n**Action Required**: Review and resolve conflicts before merging.\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Timeline link
|
|
185
|
+
if (timelineUrl) {
|
|
186
|
+
sections.push('## Session Timeline\n');
|
|
187
|
+
sections.push(`View the complete session timeline: [${sessionId}](${timelineUrl})\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Metadata footer
|
|
191
|
+
sections.push('---\n');
|
|
192
|
+
sections.push('## Session Metadata\n');
|
|
193
|
+
sections.push('');
|
|
194
|
+
sections.push(`- **Session ID**: \`${sessionId}\``);
|
|
195
|
+
|
|
196
|
+
if (metadata.provider) {
|
|
197
|
+
sections.push(`- **Provider**: ${metadata.provider}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (metadata.duration) {
|
|
201
|
+
const duration = this.formatDuration(metadata.duration);
|
|
202
|
+
sections.push(`- **Duration**: ${duration}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (metadata.machineId) {
|
|
206
|
+
sections.push(`- **Machine ID**: \`${metadata.machineId}\``);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
sections.push('');
|
|
210
|
+
sections.push('---');
|
|
211
|
+
sections.push('');
|
|
212
|
+
sections.push('🤖 *Generated by [Teleportation](https://teleportation.dev) - Remote AI Coding*');
|
|
213
|
+
|
|
214
|
+
return sections.join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a pull request
|
|
219
|
+
*
|
|
220
|
+
* @param {Object} options
|
|
221
|
+
* @param {string} options.head - Head branch (remote branch with changes)
|
|
222
|
+
* @param {string} options.base - Base branch (target branch, usually main)
|
|
223
|
+
* @param {string} options.title - PR title
|
|
224
|
+
* @param {string} options.body - PR body (description)
|
|
225
|
+
* @returns {Promise<Object>} PR details
|
|
226
|
+
* @returns {number} return.number - PR number
|
|
227
|
+
* @returns {string} return.url - PR URL
|
|
228
|
+
* @returns {string} return.title - PR title
|
|
229
|
+
* @throws {Error} If PR creation fails
|
|
230
|
+
*/
|
|
231
|
+
async createPR(options) {
|
|
232
|
+
const { head, base, title, body } = options;
|
|
233
|
+
|
|
234
|
+
if (!head || !base || !title || !body) {
|
|
235
|
+
throw new Error('head, base, title, and body are required for PR creation');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Use temp file for body to avoid shell escaping issues (prevents command injection)
|
|
239
|
+
const tempFile = join(tmpdir(), `pr-body-${Date.now()}-${Math.random().toString(36).slice(2)}.md`);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Write body to temp file
|
|
243
|
+
writeFileSync(tempFile, body, { mode: 0o600 });
|
|
244
|
+
|
|
245
|
+
// Escape title (simple string, less risky)
|
|
246
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
247
|
+
|
|
248
|
+
// Use --body-file to avoid command injection via body content
|
|
249
|
+
const command = `pr create --head "${head}" --base "${base}" --title "${escapedTitle}" --body-file "${tempFile}"`;
|
|
250
|
+
|
|
251
|
+
const output = await this._gh(command);
|
|
252
|
+
|
|
253
|
+
// Extract PR URL from output
|
|
254
|
+
// gh CLI returns URL like: https://github.com/user/repo/pull/42
|
|
255
|
+
const prUrl = output.trim();
|
|
256
|
+
const prNumberMatch = prUrl.match(/\/pull\/(\d+)$/);
|
|
257
|
+
|
|
258
|
+
if (!prNumberMatch) {
|
|
259
|
+
throw new Error(`Failed to extract PR number from URL: ${prUrl}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const prNumber = parseInt(prNumberMatch[1], 10);
|
|
263
|
+
|
|
264
|
+
if (this.verbose) {
|
|
265
|
+
console.log(`[pr-creator] Created PR #${prNumber}: ${prUrl}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
number: prNumber,
|
|
270
|
+
url: prUrl,
|
|
271
|
+
title,
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error.message.includes('pull request already exists')) {
|
|
275
|
+
throw error; // Re-throw with original message
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw new Error(`Failed to create pull request: ${error.message}`);
|
|
279
|
+
} finally {
|
|
280
|
+
// Always clean up temp file
|
|
281
|
+
try {
|
|
282
|
+
unlinkSync(tempFile);
|
|
283
|
+
} catch {
|
|
284
|
+
// Ignore cleanup errors
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Update an existing pull request
|
|
291
|
+
*
|
|
292
|
+
* @param {Object} options
|
|
293
|
+
* @param {number} options.prNumber - PR number
|
|
294
|
+
* @param {string} [options.title] - New PR title
|
|
295
|
+
* @param {string} [options.body] - New PR body
|
|
296
|
+
* @returns {Promise<void>}
|
|
297
|
+
* @throws {Error} If PR update fails
|
|
298
|
+
*/
|
|
299
|
+
async updatePR(options) {
|
|
300
|
+
const { prNumber, title, body } = options;
|
|
301
|
+
|
|
302
|
+
if (!prNumber) {
|
|
303
|
+
throw new Error('prNumber is required for PR update');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!title && !body) {
|
|
307
|
+
throw new Error('At least one of title or body is required for PR update');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Use temp file for body to avoid shell escaping issues
|
|
311
|
+
let tempFile = null;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const args = [];
|
|
315
|
+
|
|
316
|
+
if (title) {
|
|
317
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
318
|
+
args.push(`--title "${escapedTitle}"`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (body) {
|
|
322
|
+
// Write body to temp file to avoid command injection
|
|
323
|
+
tempFile = join(tmpdir(), `pr-body-${Date.now()}-${Math.random().toString(36).slice(2)}.md`);
|
|
324
|
+
writeFileSync(tempFile, body, { mode: 0o600 });
|
|
325
|
+
args.push(`--body-file "${tempFile}"`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const command = `pr edit ${prNumber} ${args.join(' ')}`;
|
|
329
|
+
await this._gh(command);
|
|
330
|
+
|
|
331
|
+
if (this.verbose) {
|
|
332
|
+
console.log(`[pr-creator] Updated PR #${prNumber}`);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new Error(`Failed to update pull request: ${error.message}`);
|
|
336
|
+
} finally {
|
|
337
|
+
// Clean up temp file if created
|
|
338
|
+
if (tempFile) {
|
|
339
|
+
try {
|
|
340
|
+
unlinkSync(tempFile);
|
|
341
|
+
} catch {
|
|
342
|
+
// Ignore cleanup errors
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get PR status
|
|
350
|
+
*
|
|
351
|
+
* @param {number} prNumber - PR number
|
|
352
|
+
* @returns {Promise<Object>} PR status
|
|
353
|
+
* @returns {number} return.number - PR number
|
|
354
|
+
* @returns {string} return.state - PR state (OPEN, CLOSED, MERGED)
|
|
355
|
+
* @returns {string} return.title - PR title
|
|
356
|
+
* @returns {string} return.url - PR URL
|
|
357
|
+
* @returns {string} return.mergeable - Mergeable state (MERGEABLE, CONFLICTING, UNKNOWN)
|
|
358
|
+
* @throws {Error} If PR not found or query fails
|
|
359
|
+
*/
|
|
360
|
+
async getPRStatus(prNumber) {
|
|
361
|
+
if (!prNumber) {
|
|
362
|
+
throw new Error('prNumber is required');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const command = `pr view ${prNumber} --json number,state,title,url,mergeable`;
|
|
367
|
+
const output = await this._gh(command);
|
|
368
|
+
|
|
369
|
+
const prData = JSON.parse(output);
|
|
370
|
+
|
|
371
|
+
if (this.verbose) {
|
|
372
|
+
console.log(`[pr-creator] PR #${prNumber} status: ${prData.state}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return prData;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw new Error(`Failed to get PR status: ${error.message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export default PRCreator;
|