autokap 1.6.5 → 1.6.7

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.
@@ -2,6 +2,7 @@ export interface AutokapConfig {
2
2
  apiKey: string;
3
3
  apiBaseUrl: string;
4
4
  wsUrl: string;
5
+ exportDebugLogs?: boolean;
5
6
  }
6
7
  declare const DEFAULT_API_BASE_URL = "https://autokap.app";
7
8
  declare const DEFAULT_WS_URL = "wss://autokap.app/ws";
@@ -12,6 +13,8 @@ declare const RUN_TOKEN_ENV_VAR = "AUTOKAP_RUN_TOKEN";
12
13
  declare const API_BASE_URL_ENV_VAR = "AUTOKAP_API_BASE_URL";
13
14
  declare const WS_URL_ENV_VAR = "AUTOKAP_WS_URL";
14
15
  declare const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = "AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN";
16
+ declare const EXPORT_DEBUG_LOGS_ENV_VAR = "AUTOKAP_EXPORT_DEBUG_LOGS";
17
+ export declare function resolveExportDebugLogs(stored: boolean | undefined): boolean;
15
18
  export declare function getConfigDir(): string;
16
19
  export declare function getConfigPath(): string;
17
20
  export declare function getDefaultApiBaseUrl(): string;
@@ -20,4 +23,4 @@ export declare function readConfig(): Promise<AutokapConfig | null>;
20
23
  export declare function writeConfig(config: AutokapConfig): Promise<void>;
21
24
  export declare function deleteConfig(): Promise<void>;
22
25
  export declare function requireConfig(): Promise<AutokapConfig>;
23
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
26
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, EXPORT_DEBUG_LOGS_ENV_VAR, };
@@ -24,6 +24,15 @@ const RUN_TOKEN_ENV_VAR = 'AUTOKAP_RUN_TOKEN';
24
24
  const API_BASE_URL_ENV_VAR = 'AUTOKAP_API_BASE_URL';
25
25
  const WS_URL_ENV_VAR = 'AUTOKAP_WS_URL';
26
26
  const ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR = 'AUTOKAP_ALLOW_UNSAFE_SERVER_ORIGIN';
27
+ const EXPORT_DEBUG_LOGS_ENV_VAR = 'AUTOKAP_EXPORT_DEBUG_LOGS';
28
+ export function resolveExportDebugLogs(stored) {
29
+ const envRaw = process.env[EXPORT_DEBUG_LOGS_ENV_VAR]?.trim().toLowerCase();
30
+ if (envRaw === '0' || envRaw === 'false')
31
+ return false;
32
+ if (envRaw === '1' || envRaw === 'true')
33
+ return true;
34
+ return stored !== false;
35
+ }
27
36
  export function getConfigDir() {
28
37
  return path.join(os.homedir(), '.autokap');
29
38
  }
@@ -82,6 +91,7 @@ export async function readConfig() {
82
91
  return null;
83
92
  const storedApiBaseUrlRaw = typeof record.apiBaseUrl === 'string' ? record.apiBaseUrl : null;
84
93
  const storedWsUrlRaw = typeof record.wsUrl === 'string' ? record.wsUrl : null;
94
+ const storedExportDebugLogs = typeof record.exportDebugLogs === 'boolean' ? record.exportDebugLogs : undefined;
85
95
  const envApiBaseUrl = normalizeUrl(process.env[API_BASE_URL_ENV_VAR]);
86
96
  const envWsUrl = normalizeUrl(process.env[WS_URL_ENV_VAR]);
87
97
  const storedApiBaseUrl = normalizeUrl(storedApiBaseUrlRaw) ?? DEFAULT_API_BASE_URL;
@@ -98,6 +108,7 @@ export async function readConfig() {
98
108
  apiKey: record.apiKey,
99
109
  apiBaseUrl,
100
110
  wsUrl,
111
+ exportDebugLogs: storedExportDebugLogs,
101
112
  };
102
113
  }
