@turns/dev 0.0.23 → 0.0.24

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/README.md CHANGED
@@ -69,6 +69,13 @@ On macOS, credentials and daemon state live under
69
69
  LaunchAgent `~/Library/LaunchAgents/dev.turns.cli.plist`. Its output is written
70
70
  to `~/Library/Application Support/turns/daemon.log`.
71
71
 
72
+ Daemon runtime logs (including reconnects, upload progress, and errors) prefix
73
+ each line with a UTC ISO 8601 timestamp, for example
74
+ `[2026-10-19T09:12:34.567Z] Reconnected to wss://turns.dev/api/socket ...`.
75
+ Foreground uploads use the same timestamped runtime logs. Multiline messages
76
+ share one timestamp and retain their indentation; stdout and stderr remain
77
+ separate. Help, version, and service-management output keep their plain format.
78
+
72
79
  The credential file contains a seven-day access token and a signed refresh
73
80
  token. The CLI refreshes the access token at least once every 24 hours. A new
74
81
  machine opens a one-time turns.dev URL, waits on an authentication WebSocket,
@@ -372,7 +372,7 @@ export function processInvocation({ harness, sessionId, transcriptPath, cwd, pro
372
372
  sessionId,
373
373
  ...(!createSession ? ['--allow-workspace-switch'] : []),
374
374
  ...(model && model !== 'auto' ? ['--model', model] : []),
375
- ...(thinkingLevel ? ['--reasoning-effort', thinkingLevel === 'max' ? 'ultra' : thinkingLevel] : []),
375
+ ...(thinkingLevel ? ['--reasoning-effort', thinkingLevel] : []),
376
376
  prompt,
377
377
  ],
378
378
  };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { applicationPaths, authenticate, CREDENTIAL_REFRESH_INTERVAL_MS, credentialsNeedRefresh, exchangeLegacyToken, normalizeOrigin, readCredentials, recoverRejectedCredentials, refreshCredentials, removeCredentials, uploadSocketUrl, writeCredentials, } from "./auth.js";
7
7
  import { daemonStatus, installDaemon, uninstallDaemon } from "./daemon.js";
8
8
  import { normalizeAllowedDirectories } from "./directory-policy.js";
9
+ import { logError, logInfo } from "./logging.js";
9
10
  const HERE = path.dirname(fileURLToPath(import.meta.url));
10
11
  const CLI_DIRECTORY = path.basename(HERE) === 'dist' ? path.dirname(HERE) : HERE;
11
12
  const pkg = JSON.parse(fs.readFileSync(path.join(CLI_DIRECTORY, 'package.json'), 'utf8'));
@@ -50,11 +51,11 @@ export async function migrateLegacyCredentials(paths, origin, { legacyTokenFile
50
51
  const credentials = await exchangeLegacyToken(normalizeOrigin(origin || 'https://turns.dev'), legacyToken);
51
52
  writeCredentials(credentials, paths);
52
53
  fs.unlinkSync(legacyTokenFile);
53
- process.stdout.write(`Upgraded the existing uploader credential for @${credentials.user.login}.\n`);
54
+ logInfo(`Upgraded the existing uploader credential for @${credentials.user.login}.\n`);
54
55
  return credentials;
55
56
  }
56
57
  catch (error) {
57
- process.stderr.write(`Could not upgrade the existing uploader credential: ${error.message}\n`);
58
+ logError(`Could not upgrade the existing uploader credential: ${error.message}\n`);
58
59
  return null;
59
60
  }
60
61
  }
@@ -166,7 +167,7 @@ function credentialProvider(initial, { paths, interactive }) {
166
167
  };
167
168
  const timer = setInterval(() => {
168
169
  refreshIfDue().catch((error) => {
169
- process.stderr.write(`Turns credential refresh failed: ${error.message}; retrying later.\n`);
170
+ logError(`Turns credential refresh failed: ${error.message}; retrying later.\n`);
170
171
  });
171
172
  }, Math.min(CREDENTIAL_REFRESH_INTERVAL_MS, 60 * 60_000));
