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
|
@@ -4,26 +4,33 @@
|
|
|
4
4
|
* Teleportation Daemon
|
|
5
5
|
*
|
|
6
6
|
* Persistent background service that:
|
|
7
|
-
* - Polls relay API for approved tool requests
|
|
8
|
-
* -
|
|
7
|
+
* - Polls relay API for approved tool requests and inbox messages
|
|
8
|
+
* - Routes all remote messages to Claude Code via the machine coder interface
|
|
9
9
|
* - Executes approved tools asynchronously when user is away
|
|
10
10
|
* - Maintains session registry and approval queue
|
|
11
11
|
* - Provides HTTP server for hook communication
|
|
12
12
|
*
|
|
13
13
|
* SECURITY ARCHITECTURE:
|
|
14
14
|
* ----------------------
|
|
15
|
-
*
|
|
16
|
-
* Claude CLI's built-in security controls. This is an intentional architectural decision
|
|
17
|
-
* to enable remote approval/execution, but requires defense-in-depth measures:
|
|
15
|
+
* Remote message execution is delegated to Claude Code's security model:
|
|
18
16
|
*
|
|
19
|
-
* 1.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* 1. CLAUDE CODE PERMISSIONS: All tool calls go through Claude Code's permission system
|
|
18
|
+
* - When hooks are active, tool approvals are routed via Teleportation Relay
|
|
19
|
+
* - When using --dangerously-skip-permissions, Claude auto-approves tool calls
|
|
20
|
+
* 2. SESSION VALIDATION: Each execution validates the session is still active via Relay API
|
|
21
|
+
* 3. APPROVAL CONTEXT: All executions require an approvalContext (inbox_message, approval_queue)
|
|
22
|
+
* for audit trail
|
|
23
|
+
* 4. MACHINE CODER INTERFACE: Unified interface supports Claude Code, Gemini CLI, and future backends
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* LEGACY SHELL EXECUTION:
|
|
26
|
+
* The daemon also supports direct shell command execution via executeCommand() for specific
|
|
27
|
+
* use cases like approval queue processing. This path uses:
|
|
28
|
+
* - COMMAND WHITELIST: Only pre-approved command prefixes (see ALLOWED_COMMAND_PREFIXES)
|
|
29
|
+
* - SHELL INJECTION BLOCKING: Commands with metacharacters (;|&`$() etc.) are rejected
|
|
30
|
+
*
|
|
31
|
+
* For production deployments, consider:
|
|
32
|
+
* - Using --dangerously-skip-permissions only in sandboxed environments
|
|
33
|
+
* - Implementing rate limiting on the relay API
|
|
27
34
|
* - Enabling audit logging by setting DEBUG=1
|
|
28
35
|
*/
|
|
29
36
|
|
|
@@ -54,6 +61,11 @@ const CLAUDE_CLI = process.env.CLAUDE_CLI_PATH || 'claude'; // Configurable Clau
|
|
|
54
61
|
const ALLOW_ALL_COMMANDS = process.env.TELEPORTATION_DAEMON_ALLOW_ALL_COMMANDS === 'true';
|
|
55
62
|
const HEARTBEAT_INTERVAL_MS = parseInt(process.env.DAEMON_HEARTBEAT_INTERVAL_MS || '30000', 10); // 30 sec default
|
|
56
63
|
|
|
64
|
+
// Message routing configuration
|
|
65
|
+
// REQUIRE_COMMAND_WHITELIST: If true, use legacy shell execution with command whitelist
|
|
66
|
+
// Default: false (all messages route to Claude Code)
|
|
67
|
+
const REQUIRE_COMMAND_WHITELIST = process.env.TELEPORTATION_REQUIRE_COMMAND_WHITELIST === 'true';
|
|
68
|
+
|
|
57
69
|
// Machine coder configuration
|
|
58
70
|
// PREFERRED_CODER: 'claude-code' | 'gemini-cli' | 'auto' (default: auto)
|
|
59
71
|
// 'auto' will use Claude Code if available, otherwise Gemini CLI
|
|
@@ -68,6 +80,10 @@ const ROUTER_ENABLED = process.env.TELEPORTATION_ROUTER_ENABLED !== 'false' && !
|
|
|
68
80
|
const ROUTER_VERBOSE = process.env.TELEPORTATION_ROUTER_VERBOSE === 'true';
|
|
69
81
|
const ROUTER_MAX_ESCALATIONS = parseInt(process.env.TELEPORTATION_ROUTER_MAX_ESCALATIONS || '2', 10);
|
|
70
82
|
|
|
83
|
+
// Test helper: allows mocking executeWithMachineCoder in tests
|
|
84
|
+
// In production, this just holds null and the real function is called
|
|
85
|
+
const _executeWithMachineCoderRef = { fn: null };
|
|
86
|
+
|
|
71
87
|
// Lazy-initialized router instance
|
|
72
88
|
let _router = null;
|
|
73
89
|
function getRouter() {
|
|
@@ -184,8 +200,17 @@ const MAX_EXECUTIONS = 1000; // Maximum executions to keep in memory (LRU cache)
|
|
|
184
200
|
// Maximum output size to prevent memory issues
|
|
185
201
|
const MAX_OUTPUT_SIZE = 100_000; // 100KB
|
|
186
202
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
203
|
+
// LEGACY SHELL EXECUTION:
|
|
204
|
+
// These constants and functions are kept for:
|
|
205
|
+
// 1. Whitelist fallback mode (TELEPORTATION_REQUIRE_COMMAND_WHITELIST=true)
|
|
206
|
+
// 2. Direct shell execution via approval queue processing
|
|
207
|
+
// 3. Backward compatibility with existing tests
|
|
208
|
+
//
|
|
209
|
+
// For inbox messages, all prompts are now routed to Claude Code by default.
|
|
210
|
+
// See handleInboxMessage() for the current flow.
|
|
211
|
+
|
|
212
|
+
// Command whitelist for legacy shell execution (security: prevents arbitrary command execution)
|
|
213
|
+
// Only commands starting with these prefixes are allowed when TELEPORTATION_REQUIRE_COMMAND_WHITELIST=true
|
|
189
214
|
const ALLOWED_COMMAND_PREFIXES = [
|
|
190
215
|
'git ', // Git operations
|
|
191
216
|
'npm ', // NPM package management
|
|
@@ -253,7 +278,15 @@ function sanitizeCommand(command) {
|
|
|
253
278
|
}
|
|
254
279
|
|
|
255
280
|
/**
|
|
256
|
-
* Check if a command is allowed based on the whitelist
|
|
281
|
+
* LEGACY: Check if a command is allowed based on the whitelist
|
|
282
|
+
*
|
|
283
|
+
* This function is kept for:
|
|
284
|
+
* - Whitelist fallback mode (TELEPORTATION_REQUIRE_COMMAND_WHITELIST=true)
|
|
285
|
+
* - executeCommand() which is used by approval queue processing
|
|
286
|
+
* - Backward compatibility with existing tests
|
|
287
|
+
*
|
|
288
|
+
* For inbox messages, all prompts are now routed to Claude Code by default.
|
|
289
|
+
*
|
|
257
290
|
* @param {string} command - The command to validate
|
|
258
291
|
* @returns {{ allowed: boolean, reason?: string }}
|
|
259
292
|
*/
|
|
@@ -685,9 +718,17 @@ async function checkIdleTimeout() {
|
|
|
685
718
|
}
|
|
686
719
|
|
|
687
720
|
/**
|
|
688
|
-
* Execute a shell command in the session's working directory
|
|
721
|
+
* LEGACY: Execute a shell command in the session's working directory
|
|
689
722
|
* Returns { success, stdout, stderr, exit_code, error }
|
|
690
723
|
*
|
|
724
|
+
* This function is kept for:
|
|
725
|
+
* - Approval queue processing (executing approved tool calls)
|
|
726
|
+
* - Whitelist fallback mode (TELEPORTATION_REQUIRE_COMMAND_WHITELIST=true)
|
|
727
|
+
* - Backward compatibility with existing tests
|
|
728
|
+
*
|
|
729
|
+
* For inbox messages, all prompts are now routed to Claude Code by default.
|
|
730
|
+
* See handleInboxMessage() and executeWithMachineCoder() for the current flow.
|
|
731
|
+
*
|
|
691
732
|
* Security: Commands must be in the ALLOWED_COMMAND_PREFIXES whitelist
|
|
692
733
|
*/
|
|
693
734
|
async function executeCommand(session_id, command) {
|
|
@@ -755,7 +796,18 @@ async function handleInboxMessage(session_id, message) {
|
|
|
755
796
|
// For command messages, execute the command and post result back to the main agent inbox
|
|
756
797
|
if (meta.type === 'command') {
|
|
757
798
|
const replyAgentId = meta.reply_agent_id || 'main';
|
|
758
|
-
const commandText = message.text || '';
|
|
799
|
+
const commandText = (message.text || '').trim();
|
|
800
|
+
|
|
801
|
+
// Validate non-empty message before processing
|
|
802
|
+
if (!commandText) {
|
|
803
|
+
console.warn(`[daemon] Received empty message ${message.id}, skipping execution`);
|
|
804
|
+
// Still acknowledge the message to prevent re-delivery
|
|
805
|
+
await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
806
|
+
method: 'POST',
|
|
807
|
+
headers: { 'Authorization': `Bearer ${RELAY_API_KEY}` }
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
759
811
|
|
|
760
812
|
// Invalidate pending approvals BEFORE executing new command
|
|
761
813
|
// This prevents race conditions where stale approvals could be acted upon
|
|
@@ -783,39 +835,57 @@ async function handleInboxMessage(session_id, message) {
|
|
|
783
835
|
// Continue with execution - this is not critical
|
|
784
836
|
}
|
|
785
837
|
|
|
786
|
-
//
|
|
787
|
-
// 1. Check if it's a valid whitelisted shell command
|
|
788
|
-
const validation = isCommandAllowed(commandText);
|
|
838
|
+
// Route message to Claude Code (default) or use legacy shell execution (with feature flag)
|
|
789
839
|
let executionResult;
|
|
790
|
-
let executionType
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
840
|
+
let executionType;
|
|
841
|
+
|
|
842
|
+
// Feature flag: TELEPORTATION_REQUIRE_COMMAND_WHITELIST enables legacy whitelist-based execution
|
|
843
|
+
if (REQUIRE_COMMAND_WHITELIST) {
|
|
844
|
+
// Legacy mode: check if command is in whitelist, execute via shell if allowed
|
|
845
|
+
const validation = isCommandAllowed(commandText);
|
|
846
|
+
if (validation.allowed) {
|
|
847
|
+
executionType = 'shell';
|
|
848
|
+
console.log(`[daemon] Legacy shell execution: ${commandText.substring(0, 100)}`);
|
|
849
|
+
executionResult = await executeCommand(session_id, commandText);
|
|
850
|
+
} else {
|
|
851
|
+
// Command not in whitelist - reject in legacy mode
|
|
852
|
+
console.log(`[daemon] Command rejected by whitelist: ${commandText.substring(0, 100)}`);
|
|
853
|
+
executionResult = {
|
|
854
|
+
success: false,
|
|
855
|
+
exit_code: -1,
|
|
856
|
+
stdout: '',
|
|
857
|
+
stderr: '',
|
|
858
|
+
error: validation.reason
|
|
859
|
+
};
|
|
860
|
+
executionType = 'rejected';
|
|
861
|
+
}
|
|
796
862
|
} else {
|
|
797
|
-
//
|
|
798
|
-
console.log(`[daemon] Command not in whitelist, handing off to machine coder: ${commandText}`);
|
|
863
|
+
// Default mode: route all messages to Claude Code
|
|
799
864
|
executionType = 'agent';
|
|
800
|
-
|
|
801
|
-
|
|
865
|
+
console.log(`[daemon] Routing message to Claude Code: ${commandText.substring(0, 100)}`);
|
|
866
|
+
|
|
867
|
+
// Stream output callback for message execution
|
|
802
868
|
const onOutput = createStreamingCallback(session_id, message.id, {
|
|
803
869
|
message_id: message.id
|
|
804
870
|
});
|
|
805
|
-
|
|
871
|
+
|
|
806
872
|
// Use the unified machine coder interface
|
|
807
873
|
// This supports Claude Code, Gemini CLI, and future backends
|
|
874
|
+
// Note: _executeWithMachineCoderRef.fn is used for test mocking
|
|
808
875
|
try {
|
|
809
|
-
|
|
876
|
+
const executeFn = _executeWithMachineCoderRef.fn || executeWithMachineCoder;
|
|
877
|
+
executionResult = await executeFn(session_id, commandText, {
|
|
810
878
|
onOutput,
|
|
811
879
|
approvalContext: { type: 'inbox_message', id: message.id },
|
|
812
880
|
});
|
|
813
|
-
|
|
881
|
+
|
|
814
882
|
// Track which coder was used
|
|
815
883
|
if (executionResult.coder_used) {
|
|
816
884
|
console.log(`[daemon] Executed via ${executionResult.coder_used}`);
|
|
817
885
|
}
|
|
818
886
|
} catch (error) {
|
|
887
|
+
const preview = commandText.substring(0, 50);
|
|
888
|
+
console.error(`[daemon] Machine coder execution failed for "${preview}...":`, error.message);
|
|
819
889
|
executionResult = {
|
|
820
890
|
success: false,
|
|
821
891
|
exit_code: -1,
|
|
@@ -838,13 +908,17 @@ async function handleInboxMessage(session_id, message) {
|
|
|
838
908
|
);
|
|
839
909
|
|
|
840
910
|
// Build result message with execution details
|
|
911
|
+
// Note: executeWithMachineCoder returns 'output', legacy executeCommand returns 'stdout'
|
|
912
|
+
// Using ?? to handle empty string correctly (|| would skip empty string)
|
|
841
913
|
let resultText = '';
|
|
842
914
|
if (executionResult.success) {
|
|
843
|
-
const
|
|
844
|
-
resultText =
|
|
915
|
+
const resultOutput = executionResult.output ?? executionResult.stdout ?? '';
|
|
916
|
+
resultText = `Claude completed your request:\n\n${resultOutput}`;
|
|
845
917
|
} else {
|
|
846
|
-
|
|
847
|
-
|
|
918
|
+
resultText = `Claude failed to complete request:\n\nError: ${executionResult.error}`;
|
|
919
|
+
if (executionResult.stderr) {
|
|
920
|
+
resultText += `\n\nDetails:\n${executionResult.stderr}`;
|
|
921
|
+
}
|
|
848
922
|
}
|
|
849
923
|
|
|
850
924
|
try {
|
|
@@ -1525,11 +1599,19 @@ async function executeWithMachineCoder(session_id, prompt, options = {}) {
|
|
|
1525
1599
|
|
|
1526
1600
|
// Execute via the coder
|
|
1527
1601
|
const startedAt = Date.now();
|
|
1528
|
-
|
|
1602
|
+
|
|
1529
1603
|
try {
|
|
1530
|
-
//
|
|
1604
|
+
// Always use execute() for remote inbox messages since our claude_session_id
|
|
1605
|
+
// is actually the teleportation UUID, not a real Claude Code session ID.
|
|
1606
|
+
// Passing UUID to claude --resume causes it to fail.
|
|
1607
|
+
// Use UUID v4 regex pattern for robust detection instead of fragile hyphen check.
|
|
1608
|
+
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1531
1609
|
let result;
|
|
1532
|
-
|
|
1610
|
+
const isValidClaudeSession = session.claude_session_id &&
|
|
1611
|
+
session.claude_session_id !== session.session_id &&
|
|
1612
|
+
!UUID_V4_PATTERN.test(session.claude_session_id);
|
|
1613
|
+
|
|
1614
|
+
if (coder.name === 'claude-code' && isValidClaudeSession) {
|
|
1533
1615
|
result = await coder.resume(session.claude_session_id, prompt, execOptions);
|
|
1534
1616
|
} else {
|
|
1535
1617
|
result = await coder.execute(execOptions);
|
|
@@ -1975,7 +2057,14 @@ const __test = {
|
|
|
1975
2057
|
_setLastSessionActivityAt: (value) => {
|
|
1976
2058
|
lastSessionActivityAt = value;
|
|
1977
2059
|
},
|
|
1978
|
-
_getSessionsMap: () => sessions
|
|
2060
|
+
_getSessionsMap: () => sessions,
|
|
2061
|
+
// Test helper: inject mock for executeWithMachineCoder
|
|
2062
|
+
_setExecuteWithMachineCoder: (mockFn) => {
|
|
2063
|
+
_executeWithMachineCoderRef.fn = mockFn;
|
|
2064
|
+
},
|
|
2065
|
+
_resetExecuteWithMachineCoder: () => {
|
|
2066
|
+
_executeWithMachineCoderRef.fn = null;
|
|
2067
|
+
}
|
|
1979
2068
|
};
|
|
1980
2069
|
|
|
1981
2070
|
export {
|
|
@@ -2000,6 +2089,7 @@ export {
|
|
|
2000
2089
|
sendStreamingMessage,
|
|
2001
2090
|
MAX_EXECUTIONS,
|
|
2002
2091
|
PREFERRED_CODER, // New: machine coder preference
|
|
2092
|
+
REQUIRE_COMMAND_WHITELIST, // Feature flag for legacy whitelist mode
|
|
2003
2093
|
// Router integration for cost-aware LLM calls
|
|
2004
2094
|
routedCompletion,
|
|
2005
2095
|
classifyTaskTier,
|
package/lib/install/installer.js
CHANGED
|
@@ -40,7 +40,7 @@ function getProjectSettings() {
|
|
|
40
40
|
|
|
41
41
|
// Claude Code reads settings from ~/.claude/settings.json (user-level)
|
|
42
42
|
function getUserSettings() {
|
|
43
|
-
return join(HOME_DIR, '.claude', 'settings.json');
|
|
43
|
+
return process.env.TEST_USER_SETTINGS || join(HOME_DIR, '.claude', 'settings.json');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -133,7 +133,11 @@ export async function verifyHooks(sourceHooksDir) {
|
|
|
133
133
|
try {
|
|
134
134
|
await stat(hookPath);
|
|
135
135
|
// Set executable permissions (755)
|
|
136
|
-
|
|
136
|
+
try {
|
|
137
|
+
await chmod(hookPath, 0o755);
|
|
138
|
+
} catch (_) {
|
|
139
|
+
// Ignore chmod errors
|
|
140
|
+
}
|
|
137
141
|
found.push(hook);
|
|
138
142
|
} catch (e) {
|
|
139
143
|
if (e.code === 'ENOENT') {
|
|
@@ -154,6 +158,14 @@ export async function installHooks(sourceHooksDir) {
|
|
|
154
158
|
const destHooksDir = getProjectHooksDir();
|
|
155
159
|
const copyFailed = [];
|
|
156
160
|
|
|
161
|
+
try {
|
|
162
|
+
await mkdir(destHooksDir, { recursive: true });
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (e.code !== 'EEXIST') {
|
|
165
|
+
throw e;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
// Copy hooks from source to destination if they're different
|
|
158
170
|
if (sourceHooksDir !== destHooksDir) {
|
|
159
171
|
for (const hook of result.found) {
|
|
@@ -298,7 +310,8 @@ export async function installLibFiles() {
|
|
|
298
310
|
const libFiles = [
|
|
299
311
|
{ subdir: 'auth', files: ['credentials.js', 'api-key.js'] },
|
|
300
312
|
{ subdir: 'session', files: ['metadata.js'] },
|
|
301
|
-
{ subdir: 'config', files: ['manager.js'] }
|
|
313
|
+
{ subdir: 'config', files: ['manager.js'] },
|
|
314
|
+
{ subdir: 'daemon', files: ['lifecycle.js', 'pid-manager.js'] } // Required for heartbeat daemon auto-start
|
|
302
315
|
];
|
|
303
316
|
|
|
304
317
|
const installed = [];
|
|
@@ -425,11 +438,13 @@ export async function createSettings() {
|
|
|
425
438
|
|
|
426
439
|
// IMPORTANT: Also write to user-level settings (~/.claude/settings.json)
|
|
427
440
|
// Claude Code reads from this location, not from the project directory
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
441
|
+
if (!process.env.TEST_SETTINGS) {
|
|
442
|
+
const userSettings = getUserSettings();
|
|
443
|
+
await mkdir(dirname(userSettings), { recursive: true });
|
|
444
|
+
await writeFile(userSettings, JSON.stringify(settings, null, 2));
|
|
445
|
+
}
|
|
431
446
|
|
|
432
|
-
return
|
|
447
|
+
return projectSettings;
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
/**
|