rudel 0.2.1 → 0.2.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +352 -132
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1887,7 +1887,7 @@ async function run(app, inputs, context) {
1887
1887
  // package.json
1888
1888
  var package_default = {
1889
1889
  name: "rudel",
1890
- version: "0.2.1",
1890
+ version: "0.2.3",
1891
1891
  type: "module",
1892
1892
  description: "CLI for the Coding Agent Analytics Platform rudel.ai",
1893
1893
  license: "MIT",
@@ -1951,9 +1951,9 @@ var package_default = {
1951
1951
  };
1952
1952
 
1953
1953
  // ../../packages/agent-adapters/src/adapters/claude-code/index.ts
1954
- import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
1954
+ import { readdir as readdir2, readFile as readFile2, realpath, stat } from "node:fs/promises";
1955
1955
  import { homedir as homedir2 } from "node:os";
1956
- import { dirname as dirname2, join as join3 } from "node:path";
1956
+ import { dirname as dirname2, isAbsolute, join as join3, relative, sep } from "node:path";
1957
1957
 
1958
1958
  // ../../node_modules/zod/v3/external.js
1959
1959
  var exports_external = {};
@@ -6070,12 +6070,31 @@ async function ingestRudelCodexSessions(ingestor, rows, options) {
6070
6070
  const data = options?.validate ? rows.map((row) => RudelCodexSessionsRowSchema.parse(row)) : rows;
6071
6071
  await ingestor.insert({ table: "rudel.codex_sessions", values: data });
6072
6072
  }
6073
+ // ../../packages/agent-adapters/src/errors.ts
6074
+ var SOURCE_DISPLAY_NAMES = {
6075
+ claude_code: "Claude Code",
6076
+ codex: "Codex"
6077
+ };
6078
+ function getMissingTranscriptTimestampMessage(source) {
6079
+ return `${SOURCE_DISPLAY_NAMES[source]} transcript contains no valid timestamp`;
6080
+ }
6081
+ function isMissingTranscriptTimestampMessage(source, message) {
6082
+ return message === getMissingTranscriptTimestampMessage(source);
6083
+ }
6084
+
6085
+ class MissingTranscriptTimestampError extends Error {
6086
+ constructor(source) {
6087
+ super(getMissingTranscriptTimestampMessage(source));
6088
+ this.name = "MissingTranscriptTimestampError";
6089
+ }
6090
+ }
6091
+
6073
6092
  // ../../packages/agent-adapters/src/utils.ts
6074
6093
  import { readdir, readFile } from "node:fs/promises";
6075
6094
  import { homedir } from "node:os";
6076
6095
  import { join } from "node:path";
6077
6096
  function toClickHouseDateTime(isoString) {
6078
- return isoString.replace("T", " ").replace("Z", "").replace(/\+.*$/, "");
6097
+ return new Date(isoString).toISOString().replace("T", " ").replace("Z", "");
6079
6098
  }
6080
6099
  async function readFileWithRetry(filePath, maxRetries = 5) {
6081
6100
  const delayMs = 500;
@@ -6210,6 +6229,7 @@ function removeHook() {
6210
6229
 
6211
6230
  // ../../packages/agent-adapters/src/adapters/claude-code/index.ts
6212
6231
  var SESSIONS_BASE_DIR = join3(homedir2(), ".claude", "projects");
6232
+ var SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_-]{1,200}$/;
6213
6233
  function encodeProjectPath(projectPath) {
6214
6234
  return projectPath.replace(/\//g, "-");
6215
6235
  }
@@ -6254,8 +6274,11 @@ function extractAgentIds(sessionContent) {
6254
6274
  continue;
6255
6275
  try {
6256
6276
  const entry = JSON.parse(line);
6257
- if (entry.toolUseResult?.agentId) {
6258
- agentIds.add(entry.toolUseResult.agentId);
6277
+ if (!isRecord(entry) || !isRecord(entry.toolUseResult))
6278
+ continue;
6279
+ const agentId = entry.toolUseResult.agentId;
6280
+ if (isSafeBasename(agentId)) {
6281
+ agentIds.add(agentId);
6259
6282
  }
6260
6283
  } catch {}
6261
6284
  }
@@ -6263,12 +6286,19 @@ function extractAgentIds(sessionContent) {
6263
6286
  }
6264
6287
  async function readSubagentFiles(sessionDir, agentIds, sessionId) {
6265
6288
  const subagents = [];
6289
+ const subagentDirs = await resolveSubagentDirectories(sessionDir, sessionId);
6266
6290
  for (const agentId of agentIds) {
6267
- const possiblePaths = [
6268
- join3(sessionDir, `agent-${agentId}.jsonl`),
6269
- ...sessionId ? [join3(sessionDir, sessionId, "subagents", `agent-${agentId}.jsonl`)] : []
6270
- ];
6271
- for (const agentPath of possiblePaths) {
6291
+ if (!isSafeBasename(agentId))
6292
+ continue;
6293
+ for (const subagentDir of subagentDirs) {
6294
+ let agentPath;
6295
+ try {
6296
+ agentPath = await realpath(join3(subagentDir, `agent-${agentId}.jsonl`));
6297
+ } catch {
6298
+ continue;
6299
+ }
6300
+ if (!isContainedPath(subagentDir, agentPath))
6301
+ continue;
6272
6302
  try {
6273
6303
  const content = await readFile2(agentPath, "utf-8");
6274
6304
  subagents.push({ agentId, content });
@@ -6343,6 +6373,9 @@ class ClaudeCodeAdapter {
6343
6373
  }
6344
6374
  async buildUploadRequest(session, context) {
6345
6375
  const content = await readFileWithRetry(session.transcriptPath);
6376
+ if (!this.extractTimestamps(content)) {
6377
+ throw new MissingTranscriptTimestampError(this.source);
6378
+ }
6346
6379
  const agentIds = extractAgentIds(content);
6347
6380
  const sessionDir = dirname2(session.transcriptPath);
6348
6381
  const subagents = agentIds.length > 0 ? await readSubagentFiles(sessionDir, agentIds, session.sessionId) : [];
@@ -6365,6 +6398,8 @@ class ClaudeCodeAdapter {
6365
6398
  extractTimestamps(content) {
6366
6399
  let min = null;
6367
6400
  let max = null;
6401
+ let minTime = Number.POSITIVE_INFINITY;
6402
+ let maxTime = Number.NEGATIVE_INFINITY;
6368
6403
  for (const line of content.split(`
6369
6404
  `)) {
6370
6405
  if (!line)
@@ -6375,12 +6410,23 @@ class ClaudeCodeAdapter {
6375
6410
  } catch {
6376
6411
  continue;
6377
6412
  }
6378
- if ((parsed.type === "user" || parsed.type === "assistant") && parsed.timestamp) {
6379
- const ts = parsed.timestamp;
6380
- if (!min || ts < min)
6381
- min = ts;
6382
- if (!max || ts > max)
6383
- max = ts;
6413
+ if (!isRecord(parsed))
6414
+ continue;
6415
+ if (parsed.type !== "user" && parsed.type !== "assistant")
6416
+ continue;
6417
+ if (typeof parsed.timestamp !== "string")
6418
+ continue;
6419
+ const timestampTime = Date.parse(parsed.timestamp);
6420
+ if (!Number.isFinite(timestampTime))
6421
+ continue;
6422
+ const timestamp = new Date(timestampTime).toISOString();
6423
+ if (timestampTime < minTime) {
6424
+ min = timestamp;
6425
+ minTime = timestampTime;
6426
+ }
6427
+ if (timestampTime > maxTime) {
6428
+ max = timestamp;
6429
+ maxTime = timestampTime;
6384
6430
  }
6385
6431
  }
6386
6432
  if (!min || !max)
@@ -6399,10 +6445,13 @@ class ClaudeCodeAdapter {
6399
6445
  subagents[sub.agentId] = sub.content;
6400
6446
  }
6401
6447
  }
6402
- const timestamps = this.extractTimestamps(input.content);
6448
+ const timestamps = context.timestamps ?? this.extractTimestamps(input.content);
6449
+ if (!timestamps) {
6450
+ throw new MissingTranscriptTimestampError(this.source);
6451
+ }
6403
6452
  return {
6404
- session_date: timestamps ? toClickHouseDateTime(timestamps.sessionDate) : now,
6405
- last_interaction_date: timestamps ? toClickHouseDateTime(timestamps.lastInteractionDate) : now,
6453
+ session_date: toClickHouseDateTime(timestamps.sessionDate),
6454
+ last_interaction_date: toClickHouseDateTime(timestamps.lastInteractionDate),
6406
6455
  session_id: input.sessionId,
6407
6456
  organization_id: context.organizationId,
6408
6457
  project_path: input.projectPath,
@@ -6451,6 +6500,35 @@ class ClaudeCodeAdapter {
6451
6500
  }
6452
6501
  }
6453
6502
  var claudeCodeAdapter = new ClaudeCodeAdapter;
6503
+ function isRecord(value) {
6504
+ return typeof value === "object" && value !== null;
6505
+ }
6506
+ function isSafeBasename(value) {
6507
+ return typeof value === "string" && SAFE_BASENAME_PATTERN.test(value);
6508
+ }
6509
+ async function resolveSubagentDirectories(sessionDir, sessionId) {
6510
+ let canonicalSessionDir;
6511
+ try {
6512
+ canonicalSessionDir = await realpath(sessionDir);
6513
+ } catch {
6514
+ return [];
6515
+ }
6516
+ const directories = [canonicalSessionDir];
6517
+ if (!isSafeBasename(sessionId)) {
6518
+ return directories;
6519
+ }
6520
+ try {
6521
+ const nestedDir = await realpath(join3(canonicalSessionDir, sessionId, "subagents"));
6522
+ if (isContainedPath(canonicalSessionDir, nestedDir)) {
6523
+ directories.push(nestedDir);
6524
+ }
6525
+ } catch {}
6526
+ return directories;
6527
+ }
6528
+ function isContainedPath(parentPath, candidatePath) {
6529
+ const pathFromParent = relative(parentPath, candidatePath);
6530
+ return pathFromParent !== "" && pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent);
6531
+ }
6454
6532
  // ../../packages/agent-adapters/src/adapters/codex/index.ts
6455
6533
  import { readFile as readFile3 } from "node:fs/promises";
6456
6534
  import { homedir as homedir4 } from "node:os";
@@ -6598,7 +6676,7 @@ function skipVoid(str, ptr, banNewLines, banComments) {
6598
6676
  ptr++;
6599
6677
  return banComments || c !== "#" ? ptr : skipVoid(str, skipComment(str, ptr), banNewLines);
6600
6678
  }
6601
- function skipUntil(str, ptr, sep, end, banNewLines = false) {
6679
+ function skipUntil(str, ptr, sep2, end, banNewLines = false) {
6602
6680
  if (!end) {
6603
6681
  ptr = indexOfNewline(str, ptr);
6604
6682
  return ptr < 0 ? str.length : ptr;
@@ -6607,7 +6685,7 @@ function skipUntil(str, ptr, sep, end, banNewLines = false) {
6607
6685
  let c = str[i];
6608
6686
  if (c === "#") {
6609
6687
  i = indexOfNewline(str, i);
6610
- } else if (c === sep) {
6688
+ } else if (c === sep2) {
6611
6689
  return i + 1;
6612
6690
  } else if (c === end || banNewLines && (c === `
6613
6691
  ` || c === "\r" && str[i + 1] === `
@@ -7680,6 +7758,9 @@ class CodexAdapter {
7680
7758
  }
7681
7759
  async buildUploadRequest(session, context) {
7682
7760
  const content = await readFile3(session.transcriptPath, "utf-8");
7761
+ if (!this.extractTimestamps(content)) {
7762
+ throw new MissingTranscriptTimestampError(this.source);
7763
+ }
7683
7764
  return {
7684
7765
  source: this.source,
7685
7766
  sessionId: session.sessionId,
@@ -7698,6 +7779,8 @@ class CodexAdapter {
7698
7779
  extractTimestamps(content) {
7699
7780
  let min = null;
7700
7781
  let max = null;
7782
+ let minTime = Number.POSITIVE_INFINITY;
7783
+ let maxTime = Number.NEGATIVE_INFINITY;
7701
7784
  for (const line of content.split(`
7702
7785
  `)) {
7703
7786
  if (!line)
@@ -7708,12 +7791,19 @@ class CodexAdapter {
7708
7791
  } catch {
7709
7792
  continue;
7710
7793
  }
7711
- if (parsed.timestamp) {
7712
- const ts = parsed.timestamp;
7713
- if (!min || ts < min)
7714
- min = ts;
7715
- if (!max || ts > max)
7716
- max = ts;
7794
+ if (!isRecord2(parsed) || typeof parsed.timestamp !== "string")
7795
+ continue;
7796
+ const timestampTime = Date.parse(parsed.timestamp);
7797
+ if (!Number.isFinite(timestampTime))
7798
+ continue;
7799
+ const timestamp = new Date(timestampTime).toISOString();
7800
+ if (timestampTime < minTime) {
7801
+ min = timestamp;
7802
+ minTime = timestampTime;
7803
+ }
7804
+ if (timestampTime > maxTime) {
7805
+ max = timestamp;
7806
+ maxTime = timestampTime;
7717
7807
  }
7718
7808
  }
7719
7809
  if (!min || !max)
@@ -7726,10 +7816,13 @@ class CodexAdapter {
7726
7816
  }
7727
7817
  buildRow(input, context) {
7728
7818
  const now = context.ingestedAt.toISOString().replace("Z", "");
7729
- const timestamps = this.extractTimestamps(input.content);
7819
+ const timestamps = context.timestamps ?? this.extractTimestamps(input.content);
7820
+ if (!timestamps) {
7821
+ throw new MissingTranscriptTimestampError(this.source);
7822
+ }
7730
7823
  return {
7731
- session_date: timestamps ? toClickHouseDateTime(timestamps.sessionDate) : now,
7732
- last_interaction_date: timestamps ? toClickHouseDateTime(timestamps.lastInteractionDate) : now,
7824
+ session_date: toClickHouseDateTime(timestamps.sessionDate),
7825
+ last_interaction_date: toClickHouseDateTime(timestamps.lastInteractionDate),
7733
7826
  session_id: input.sessionId,
7734
7827
  organization_id: context.organizationId,
7735
7828
  project_path: input.projectPath,
@@ -7747,6 +7840,9 @@ class CodexAdapter {
7747
7840
  }
7748
7841
  }
7749
7842
  var codexAdapter = new CodexAdapter;
7843
+ function isRecord2(value) {
7844
+ return typeof value === "object" && value !== null;
7845
+ }
7750
7846
  // ../../packages/agent-adapters/src/registry.ts
7751
7847
  import { existsSync as existsSync3 } from "node:fs";
7752
7848
  var adapters = new Map;
@@ -11021,7 +11117,6 @@ var SessionAnalyticsSchema = exports_external.object({
11021
11117
  skills: exports_external.array(exports_external.string()),
11022
11118
  slash_commands: exports_external.array(exports_external.string()),
11023
11119
  has_commit: exports_external.boolean(),
11024
- session_archetype: exports_external.string(),
11025
11120
  model_used: exports_external.string(),
11026
11121
  used_plan_mode: exports_external.boolean(),
11027
11122
  source: SourceSchema.optional()
@@ -11044,6 +11139,8 @@ var SessionAnalyticsSummaryComparisonSchema = exports_external.object({
11044
11139
  })
11045
11140
  });
11046
11141
  var SessionListInputSchema = DaysInputSchema.extend({
11142
+ startDate: exports_external.string().date().optional(),
11143
+ endDate: exports_external.string().date().optional(),
11047
11144
  userId: exports_external.string().max(MAX_ID_FILTER_LENGTH).optional(),
11048
11145
  projectPath: exports_external.string().max(MAX_PATH_FILTER_LENGTH).optional(),
11049
11146
  repository: exports_external.string().max(MAX_PATH_FILTER_LENGTH).optional(),
@@ -11057,7 +11154,6 @@ var VALID_DIMENSIONS = [
11057
11154
  "user_id",
11058
11155
  "project_path",
11059
11156
  "repository",
11060
- "session_archetype",
11061
11157
  "model_used",
11062
11158
  "has_commit",
11063
11159
  "used_plan_mode",
@@ -11111,7 +11207,6 @@ var SessionDetailSchema = exports_external.object({
11111
11207
  success_score: exports_external.number().optional(),
11112
11208
  duration_min: exports_external.number().optional(),
11113
11209
  total_interactions: exports_external.number().optional(),
11114
- session_archetype: exports_external.string().optional(),
11115
11210
  model_used: exports_external.string().optional(),
11116
11211
  source: SourceSchema.optional()
11117
11212
  });
@@ -11367,74 +11462,90 @@ var WrappedDecimalClaimEntitlementSchema = exports_external.object({
11367
11462
  });
11368
11463
 
11369
11464
  // ../../packages/api-routes/src/schemas/wrapped-share.ts
11465
+ var WRAPPED_SHARE_RESOURCE_LIMITS = {
11466
+ backMetricCount: 20,
11467
+ classNameLength: 512,
11468
+ imageUrlLength: 2048,
11469
+ snapshotBytes: 64 * 1024,
11470
+ socialImageBytes: 1024 * 1024,
11471
+ statItemCount: 8,
11472
+ textLength: 256
11473
+ };
11474
+ var WRAPPED_SHARE_SOCIAL_IMAGE_DATA_URL_PREFIX = "data:image/png;base64,";
11475
+ var WRAPPED_SHARE_SOCIAL_IMAGE_DATA_URL_MAX_LENGTH = WRAPPED_SHARE_SOCIAL_IMAGE_DATA_URL_PREFIX.length + Math.ceil(WRAPPED_SHARE_RESOURCE_LIMITS.socialImageBytes / 3) * 4;
11476
+ var WrappedShareTextSchema = exports_external.string().max(WRAPPED_SHARE_RESOURCE_LIMITS.textLength);
11477
+ var WrappedShareRequiredTextSchema = WrappedShareTextSchema.min(1);
11370
11478
  var WrappedShareThemeSchema = exports_external.enum(["dark", "light", "muted"]);
11371
11479
  var WrappedShareVariantSchema = exports_external.enum(["normal", "decimal"]);
11372
11480
  var WrappedShareLayoutModeSchema = exports_external.enum(["front", "front_back"]);
11373
11481
  var WrappedShareIdSchema = exports_external.string().trim().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u);
11374
11482
  var WrappedShareHeaderMetricSchema = exports_external.object({
11375
- label: exports_external.string().optional(),
11376
- title: exports_external.string().optional(),
11377
- value: exports_external.string().min(1)
11378
- });
11483
+ label: WrappedShareTextSchema.optional(),
11484
+ title: WrappedShareTextSchema.optional(),
11485
+ value: WrappedShareRequiredTextSchema
11486
+ }).strict();
11379
11487
  var WrappedShareStatItemIconSchema = exports_external.enum(["claude", "codex"]);
11380
11488
  var WrappedShareStatItemSchema = exports_external.object({
11381
11489
  icon: WrappedShareStatItemIconSchema.optional(),
11382
- key: exports_external.string().min(1),
11383
- label: exports_external.string().optional(),
11384
- title: exports_external.string().optional(),
11385
- value: exports_external.string().min(1)
11386
- });
11490
+ key: WrappedShareRequiredTextSchema.regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/u),
11491
+ label: WrappedShareTextSchema.optional(),
11492
+ title: WrappedShareTextSchema.optional(),
11493
+ value: WrappedShareRequiredTextSchema
11494
+ }).strict();
11387
11495
  var WrappedShareBackMetricSchema = exports_external.object({
11388
- label: exports_external.string(),
11496
+ label: WrappedShareTextSchema,
11389
11497
  slot: exports_external.enum(["body", "footer"]).optional(),
11390
- value: exports_external.string().min(1)
11391
- });
11498
+ value: WrappedShareRequiredTextSchema
11499
+ }).strict();
11392
11500
  var WrappedShareRevealMetricsSchema = exports_external.object({
11393
11501
  avgSessionMin: exports_external.number().nonnegative().nullable(),
11394
11502
  commitRate: exports_external.number().min(0).max(100).nullable(),
11395
11503
  daysSinceFirst: exports_external.number().nonnegative(),
11396
11504
  distinctProjectCount: exports_external.number().nonnegative(),
11397
11505
  longestSessionMin: exports_external.number().nonnegative().nullable()
11398
- });
11506
+ }).strict();
11399
11507
  var WrappedShareAppearanceSchema = exports_external.object({
11400
11508
  layoutMode: WrappedShareLayoutModeSchema,
11401
11509
  showArchetypeLabel: exports_external.boolean()
11510
+ }).strict();
11511
+ var WrappedShareSocialImageDataUrlSchema = exports_external.string().max(WRAPPED_SHARE_SOCIAL_IMAGE_DATA_URL_MAX_LENGTH).regex(/^data:image\/png;base64,[A-Za-z0-9+/]+={0,2}$/u).refine(isWrappedShareSocialImageWithinByteLimit, {
11512
+ message: `Wrapped share social image must be at most ${WRAPPED_SHARE_RESOURCE_LIMITS.socialImageBytes} bytes`
11402
11513
  });
11403
- var WrappedShareSocialImageDataUrlSchema = exports_external.string().max(7000000).regex(/^data:image\/png;base64,[A-Za-z0-9+/]+={0,2}$/u);
11404
11514
  var WrappedShareRowSchema = exports_external.object({
11405
11515
  activeDays: exports_external.number().nonnegative(),
11406
11516
  cost: exports_external.number().nonnegative(),
11407
- displayName: exports_external.string().min(1),
11408
- favoriteModel: exports_external.string().nullable(),
11517
+ displayName: WrappedShareRequiredTextSchema,
11518
+ favoriteModel: WrappedShareTextSchema.nullable(),
11409
11519
  hasActivity: exports_external.boolean(),
11410
- imageUrl: exports_external.string().nullable(),
11520
+ imageUrl: exports_external.string().max(WRAPPED_SHARE_RESOURCE_LIMITS.imageUrlLength).nullable(),
11411
11521
  inputTokens: exports_external.number().nonnegative(),
11412
- lastActiveDate: exports_external.string().nullable(),
11522
+ lastActiveDate: WrappedShareTextSchema.nullable(),
11413
11523
  outputTokens: exports_external.number().nonnegative(),
11414
- role: exports_external.string().min(1),
11524
+ role: WrappedShareRequiredTextSchema,
11415
11525
  totalSessions: exports_external.number().nonnegative(),
11416
11526
  totalTokens: exports_external.number().nonnegative()
11417
- });
11418
- var WrappedShareSnapshotSchema = exports_external.object({
11527
+ }).strict();
11528
+ var WrappedShareSnapshotObjectSchema = exports_external.object({
11419
11529
  appearance: WrappedShareAppearanceSchema.optional(),
11420
- archetypeLabel: exports_external.string().min(1),
11421
- backMetrics: exports_external.array(WrappedShareBackMetricSchema).optional(),
11530
+ archetypeLabel: WrappedShareRequiredTextSchema,
11531
+ backMetrics: exports_external.array(WrappedShareBackMetricSchema).max(WRAPPED_SHARE_RESOURCE_LIMITS.backMetricCount).optional(),
11422
11532
  headerLeftMetric: WrappedShareHeaderMetricSchema.optional(),
11423
11533
  headerRightMetric: WrappedShareHeaderMetricSchema.optional(),
11424
11534
  revealMetrics: WrappedShareRevealMetricsSchema.optional(),
11425
11535
  row: WrappedShareRowSchema,
11426
- shellClassName: exports_external.string().min(1),
11427
- socialImageDataUrl: WrappedShareSocialImageDataUrlSchema.optional(),
11428
- statItems: exports_external.array(WrappedShareStatItemSchema),
11536
+ shellClassName: exports_external.string().min(1).max(WRAPPED_SHARE_RESOURCE_LIMITS.classNameLength),
11537
+ statItems: exports_external.array(WrappedShareStatItemSchema).max(WRAPPED_SHARE_RESOURCE_LIMITS.statItemCount),
11429
11538
  theme: WrappedShareThemeSchema
11539
+ }).strict();
11540
+ var WrappedShareSnapshotSchema = WrappedShareSnapshotObjectSchema.refine(isWrappedShareSnapshotWithinByteLimit, {
11541
+ message: `Wrapped share snapshot must be at most ${WRAPPED_SHARE_RESOURCE_LIMITS.snapshotBytes} bytes`
11430
11542
  });
11431
- var PublicWrappedShareSnapshotSchema = WrappedShareSnapshotSchema.omit({
11432
- socialImageDataUrl: true
11433
- });
11543
+ var PublicWrappedShareSnapshotSchema = WrappedShareSnapshotSchema;
11434
11544
  var CreateWrappedShareInputSchema = exports_external.object({
11545
+ socialImageDataUrl: WrappedShareSocialImageDataUrlSchema.optional(),
11435
11546
  snapshot: WrappedShareSnapshotSchema,
11436
11547
  variant: WrappedShareVariantSchema.default("normal")
11437
- });
11548
+ }).strict();
11438
11549
  var GetPublicWrappedShareInputSchema = exports_external.object({
11439
11550
  shareId: WrappedShareIdSchema
11440
11551
  });
@@ -11447,6 +11558,22 @@ var WrappedShareRecordSchema = exports_external.object({
11447
11558
  var PublicWrappedShareSchema = WrappedShareRecordSchema.extend({
11448
11559
  snapshot: PublicWrappedShareSnapshotSchema
11449
11560
  });
11561
+ function getWrappedShareSnapshotByteLength(snapshot) {
11562
+ const snapshotJson = JSON.stringify(snapshot);
11563
+ if (snapshotJson === undefined) {
11564
+ return 0;
11565
+ }
11566
+ return new TextEncoder().encode(snapshotJson).byteLength;
11567
+ }
11568
+ function isWrappedShareSnapshotWithinByteLimit(snapshot) {
11569
+ return getWrappedShareSnapshotByteLength(snapshot) <= WRAPPED_SHARE_RESOURCE_LIMITS.snapshotBytes;
11570
+ }
11571
+ function isWrappedShareSocialImageWithinByteLimit(dataUrl) {
11572
+ const base64 = dataUrl.slice(WRAPPED_SHARE_SOCIAL_IMAGE_DATA_URL_PREFIX.length);
11573
+ const paddingLength = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
11574
+ const decodedBytes = base64.length * 3 / 4 - paddingLength;
11575
+ return Number.isInteger(decodedBytes) && decodedBytes <= WRAPPED_SHARE_RESOURCE_LIMITS.socialImageBytes;
11576
+ }
11450
11577
 
11451
11578
  // ../../packages/api-routes/src/schemas/wrapped-resume.ts
11452
11579
  var CreateWrappedResumeInputSchema = exports_external.object({
@@ -11639,6 +11766,10 @@ var CliUserSchema = exports_external.object({
11639
11766
  var CliSetupStatusSchema = exports_external.object({
11640
11767
  hasCliLogin: exports_external.boolean()
11641
11768
  });
11769
+ var ChatwootIdentitySchema = exports_external.object({
11770
+ identifier: exports_external.string(),
11771
+ identifier_hash: exports_external.string().regex(/^[a-f0-9]{64}$/u)
11772
+ });
11642
11773
  var OrganizationSchema = exports_external.object({
11643
11774
  id: exports_external.string(),
11644
11775
  name: exports_external.string(),
@@ -11646,6 +11777,7 @@ var OrganizationSchema = exports_external.object({
11646
11777
  logo: exports_external.string().nullable()
11647
11778
  });
11648
11779
  var TeamInviteLinkSchema = exports_external.object({
11780
+ expires_at: exports_external.string().datetime(),
11649
11781
  invite_url: exports_external.string().url(),
11650
11782
  organization_id: exports_external.string(),
11651
11783
  organization_name: exports_external.string()
@@ -11732,6 +11864,9 @@ var contract = {
11732
11864
  revokeToken: oc.output(exports_external.object({ success: exports_external.literal(true) })),
11733
11865
  setupStatus: oc.output(CliSetupStatusSchema)
11734
11866
  },
11867
+ chatwoot: {
11868
+ identity: oc.output(ChatwootIdentitySchema.nullable())
11869
+ },
11735
11870
  listMyOrganizations: oc.output(exports_external.array(OrganizationSchema)),
11736
11871
  ingestSession: oc.input(IngestSessionInputSchema).output(IngestSessionOutputSchema).errors({
11737
11872
  [REDACTION_BUDGET_EXCEEDED_CODE]: {
@@ -11758,7 +11893,8 @@ var contract = {
11758
11893
  getOrganizationSessionCount: oc.input(exports_external.object({ organizationId: exports_external.string(), userId: exports_external.string().optional() })).output(exports_external.object({ count: exports_external.number() })),
11759
11894
  deleteOrganization: oc.input(exports_external.object({ organizationId: exports_external.string() })).output(exports_external.object({ success: exports_external.literal(true) })),
11760
11895
  teamInviteLink: {
11761
- get: oc.input(exports_external.object({ organizationId: exports_external.string() })).output(TeamInviteLinkSchema),
11896
+ create: oc.input(exports_external.object({ organizationId: exports_external.string() })).output(TeamInviteLinkSchema),
11897
+ revoke: oc.input(exports_external.object({ organizationId: exports_external.string() })).output(exports_external.object({ success: exports_external.literal(true) })),
11762
11898
  accept: oc.input(exports_external.object({ token: exports_external.string().min(1) })).output(TeamInviteAcceptResultSchema)
11763
11899
  },
11764
11900
  wrappedShare: {
@@ -11848,43 +11984,77 @@ var contract = {
11848
11984
  };
11849
11985
 
11850
11986
  // src/lib/credentials.ts
11987
+ import { randomUUID } from "node:crypto";
11851
11988
  import {
11989
+ chmodSync,
11852
11990
  existsSync as existsSync6,
11853
11991
  mkdirSync as mkdirSync2,
11854
11992
  readFileSync as readFileSync5,
11993
+ renameSync,
11855
11994
  rmSync,
11995
+ statSync,
11856
11996
  writeFileSync as writeFileSync3
11857
11997
  } from "node:fs";
11858
11998
  import { homedir as homedir7 } from "node:os";
11859
11999
  import { join as join9 } from "node:path";
12000
+ var PRIVATE_DIRECTORY_MODE2 = 448;
12001
+ var PRIVATE_FILE_MODE2 = 384;
11860
12002
  function getConfigDir() {
11861
12003
  return process.env.RUDEL_CONFIG_DIR ?? join9(homedir7(), ".rudel");
11862
12004
  }
11863
- function getCredentialsPath() {
11864
- return join9(getConfigDir(), "credentials.json");
11865
- }
11866
12005
  function saveCredentials(credentials) {
11867
12006
  const dir = getConfigDir();
11868
- if (!existsSync6(dir)) {
11869
- mkdirSync2(dir, { recursive: true, mode: 448 });
12007
+ const path = getCredentialsPath(dir);
12008
+ const content = JSON.stringify(credentials, null, 2);
12009
+ mkdirSync2(dir, { recursive: true, mode: PRIVATE_DIRECTORY_MODE2 });
12010
+ enforcePrivateMode(dir, PRIVATE_DIRECTORY_MODE2);
12011
+ if (existsSync6(path)) {
12012
+ enforcePrivateMode(path, PRIVATE_FILE_MODE2);
11870
12013
  }
11871
- writeFileSync3(getCredentialsPath(), JSON.stringify(credentials, null, 2), {
11872
- mode: 384
11873
- });
12014
+ replaceCredentialsFile(path, content);
11874
12015
  }
11875
12016
  function loadCredentials() {
11876
- const path = getCredentialsPath();
12017
+ const dir = getConfigDir();
12018
+ const path = getCredentialsPath(dir);
11877
12019
  if (!existsSync6(path))
11878
12020
  return null;
12021
+ enforcePrivateMode(dir, PRIVATE_DIRECTORY_MODE2);
12022
+ enforcePrivateMode(path, PRIVATE_FILE_MODE2);
11879
12023
  const content = readFileSync5(path, "utf-8");
11880
12024
  return JSON.parse(content);
11881
12025
  }
11882
12026
  function clearCredentials() {
11883
- const path = getCredentialsPath();
12027
+ const path = getCredentialsPath(getConfigDir());
11884
12028
  if (existsSync6(path)) {
11885
12029
  rmSync(path);
11886
12030
  }
11887
12031
  }
12032
+ function getCredentialsPath(configDir) {
12033
+ return join9(configDir, "credentials.json");
12034
+ }
12035
+ function replaceCredentialsFile(path, content) {
12036
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
12037
+ try {
12038
+ writeFileSync3(temporaryPath, content, {
12039
+ encoding: "utf8",
12040
+ flag: "wx",
12041
+ mode: PRIVATE_FILE_MODE2
12042
+ });
12043
+ enforcePrivateMode(temporaryPath, PRIVATE_FILE_MODE2);
12044
+ renameSync(temporaryPath, path);
12045
+ enforcePrivateMode(path, PRIVATE_FILE_MODE2);
12046
+ } finally {
12047
+ rmSync(temporaryPath, { force: true });
12048
+ }
12049
+ }
12050
+ function enforcePrivateMode(path, mode) {
12051
+ if (process.platform === "win32")
12052
+ return;
12053
+ chmodSync(path, mode);
12054
+ if ((statSync(path).mode & 511) !== mode) {
12055
+ throw new Error("Unable to establish private credential permissions");
12056
+ }
12057
+ }
11888
12058
 
11889
12059
  // src/lib/insecure-url-opt-in.ts
11890
12060
  var TRUTHY_ENV_VALUES = ["1", "true", "yes", "on"];
@@ -13966,14 +14136,17 @@ async function batchUpload(options) {
13966
14136
  let succeeded = 0;
13967
14137
  let failed = 0;
13968
14138
  let skipped = 0;
14139
+ let deferred = 0;
13969
14140
  let completed = 0;
13970
14141
  let rateLimited = false;
13971
14142
  const errors2 = [];
14143
+ const skippedItems = [];
14144
+ const skippedSessionIds = new Set;
13972
14145
  let redacted = {};
13973
14146
  let redactedBytes = 0;
13974
14147
  await pMap(items, async (item) => {
13975
14148
  if (rateLimited) {
13976
- skipped++;
14149
+ deferred++;
13977
14150
  const error = "Skipped — rate limit reached. Run `rudel upload --retry` to upload remaining sessions.";
13978
14151
  await recordFailedUpload({
13979
14152
  sessionId: item.sessionId,
@@ -13997,6 +14170,13 @@ async function batchUpload(options) {
13997
14170
  redacted = mergeRedactionCounts(redacted, result.redacted ?? {});
13998
14171
  redactedBytes += result.redactedBytes ?? 0;
13999
14172
  await removeFailedUpload(item.sessionId);
14173
+ } else if (result.retryable === false) {
14174
+ skipped++;
14175
+ skippedItems.push({
14176
+ label: item.label,
14177
+ reason: result.error ?? "Upload cannot be retried"
14178
+ });
14179
+ skippedSessionIds.add(item.sessionId);
14000
14180
  } else {
14001
14181
  failed++;
14002
14182
  const error = result.error ?? "Unknown error";
@@ -14014,30 +14194,48 @@ async function batchUpload(options) {
14014
14194
  });
14015
14195
  }
14016
14196
  } catch (err) {
14017
- failed++;
14018
14197
  const error = err instanceof Error ? err.message : String(err);
14019
- errors2.push({ label: item.label, error });
14020
- await recordFailedUpload({
14021
- sessionId: item.sessionId,
14022
- transcriptPath: item.transcriptPath,
14023
- projectPath: item.projectPath,
14024
- source: item.source,
14025
- organizationId: item.organizationId,
14026
- error
14027
- });
14198
+ if (err instanceof MissingTranscriptTimestampError) {
14199
+ skipped++;
14200
+ skippedItems.push({ label: item.label, reason: error });
14201
+ skippedSessionIds.add(item.sessionId);
14202
+ } else {
14203
+ failed++;
14204
+ errors2.push({ label: item.label, error });
14205
+ await recordFailedUpload({
14206
+ sessionId: item.sessionId,
14207
+ transcriptPath: item.transcriptPath,
14208
+ projectPath: item.projectPath,
14209
+ source: item.source,
14210
+ organizationId: item.organizationId,
14211
+ error
14212
+ });
14213
+ }
14028
14214
  } finally {
14029
14215
  completed++;
14030
14216
  onItemComplete?.(completed, total);
14031
14217
  }
14032
14218
  }, { concurrency, stopOnError: false });
14033
- if (rateLimited && skipped > 0) {
14219
+ for (const sessionId of skippedSessionIds) {
14220
+ await removeFailedUpload(sessionId);
14221
+ }
14222
+ if (rateLimited && deferred > 0) {
14034
14223
  errors2.push({
14035
14224
  label: "Rate limit",
14036
- error: `${skipped} session(s) skipped. Run \`rudel upload --retry\` later to upload them.`
14225
+ error: `${deferred} session(s) skipped. Run \`rudel upload --retry\` later to upload them.`
14037
14226
  });
14038
- failed += skipped;
14227
+ failed += deferred;
14039
14228
  }
14040
- return { succeeded, failed, total, errors: errors2, redacted, redactedBytes };
14229
+ return {
14230
+ succeeded,
14231
+ failed,
14232
+ skipped,
14233
+ total,
14234
+ errors: errors2,
14235
+ skippedItems,
14236
+ redacted,
14237
+ redactedBytes
14238
+ };
14041
14239
  }
14042
14240
 
14043
14241
  // src/lib/upload-endpoint.ts
@@ -14156,13 +14354,13 @@ function formatServerUploadError(error) {
14156
14354
  return `Rudel server error (${status}). This is not an auth problem. Retry later with: rudel upload --retry; if it repeats, share this status with the Rudel team.`;
14157
14355
  }
14158
14356
  function getPayloadTooLargeDetail(error) {
14159
- const data = isRecord(error.data) ? error.data : null;
14357
+ const data = isRecord3(error.data) ? error.data : null;
14160
14358
  const bodyValue = data?.body;
14161
- const body = isRecord(bodyValue) ? bodyValue : null;
14359
+ const body = isRecord3(bodyValue) ? bodyValue : null;
14162
14360
  return getStringField(body, "error") ?? getStringField(data, "error");
14163
14361
  }
14164
14362
  function getErrorData(error) {
14165
- const data = isRecord(error.data) ? error.data : null;
14363
+ const data = isRecord3(error.data) ? error.data : null;
14166
14364
  return {
14167
14365
  authMessage: getStringField(data, "authMessage"),
14168
14366
  actualBytes: getNumberField(data, "actualBytes"),
@@ -14175,7 +14373,7 @@ function getErrorData(error) {
14175
14373
  };
14176
14374
  }
14177
14375
  function getRedactionBudgetErrorData(error) {
14178
- const data = isRecord(error.data) ? error.data : null;
14376
+ const data = isRecord3(error.data) ? error.data : null;
14179
14377
  const inputBytes = getNumberField(data, "inputBytes");
14180
14378
  const redactedBytes = getNumberField(data, "redactedBytes");
14181
14379
  const ruleIdsValue = data?.ruleIds;
@@ -14192,7 +14390,7 @@ function getNumberField(record, key) {
14192
14390
  const value3 = record?.[key];
14193
14391
  return typeof value3 === "number" && Number.isFinite(value3) ? value3 : null;
14194
14392
  }
14195
- function isRecord(value3) {
14393
+ function isRecord3(value3) {
14196
14394
  return typeof value3 === "object" && value3 !== null;
14197
14395
  }
14198
14396
  function formatWait(milliseconds) {
@@ -14280,6 +14478,14 @@ async function uploadSession(request, config) {
14280
14478
  redactedBytes: filteredText.redactedBytes + (response.redactedBytes ?? 0)
14281
14479
  };
14282
14480
  } catch (error) {
14481
+ if (error instanceof ORPCError2 && error.status === 400 && isMissingTranscriptTimestampMessage(request.source, error.message)) {
14482
+ return {
14483
+ success: false,
14484
+ error: error.message,
14485
+ attempts: attempt,
14486
+ retryable: false
14487
+ };
14488
+ }
14283
14489
  const errorMessage = formatUploadError(error);
14284
14490
  if (isRateLimited(error) || isApiKeyRateLimited(error)) {
14285
14491
  return {
@@ -14328,13 +14534,13 @@ function formatRedactionBudgetError(anomaly) {
14328
14534
  return `Redaction safety check stopped upload: known-pattern redaction would replace ${formatBytes(anomaly.redactedBytes)} of ${formatBytes(anomaly.inputBytes)} (${ratio}%), above the 20% transcript budget (${rules}). The unfiltered transcript was not uploaded.`;
14329
14535
  }
14330
14536
  function isIngestSessionResponse(value3) {
14331
- if (!isRecord(value3) || value3.success !== true) {
14537
+ if (!isRecord3(value3) || value3.success !== true) {
14332
14538
  return false;
14333
14539
  }
14334
14540
  if (typeof value3.sessionId !== "string") {
14335
14541
  return false;
14336
14542
  }
14337
- if (value3.redacted !== undefined && !isRecord(value3.redacted)) {
14543
+ if (value3.redacted !== undefined && !isRecord3(value3.redacted)) {
14338
14544
  return false;
14339
14545
  }
14340
14546
  return value3.redactedBytes === undefined || typeof value3.redactedBytes === "number";
@@ -14382,7 +14588,7 @@ async function runBatchUpload(options) {
14382
14588
  bar.message(`Retrying ${itemLabel} (${attempt}/${maxAttempts}) after ${error}`);
14383
14589
  }
14384
14590
  });
14385
- bar.stop(summary.failed > 0 ? `Completed with ${summary.failed} error(s)` : "Upload complete");
14591
+ bar.stop(summary.failed > 0 ? `Completed with ${summary.failed} error(s)` : summary.skipped > 0 ? `Completed with ${summary.skipped} skipped` : "Upload complete");
14386
14592
  return summary;
14387
14593
  }
14388
14594
  function renderBatchSummary(summary, options) {
@@ -14405,6 +14611,15 @@ function renderBatchSummary(summary, options) {
14405
14611
  lines.push(` ...and ${summary.errors.length - maxErrors} more`);
14406
14612
  }
14407
14613
  }
14614
+ if (summary.skipped > 0) {
14615
+ lines.push(`${prefix}${summary.skipped} session(s) skipped`);
14616
+ for (const item of summary.skippedItems.slice(0, maxErrors)) {
14617
+ lines.push(` ${item.label}: ${item.reason}`);
14618
+ }
14619
+ if (summary.skippedItems.length > maxErrors) {
14620
+ lines.push(` ...and ${summary.skippedItems.length - maxErrors} more`);
14621
+ }
14622
+ }
14408
14623
  if (showRetryHint && summary.failed > 0) {
14409
14624
  lines.push("");
14410
14625
  lines.push("Run `rudel upload --retry` to retry failed uploads.");
@@ -14416,13 +14631,13 @@ function renderBatchSummary(summary, options) {
14416
14631
  }
14417
14632
 
14418
14633
  // src/lib/product-analytics.ts
14419
- import { randomUUID } from "node:crypto";
14634
+ import { randomUUID as randomUUID2 } from "node:crypto";
14420
14635
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
14421
14636
  import { homedir as homedir8 } from "node:os";
14422
14637
  import { join as join11 } from "node:path";
14423
14638
 
14424
14639
  // ../../node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
14425
- import { dirname as dirname4, posix, sep } from "path";
14640
+ import { dirname as dirname4, posix, sep as sep2 } from "path";
14426
14641
  function createModulerModifier() {
14427
14642
  const getModuleFromFileName = createGetModuleFromFilename();
14428
14643
  return async (frames) => {
@@ -14431,7 +14646,7 @@ function createModulerModifier() {
14431
14646
  return frames;
14432
14647
  };
14433
14648
  }
14434
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname4(process.argv[1]) : process.cwd(), isWindows = sep === "\\") {
14649
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname4(process.argv[1]) : process.cwd(), isWindows = sep2 === "\\") {
14435
14650
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
14436
14651
  return (filename) => {
14437
14652
  if (!filename)
@@ -18938,7 +19153,7 @@ function getOrCreateCliInstallationId() {
18938
19153
  if (typeof state.cli_installation_id === "string") {
18939
19154
  return state.cli_installation_id;
18940
19155
  }
18941
- const cliInstallationId = randomUUID();
19156
+ const cliInstallationId = randomUUID2();
18942
19157
  writeAnalyticsState({
18943
19158
  ...state,
18944
19159
  cli_installation_id: cliInstallationId
@@ -19048,7 +19263,7 @@ var CliProductAnalyticsEvents = PRODUCT_ANALYTICS_EVENTS;
19048
19263
 
19049
19264
  // src/lib/project-config.ts
19050
19265
  import {
19051
- chmodSync,
19266
+ chmodSync as chmodSync2,
19052
19267
  existsSync as existsSync9,
19053
19268
  mkdirSync as mkdirSync4,
19054
19269
  readFileSync as readFileSync8,
@@ -19066,18 +19281,18 @@ function loadProjectsConfig() {
19066
19281
  const path = getProjectsConfigPath();
19067
19282
  if (!existsSync9(path))
19068
19283
  return { projects: {} };
19069
- chmodSync(getConfigDir3(), 448);
19070
- chmodSync(path, 384);
19284
+ chmodSync2(getConfigDir3(), 448);
19285
+ chmodSync2(path, 384);
19071
19286
  const content = readFileSync8(path, "utf-8");
19072
19287
  return JSON.parse(content);
19073
19288
  }
19074
19289
  function saveProjectsConfig(config) {
19075
19290
  const dir = getConfigDir3();
19076
19291
  mkdirSync4(dir, { recursive: true, mode: 448 });
19077
- chmodSync(dir, 448);
19292
+ chmodSync2(dir, 448);
19078
19293
  const path = getProjectsConfigPath();
19079
19294
  writeFileSync5(path, JSON.stringify(config, null, 2), { mode: 384 });
19080
- chmodSync(path, 384);
19295
+ chmodSync2(path, 384);
19081
19296
  }
19082
19297
  async function getProjectKey(cwd) {
19083
19298
  try {
@@ -19298,25 +19513,18 @@ async function runEnable() {
19298
19513
  if (!session) {
19299
19514
  return { success: false, error: "Session not found" };
19300
19515
  }
19301
- try {
19302
- const request = await adapter.buildUploadRequest(session, {
19303
- gitInfo,
19304
- organizationId: selectedOrgId,
19305
- uploadMode: "manual"
19306
- });
19307
- return uploadSession(request, {
19308
- endpoint,
19309
- token: credentials.token,
19310
- allowInsecureEndpoint: allowPlaintextEndpoint,
19311
- authType: credentials.authType,
19312
- onRetry
19313
- });
19314
- } catch (error) {
19315
- return {
19316
- success: false,
19317
- error: error instanceof Error ? error.message : String(error)
19318
- };
19319
- }
19516
+ const request = await adapter.buildUploadRequest(session, {
19517
+ gitInfo,
19518
+ organizationId: selectedOrgId,
19519
+ uploadMode: "manual"
19520
+ });
19521
+ return uploadSession(request, {
19522
+ endpoint,
19523
+ token: credentials.token,
19524
+ allowInsecureEndpoint: allowPlaintextEndpoint,
19525
+ authType: credentials.authType,
19526
+ onRetry
19527
+ });
19320
19528
  }
19321
19529
  });
19322
19530
  renderBatchSummary(summary, { context: adapter.name });
@@ -20940,6 +21148,10 @@ async function reportHookUploadFailure(logger2, result, failure) {
20940
21148
  sessionId: failure.sessionId,
20941
21149
  error: uploadError
20942
21150
  });
21151
+ if (result.retryable === false) {
21152
+ await removeFailedUpload(failure.sessionId);
21153
+ return;
21154
+ }
20943
21155
  if (result.endpointRejected) {
20944
21156
  process.stderr.write(`Rudel hook upload refused for session ${failure.sessionId}: ${uploadError}
20945
21157
  `);
@@ -22153,12 +22365,20 @@ async function runSingleUpload(flags, session, allowPlaintextEndpoint) {
22153
22365
  transcriptPath: sessionInfo.transcriptPath,
22154
22366
  projectPath: sessionInfo.projectPath
22155
22367
  };
22156
- const request = await claudeCodeAdapter.buildUploadRequest(sessionFile, {
22157
- tag: flags.tag,
22158
- gitInfo,
22159
- organizationId,
22160
- uploadMode: "manual"
22161
- });
22368
+ let request;
22369
+ try {
22370
+ request = await claudeCodeAdapter.buildUploadRequest(sessionFile, {
22371
+ tag: flags.tag,
22372
+ gitInfo,
22373
+ organizationId,
22374
+ uploadMode: "manual"
22375
+ });
22376
+ } catch (error) {
22377
+ if (error instanceof MissingTranscriptTimestampError) {
22378
+ return new Error("This transcript has no timestamped user/assistant messages, so it cannot be uploaded.");
22379
+ }
22380
+ throw error;
22381
+ }
22162
22382
  write(`Transcript: ${request.content.length} bytes`);
22163
22383
  if (request.subagents && request.subagents.length > 0) {
22164
22384
  write(`Subagents: ${request.subagents.length} file(s)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rudel",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "CLI for the Coding Agent Analytics Platform rudel.ai",
6
6
  "license": "MIT",