103
114
  export async function writeConfig(config) {
@@ -116,6 +127,9 @@ export async function writeConfig(config) {
116
127
  ...config,
117
128
  apiBaseUrl: normalizeUrl(config.apiBaseUrl) ?? DEFAULT_API_BASE_URL,
118
129
  wsUrl: normalizeUrl(config.wsUrl) ?? deriveWsUrl(config.apiBaseUrl),
130
+ ...(typeof config.exportDebugLogs === 'boolean'
131
+ ? { exportDebugLogs: config.exportDebugLogs }
132
+ : {}),
119
133
  };
120
134
  // Atomic write — see packages/core/src/config.ts for the canonical impl.
121
135
  const tmpPath = `${configPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
@@ -215,5 +229,5 @@ function assertAllowedApiOrigin(candidateUrl, baselineUrl, envVar) {
215
229
  }
216
230
  throw new Error(`Refusing unsafe server override to ${candidateOrigin}. Set ${ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR}=1 to allow ${envVar ?? 'this override'} explicitly.`);
217
231
  }
218
- export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, };
232
+ export { DEFAULT_API_BASE_URL, DEFAULT_WS_URL, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, RUN_TOKEN_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, ALLOW_UNSAFE_SERVER_ORIGIN_ENV_VAR, EXPORT_DEBUG_LOGS_ENV_VAR, };
219
233
  //# sourceMappingURL=cli-config.js.map
@@ -13,4 +13,5 @@ export declare function runLocal(presetId: string, opts: {
13
13
  allowUploadFailure?: boolean;
14
14
  dry?: boolean;
15
15
  regenerateTts?: boolean;
16
+ exportLogs?: boolean;
16
17
  }): Promise<void>;
@@ -33,6 +33,7 @@ export async function runLocal(presetId, opts) {
33
33
  dryRun: opts.dry,
34
34
  regenerateTts: opts.regenerateTts,
35
35
  headed: opts.headed,
36
+ exportDebugLogs: opts.exportLogs,
36
37
  onProgress: (event) => {
37
38
  const prefix = `[capture][${event.variantId}]`;
38
39
  switch (event.type) {
@@ -45,6 +45,11 @@ export interface CLIRunnerOptions {
45
45
  abortSignal?: AbortSignal;
46
46
  /** Progress callback */
47
47
  onProgress?: (event: ProgressEvent) => void;
48
+ /**
49
+ * Override the user/CLI `exportDebugLogs` preference for this run.
50
+ * When `false`, skips the failed-run debug logs export (AUT-149).
51
+ */
52
+ exportDebugLogs?: boolean;
48
53
  }
49
54
  export interface CLIRunResult {
50
55
  success: boolean;
@@ -16,7 +16,7 @@ import path from 'node:path';
16
16
  import { createHash, randomUUID } from 'node:crypto';
17
17
  import sharp from 'sharp';
18
18
  import { Browser } from './browser.js';
19
- import { API_BASE_URL_ENV_VAR, requireConfig } from './cli-config.js';
19
+ import { API_BASE_URL_ENV_VAR, requireConfig, resolveExportDebugLogs } from './cli-config.js';
20
20
  import { WebPlaywrightLocal } from './web-playwright-local.js';
21
21
  import { executeProgram } from './opcode-runner.js';
22
22
  import { ensureChromiumInstalled } from './playwright-installer.js';
@@ -32,6 +32,8 @@ import { logger } from './logger.js';
32
32
  import { callLLM } from './llm-provider.js';
33
33
  import { APP_VERSION } from './version.js';
34
34
  import { normalizeAllowedOrigins, normalizeHttpOrigin, verifySignedExecutionProgramEnvelope, } from './program-signing.js';
35
+ import { redactTelemetryText, redactUrl } from './telemetry-redaction.js';
36
+ import { LogCollector } from './log-collector.js';
35
37
  const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
36
38
  const DEFAULT_VIDEO_DELIVERY_RESOLUTION = { width: 1920, height: 1080 };
37
39
  const DEFAULT_VIDEO_CAPTURE_RESOLUTION = DEFAULT_VIDEO_DELIVERY_RESOLUTION;
@@ -218,6 +220,9 @@ export async function runCapture(options) {
218
220
  // honors the requested concurrency.
219
221
  const isRecordable = program.mediaMode === 'clip' || program.mediaMode === 'video';
220
222
  const maxParallelVariants = isRecordable ? 1 : program.maxParallelCaptures;
223
+ // AUT-149: collect structured logs + progress events for export on failure.
224
+ const logCollector = new LogCollector();
225
+ logCollector.start();
221
226
  const runOptions = {
222
227
  recoveryChain,
223
228
  abortSignal: options.abortSignal,
@@ -226,6 +231,7 @@ export async function runCapture(options) {
226
231
  presetName: program.presetId,
227
232
  dryRun: options.dryRun,
228
233
  onProgress: (event) => {
234
+ logCollector.onProgress(event);
229
235
  if (!options.onProgress) {
230
236
  logProgress(event);
231
237
  }
@@ -286,59 +292,81 @@ export async function runCapture(options) {
286
292
  await applyPreconditions(browser, program);
287
293
  return new WebPlaywrightLocal(browser, recordingDir);
288
294
  };
289
- const runResult = await executeProgram(program, createAdapter, runOptions);
290
- if (runResult.success) {
291
- logger.info(`[capture] Run completed successfully — ${runResult.telemetry.totalOpcodes} opcodes, ${runResult.telemetry.recoveredOpcodes} recovered, ${runResult.totalDurationMs}ms`);
292
- }
293
- else {
294
- logger.error(`[capture] Run failed: ${runResult.error}`);
295
- }
296
- if (options.dryRun) {
297
- logger.info(`[capture] DRY RUN complete — ${runResult.telemetry.totalOpcodes} opcodes executed, 0 captures, 0 credits charged`);
298
- return { success: runResult.success, runId, runResult };
299
- }
295
+ let runResult;
296
+ let cliResult;
300
297
  try {
301
- logger.info('[capture] Saving captures, might take a few seconds...');
302
- options.onProgress?.({
303
- type: 'upload_start',
304
- variantId: 'run',
305
- message: 'saving captures',
306
- });
307
- const uploadOutcome = await uploadResults(config, program, runResult, runId);
308
- if (program.mediaMode === 'video' && runResult.success) {
309
- await signalVideoComplete(config, program, runResult, uploadOutcome.runId, videoAudioAssets, videoAudioAssetsByLocale);
298
+ runResult = await executeProgram(program, createAdapter, runOptions);
299
+ if (runResult.success) {
300
+ logger.info(`[capture] Run completed successfully — ${runResult.telemetry.totalOpcodes} opcodes, ${runResult.telemetry.recoveredOpcodes} recovered, ${runResult.totalDurationMs}ms`);
301
+ }
302
+ else {
303
+ logger.error(`[capture] Run failed: ${runResult.error}`);
304
+ }
305
+ if (options.dryRun) {
306
+ logger.info(`[capture] DRY RUN complete — ${runResult.telemetry.totalOpcodes} opcodes executed, 0 captures, 0 credits charged`);
307
+ cliResult = { success: runResult.success, runId, runResult };
308
+ }
309
+ else {
310
+ try {
311
+ logger.info('[capture] Saving captures, might take a few seconds...');
312
+ options.onProgress?.({
313
+ type: 'upload_start',
314
+ variantId: 'run',
315
+ message: 'saving captures',
316
+ });
317
+ const uploadOutcome = await uploadResults(config, program, runResult, runId);
318
+ if (program.mediaMode === 'video' && runResult.success) {
319
+ await signalVideoComplete(config, program, runResult, uploadOutcome.runId, videoAudioAssets, videoAudioAssetsByLocale);
320
+ }
321
+ const totalDurationSec = ((Date.now() - captureStart) / 1000).toFixed(1);
322
+ logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
323
+ options.onProgress?.({
324
+ type: 'upload_end',
325
+ variantId: 'run',
326
+ status: 'ok',
327
+ message: 'captures saved',
328
+ });
329
+ cliResult = { success: runResult.success, runId, runResult };
330
+ }
331
+ catch (err) {
332
+ const message = err instanceof Error ? err.message : String(err);
333
+ logger.error(`[capture] Failed to upload results: ${message}`);
334
+ options.onProgress?.({
335
+ type: 'upload_end',
336
+ variantId: 'run',
337
+ status: 'failed',
338
+ message,
339
+ });
340
+ if (!options.allowUploadFailure) {
341
+ cliResult = {
342
+ success: false,
343
+ runId,
344
+ runResult,
345
+ error: runResult.success
346
+ ? `upload failed: ${message}`
347
+ : `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
348
+ };
349
+ }
350
+ else {
351
+ logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
352
+ cliResult = { success: runResult.success, runId, runResult };
353
+ }
354
+ }
310
355
  }
