rudel 0.2.2 → 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 +299 -112
  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.2",
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;
@@ -11366,74 +11462,90 @@ var WrappedDecimalClaimEntitlementSchema = exports_external.object({
11366
11462
  });
11367
11463
 
11368
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);
11369
11478
  var WrappedShareThemeSchema = exports_external.enum(["dark", "light", "muted"]);
11370
11479
  var WrappedShareVariantSchema = exports_external.enum(["normal", "decimal"]);
11371
11480
  var WrappedShareLayoutModeSchema = exports_external.enum(["front", "front_back"]);
11372
11481
  var WrappedShareIdSchema = exports_external.string().trim().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u);
11373
11482
  var WrappedShareHeaderMetricSchema = exports_external.object({
11374
- label: exports_external.string().optional(),
11375
- title: exports_external.string().optional(),
11376
- value: exports_external.string().min(1)
11377
- });
11483
+ label: WrappedShareTextSchema.optional(),
11484
+ title: WrappedShareTextSchema.optional(),
11485
+ value: WrappedShareRequiredTextSchema
11486
+ }).strict();
11378
11487
  var WrappedShareStatItemIconSchema = exports_external.enum(["claude", "codex"]);
11379
11488
  var WrappedShareStatItemSchema = exports_external.object({
11380
11489
  icon: WrappedShareStatItemIconSchema.optional(),
11381
- key: exports_external.string().min(1),
11382
- label: exports_external.string().optional(),
11383
- title: exports_external.string().optional(),
11384
- value: exports_external.string().min(1)
11385
- });
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();
11386
11495
  var WrappedShareBackMetricSchema = exports_external.object({
11387
- label: exports_external.string(),
11496
+ label: WrappedShareTextSchema,
11388
11497
  slot: exports_external.enum(["body", "footer"]).optional(),
11389
- value: exports_external.string().min(1)
11390
- });
11498
+ value: WrappedShareRequiredTextSchema
11499
+ }).strict();
11391
11500
  var WrappedShareRevealMetricsSchema = exports_external.object({
11392
11501
  avgSessionMin: exports_external.number().nonnegative().nullable(),
11393
11502
  commitRate: exports_external.number().min(0).max(100).nullable(),
11394
11503
  daysSinceFirst: exports_external.number().nonnegative(),
11395
11504
  distinctProjectCount: exports_external.number().nonnegative(),
11396
11505
  longestSessionMin: exports_external.number().nonnegative().nullable()
11397
- });
11506
+ }).strict();
11398
11507
  var WrappedShareAppearanceSchema = exports_external.object({
11399
11508
  layoutMode: WrappedShareLayoutModeSchema,
11400
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`
11401
11513
  });
11402
- var WrappedShareSocialImageDataUrlSchema = exports_external.string().max(7000000).regex(/^data:image\/png;base64,[A-Za-z0-9+/]+={0,2}$/u);
11403
11514
  var WrappedShareRowSchema = exports_external.object({
11404
11515
  activeDays: exports_external.number().nonnegative(),
11405
11516
  cost: exports_external.number().nonnegative(),
11406
- displayName: exports_external.string().min(1),
11407
- favoriteModel: exports_external.string().nullable(),
11517
+ displayName: WrappedShareRequiredTextSchema,
11518
+ favoriteModel: WrappedShareTextSchema.nullable(),
11408
11519
  hasActivity: exports_external.boolean(),
11409
- imageUrl: exports_external.string().nullable(),
11520
+ imageUrl: exports_external.string().max(WRAPPED_SHARE_RESOURCE_LIMITS.imageUrlLength).nullable(),
11410
11521
  inputTokens: exports_external.number().nonnegative(),
11411
- lastActiveDate: exports_external.string().nullable(),
11522
+ lastActiveDate: WrappedShareTextSchema.nullable(),
11412
11523
  outputTokens: exports_external.number().nonnegative(),
11413
- role: exports_external.string().min(1),
11524
+ role: WrappedShareRequiredTextSchema,
11414
11525
  totalSessions: exports_external.number().nonnegative(),
11415
11526
  totalTokens: exports_external.number().nonnegative()
11416
- });
11417
- var WrappedShareSnapshotSchema = exports_external.object({
11527
+ }).strict();
11528
+ var WrappedShareSnapshotObjectSchema = exports_external.object({
11418
11529
  appearance: WrappedShareAppearanceSchema.optional(),
11419
- archetypeLabel: exports_external.string().min(1),
11420
- backMetrics: exports_external.array(WrappedShareBackMetricSchema).optional(),
11530
+ archetypeLabel: WrappedShareRequiredTextSchema,
11531
+ backMetrics: exports_external.array(WrappedShareBackMetricSchema).max(WRAPPED_SHARE_RESOURCE_LIMITS.backMetricCount).optional(),
11421
11532
  headerLeftMetric: WrappedShareHeaderMetricSchema.optional(),
11422
11533
  headerRightMetric: WrappedShareHeaderMetricSchema.optional(),
11423
11534
  revealMetrics: WrappedShareRevealMetricsSchema.optional(),
11424
11535
  row: WrappedShareRowSchema,
11425
- shellClassName: exports_external.string().min(1),
11426
- socialImageDataUrl: WrappedShareSocialImageDataUrlSchema.optional(),
11427
- 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),
11428
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`
11429
11542
  });
