teleportation-cli 1.1.3 → 1.1.5
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/config-loader.mjs +88 -34
- package/.claude/hooks/permission_request.mjs +98 -37
- package/.claude/hooks/post_tool_use.mjs +246 -28
- package/.claude/hooks/pre_tool_use.mjs +40 -17
- package/.claude/hooks/session-register.mjs +8 -4
- package/README.md +7 -0
- package/lib/auth/api-key.js +12 -0
- package/lib/auth/token-refresh.js +286 -0
- package/lib/config/manager.js +0 -1
- package/lib/daemon/pid-manager.js +34 -11
- package/lib/daemon/response-classifier.js +326 -0
- package/lib/daemon/task-executor.js +1061 -0
- package/lib/daemon/teleportation-daemon.js +218 -13
- package/lib/utils/log-sanitizer.js +111 -0
- package/lib/utils/logger.js +74 -127
- package/package.json +6 -3
- package/teleportation-cli.cjs +173 -9
|
@@ -41,12 +41,31 @@ import { promisify } from 'util';
|
|
|
41
41
|
import { acquirePidLock, releasePidLock } from './pid-manager.js';
|
|
42
42
|
import { setupSignalHandlers } from './lifecycle.js';
|
|
43
43
|
|
|
44
|
+
// Task executor for autonomous tasks (PRD-0016)
|
|
45
|
+
import {
|
|
46
|
+
startTask,
|
|
47
|
+
stopTask,
|
|
48
|
+
stopAllTasks,
|
|
49
|
+
cleanupStaleLocks,
|
|
50
|
+
cleanupOrphanedProcesses,
|
|
51
|
+
removeTaskLock,
|
|
52
|
+
pauseTask,
|
|
53
|
+
resumeTask,
|
|
54
|
+
getTaskSession,
|
|
55
|
+
answerTaskQuestion,
|
|
56
|
+
increaseBudget
|
|
57
|
+
} from './task-executor.js';
|
|
58
|
+
|
|
44
59
|
// Machine coder adapters for multi-provider support
|
|
45
60
|
import { getAvailableCoders, getBestCoder } from '../machine-coders/index.js';
|
|
46
61
|
|
|
47
62
|
// Cost-aware model router for LLM calls
|
|
48
63
|
import { createRouter, classifyTask } from '../router/index.js';
|
|
49
64
|
|
|
65
|
+
// Logging utilities with level control and sanitization
|
|
66
|
+
import { logError, logWarn, logInfo, logDebug, logVerbose } from '../utils/logger.js';
|
|
67
|
+
import { sanitizeForLog, truncateForLog, prepareForLog } from '../utils/log-sanitizer.js';
|
|
68
|
+
|
|
50
69
|
const execAsync = promisify(exec);
|
|
51
70
|
console.log('[daemon] Starting up...');
|
|
52
71
|
|
|
@@ -180,9 +199,54 @@ const OUTPUT_PREVIEW_LONG = 1000; // Full output displays
|
|
|
180
199
|
const heartbeatState = new Map();
|
|
181
200
|
let lastHeartbeatTime = 0;
|
|
182
201
|
|
|
183
|
-
//
|
|
202
|
+
// In-memory registry of active teleportation sessions handled by this daemon
|
|
184
203
|
const sessions = new Map();
|
|
185
204
|
|
|
205
|
+
// Constants
|
|
206
|
+
const STARTING_TASK_STALE_MS = 300000; // 5 minutes
|
|
207
|
+
const STARTING_TASK_CLEANUP_INTERVAL_MS = 60000; // 1 minute
|
|
208
|
+
|
|
209
|
+
// Track tasks currently being started to prevent duplicates (taskId -> timestamp)
|
|
210
|
+
const startingTasks = new Map();
|
|
211
|
+
|
|
212
|
+
// Track tasks with active execution loops to prevent recursion
|
|
213
|
+
const activeLoops = new Set();
|
|
214
|
+
|
|
215
|
+
// Session activity tracking for cleanup
|
|
216
|
+
const sessionActivity = new Map(); // sessionId -> lastActivityTimestamp
|
|
217
|
+
|
|
218
|
+
// Periodically clear stale startingTasks (older than 5 minutes)
|
|
219
|
+
setInterval(() => {
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
let cleanedCount = 0;
|
|
222
|
+
for (const [taskId, timestamp] of startingTasks) {
|
|
223
|
+
if (now - timestamp > STARTING_TASK_STALE_MS) {
|
|
224
|
+
startingTasks.delete(taskId);
|
|
225
|
+
cleanedCount++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (cleanedCount > 0) {
|
|
229
|
+
console.log(`[daemon] Routine cleanup: cleared ${cleanedCount} stale starting task entries`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// PRD-0016: Cleanup stale sessions from memory (inactive for > 1 hour)
|
|
233
|
+
let sessionCleanedCount = 0;
|
|
234
|
+
for (const [sessionId, lastActivity] of sessionActivity) {
|
|
235
|
+
if (now - lastActivity > 3600000) {
|
|
236
|
+
sessions.delete(sessionId);
|
|
237
|
+
sessionActivity.delete(sessionId);
|
|
238
|
+
sessionCleanedCount++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (sessionCleanedCount > 0) {
|
|
242
|
+
console.log(`[daemon] Routine cleanup: cleared ${sessionCleanedCount} inactive sessions from memory`);
|
|
243
|
+
}
|
|
244
|
+
}, STARTING_TASK_CLEANUP_INTERVAL_MS);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sync daemon state to relay API
|
|
248
|
+
*/
|
|
249
|
+
|
|
186
250
|
// Approval queue: FIFO queue of pending approvals
|
|
187
251
|
// { approval_id, session_id, tool_name, tool_input, queued_at }
|
|
188
252
|
const approvalQueue = [];
|
|
@@ -810,8 +874,10 @@ async function executeCommand(session_id, command) {
|
|
|
810
874
|
|
|
811
875
|
async function handleInboxMessage(session_id, message) {
|
|
812
876
|
try {
|
|
813
|
-
const
|
|
814
|
-
|
|
877
|
+
const sanitizedPreview = prepareForLog(message.text || '', 200).replace(/\s+/g, ' ');
|
|
878
|
+
logDebug(`[daemon] 📨 Inbox message received for session ${session_id}`);
|
|
879
|
+
logDebug(`[daemon] ↳ Message ID: ${message.id}`);
|
|
880
|
+
logDebug(`[daemon] ↳ Preview: ${sanitizedPreview}`);
|
|
815
881
|
|
|
816
882
|
const meta = message.meta || {};
|
|
817
883
|
|
|
@@ -820,6 +886,11 @@ async function handleInboxMessage(session_id, message) {
|
|
|
820
886
|
const replyAgentId = meta.reply_agent_id || 'main';
|
|
821
887
|
const commandText = (message.text || '').trim();
|
|
822
888
|
|
|
889
|
+
logDebug(`[daemon] 🎯 Command message routing:`);
|
|
890
|
+
logDebug(`[daemon] ↳ Type: ${meta.type}`);
|
|
891
|
+
logDebug(`[daemon] ↳ Target agent (reply to): ${replyAgentId}`);
|
|
892
|
+
logDebug(`[daemon] ↳ Command length: ${commandText.length} chars`);
|
|
893
|
+
|
|
823
894
|
// Validate non-empty message before processing
|
|
824
895
|
if (!commandText) {
|
|
825
896
|
console.warn(`[daemon] Received empty message ${message.id}, skipping execution`);
|
|
@@ -884,7 +955,8 @@ async function handleInboxMessage(session_id, message) {
|
|
|
884
955
|
} else {
|
|
885
956
|
// Default mode: route all messages to Claude Code
|
|
886
957
|
executionType = 'agent';
|
|
887
|
-
|
|
958
|
+
const sanitizedCommand = prepareForLog(commandText, 100);
|
|
959
|
+
logInfo(`[daemon] 🚀 Routing message to Claude Code: ${sanitizedCommand}`);
|
|
888
960
|
|
|
889
961
|
// Stream output callback for message execution
|
|
890
962
|
const onOutput = createStreamingCallback(session_id, message.id, {
|
|
@@ -894,6 +966,9 @@ async function handleInboxMessage(session_id, message) {
|
|
|
894
966
|
// Use the unified machine coder interface
|
|
895
967
|
// This supports Claude Code, Gemini CLI, and future backends
|
|
896
968
|
// Note: _executeWithMachineCoderRef.fn is used for test mocking
|
|
969
|
+
const executionStartTime = Date.now();
|
|
970
|
+
logDebug(`[daemon] ⏱️ Execution started at ${new Date(executionStartTime).toISOString()}`);
|
|
971
|
+
|
|
897
972
|
try {
|
|
898
973
|
const executeFn = _executeWithMachineCoderRef.fn || executeWithMachineCoder;
|
|
899
974
|
executionResult = await executeFn(session_id, commandText, {
|
|
@@ -901,13 +976,26 @@ async function handleInboxMessage(session_id, message) {
|
|
|
901
976
|
approvalContext: { type: 'inbox_message', id: message.id },
|
|
902
977
|
});
|
|
903
978
|
|
|
979
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
980
|
+
const durationSec = (executionDuration / 1000).toFixed(2);
|
|
981
|
+
|
|
904
982
|
// Track which coder was used
|
|
905
983
|
if (executionResult.coder_used) {
|
|
906
|
-
|
|
984
|
+
logInfo(`[daemon] ✅ Executed via ${executionResult.coder_used} in ${durationSec}s`);
|
|
985
|
+
} else {
|
|
986
|
+
logInfo(`[daemon] ✅ Execution completed in ${durationSec}s`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
logDebug(`[daemon] ↳ Success: ${executionResult.success}`);
|
|
990
|
+
logDebug(`[daemon] ↳ Exit code: ${executionResult.exit_code}`);
|
|
991
|
+
if (executionResult.model_used) {
|
|
992
|
+
logDebug(`[daemon] ↳ Model: ${executionResult.model_used}`);
|
|
907
993
|
}
|
|
908
994
|
} catch (error) {
|
|
909
|
-
const
|
|
910
|
-
|
|
995
|
+
const executionDuration = Date.now() - executionStartTime;
|
|
996
|
+
const durationSec = (executionDuration / 1000).toFixed(2);
|
|
997
|
+
const sanitizedPreview = prepareForLog(commandText, 50);
|
|
998
|
+
logError(`[daemon] ❌ Machine coder execution failed for session ${session_id}: "${sanitizedPreview}..." after ${durationSec}s:`, error.message);
|
|
911
999
|
executionResult = {
|
|
912
1000
|
success: false,
|
|
913
1001
|
exit_code: -1,
|
|
@@ -944,6 +1032,10 @@ async function handleInboxMessage(session_id, message) {
|
|
|
944
1032
|
}
|
|
945
1033
|
|
|
946
1034
|
try {
|
|
1035
|
+
logInfo(`[daemon] 📤 Sending result message to agent '${replyAgentId}' for session ${session_id}`);
|
|
1036
|
+
logDebug(`[daemon] ↳ In reply to message: ${message.id}`);
|
|
1037
|
+
logDebug(`[daemon] ↳ Execution ${executionResult.success ? 'succeeded' : 'failed'} (exit code: ${executionResult.exit_code})`);
|
|
1038
|
+
|
|
947
1039
|
const resultResponse = await fetch(`${RELAY_API_URL}/api/messages`, {
|
|
948
1040
|
method: 'POST',
|
|
949
1041
|
headers: {
|
|
@@ -967,22 +1059,33 @@ async function handleInboxMessage(session_id, message) {
|
|
|
967
1059
|
|
|
968
1060
|
if (!resultResponse.ok) {
|
|
969
1061
|
const errorText = await resultResponse.text();
|
|
970
|
-
|
|
1062
|
+
logError(`[daemon] ❌ Failed to post result message for session ${session_id}: HTTP ${resultResponse.status} - ${errorText}`);
|
|
1063
|
+
} else {
|
|
1064
|
+
const responseData = await resultResponse.json();
|
|
1065
|
+
logInfo(`[daemon] ✅ Result message delivered to relay (message_id: ${responseData.id || 'unknown'})`);
|
|
1066
|
+
logDebug(`[daemon] ↳ Agent '${replyAgentId}' should receive result on next poll`);
|
|
971
1067
|
}
|
|
972
1068
|
} catch (sendError) {
|
|
973
|
-
|
|
1069
|
+
logError(`[daemon] ❌ Failed to send result message for session ${session_id}: ${sendError.message}`);
|
|
974
1070
|
}
|
|
975
1071
|
}
|
|
976
1072
|
|
|
977
1073
|
// Acknowledge the message so it is not re-delivered
|
|
978
|
-
|
|
1074
|
+
logDebug(`[daemon] ✓ Acknowledging inbox message ${message.id}`);
|
|
1075
|
+
const ackResponse = await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
|
|
979
1076
|
method: 'POST',
|
|
980
1077
|
headers: {
|
|
981
1078
|
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
982
1079
|
}
|
|
983
1080
|
});
|
|
1081
|
+
|
|
1082
|
+
if (ackResponse.ok) {
|
|
1083
|
+
logDebug(`[daemon] ✅ Message ${message.id} acknowledged successfully`);
|
|
1084
|
+
} else {
|
|
1085
|
+
logError(`[daemon] ❌ Failed to acknowledge message ${message.id} for session ${session_id}: HTTP ${ackResponse.status}`);
|
|
1086
|
+
}
|
|
984
1087
|
} catch (error) {
|
|
985
|
-
|
|
1088
|
+
logError(`[daemon] ❌ Failed to handle inbox message for session ${session_id}: ${error.message}`);
|
|
986
1089
|
}
|
|
987
1090
|
}
|
|
988
1091
|
|
|
@@ -1083,12 +1186,15 @@ async function pollRelayAPI() {
|
|
|
1083
1186
|
try {
|
|
1084
1187
|
// Fetch pending approvals and inbox messages for all registered sessions
|
|
1085
1188
|
const TEST_SESSION_FILTER = process.env.TELEPORTATION_TEST_SESSION_FILTER;
|
|
1086
|
-
for (const [session_id] of sessions) {
|
|
1189
|
+
for (const [session_id, sessionData] of sessions) {
|
|
1087
1190
|
// Optional: Filter sessions for testing (if TEST_SESSION_FILTER env var set)
|
|
1088
1191
|
if (TEST_SESSION_FILTER && !session_id.startsWith(TEST_SESSION_FILTER)) {
|
|
1089
1192
|
continue;
|
|
1090
1193
|
}
|
|
1091
1194
|
console.log(`Polling for session ${session_id}`);
|
|
1195
|
+
|
|
1196
|
+
// Update activity timestamp for cleanup tracking
|
|
1197
|
+
sessionActivity.set(session_id, Date.now());
|
|
1092
1198
|
|
|
1093
1199
|
// 1) Approvals polling (existing behavior)
|
|
1094
1200
|
try {
|
|
@@ -1160,7 +1266,91 @@ async function pollRelayAPI() {
|
|
|
1160
1266
|
console.error(`[daemon] Inbox polling error for session ${session_id}:`, inboxError.message);
|
|
1161
1267
|
}
|
|
1162
1268
|
|
|
1163
|
-
// 3)
|
|
1269
|
+
// 3) Tasks polling (PRD-0016)
|
|
1270
|
+
try {
|
|
1271
|
+
const tasksResponse = await fetch(
|
|
1272
|
+
`${RELAY_API_URL}/api/sessions/${encodeURIComponent(session_id)}/tasks`,
|
|
1273
|
+
{
|
|
1274
|
+
headers: {
|
|
1275
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
if (tasksResponse.ok) {
|
|
1281
|
+
const tasks = await tasksResponse.json();
|
|
1282
|
+
for (const task of tasks) {
|
|
1283
|
+
// If task is 'running' but not in our local executor, start it
|
|
1284
|
+
if (task.status === 'running' && !activeLoops.has(task.id) && !startingTasks.has(task.id)) {
|
|
1285
|
+
console.log(`[daemon] 🚀 Starting autonomous task loop: ${task.id}`);
|
|
1286
|
+
startingTasks.set(task.id, Date.now());
|
|
1287
|
+
activeLoops.add(task.id);
|
|
1288
|
+
|
|
1289
|
+
startTask({
|
|
1290
|
+
task: task.task,
|
|
1291
|
+
task_id: task.id,
|
|
1292
|
+
session_id: task.session_id,
|
|
1293
|
+
cwd: task.cwd || sessionData.cwd,
|
|
1294
|
+
budget_usd: task.budget_usd,
|
|
1295
|
+
onEvent: async (event) => {
|
|
1296
|
+
// Forward task events to the timeline via Relay API
|
|
1297
|
+
try {
|
|
1298
|
+
await fetch(`${RELAY_API_URL}/api/timeline`, {
|
|
1299
|
+
method: 'POST',
|
|
1300
|
+
headers: {
|
|
1301
|
+
'Content-Type': 'application/json',
|
|
1302
|
+
'Authorization': `Bearer ${RELAY_API_KEY}`
|
|
1303
|
+
},
|
|
1304
|
+
body: JSON.stringify({
|
|
1305
|
+
session_id: session_id,
|
|
1306
|
+
type: event.type,
|
|
1307
|
+
data: event
|
|
1308
|
+
})
|
|
1309
|
+
});
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
console.error(`[daemon] Failed to log task event ${event.type}:`, e.message);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}).then((result) => {
|
|
1315
|
+
startingTasks.delete(task.id);
|
|
1316
|
+
// Note: activeLoops stays active as long as the loop is running.
|
|
1317
|
+
// If startTask returns because it's already running, clean up.
|
|
1318
|
+
if (result.status === 'already_running') {
|
|
1319
|
+
activeLoops.delete(task.id);
|
|
1320
|
+
// No need to remove lock here, someone else owns it
|
|
1321
|
+
}
|
|
1322
|
+
}).catch(err => {
|
|
1323
|
+
console.error(`[daemon] Failed to start autonomous task ${task.id}:`, err.message);
|
|
1324
|
+
startingTasks.delete(task.id);
|
|
1325
|
+
// PRD-0016 Fix: Remove from activeLoops on error so it can be retried
|
|
1326
|
+
activeLoops.delete(task.id);
|
|
1327
|
+
removeTaskLock(task.id);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Sync status and manage lifecycle
|
|
1332
|
+
const localTask = getTaskSession(task.id);
|
|
1333
|
+
if (localTask) {
|
|
1334
|
+
if (task.status === 'stopped' && localTask.status !== 'stopped') {
|
|
1335
|
+
stopTask(task.id);
|
|
1336
|
+
activeLoops.delete(task.id);
|
|
1337
|
+
removeTaskLock(task.id);
|
|
1338
|
+
} else if (task.status === 'paused' && localTask.status === 'running') {
|
|
1339
|
+
pauseTask(task.id);
|
|
1340
|
+
} else if (task.status === 'running' && localTask.status === 'paused') {
|
|
1341
|
+
resumeTask(task.id);
|
|
1342
|
+
} else if (localTask.status === 'completed' || localTask.status === 'stopped') {
|
|
1343
|
+
activeLoops.delete(task.id);
|
|
1344
|
+
removeTaskLock(task.id);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
} catch (taskError) {
|
|
1350
|
+
console.error(`[daemon] Task polling error for session ${session_id}:`, taskError.message);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// 4) Heartbeat - send periodically to keep session alive
|
|
1164
1354
|
// Only send heartbeat if enough time has passed since last one (throttled per session)
|
|
1165
1355
|
const now = Date.now();
|
|
1166
1356
|
const sessionHeartbeat = heartbeatState.get(session_id);
|
|
@@ -2047,6 +2237,13 @@ async function cleanup() {
|
|
|
2047
2237
|
console.log('[daemon] Cleaning up...');
|
|
2048
2238
|
isShuttingDown = true;
|
|
2049
2239
|
|
|
2240
|
+
// Stop all task tasks (PRD-0016)
|
|
2241
|
+
try {
|
|
2242
|
+
stopAllTasks();
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
console.error('[daemon] Error stopping task tasks during cleanup:', e.message);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2050
2247
|
// Stop polling
|
|
2051
2248
|
if (pollingTimer) {
|
|
2052
2249
|
clearTimeout(pollingTimer);
|
|
@@ -2117,6 +2314,14 @@ async function main() {
|
|
|
2117
2314
|
// Setup signal handlers
|
|
2118
2315
|
setupSignalHandlers(cleanup);
|
|
2119
2316
|
|
|
2317
|
+
// PRD-0016: Cleanup orphaned task locks on startup
|
|
2318
|
+
try {
|
|
2319
|
+
cleanupStaleLocks();
|
|
2320
|
+
cleanupOrphanedProcesses();
|
|
2321
|
+
} catch (e) {
|
|
2322
|
+
console.error('[daemon] Error during startup cleanup:', e.message);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2120
2325
|
// Start HTTP server (using built-in http module)
|
|
2121
2326
|
server = http.createServer(handleRequest);
|
|
2122
2327
|
server.listen(PORT, '127.0.0.1', () => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes text for logging by redacting sensitive information
|
|
3
|
+
* Prevents accidental exposure of API keys, tokens, passwords, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitizes a string by replacing sensitive patterns with redacted versions
|
|
8
|
+
* @param {string} text - The text to sanitize
|
|
9
|
+
* @returns {string} - Sanitized text with sensitive data redacted
|
|
10
|
+
*/
|
|
11
|
+
export function sanitizeForLog(text) {
|
|
12
|
+
if (!text || typeof text !== 'string') {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let sanitized = text;
|
|
17
|
+
|
|
18
|
+
// API keys (various formats)
|
|
19
|
+
// Matches: api_key=abc123, apiKey: "abc123", api-key="abc123"
|
|
20
|
+
sanitized = sanitized.replace(
|
|
21
|
+
/api[_-]?key[\"']?\s*[:=]\s*[\"']?([A-Za-z0-9_-]{20,})[\"']?/gi,
|
|
22
|
+
'api_key: ***'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Bearer tokens
|
|
26
|
+
// Matches: Bearer abc123token
|
|
27
|
+
sanitized = sanitized.replace(
|
|
28
|
+
/Bearer\s+[A-Za-z0-9_-]+/gi,
|
|
29
|
+
'Bearer ***'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Authorization headers
|
|
33
|
+
// Matches: Authorization: "abc123"
|
|
34
|
+
sanitized = sanitized.replace(
|
|
35
|
+
/Authorization[\"']?\s*[:=]\s*[\"']?([A-Za-z0-9_-]{20,})[\"']?/gi,
|
|
36
|
+
'Authorization: ***'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Passwords (various formats)
|
|
40
|
+
// Matches: password=abc, password: "abc", pwd="abc"
|
|
41
|
+
sanitized = sanitized.replace(
|
|
42
|
+
/(password|passwd|pwd)[\"']?\s*[:=]\s*[\"']?([^\s\"']+)[\"']?/gi,
|
|
43
|
+
'$1: ***'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Generic secrets
|
|
47
|
+
// Matches: secret=abc, secret: "abc"
|
|
48
|
+
sanitized = sanitized.replace(
|
|
49
|
+
/secret[\"']?\s*[:=]\s*[\"']?([^\s\"']+)[\"']?/gi,
|
|
50
|
+
'secret: ***'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Tokens (generic)
|
|
54
|
+
// Matches: token=abc123, token: "abc123"
|
|
55
|
+
sanitized = sanitized.replace(
|
|
56
|
+
/token[\"']?\s*[:=]\s*[\"']?([A-Za-z0-9_-]{20,})[\"']?/gi,
|
|
57
|
+
'token: ***'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// AWS keys
|
|
61
|
+
// Matches: AKIA... (AWS access keys start with AKIA)
|
|
62
|
+
sanitized = sanitized.replace(
|
|
63
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
64
|
+
'AKIA***'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Private keys (PEM format)
|
|
68
|
+
// Matches: -----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----
|
|
69
|
+
sanitized = sanitized.replace(
|
|
70
|
+
/-----BEGIN[A-Z\s]+PRIVATE KEY-----[\s\S]*?-----END[A-Z\s]+PRIVATE KEY-----/g,
|
|
71
|
+
'-----BEGIN PRIVATE KEY----- *** -----END PRIVATE KEY-----'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// JWT tokens (three base64 segments separated by dots)
|
|
75
|
+
// Matches: eyJhbGc...
|
|
76
|
+
sanitized = sanitized.replace(
|
|
77
|
+
/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
78
|
+
'eyJ***.***'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Truncates a string to a maximum length and adds ellipsis if needed
|
|
86
|
+
* @param {string} text - The text to truncate
|
|
87
|
+
* @param {number} maxLen - Maximum length (default: 100)
|
|
88
|
+
* @returns {string} - Truncated text
|
|
89
|
+
*/
|
|
90
|
+
export function truncateForLog(text, maxLen = 100) {
|
|
91
|
+
if (!text || typeof text !== 'string') {
|
|
92
|
+
return text || '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (text.length <= maxLen) {
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return text.substring(0, maxLen) + '...';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sanitizes and truncates text for safe logging
|
|
104
|
+
* @param {string} text - The text to process
|
|
105
|
+
* @param {number} maxLen - Maximum length (default: 100)
|
|
106
|
+
* @returns {string} - Sanitized and truncated text
|
|
107
|
+
*/
|
|
108
|
+
export function prepareForLog(text, maxLen = 100) {
|
|
109
|
+
const sanitized = sanitizeForLog(text);
|
|
110
|
+
return truncateForLog(sanitized, maxLen);
|
|
111
|
+
}
|
package/lib/utils/logger.js
CHANGED
|
@@ -1,148 +1,95 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Logging utility with configurable log levels
|
|
3
|
+
* Controls verbosity based on TELEPORTATION_LOG_LEVEL environment variable
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import os from 'os';
|
|
10
|
-
|
|
11
6
|
const LOG_LEVELS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const LOG_LEVEL_NAMES = {
|
|
20
|
-
0: 'DEBUG',
|
|
21
|
-
1: 'INFO',
|
|
22
|
-
2: 'WARN',
|
|
23
|
-
3: 'ERROR',
|
|
24
|
-
4: 'NONE'
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
debug: 3,
|
|
11
|
+
verbose: 4
|
|
25
12
|
};
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.logFile = options.logFile || path.join(os.homedir(), '.teleportation', 'logs', 'cli.log');
|
|
31
|
-
this.enableFileLogging = options.enableFileLogging !== false;
|
|
32
|
-
this.enableColors = options.enableColors !== false && process.stdout.isTTY;
|
|
33
|
-
|
|
34
|
-
// Ensure log directory exists
|
|
35
|
-
if (this.enableFileLogging) {
|
|
36
|
-
const logDir = path.dirname(this.logFile);
|
|
37
|
-
if (!fs.existsSync(logDir)) {
|
|
38
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_colorize(level, message) {
|
|
44
|
-
if (!this.enableColors) return message;
|
|
45
|
-
|
|
46
|
-
const colors = {
|
|
47
|
-
DEBUG: '\x1b[0;36m', // Cyan
|
|
48
|
-
INFO: '\x1b[0;32m', // Green
|
|
49
|
-
WARN: '\x1b[1;33m', // Yellow
|
|
50
|
-
ERROR: '\x1b[0;31m' // Red
|
|
51
|
-
};
|
|
52
|
-
const reset = '\x1b[0m';
|
|
53
|
-
|
|
54
|
-
return `${colors[level] || ''}${message}${reset}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
_formatMessage(level, message, data = null) {
|
|
58
|
-
const timestamp = new Date().toISOString();
|
|
59
|
-
const levelName = LOG_LEVEL_NAMES[level];
|
|
60
|
-
const prefix = `[${timestamp}] [${levelName}]`;
|
|
61
|
-
|
|
62
|
-
let formatted = `${prefix} ${message}`;
|
|
63
|
-
if (data) {
|
|
64
|
-
formatted += ` ${JSON.stringify(data)}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return formatted;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
_write(level, message, data = null) {
|
|
71
|
-
if (level < this.level) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const formatted = this._formatMessage(level, message, data);
|
|
76
|
-
const levelName = LOG_LEVEL_NAMES[level];
|
|
77
|
-
|
|
78
|
-
// Console output (with colors)
|
|
79
|
-
const consoleMessage = this._colorize(levelName, formatted);
|
|
80
|
-
if (level >= LOG_LEVELS.ERROR) {
|
|
81
|
-
console.error(consoleMessage);
|
|
82
|
-
} else {
|
|
83
|
-
console.log(consoleMessage);
|
|
84
|
-
}
|
|
14
|
+
// Get log level from environment variable, default to 'info'
|
|
15
|
+
const LOG_LEVEL = process.env.TELEPORTATION_LOG_LEVEL || 'info';
|
|
16
|
+
const currentLevel = LOG_LEVELS[LOG_LEVEL] !== undefined ? LOG_LEVELS[LOG_LEVEL] : LOG_LEVELS.info;
|
|
85
17
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
debug(message, data) {
|
|
97
|
-
this._write(LOG_LEVELS.DEBUG, message, data);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
info(message, data) {
|
|
101
|
-
this._write(LOG_LEVELS.INFO, message, data);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
warn(message, data) {
|
|
105
|
-
this._write(LOG_LEVELS.WARN, message, data);
|
|
106
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if a message at the given level should be logged
|
|
20
|
+
* @param {string} level - Log level: 'error', 'warn', 'info', 'debug', 'verbose'
|
|
21
|
+
* @returns {boolean} - True if message should be logged
|
|
22
|
+
*/
|
|
23
|
+
function shouldLog(level) {
|
|
24
|
+
const messageLevel = LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.info;
|
|
25
|
+
return messageLevel <= currentLevel;
|
|
26
|
+
}
|
|
107
27
|
|
|
108
|
-
|
|
109
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Log an error message (always shown)
|
|
30
|
+
* @param {...any} args - Arguments to log
|
|
31
|
+
*/
|
|
32
|
+
export function logError(...args) {
|
|
33
|
+
if (shouldLog('error')) {
|
|
34
|
+
console.error(...args);
|
|
110
35
|
}
|
|
36
|
+
}
|
|
111
37
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Log a warning message
|
|
40
|
+
* @param {...any} args - Arguments to log
|
|
41
|
+
*/
|
|
42
|
+
export function logWarn(...args) {
|
|
43
|
+
if (shouldLog('warn')) {
|
|
44
|
+
console.warn(...args);
|
|
119
45
|
}
|
|
46
|
+
}
|
|
120
47
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Log an info message (default level)
|
|
50
|
+
* @param {...any} args - Arguments to log
|
|
51
|
+
*/
|
|
52
|
+
export function logInfo(...args) {
|
|
53
|
+
if (shouldLog('info')) {
|
|
54
|
+
console.log(...args);
|
|
127
55
|
}
|
|
56
|
+
}
|
|
128
57
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Log a debug message (requires LOG_LEVEL=debug or verbose)
|
|
60
|
+
* @param {...any} args - Arguments to log
|
|
61
|
+
*/
|
|
62
|
+
export function logDebug(...args) {
|
|
63
|
+
if (shouldLog('debug')) {
|
|
64
|
+
console.log(...args);
|
|
132
65
|
}
|
|
66
|
+
}
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Log a verbose message (requires LOG_LEVEL=verbose)
|
|
70
|
+
* @param {...any} args - Arguments to log
|
|
71
|
+
*/
|
|
72
|
+
export function logVerbose(...args) {
|
|
73
|
+
if (shouldLog('verbose')) {
|
|
74
|
+
console.log(...args);
|
|
140
75
|
}
|
|
141
76
|
}
|
|
142
77
|
|
|
143
|
-
|
|
144
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Get the current log level
|
|
80
|
+
* @returns {string} - Current log level
|
|
81
|
+
*/
|
|
82
|
+
export function getLogLevel() {
|
|
83
|
+
return LOG_LEVEL;
|
|
84
|
+
}
|
|
145
85
|
|
|
146
|
-
|
|
147
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Check if debug logging is enabled
|
|
88
|
+
* @returns {boolean} - True if debug or verbose logging is enabled
|
|
89
|
+
*/
|
|
90
|
+
export function isDebugEnabled() {
|
|
91
|
+
return shouldLog('debug');
|
|
92
|
+
}
|
|
148
93
|
|
|
94
|
+
// Export shouldLog for custom level checking
|
|
95
|
+
export { shouldLog };
|