311
- const totalDurationSec = ((Date.now() - captureStart) / 1000).toFixed(1);
312
- logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
313
- options.onProgress?.({
314
- type: 'upload_end',
315
- variantId: 'run',
316
- status: 'ok',
317
- message: 'captures saved',
318
- });
319
356
  }
320
- catch (err) {
321
- const message = err instanceof Error ? err.message : String(err);
322
- logger.error(`[capture] Failed to upload results: ${message}`);
323
- options.onProgress?.({
324
- type: 'upload_end',
325
- variantId: 'run',
326
- status: 'failed',
327
- message,
328
- });
329
- if (!options.allowUploadFailure) {
330
- return {
331
- success: false,
332
- runId,
333
- runResult,
334
- error: runResult.success
335
- ? `upload failed: ${message}`
336
- : `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
337
- };
357
+ finally {
358
+ // AUT-149: export structured debug logs to AutoKap on capture failure.
359
+ // Best-effort the LogCollector swallows network errors.
360
+ const shouldExport = options.exportDebugLogs !== false
361
+ && resolveExportDebugLogs(config.exportDebugLogs)
362
+ && (runResult ? !runResult.success : true);
363
+ if (shouldExport) {
364
+ logger.info('[debug-logs] Exporting debug logs to AutoKap…');
365
+ await logCollector.flushTo(runId, program.presetId, config.apiBaseUrl, config.apiKey, options.env);
338
366
  }
339
- logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
367
+ logCollector.stop();
340
368
  }
341
- return { success: runResult.success, runId, runResult };
369
+ return cliResult;
342
370
  }
343
371
  // ── Server communication ────────────────────────────────────────────
344
372
  async function fetchProgram(config, presetId, environmentName) {
@@ -1388,30 +1416,7 @@ function redactOpcodeForTelemetry(opcode) {
1388
1416
  }
1389
1417
  return base;
1390
1418
  }
1391
- function redactTelemetryText(value) {
1392
- if (!value)
1393
- return value;
1394
- return value
1395
- .replace(/\{\{(email|password|loginUrl)\}\}/g, '<credential-placeholder>')
1396
- .replace(/https?:\/\/[^\s"'<>]+/gi, (match) => redactUrl(match) ?? '<redacted-url>')
1397
- .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
1398
- .slice(0, 512);
1399
- }
1400
- function redactUrl(value) {
1401
- if (!value)
1402
- return value;
1403
- try {
1404
- const parsed = new URL(value);
1405
- parsed.username = '';
1406
- parsed.password = '';
1407
- parsed.search = parsed.search ? '?<redacted>' : '';
1408
- parsed.hash = parsed.hash ? '#<redacted>' : '';
1409
- return parsed.toString();
1410
- }
1411
- catch {
1412
- return value.slice(0, 256);
1413
- }
1414
- }
1419
+ // redactTelemetryText / redactUrl moved to ./telemetry-redaction
1415
1420
  // ── Progress logging ────────────────────────────────────────────────
1416
1421
  function logProgress(event) {
1417
1422
  const prefix = `[capture][${event.variantId}]`;
package/dist/cli.js CHANGED
@@ -87,6 +87,7 @@ program
87
87
  .option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
88
88
  .action(async (key, opts) => {
89
89
  const wsUrl = opts.wsUrl?.trim() || getDefaultWsUrl(opts.apiBaseUrl);
90
+ let exportDebugLogs;
90
91
  try {
91
92
  const res = await fetch(`${opts.apiBaseUrl}/api/cli/validate`, {
92
93
  headers: { Authorization: `Bearer ${key}` },
@@ -95,6 +96,15 @@ program
95
96
  logger.error('Invalid API key. Generate one in the AutoKap dashboard.');
96
97
  process.exit(1);
97
98
  }
99
+ try {
100
+ const body = (await res.json());
101
+ if (typeof body.exportDebugLogs === 'boolean') {
102
+ exportDebugLogs = body.exportDebugLogs;
103
+ }
104
+ }
105
+ catch {
106
+ // ignore parse failures — preference will fall back to its default
107
+ }
98
108
  }
99
109
  catch (err) {
100
110
  logger.error(`Cannot reach API: ${err.message}`);
@@ -104,6 +114,7 @@ program
104
114
  apiKey: key,
105
115
  apiBaseUrl: opts.apiBaseUrl,
106
116
  wsUrl,
117
+ ...(typeof exportDebugLogs === 'boolean' ? { exportDebugLogs } : {}),
107
118
  });
108
119
  logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
109
120
  process.exit(0);
@@ -166,6 +177,7 @@ program
166
177
  .option('--dry', 'Dry run: execute all opcodes without capturing or uploading artifacts (0 credits charged)', false)
167
178
  .option('--regenerate-tts', 'Force fresh TTS synthesis for video presets — ignore cached audio segments. Reused segments become billable at full rate.', false)
168
179
  .option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
180
+ .option('--no-export-logs', 'Disable exporting debug logs to AutoKap on capture failure (overrides user/CLI setting)')
169
181
  .action(async (presetId, opts) => {
170
182
  if (opts.debug) {
171
183
  const { setDebugEnabled } = await import('./logger.js');
@@ -0,0 +1,46 @@
1
+ import type { LogEntry } from './logger.js';
2
+ import type { ProgressEvent } from './opcode-runner.js';
3
+ export interface OpcodeContext {
4
+ index: number;
5
+ kind: string;
6
+ targetId?: string;
7
+ }
8
+ export interface ExportedLogEntry extends LogEntry {
9
+ opcode?: OpcodeContext;
10
+ recovery?: {
11
+ strategy: string;
12
+ reason: string;
13
+ succeeded: boolean;
14
+ };
15
+ postcondition?: {
16
+ type: string;
17
+ reason: string;
18
+ };
19
+ meta?: Record<string, unknown>;
20
+ }
21
+ export interface ErrorLogsPayload {
22
+ runId: string;
23
+ presetId: string;
24
+ entries: ExportedLogEntry[];
25
+ envName?: string;
26
+ endedAt: string;
27
+ }
28
+ export declare class LogCollector {
29
+ private entries;
30
+ private previousLogCallback;
31
+ private started;
32
+ private runStartedAt;
33
+ start(): void;
34
+ stop(): void;
35
+ onProgress(event: ProgressEvent): void;
36
+ snapshot(): ExportedLogEntry[];
37
+ size(): number;
38
+ flushTo(runId: string, presetId: string, apiBaseUrl: string, apiKey: string, envName?: string): Promise<{
39
+ ok: boolean;
40
+ status?: number;
41
+ error?: string;
42
+ }>;
43
+ private push;
44
+ private mapLogEntry;
45
+ private captureExistingCallback;
46
+ }
@@ -0,0 +1,120 @@
1
+ import { setOnLog, getOnLog, logger } from './logger.js';
2
+ import { redactTelemetryText } from './telemetry-redaction.js';
3
+ function redactParams(params) {
4
+ const out = {};
5
+ for (const [key, value] of Object.entries(params)) {
6
+ if (typeof value === 'string') {
7
+ out[key] = redactTelemetryText(value) ?? value;
8
+ }
9
+ else {
10
+ out[key] = value;
11
+ }
12
+ }
13
+ return out;
14
+ }
15
+ const MAX_ENTRIES = 1000;
16
+ const FLUSH_TIMEOUT_MS = 5000;
17
+ export class LogCollector {
18
+ entries = [];
19
+ previousLogCallback = null;
20
+ started = false;
21
+ runStartedAt = null;
22
+ start() {
23
+ if (this.started)
24
+ return;
25
+ this.started = true;
26
+ this.runStartedAt = Date.now();
27
+ this.previousLogCallback = this.captureExistingCallback();
28
+ setOnLog((entry) => {
29
+ this.push(this.mapLogEntry(entry));
30
+ if (this.previousLogCallback)
31
+ this.previousLogCallback(entry);
32
+ });
33
+ }
34
+ stop() {
35
+ if (!this.started)
36
+ return;
37
+ this.started = false;
38
+ setOnLog(this.previousLogCallback);
39
+ this.previousLogCallback = null;
40
+ }
41
+ onProgress(event) {
42
+ if (!this.started)
43
+ return;
44
+ const level = event.status === 'failed' ? 'error' : event.status === 'recovered' ? 'warn' : 'info';
45
+ const message = redactTelemetryText(event.message) ?? '';
46
+ this.push({
47
+ level: level,
48
+ message,
49
+ timestamp: Date.now(),
50
+ runStartedAt: this.runStartedAt ?? undefined,
51
+ runElapsedMs: this.runStartedAt ? Date.now() - this.runStartedAt : undefined,
52
+ opcode: event.opcodeIndex !== undefined && event.opcodeKind
53
+ ? { index: event.opcodeIndex, kind: event.opcodeKind }
54
+ : undefined,
55
+ meta: { progressType: event.type, variantId: event.variantId, status: event.status },
56
+ });
57
+ }
58
+ snapshot() {
59
+ return [...this.entries];
60
+ }
61
+ size() {
62
+ return this.entries.length;
63
+ }
64
+ async flushTo(runId, presetId, apiBaseUrl, apiKey, envName) {
65
+ if (this.entries.length === 0) {
66
+ return { ok: true, status: 204 };
67
+ }
68
+ const payload = {
69
+ runId,
70
+ presetId,
71
+ entries: this.entries,
72
+ envName,
73
+ endedAt: new Date().toISOString(),
74
+ };
75
+ const controller = new AbortController();
76
+ const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
77
+ try {
78
+ const response = await fetch(`${apiBaseUrl}/api/cli/error-logs`, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Authorization': `Bearer ${apiKey}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify(payload),
85
+ signal: controller.signal,
86
+ });
87
+ if (!response.ok && response.status !== 204) {
88
+ const text = await response.text().catch(() => '');
89
+ logger.warn(`[debug-logs] Export failed (HTTP ${response.status}): ${text.slice(0, 200)}`);
90
+ return { ok: false, status: response.status, error: text };
91
+ }
92
+ return { ok: true, status: response.status };
93
+ }
94
+ catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ logger.warn(`[debug-logs] Export error: ${message}`);
97
+ return { ok: false, error: message };
98
+ }
99
+ finally {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
103
+ push(entry) {
104
+ this.entries.push(entry);
105
+ if (this.entries.length > MAX_ENTRIES) {
106
+ this.entries.splice(0, this.entries.length - MAX_ENTRIES);
107
+ }
108
+ }
109
+ mapLogEntry(entry) {
110
+ return {
111
+ ...entry,
112
+ message: redactTelemetryText(entry.message) ?? entry.message,
113
+ params: entry.params ? redactParams(entry.params) : entry.params,
114
+ };
115
+ }
116
+ captureExistingCallback() {
117
+ return getOnLog();
118
+ }
119
+ }
120
+ //# sourceMappingURL=log-collector.js.map
package/dist/logger.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare function setDebugEnabled(enabled: boolean): void;
29
29
  export declare function isDebugEnabled(): boolean;