11430
- var PublicWrappedShareSnapshotSchema = WrappedShareSnapshotSchema.omit({
11431
- socialImageDataUrl: true
11432
- });
11543
+ var PublicWrappedShareSnapshotSchema = WrappedShareSnapshotSchema;
11433
11544
  var CreateWrappedShareInputSchema = exports_external.object({
11545
+ socialImageDataUrl: WrappedShareSocialImageDataUrlSchema.optional(),
11434
11546
  snapshot: WrappedShareSnapshotSchema,
11435
11547
  variant: WrappedShareVariantSchema.default("normal")
11436
- });
11548
+ }).strict();
11437
11549
  var GetPublicWrappedShareInputSchema = exports_external.object({
11438
11550
  shareId: WrappedShareIdSchema
11439
11551
  });
@@ -11446,6 +11558,22 @@ var WrappedShareRecordSchema = exports_external.object({
11446
11558
  var PublicWrappedShareSchema = WrappedShareRecordSchema.extend({
11447
11559
  snapshot: PublicWrappedShareSnapshotSchema
11448
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
+ }
11449
11577
 
11450
11578
  // ../../packages/api-routes/src/schemas/wrapped-resume.ts
11451
11579
  var CreateWrappedResumeInputSchema = exports_external.object({
@@ -11638,6 +11766,10 @@ var CliUserSchema = exports_external.object({
11638
11766
  var CliSetupStatusSchema = exports_external.object({
11639
11767
  hasCliLogin: exports_external.boolean()
11640
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
+ });
11641
11773
  var OrganizationSchema = exports_external.object({
11642
11774
  id: exports_external.string(),
11643
11775
  name: exports_external.string(),
@@ -11645,6 +11777,7 @@ var OrganizationSchema = exports_external.object({
11645
11777
  logo: exports_external.string().nullable()
11646
11778
  });
11647
11779
  var TeamInviteLinkSchema = exports_external.object({
11780
+ expires_at: exports_external.string().datetime(),
11648
11781
  invite_url: exports_external.string().url(),
11649
11782
  organization_id: exports_external.string(),
11650
11783
  organization_name: exports_external.string()
@@ -11731,6 +11864,9 @@ var contract = {
11731
11864
  revokeToken: oc.output(exports_external.object({ success: exports_external.literal(true) })),
11732
11865
  setupStatus: oc.output(CliSetupStatusSchema)
11733
11866
  },
11867
+ chatwoot: {
11868
+ identity: oc.output(ChatwootIdentitySchema.nullable())
11869
+ },
11734
11870
  listMyOrganizations: oc.output(exports_external.array(OrganizationSchema)),
11735
11871
  ingestSession: oc.input(IngestSessionInputSchema).output(IngestSessionOutputSchema).errors({
11736
11872
  [REDACTION_BUDGET_EXCEEDED_CODE]: {
@@ -11757,7 +11893,8 @@ var contract = {
11757
11893
  getOrganizationSessionCount: oc.input(exports_external.object({ organizationId: exports_external.string(), userId: exports_external.string().optional() })).output(exports_external.object({ count: exports_external.number() })),
11758
11894
  deleteOrganization: oc.input(exports_external.object({ organizationId: exports_external.string() })).output(exports_external.object({ success: exports_external.literal(true) })),
11759
11895
  teamInviteLink: {
11760
- 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) })),
11761
11898
  accept: oc.input(exports_external.object({ token: exports_external.string().min(1) })).output(TeamInviteAcceptResultSchema)
11762
11899
  },
11763
11900
  wrappedShare: {
@@ -13999,14 +14136,17 @@ async function batchUpload(options) {
13999
14136
  let succeeded = 0;
14000
14137
  let failed = 0;
14001
14138
  let skipped = 0;
14139
+ let deferred = 0;
14002
14140
  let completed = 0;
14003
14141
  let rateLimited = false;
14004
14142
  const errors2 = [];
14143
+ const skippedItems = [];
14144
+ const skippedSessionIds = new Set;
14005
14145
  let redacted = {};
14006
14146
  let redactedBytes = 0;
14007
14147
  await pMap(items, async (item) => {
14008
14148
  if (rateLimited) {
14009
- skipped++;
14149
+ deferred++;
14010
14150
  const error = "Skipped — rate limit reached. Run `rudel upload --retry` to upload remaining sessions.";
14011
14151
  await recordFailedUpload({
14012
14152
  sessionId: item.sessionId,
@@ -14030,6 +14170,13 @@ async function batchUpload(options) {
14030
14170
  redacted = mergeRedactionCounts(redacted, result.redacted ?? {});
14031
14171
  redactedBytes += result.redactedBytes ?? 0;
14032
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);
14033
14180
  } else {
14034
14181
  failed++;
14035
14182
  const error = result.error ?? "Unknown error";
@@ -14047,30 +14194,48 @@ async function batchUpload(options) {
14047
14194
  });
14048
14195
  }
14049
14196
  } catch (err) {
14050
- failed++;
14051
14197
  const error = err instanceof Error ? err.message : String(err);
14052
- errors2.push({ label: item.label, error });
14053
- await recordFailedUpload({
14054
- sessionId: item.sessionId,
14055
- transcriptPath: item.transcriptPath,
14056
- projectPath: item.projectPath,
14057
- source: item.source,
14058
- organizationId: item.organizationId,
14059
- error
14060
- });
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
+ }
14061
14214
  } finally {
14062
14215
  completed++;
14063
14216
  onItemComplete?.(completed, total);
14064
14217
  }
14065
14218
  }, { concurrency, stopOnError: false });
14066
- if (rateLimited && skipped > 0) {
14219
+ for (const sessionId of skippedSessionIds) {
14220
+ await removeFailedUpload(sessionId);
14221
+ }
14222
+ if (rateLimited && deferred > 0) {
14067
14223
  errors2.push({
14068
14224
  label: "Rate limit",
14069
- 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.`
14070
14226
  });
14071
- failed += skipped;
14227
+ failed += deferred;
14072
14228
  }
14073
- 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
+ };
14074
14239
  }
14075
14240
 
14076
14241
  // src/lib/upload-endpoint.ts
@@ -14189,13 +14354,13 @@ function formatServerUploadError(error) {
14189
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.`;
14190
14355
  }
14191
14356
  function getPayloadTooLargeDetail(error) {
14192
- const data = isRecord(error.data) ? error.data : null;
14357
+ const data = isRecord3(error.data) ? error.data : null;
14193
14358
  const bodyValue = data?.body;
14194
- const body = isRecord(bodyValue) ? bodyValue : null;
14359
+ const body = isRecord3(bodyValue) ? bodyValue : null;
14195
14360
  return getStringField(body, "error") ?? getStringField(data, "error");
14196
14361
  }
14197
14362
  function getErrorData(error) {
14198
- const data = isRecord(error.data) ? error.data : null;
14363
+ const data = isRecord3(error.data) ? error.data : null;
14199
14364
  return {
14200
14365
  authMessage: getStringField(data, "authMessage"),
14201
14366
  actualBytes: getNumberField(data, "actualBytes"),
@@ -14208,7 +14373,7 @@ function getErrorData(error) {
14208
14373
  };
14209
14374
  }
14210
14375
  function getRedactionBudgetErrorData(error) {
14211
- const data = isRecord(error.data) ? error.data : null;
14376
+ const data = isRecord3(error.data) ? error.data : null;
14212
14377
  const inputBytes = getNumberField(data, "inputBytes");
14213
14378
  const redactedBytes = getNumberField(data, "redactedBytes");
14214
14379
  const ruleIdsValue = data?.ruleIds;
@@ -14225,7 +14390,7 @@ function getNumberField(record, key) {
14225
14390
  const value3 = record?.[key];
14226
14391
  return typeof value3 === "number" && Number.isFinite(value3) ? value3 : null;
14227
14392
  }
14228
- function isRecord(value3) {
14393
+ function isRecord3(value3) {
14229
14394
  return typeof value3 === "object" && value3 !== null;
14230
14395
  }
14231
14396
  function formatWait(milliseconds) {
@@ -14313,6 +14478,14 @@ async function uploadSession(request, config) {
14313
14478
  redactedBytes: filteredText.redactedBytes + (response.redactedBytes ?? 0)
14314
14479
  };
14315
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
+ }
14316
14489
  const errorMessage = formatUploadError(error);
14317
14490
  if (isRateLimited(error) || isApiKeyRateLimited(error)) {
14318
14491
  return {
@@ -14361,13 +14534,13 @@ function formatRedactionBudgetError(anomaly) {
14361
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.`;
14362
14535
  }
14363
14536
  function isIngestSessionResponse(value3) {
14364
- if (!isRecord(value3) || value3.success !== true) {
14537
+ if (!isRecord3(value3) || value3.success !== true) {
14365
14538
  return false;
14366
14539
  }
14367
14540
  if (typeof value3.sessionId !== "string") {
14368
14541
  return false;
14369
14542
  }
14370
- if (value3.redacted !== undefined && !isRecord(value3.redacted)) {
14543
+ if (value3.redacted !== undefined && !isRecord3(value3.redacted)) {
14371
14544
  return false;
14372
14545
  }
14373
14546
  return value3.redactedBytes === undefined || typeof value3.redactedBytes === "number";
@@ -14415,7 +14588,7 @@ async function runBatchUpload(options) {
14415
14588
  bar.message(`Retrying ${itemLabel} (${attempt}/${maxAttempts}) after ${error}`);
14416
14589
  }
14417
14590
  });
14418
- 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");
14419
14592
  return summary;
14420
14593
  }
14421
14594
  function renderBatchSummary(summary, options) {
@@ -14438,6 +14611,15 @@ function renderBatchSummary(summary, options) {
14438
14611
  lines.push(` ...and ${summary.errors.length - maxErrors} more`);
14439
14612
  }
14440
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
+ }
14441
14623
  if (showRetryHint && summary.failed > 0) {
14442
14624
  lines.push("");
14443
14625
  lines.push("Run `rudel upload --retry` to retry failed uploads.");
@@ -14455,7 +14637,7 @@ import { homedir as homedir8 } from "node:os";
14455
14637
  import { join as join11 } from "node:path";
14456
14638
 
14457
14639
  // ../../node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
14458
- import { dirname as dirname4, posix, sep } from "path";
14640
+ import { dirname as dirname4, posix, sep as sep2 } from "path";
14459
14641
  function createModulerModifier() {
14460
14642
  const getModuleFromFileName = createGetModuleFromFilename();
14461
14643
  return async (frames) => {
@@ -14464,7 +14646,7 @@ function createModulerModifier() {
14464
14646
  return frames;
14465
14647
  };
14466
14648
  }
14467
- 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 === "\\") {
14468
14650
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
14469
14651
  return (filename) => {
14470
14652
  if (!filename)
@@ -19331,25 +19513,18 @@ async function runEnable() {
19331
19513
  if (!session) {
19332
19514
  return { success: false, error: "Session not found" };
19333
19515
  }
19334
- try {
19335
- const request = await adapter.buildUploadRequest(session, {
19336
- gitInfo,
19337
- organizationId: selectedOrgId,
19338
- uploadMode: "manual"
19339
- });
19340
- return uploadSession(request, {
19341
- endpoint,
19342
- token: credentials.token,
19343
- allowInsecureEndpoint: allowPlaintextEndpoint,
19344
- authType: credentials.authType,
19345
- onRetry
19346
- });
19347
- } catch (error) {
19348
- return {
19349
- success: false,
19350
- error: error instanceof Error ? error.message : String(error)
19351
- };
19352
- }
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
+ });
19353
19528
  }
19354
19529
  });
19355
19530
  renderBatchSummary(summary, { context: adapter.name });
@@ -20973,6 +21148,10 @@ async function reportHookUploadFailure(logger2, result, failure) {
20973
21148
  sessionId: failure.sessionId,
20974
21149
  error: uploadError
20975
21150
  });
21151
+ if (result.retryable === false) {
21152
+ await removeFailedUpload(failure.sessionId);
21153
+ return;
21154
+ }
20976
21155
  if (result.endpointRejected) {
20977
21156
  process.stderr.write(`Rudel hook upload refused for session ${failure.sessionId}: ${uploadError}
20978
21157
  `);
@@ -22186,12 +22365,20 @@ async function runSingleUpload(flags, session, allowPlaintextEndpoint) {
22186
22365
  transcriptPath: sessionInfo.transcriptPath,
22187
22366
  projectPath: sessionInfo.projectPath
22188
22367
  };
22189
- const request = await claudeCodeAdapter.buildUploadRequest(sessionFile, {
22190
- tag: flags.tag,
22191
- gitInfo,
22192
- organizationId,
22193
- uploadMode: "manual"
22194
- });
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
+ }
22195
22382
  write(`Transcript: ${request.content.length} bytes`);
22196
22383
  if (request.subagents && request.subagents.length > 0) {
22197
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.2",
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",