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.
@@ -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
- // Session registry: session_id -> { session_id, cwd, meta, registered_at }
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 preview = (message.text || '').slice(0, 200).replace(/\s+/g, ' ');
814
- console.log(`[daemon] Inbox message for session ${session_id}: ${message.id} - ${preview}`);
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
- console.log(`[daemon] Routing message to Claude Code: ${commandText.substring(0, 100)}`);
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
- console.log(`[daemon] Executed via ${executionResult.coder_used}`);
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 preview = commandText.substring(0, 50);
910
- console.error(`[daemon] Machine coder execution failed for "${preview}...":`, error.message);
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
- console.error(`[daemon] Failed to post result message: HTTP ${resultResponse.status} - ${errorText}`);
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
- console.error('[daemon] Failed to send result message:', sendError.message);
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
- await fetch(`${RELAY_API_URL}/api/messages/${encodeURIComponent(message.id)}/ack`, {
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
- console.error('[daemon] Failed to handle inbox message:', error.message);
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) Heartbeat - send periodically to keep session alive
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
+ }
@@ -1,148 +1,95 @@
1
- #!/usr/bin/env node
2
1
  /**
3
- * Structured logging utility for Teleportation CLI
4
- * Supports different log levels and output formats
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
- DEBUG: 0,
13
- INFO: 1,
14
- WARN: 2,
15
- ERROR: 3,
16
- NONE: 4
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
- class Logger {
28
- constructor(options = {}) {
29
- this.level = options.level || (process.env.DEBUG ? LOG_LEVELS.DEBUG : LOG_LEVELS.INFO);
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
- // File output (without colors)
87
- if (this.enableFileLogging) {
88
- try {
89
- fs.appendFileSync(this.logFile, formatted + '\n', { flag: 'a' });
90
- } catch (e) {
91
- // Silently fail if log file can't be written
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
- error(message, data) {
109
- this._write(LOG_LEVELS.ERROR, message, data);
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
- // Convenience methods for common patterns
113
- success(message) {
114
- if (this.enableColors) {
115
- console.log(`\x1b[0;32m✓\x1b[0m ${message}`);
116
- } else {
117
- console.log(`✓ ${message}`);
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
- failure(message) {
122
- if (this.enableColors) {
123
- console.error(`\x1b[0;31m✗\x1b[0m ${message}`);
124
- } else {
125
- console.error(`✗ ${message}`);
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
- // Get log file path
130
- getLogFile() {
131
- return this.logFile;
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
- // Set log level
135
- setLevel(level) {
136
- if (typeof level === 'string') {
137
- level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
138
- }
139
- this.level = level;
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
- // Create default logger instance
144
- const logger = new Logger();
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
- export default logger;
147
- export { Logger, LOG_LEVELS };
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 };