30
30
  export declare function runWithLoggerCallbacks<T>(callbacks: LoggerContext, fn: () => Promise<T>): Promise<T>;
31
31
  export declare function setOnLog(cb: OnLogCallback | null): void;
32
+ export declare function getOnLog(): OnLogCallback | null;
32
33
  export declare function setOnScreenshot(cb: OnScreenshotCallback | null): void;
33
34
  export declare function emitScreenshot(base64: string): void;
34
35
  export declare function emitReasoningDelta(delta: string, messageId: string): void;
package/dist/logger.js CHANGED
@@ -16,6 +16,9 @@ export async function runWithLoggerCallbacks(callbacks, fn) {
16
16
  export function setOnLog(cb) {
17
17
  _onLog = cb;
18
18
  }
19
+ export function getOnLog() {
20
+ return _onLog;
21
+ }
19
22
  export function setOnScreenshot(cb) {
20
23
  _onScreenshot = cb;
21
24
  }
@@ -1,5 +1,5 @@
1
1
  import type { SupabaseClient } from '@supabase/supabase-js';
2
- export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration';
2
+ export type CreditUsageType = 'screenshot' | 'clip' | 'video' | 'cloud_recapture' | 'ai_chat' | 'studio_creation' | 'studio_iteration' | 'error_analysis';
3
3
  export declare function recordCreditUsage(supabase: SupabaseClient, params: {
4
4
  userId: string;
5
5
  projectId: string | null;
@@ -0,0 +1,2 @@
1
+ export declare function redactUrl(value: string | undefined): string | undefined;
2
+ export declare function redactTelemetryText(value: string | undefined): string | undefined;
@@ -0,0 +1,25 @@
1
+ export function redactUrl(value) {
2
+ if (!value)
3
+ return value;
4
+ try {
5
+ const parsed = new URL(value);
6
+ parsed.username = '';
7
+ parsed.password = '';
8
+ parsed.search = parsed.search ? '?<redacted>' : '';
9
+ parsed.hash = parsed.hash ? '#<redacted>' : '';
10
+ return parsed.toString();
11
+ }
12
+ catch {
13
+ return value.slice(0, 256);
14
+ }
15
+ }
16
+ export function redactTelemetryText(value) {
17
+ if (!value)
18
+ return value;
19
+ return value
20
+ .replace(/\{\{(email|password|loginUrl)\}\}/g, '<credential-placeholder>')
21
+ .replace(/https?:\/\/[^\s"'<>]+/gi, (match) => redactUrl(match) ?? '<redacted-url>')
22
+ .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
23
+ .slice(0, 512);
24
+ }
25
+ //# sourceMappingURL=telemetry-redaction.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "AI-powered CLI tool for capturing clean screenshots of websites",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",