172
173
  timer.unref?.();
@@ -282,7 +283,7 @@ async function main(rawArguments = process.argv.slice(2)) {
282
283
  credentials = await authenticate({ origin: parsed.origin, paths, interactive: false });
283
284
  }
284
285
  if (allowedDirectories.length) {
285
- process.stdout.write(`Daemon directory allowlist:\n${allowedDirectories.map((value) => ` ${value}`).join('\n')}\n`);
286
+ logInfo(`Daemon directory allowlist:\n${allowedDirectories.map((value) => ` ${value}`).join('\n')}\n`);
286
287
  }
287
288
  await runUpload(credentials, {
288
289
  paths,
@@ -312,7 +313,7 @@ async function main(rawArguments = process.argv.slice(2)) {
312
313
  const selected = scopedWatchOptions(command === 'upload' ? argv[1] || process.cwd() : process.cwd(), {
313
314
  once: parsed.once,
314
315
  });
315
- process.stdout.write(selected.watch
316
+ logInfo(selected.watch
316
317
  ? `Watching sessions in ${selected.directory} as @${credentials.user.login}. Press Ctrl-C to stop.\n`
317
318
  : `Uploading sessions in ${selected.directory} as @${credentials.user.login}.\n`);
318
319
  await runUpload(credentials, {
@@ -330,7 +331,7 @@ try {
330
331
  catch { }
331
332
  if (invokedAsMain) {
332
333
  main().catch((error) => {
333
- process.stderr.write(`turns: ${error?.message || error}\n`);
334
+ logError(`turns: ${error?.message || error}\n`);
334
335
  process.exitCode = 1;
335
336
  });
336
337
  }
@@ -0,0 +1,14 @@
1
+ import process from 'node:process';
2
+ /** Callers supply complete log messages, including their trailing newline. */
3
+ export function formatLogMessage(message, now = new Date()) {
4
+ const prefix = `[${now.toISOString()}] `;
5
+ // Timestamp every physical line, but do not add a dangling prefix after the
6
+ // final newline. Keep indentation and use one time for the whole message.
7
+ return message.replace(/^(?=[\s\S])/gmu, prefix);
8
+ }
9
+ export function logInfo(message) {
10
+ return process.stdout.write(formatLogMessage(message));
11
+ }
12
+ export function logError(message) {
13
+ return process.stderr.write(formatLogMessage(message));
14
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { Transform } from 'node:stream';
5
+ import { logError } from "./logging.js";
5
6
  /**
6
7
  * Turning the paths an agent writes into things a reader can open.
7
8
  *
@@ -526,7 +527,7 @@ export function createPathResolver({ options, machine, baseUrl, appendResolution
526
527
  }
527
528
  const chain = (chains.get(sessionId) || Promise.resolve()).then(() => resolveBatch(sessionId, workingDirectory, fresh).catch((error) => {
528
529
  const message = error?.message || String(error);
529
- process.stderr.write(`Path resolution failed: ${redactor ? redactor.redactText(message) : message}\n`);
530
+ logError(`Path resolution failed: ${redactor ? redactor.redactText(message) : message}\n`);
530
531
  }));
531
532
  chains.set(sessionId, chain);
532
533
  return chain;
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { Readable } from 'node:stream';
6
6
  import { pipeline } from 'node:stream/promises';
7
+ import { logError } from "./logging.js";
7
8
  import { anchorId, connectExistingAgentAnchors, launchAgentAnchor, waitForAgentTurn } from "./agent-anchor-client.js";
8
9
  import { createCodexUpdateManager } from "./codex-update.js";
9
10
  import { isAsyncQuestionId } from "./codex-async-question-recovery.js";
@@ -384,7 +385,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
384
385
  catch (error) {
385
386
  const message = String(error?.message || error);
386
387
  state.failures = (state.failures || 0) + 1;
387
- process.stderr.write(`Runtime event append failed for ${sessionId}: ${message}\n`);
388
+ logError(`Runtime event append failed for ${sessionId}: ${message}\n`);
388
389
  // Socket failures are retried inside request(). Keep valid anchor
389
390
  // events across other transient Worker/R2 failures too. Only discard
390
391
  // a batch when the session is gone or the internally constructed row
@@ -399,7 +400,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
399
400
  state.failures = 0;
400
401
  }
401
402
  else if (state.failures >= MAX_RUNTIME_FLUSH_FAILURES) {
402
- process.stderr.write(`Dropping ${events.length} undeliverable runtime events for ${sessionId} after ` +
403
+ logError(`Dropping ${events.length} undeliverable runtime events for ${sessionId} after ` +
403
404
  `${state.failures} attempts: ${message}\n`);
404
405
  state.events.splice(0, events.length);
405
406
  state.failures = 0;
@@ -504,7 +505,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
504
505
  if (state === 'idle')
505
506
  await flushRuntime(sessionId);
506
507
  await reportHarness(sessionId, state, entry);
507
- })().catch((error) => process.stderr.write(`Harness status upload failed: ${error?.message || error}\n`));
508
+ })().catch((error) => logError(`Harness status upload failed: ${error?.message || error}\n`));
508
509
  }
509
510
  if (message.type === 'live_event') {
510
511
  const turnMessageId = String(message.messageId || entry.snapshot.currentTurn?.messageId || '');
@@ -633,7 +634,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
633
634
  // Dispatch deliberately does not wait for that publish: the queue preserves
634
635
  // order on its own, and a frame the Worker will not accept must not be able
635
636
  // to hold a claimed prompt back from the agent.
636
- void enqueueRuntime(sessionId, queuePromptBoundary(recovery?.entry || anchors.get(sessionId), claim), true).catch((error) => process.stderr.write(`Queue prompt boundary publish failed for ${sessionId}: ${error?.message || error}\n`));
637
+ void enqueueRuntime(sessionId, queuePromptBoundary(recovery?.entry || anchors.get(sessionId), claim), true).catch((error) => logError(`Queue prompt boundary publish failed for ${sessionId}: ${error?.message || error}\n`));
637
638
  let descriptor;
638
639
  let agentSessionId = '';
639
640
  let creating = false;
@@ -717,7 +718,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
717
718
  void reportHarness(sessionId, 'stopped', entry).catch(() => { });
718
719
  return;
719
720
  }
720
- void renewClaim().catch((error) => process.stderr.write(`Queue lease renewal failed: ${error?.message || error}\n`));
721
+ void renewClaim().catch((error) => logError(`Queue lease renewal failed: ${error?.message || error}\n`));
721
722
  // Never renew lifecycle state from an old in-memory snapshot. The
722
723
  // anchor may have disconnected since the claim began, which would
723
724
  // otherwise resurrect a canonical stopped edge as running.
@@ -820,7 +821,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
820
821
  queueMicrotask(() => void tryClaim(sessionId));
821
822
  }
822
823
  catch (error) {
823
- process.stderr.write(`Queue dispatch failed for ${sessionId}: ${error?.message || error}\n`);
824
+ logError(`Queue dispatch failed for ${sessionId}: ${error?.message || error}\n`);
824
825
  const held = claims.get(sessionId);
825
826
  stopClaimRenewal(held);
826
827
  if (held?.entry && !held.entry.connection.closed) {
@@ -895,7 +896,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
895
896
  await reportHarness(sessionId, 'stopped', held?.entry || entry).catch(() => { });
896
897
  }
897
898
  });
898
- process.stderr.write(`Anchor recovery failed for ${sessionId}: ${error?.message || error}\n`);
899
+ logError(`Anchor recovery failed for ${sessionId}: ${error?.message || error}\n`);
899
900
  });
900
901
  }
901
902
  };
@@ -1334,7 +1335,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
1334
1335
  // as their identity: a pane already showing them recognizes the
1335
1336
  // refresh instead of being offered its own save as a new version.
1336
1337
  contentEtag: String(edit.afterHash || ''),
1337
- }).catch((error) => process.stderr.write(`Post-edit refresh failed for ${applied.path}: ${error?.message || error}\n`));
1338
+ }).catch((error) => logError(`Post-edit refresh failed for ${applied.path}: ${error?.message || error}\n`));
1338
1339
  }
1339
1340
  replyRpc.notify({
1340
1341
  type: 'session.file.edit.result',
@@ -1420,7 +1421,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
1420
1421
  void tryClaim(sessionId);
1421
1422
  }
1422
1423
  catch (error) {
1423
- process.stderr.write(`Queue registration failed: ${error?.message || error}\n`);
1424
+ logError(`Queue registration failed: ${error?.message || error}\n`);
1424
1425
  }
1425
1426
  finally {
1426
1427
  registering.delete(control);
@@ -1439,7 +1440,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
1439
1440
  void tryClaim(sessionId);
1440
1441
  }
1441
1442
  catch (error) {
1442
- process.stderr.write(`Machine heartbeat failed: ${error?.message || error}\n`);
1443
+ logError(`Machine heartbeat failed: ${error?.message || error}\n`);
1443
1444
  }
1444
1445
  }
1445
1446
  }
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import process from 'node:process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import WebSocket from 'ws';
9
+ import { logError, logInfo } from "./logging.js";
9
10
  import { streamDeepseekTrajectoryLines } from "./deepseek-trajectories.js";
10
11
  import { normalizeAllowedDirectories, workingDirectoryIsAllowed } from "./directory-policy.js";
11
12
  import { discoverGitAssociation } from "./git-association.js";
@@ -131,7 +132,7 @@ function reportingRedactor(tokens) {
131
132
  return;
132
133
  const rules = Object.fromEntries([...counts].sort((left, right) => right[1] - left[1]));
133
134
  counts.clear();
134
- process.stderr.write(`Secret filtering applied (v${REDACTION_VERSION}): ${JSON.stringify(rules)}\n`);
135
+ logError(`Secret filtering applied (v${REDACTION_VERSION}): ${JSON.stringify(rules)}\n`);
135
136
  };
136
137
  const timer = setInterval(report, 60_000);
137
138
  timer.unref?.();
@@ -690,7 +691,7 @@ export async function connectUploader(options, machine, reconnect = false) {
690
691
  });
691
692
  try {
692
693
  const ready = await rpc.ready;
693
- process.stdout.write(`${reconnect ? 'Reconnected' : 'Connected'} to ${options.url} as @${ready.user.login} ` +
694
+ logInfo(`${reconnect ? 'Reconnected' : 'Connected'} to ${options.url} as @${ready.user.login} ` +
694
695
  `from ${os.hostname()} (${machine})\n`);
695
696
  return rpc;
696
697
  }
@@ -700,14 +701,14 @@ export async function connectUploader(options, machine, reconnect = false) {
700
701
  authenticationRecoveries += 1;
701
702
  const credentials = await options.credentialProvider.recover();
702
703
  options.token = credentials.accessToken;
703
- process.stderr.write('Uploader authentication was rejected; refreshed credentials.\n');
704
+ logError('Uploader authentication was rejected; refreshed credentials.\n');
704
705
  attempt -= 1;
705
706
  continue;
706
707
  }
707
708
  if (!isSocketError(error) || attempt === MAX_SOCKET_RETRIES)
708
709
  throw error;
709
710
  const delay = reconnectDelay(attempt);
710
- process.stderr.write(` WebSocket connection failed: ${error.message}; retrying in ` +
711
+ logError(` WebSocket connection failed: ${error.message}; retrying in ` +
711
712
  `${(delay / 1000).toFixed(1)}s (${attempt + 1}/${MAX_SOCKET_RETRIES})\n`);
712
713
  await wait(delay);
713
714
  }
@@ -756,7 +757,7 @@ export class SharedUploaderConnection {
756
757
  this.reconnectTimer = setTimeout(() => {
757
758
  this.reconnectTimer = null;
758
759
  void this.current().catch((error) => {
759
- process.stderr.write(`Shared uploader reconnect failed: ${error?.message || error}\n`);
760
+ logError(`Shared uploader reconnect failed: ${error?.message || error}\n`);
760
761
  this.scheduleReconnect();
761
762
  });
762
763
  }, 1_000);
@@ -921,7 +922,7 @@ async function uploadFile(rpc, machine, root, capture, redactor = DEFAULT_REDACT
921
922
  }
922
923
  nextRow = Number(acknowledgement.nextRow);
923
924
  sent += batch.rows.length;
924
- process.stdout.write(` ${capture.relative}: ${nextRow}/${rows.length} rows · revision ${acknowledgement.revision}${acknowledgement.archived ? ' · archived' : ''}\n`);
925
+ logInfo(` ${capture.relative}: ${nextRow}/${rows.length} rows · revision ${acknowledgement.revision}${acknowledgement.archived ? ' · archived' : ''}\n`);
925
926
  }
926
927
  return { rows: rows.length, sent, skipped: false };
927
928
  }
@@ -1372,7 +1373,7 @@ async function reportExternalManagement(rpc, machine, descriptors, previous = ne
1372
1373
  managedKeys = externallyManagedNativeDescriptorKeys(descriptors);
1373
1374
  }
1374
1375
  catch (error) {
1375
- process.stderr.write(`External ownership scan failed: ${error?.message || error}\n`);
1376
+ logError(`External ownership scan failed: ${error?.message || error}\n`);
1376
1377
  return { rpc, managed: null };
1377
1378
  }
1378
1379
  const observedAt = new Date().toISOString();
@@ -1391,7 +1392,7 @@ async function reportExternalManagement(rpc, machine, descriptors, previous = ne
1391
1392
  });
1392
1393
  }
1393
1394
  catch (error) {
1394
- process.stderr.write(`External ownership upload failed: ${error?.message || error}\n`);
1395
+ logError(`External ownership upload failed: ${error?.message || error}\n`);
1395
1396
  return { managed: null, previous };
1396
1397
  }
1397
1398
  }
@@ -1408,11 +1409,11 @@ async function watchExternalManagement(rpc, machine, getDescriptors) {
1408
1409
  previous = reported.previous;
1409
1410
  if (reported.managed !== null && reported.managed !== lastManagedCount) {
1410
1411
  lastManagedCount = reported.managed;
1411
- process.stdout.write(`External ownership: ${reported.managed} managed session${reported.managed === 1 ? '' : 's'}\n`);
1412
+ logInfo(`External ownership: ${reported.managed} managed session${reported.managed === 1 ? '' : 's'}\n`);
1412
1413
  }
1413
1414
  }
1414
1415
  catch (error) {
1415
- process.stderr.write(`External ownership watcher failed: ${error?.message || error}\n`);
1416
+ logError(`External ownership watcher failed: ${error?.message || error}\n`);
1416
1417
  }
1417
1418
  await wait(NATIVE_WATCH_INTERVAL_MS);
1418
1419
  }
@@ -1456,9 +1457,9 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1456
1457
  const byHarness = new Map();
1457
1458
  for (const session of initialSessions)
1458
1459
  byHarness.set(session.harness, (byHarness.get(session.harness) || 0) + 1);
1459
- process.stdout.write(`Native inventory: ${initialSessions.length} sessions across ${byHarness.size} harnesses\n`);
1460
+ logInfo(`Native inventory: ${initialSessions.length} sessions across ${byHarness.size} harnesses\n`);
1460
1461
  for (const [harness, count] of [...byHarness].sort())
1461
- process.stdout.write(` ${harness}: ${count}\n`);
1462
+ logInfo(` ${harness}: ${count}\n`);
1462
1463
  if (options.dryRun)
1463
1464
  return;
1464
1465
  const state = createNativeUploadState(options.nativeStateFile, machine);
@@ -1586,7 +1587,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1586
1587
  stableSessionId: stableNativeSessionId,
1587
1588
  redactor,
1588
1589
  }).catch((error) => {
1589
- process.stderr.write(`Queue worker stopped unexpectedly: ${error?.stack || error}\n`);
1590
+ logError(`Queue worker stopped unexpectedly: ${error?.stack || error}\n`);
1590
1591
  });
1591
1592
  }
1592
1593
  const totals = { current: 0, uploaded: 0, rows: 0, empty: 0, failed: 0 };
@@ -1623,7 +1624,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1623
1624
  const message = error?.message || String(error);
1624
1625
  if (message !== uploadProgressError) {
1625
1626
  uploadProgressError = message;
1626
- process.stderr.write(`Upload progress report failed: ${message}\n`);
1627
+ logError(`Upload progress report failed: ${message}\n`);
1627
1628
  }
1628
1629
  }
1629
1630
  };
@@ -1685,7 +1686,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1685
1686
  cursor = {};
1686
1687
  cursors.set(sessionId, cursor);
1687
1688
  seen.delete(sessionId);
1688
- process.stdout.write(` ${descriptor.harness} ${descriptor.sourcePath}: reset for ${redactionUpgrade ? 'secret-filtered' : 'full'} replay\n`);
1689
+ logInfo(` ${descriptor.harness} ${descriptor.sourcePath}: reset for ${redactionUpgrade ? 'secret-filtered' : 'full'} replay\n`);
1689
1690
  }
1690
1691
  result = await uploadNativeSession(rpc, machine, descriptor, cursor, (progress) => {
1691
1692
  // Persist an explicitly incomplete fingerprint while a long replay
@@ -1693,7 +1694,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1693
1694
  // but the empty fingerprint still schedules the unfinished tail.
1694
1695
  state.set(sessionId, '', cursor, REDACTION_VERSION);
1695
1696
  scheduleUploadProgress();
1696
- process.stdout.write(` ${index + 1}/${changed.length} ${descriptor.harness}: ` +
1697
+ logInfo(` ${index + 1}/${changed.length} ${descriptor.harness}: ` +
1697
1698
  `${progress.nextRow}/${progress.total} rows` +
1698
1699
  `${progress.archived ? ' · archived' : ''}\n`);
1699
1700
  }, redactor, {
@@ -1717,7 +1718,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1717
1718
  failure = error;
1718
1719
  break;
1719
1720
  }
1720
- process.stderr.write(` ${index + 1}/${changed.length} ${descriptor.harness}: ${error.message}; ` +
1721
+ logError(` ${index + 1}/${changed.length} ${descriptor.harness}: ${error.message}; ` +
1721
1722
  `reconnecting (${attempt + 1}/${MAX_SOCKET_RETRIES})\n`);
1722
1723
  rpc.close();
1723
1724
  await wait(reconnectDelay(attempt));
@@ -1734,7 +1735,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1734
1735
  failures.delete(sessionId);
1735
1736
  state.tombstone(sessionId);
1736
1737
  totals.current += 1;
1737
- process.stdout.write(` ${index + 1}/${changed.length} ${descriptor.harness}: deleted on turns.dev\n`);
1738
+ logInfo(` ${index + 1}/${changed.length} ${descriptor.harness}: deleted on turns.dev\n`);
1738
1739
  scheduleUploadProgress();
1739
1740
  continue;
1740
1741
  }
@@ -1759,7 +1760,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1759
1760
  totals.rows += result.sent;
1760
1761
  }
1761
1762
  if (!result.skipped || (index + 1) % 50 === 0 || index + 1 === changed.length) {
1762
- process.stdout.write(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ` +
1763
+ logInfo(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ` +
1763
1764
  `${result.empty ? 'no conversation rows' : result.skipped ? `current (${result.rows} rows)` : `${result.sent} rows uploaded`}\n`);
1764
1765
  }
1765
1766
  scheduleUploadProgress();
@@ -1774,7 +1775,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1774
1775
  attempts,
1775
1776
  retryAt: Date.now() + nativeFailureRetryDelay(attempts),
1776
1777
  });
1777
- process.stderr.write(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ${error?.message || error}\n`);
1778
+ logError(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ${error?.message || error}\n`);
1778
1779
  scheduleUploadProgress();
1779
1780
  }
1780
1781
  }
@@ -1800,7 +1801,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1800
1801
  rememberInventory(sessions);
1801
1802
  const unsettled = changedSessions();
1802
1803
  if (unsettled.length) {
1803
- process.stderr.write(`${unsettled.length} native sessions kept changing; run \`turns\` without --once to follow them continuously.\n`);
1804
+ logError(`${unsettled.length} native sessions kept changing; run \`turns\` without --once to follow them continuously.\n`);
1804
1805
  }
1805
1806
  break;
1806
1807
  }
@@ -1816,9 +1817,9 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1816
1817
  await reportUploadProgress(true);
1817
1818
  rpc.close();
1818
1819
  totals.failed = failures.size;
1819
- process.stdout.write(`Native upload complete: ${JSON.stringify(totals)}\n`);
1820
+ logInfo(`Native upload complete: ${JSON.stringify(totals)}\n`);
1820
1821
  if (failures.size) {
1821
- process.stderr.write(`${JSON.stringify({ failures: [...failures.values()] }, null, 2)}\n`);
1822
+ logError(`${JSON.stringify({ failures: [...failures.values()] }, null, 2)}\n`);
1822
1823
  process.exitCode = 1;
1823
1824
  }
1824
1825
  return;
@@ -1962,7 +1963,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1962
1963
  shutdown();
1963
1964
  process.exit(0);
1964
1965
  });
1965
- process.stdout.write(`Watching ${initialSessions.length} native sessions via ${nativeMonitor.count} directories; ` +
1966
+ logInfo(`Watching ${initialSessions.length} native sessions via ${nativeMonitor.count} directories; ` +
1966
1967
  `polling known files every ${NATIVE_POLL_INTERVAL_MS / 1000}s and auditing every ` +
1967
1968
  `${NATIVE_DISCOVERY_INTERVAL_MS / 1000}s…\n`);
1968
1969
  let historicalSessionId = '';
@@ -1979,7 +1980,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1979
1980
  scheduleUploadProgress();
1980
1981
  }
1981
1982
  catch (error) {
1982
- process.stderr.write(`Native inventory refresh failed: ${error?.message || error}\n`);
1983
+ logError(`Native inventory refresh failed: ${error?.message || error}\n`);
1983
1984
  }
1984
1985
  }
1985
1986
  const changed = changedSessions();
@@ -2064,7 +2065,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
2064
2065
  const result = await uploadFile(rpc, machine, options.runs, capture, filtering.redactor);
2065
2066
  seen.set(capture.file, fingerprint);
2066
2067
  if (result.skipped)
2067
- process.stdout.write(` ${capture.relative}: current (${result.rows} rows)\n`);
2068
+ logInfo(` ${capture.relative}: current (${result.rows} rows)\n`);
2068
2069
  }
2069
2070
  };
2070
2071
  await sweep();
@@ -2072,7 +2073,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
2072
2073
  rpc.close();
2073
2074
  return;
2074
2075
  }
2075
- process.stdout.write('Watching for appended capture rows…\n');
2076
+ logInfo('Watching for appended capture rows…\n');
2076
2077
  for (;;) {
2077
2078
  await new Promise((resolve) => setTimeout(resolve, 2_000));
2078
2079
  await sweep();
@@ -2085,7 +2086,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
2085
2086
  }
2086
2087
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
2087
2088
  runUploader().catch((error) => {
2088
- process.stderr.write(`${error?.stack || error}\n`);
2089
+ logError(`${error?.stack || error}\n`);
2089
2090
  process.exitCode = 1;
2090
2091
  });
2091
2092
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",