rudel 0.1.15 → 0.1.17

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 +89 -72
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1889,7 +1889,7 @@ async function run(app, inputs, context) {
1889
1889
  // package.json
1890
1890
  var package_default = {
1891
1891
  name: "rudel",
1892
- version: "0.1.15",
1892
+ version: "0.1.17",
1893
1893
  type: "module",
1894
1894
  description: "CLI for the Coding Agent Analytics Platform rudel.ai",
1895
1895
  license: "MIT",
@@ -1924,7 +1924,7 @@ var package_default = {
1924
1924
  build: "bun build src/bin/cli.ts --outdir dist --target node",
1925
1925
  prepack: "bun run build",
1926
1926
  test: "bun test",
1927
- "test:integration": "bun test ./src/__tests__/api-upload.integration.ts ./src/__tests__/auth-verify.integration.ts ./src/__tests__/auth.e2e.integration.ts"
1927
+ "test:integration": "bun test ./src/__tests__/api-upload.integration.ts ./src/__tests__/uploader-rate-limit.integration.ts ./src/__tests__/auth-verify.integration.ts ./src/__tests__/auth.e2e.integration.ts"
1928
1928
  },
1929
1929
  dependencies: {
1930
1930
  "@clack/prompts": "^1.0.1",
@@ -6385,7 +6385,7 @@ class ClaudeCodeAdapter {
6385
6385
  await ingestRudelClaudeSessions(ingestor, [row]);
6386
6386
  }
6387
6387
  buildRow(input, context) {
6388
- const now = new Date().toISOString().replace("Z", "");
6388
+ const now = context.ingestedAt.toISOString().replace("Z", "");
6389
6389
  const subagents = {};
6390
6390
  if (input.subagents) {
6391
6391
  for (const sub of input.subagents) {
@@ -7717,7 +7717,7 @@ class CodexAdapter {
7717
7717
  await ingestRudelCodexSessions(ingestor, [row]);
7718
7718
  }
7719
7719
  buildRow(input, context) {
7720
- const now = new Date().toISOString().replace("Z", "");
7720
+ const now = context.ingestedAt.toISOString().replace("Z", "");
7721
7721
  const timestamps = this.extractTimestamps(input.content);
7722
7722
  return {
7723
7723
  session_date: timestamps ? toClickHouseDateTime(timestamps.sessionDate) : now,
@@ -13157,9 +13157,18 @@ var SessionTagSchema = exports_external.enum([
13157
13157
  "tests",
13158
13158
  "other"
13159
13159
  ]);
13160
+ var INGEST_AGGREGATE_CONTENT_MAX_BYTES = 128 * 1024 * 1024;
13161
+ var INGEST_MAX_SUBAGENT_COUNT = 512;
13162
+ var INGEST_LIMIT_REASONS = {
13163
+ requestLimit: "request_limit",
13164
+ byteLimit: "byte_limit",
13165
+ sessionLimit: "session_limit",
13166
+ transcriptTooLarge: "transcript_too_large"
13167
+ };
13168
+ var INGEST_CONTENT_MAX_CODE_UNITS = 160 * 1024 * 1024;
13160
13169
  var SubagentFileSchema = exports_external.object({
13161
- agentId: exports_external.string(),
13162
- content: exports_external.string()
13170
+ agentId: exports_external.string().max(200),
13171
+ content: exports_external.string().max(INGEST_CONTENT_MAX_CODE_UNITS)
13163
13172
  });
13164
13173
  var IngestSessionInputSchema = exports_external.object({
13165
13174
  source: SourceSchema.default("claude_code"),
@@ -13171,8 +13180,8 @@ var IngestSessionInputSchema = exports_external.object({
13171
13180
  gitBranch: exports_external.string().max(200).optional(),
13172
13181
  gitSha: exports_external.string().max(200).optional(),
13173
13182
  tag: SessionTagSchema.optional(),
13174
- content: exports_external.string(),
13175
- subagents: exports_external.array(SubagentFileSchema).optional(),
13183
+ content: exports_external.string().max(INGEST_CONTENT_MAX_CODE_UNITS),
13184
+ subagents: exports_external.array(SubagentFileSchema).max(INGEST_MAX_SUBAGENT_COUNT).refine((subagents) => new Set(subagents.map((subagent) => subagent.agentId)).size === subagents.length, { message: "Subagent agentId values must be unique" }).optional(),
13176
13185
  organizationId: exports_external.string().max(200).optional(),
13177
13186
  client_surface: ProductAnalyticsClientSurfaceSchema.optional(),
13178
13187
  upload_mode: ProductAnalyticsUploadModeSchema.optional(),
@@ -18214,6 +18223,13 @@ function formatUploadError(error) {
18214
18223
  }
18215
18224
  if (isRateLimited(error)) {
18216
18225
  const data = getErrorData(error);
18226
+ const isRequestLimit = data.reason === INGEST_LIMIT_REASONS.requestLimit;
18227
+ if (isRequestLimit || data.reason === INGEST_LIMIT_REASONS.byteLimit) {
18228
+ const limit2 = data.limit === null ? null : isRequestLimit ? `${data.limit} requests` : `${formatMebibytes(data.limit)} MiB`;
18229
+ const detail = limit2 && data.windowSeconds ? ` (${limit2} per ${Math.round(data.windowSeconds / 60)} min)` : "";
18230
+ const kind = isRequestLimit ? "request" : "byte";
18231
+ return `Ingest ${kind} limit reached${detail}. Wait and retry with: rudel upload --retry`;
18232
+ }
18217
18233
  const windowMin = data?.windowSeconds ? Math.round(data.windowSeconds / 60) : 60;
18218
18234
  const limit = data?.limit ?? "unknown";
18219
18235
  return `Rate limit reached (${limit} sessions per ${windowMin} min). Wait and retry with: rudel upload --retry`;
@@ -18222,6 +18238,10 @@ function formatUploadError(error) {
18222
18238
  return "This session ID is already owned by another organization member. Upload it from the original member account or use a different session ID.";
18223
18239
  }
18224
18240
  if (isPayloadTooLarge(error)) {
18241
+ const data = getErrorData(error);
18242
+ if (data.reason === INGEST_LIMIT_REASONS.transcriptTooLarge) {
18243
+ return formatTranscriptTooLargeError(data.actualBytes, data.maxBytes);
18244
+ }
18225
18245
  return formatPayloadTooLargeError(error);
18226
18246
  }
18227
18247
  if (isServerError(error)) {
@@ -18261,8 +18281,10 @@ function getErrorData(error) {
18261
18281
  const data = isRecord(error.data) ? error.data : null;
18262
18282
  return {
18263
18283
  authMessage: getStringField(data, "authMessage"),
18284
+ actualBytes: getNumberField(data, "actualBytes"),
18264
18285
  code: getStringField(data, "code"),
18265
18286
  limit: getNumberField(data, "limit"),
18287
+ maxBytes: getNumberField(data, "maxBytes"),
18266
18288
  reason: getStringField(data, "reason"),
18267
18289
  tryAgainIn: getNumberField(data, "tryAgainIn"),
18268
18290
  windowSeconds: getNumberField(data, "windowSeconds")
@@ -18292,6 +18314,15 @@ function formatWait(milliseconds) {
18292
18314
  return `${hours} hr`;
18293
18315
  }
18294
18316
  async function uploadSession(request, config) {
18317
+ const maxAggregateBytes = config.maxAggregateBytes ?? INGEST_AGGREGATE_CONTENT_MAX_BYTES;
18318
+ const aggregateBytes = getUploadAggregateBytes(request);
18319
+ if (aggregateBytes > maxAggregateBytes) {
18320
+ return {
18321
+ success: false,
18322
+ error: formatTranscriptTooLargeError(aggregateBytes, maxAggregateBytes),
18323
+ attempts: 0
18324
+ };
18325
+ }
18295
18326
  const link = new RPCLink({
18296
18327
  url: config.endpoint,
18297
18328
  headers: config.authType === "api-key" ? { "x-api-key": config.token } : { Authorization: `Bearer ${config.token}` }
@@ -18330,6 +18361,20 @@ async function uploadSession(request, config) {
18330
18361
  attempts: MAX_ATTEMPTS
18331
18362
  };
18332
18363
  }
18364
+ function getUploadAggregateBytes(request) {
18365
+ return Buffer.byteLength(request.content, "utf8") + (request.subagents ?? []).reduce((total, subagent) => total + Buffer.byteLength(subagent.content, "utf8"), 0);
18366
+ }
18367
+ function formatTranscriptTooLargeError(actualBytes, maxBytes) {
18368
+ if (actualBytes === null || maxBytes === null) {
18369
+ return "Session transcript payload exceeds the per-session limit. Reduce the transcript/subagent payload before retrying.";
18370
+ }
18371
+ const actualText = `${formatMebibytes(actualBytes)} MiB`;
18372
+ const limitText = `the ${formatMebibytes(maxBytes)} MiB per-session limit`;
18373
+ return `Session transcript payload is ${actualText}, above ${limitText}. Reduce the transcript/subagent payload before retrying.`;
18374
+ }
18375
+ function formatMebibytes(bytes) {
18376
+ return (bytes / (1024 * 1024)).toFixed(2);
18377
+ }
18333
18378
 
18334
18379
  // src/commands/enable.ts
18335
18380
  async function runEnable() {
@@ -18356,10 +18401,8 @@ async function runEnable() {
18356
18401
  failureStage: "auth_verify",
18357
18402
  error: auth.reason
18358
18403
  });
18359
- R2.error(auth.message);
18360
18404
  Gt("Run `rudel login` to authenticate.");
18361
- process.exitCode = 1;
18362
- return;
18405
+ return new Error(auth.message);
18363
18406
  }
18364
18407
  const { credentials } = auth;
18365
18408
  let orgs;
@@ -18376,9 +18419,7 @@ async function runEnable() {
18376
18419
  error,
18377
18420
  userId: auth.user.id
18378
18421
  });
18379
- R2.error("Failed to fetch organizations. Check your connection.");
18380
- process.exitCode = 1;
18381
- return;
18422
+ return new Error("Failed to fetch organizations. Check your connection.");
18382
18423
  }
18383
18424
  }
18384
18425
  if (orgs.length === 0) {
@@ -18388,10 +18429,8 @@ async function runEnable() {
18388
18429
  error: new Error("No organizations found"),
18389
18430
  userId: auth.user.id
18390
18431
  });
18391
- R2.error("No organizations found.");
18392
18432
  Gt("Create one at app.rudel.ai first.");
18393
- process.exitCode = 1;
18394
- return;
18433
+ return new Error("No organizations found.");
18395
18434
  }
18396
18435
  const cwd = process.cwd();
18397
18436
  const existingOrgId = await getProjectOrgId(cwd);
@@ -18538,7 +18577,7 @@ async function runEnable() {
18538
18577
  }
18539
18578
  Gt("Done!");
18540
18579
  if (totalFailed > 0 || hookInstallFailures > 0) {
18541
- process.exitCode = 1;
18580
+ return new Error(`Enable completed with ${hookInstallFailures} hook installation failure(s) and ${totalFailed} upload failure(s).`);
18542
18581
  }
18543
18582
  }
18544
18583
  var enableCommand = buildCommand({
@@ -20789,9 +20828,7 @@ async function runLogin(flags) {
20789
20828
  deviceCode = await requestDeviceCode(flags.apiBase);
20790
20829
  } catch (error) {
20791
20830
  captureLoginFailure("device_code_request", error);
20792
- R2.error(error instanceof Error ? error.message : String(error));
20793
- process.exitCode = 1;
20794
- return;
20831
+ return error instanceof Error ? error : new Error(String(error));
20795
20832
  }
20796
20833
  const verifyUrl = deviceCode.verification_uri_complete ?? `${deviceCode.verification_uri}?user_code=${encodeURIComponent(deviceCode.user_code)}`;
20797
20834
  captureCliProductAnalyticsEvent({
@@ -20821,9 +20858,7 @@ ${verifyUrl}`);
20821
20858
  const failureReason = normalizeFailureReason(error);
20822
20859
  captureLoginFailure(failureReason === "timeout" ? "browser_approval_timeout" : "token_exchange", error);
20823
20860
  spin.stop("Authentication failed");
20824
- R2.error(error instanceof Error ? error.message : String(error));
20825
- process.exitCode = 1;
20826
- return;
20861
+ return error instanceof Error ? error : new Error(String(error));
20827
20862
  }
20828
20863
  spin.message("Creating ingest token...");
20829
20864
  let ingestKey;
@@ -20832,9 +20867,7 @@ ${verifyUrl}`);
20832
20867
  } catch (error) {
20833
20868
  captureLoginFailure("api_key_create", error);
20834
20869
  spin.stop("Authentication failed");
20835
- R2.error(error instanceof Error ? error.message : String(error));
20836
- process.exitCode = 1;
20837
- return;
20870
+ return error instanceof Error ? error : new Error(String(error));
20838
20871
  }
20839
20872
  const client2 = createApiClient({
20840
20873
  apiBaseUrl: flags.apiBase,
@@ -20857,9 +20890,7 @@ ${verifyUrl}`);
20857
20890
  } catch (error) {
20858
20891
  captureLoginFailure("account_fetch", error);
20859
20892
  spin.stop("Authentication failed");
20860
- R2.error("Login failed: unable to fetch account details");
20861
- process.exitCode = 1;
20862
- return;
20893
+ return new Error("Login failed: unable to fetch account details");
20863
20894
  }
20864
20895
  try {
20865
20896
  saveCredentials({
@@ -20873,9 +20904,7 @@ ${verifyUrl}`);
20873
20904
  } catch (error) {
20874
20905
  captureLoginFailure("account_fetch", error);
20875
20906
  spin.stop("Authentication failed");
20876
- R2.error("Login failed: unable to persist credentials");
20877
- process.exitCode = 1;
20878
- return;
20907
+ return new Error("Login failed: unable to persist credentials");
20879
20908
  }
20880
20909
  captureCliProductAnalyticsEvent({
20881
20910
  distinctId: user.id,
@@ -20921,26 +20950,35 @@ var loginCommand = buildCommand({
20921
20950
  });
20922
20951
 
20923
20952
  // src/commands/logout.ts
20924
- async function runLogout() {
20953
+ async function runLogout(flags) {
20925
20954
  const credentials = loadCredentials();
20926
20955
  if (!credentials) {
20927
20956
  R2.info("Not logged in.");
20928
20957
  return;
20929
20958
  }
20930
- if (credentials.authType === "api-key") {
20959
+ if (credentials.authType === "api-key" && !flags.localOnly) {
20931
20960
  try {
20932
20961
  const client2 = createApiClient(credentials);
20933
20962
  await client2.cli.revokeToken();
20934
- } catch {
20935
- R2.warn("Failed to revoke token on server. Local credentials were cleared.");
20963
+ } catch (error) {
20964
+ const message = error instanceof Error ? error.message : String(error);
20965
+ return new Error(`Failed to revoke token on server: ${message}. Credentials were kept; retry or run \`rudel logout --local-only\`.`);
20936
20966
  }
20937
20967
  }
20938
20968
  clearCredentials();
20939
- R2.success("Logged out successfully.");
20969
+ R2.success(flags.localOnly ? "Logged out locally. Server token was not revoked." : "Logged out successfully.");
20940
20970
  }
20941
20971
  var logoutCommand = buildCommand({
20942
20972
  loader: async () => ({ default: runLogout }),
20943
- parameters: {},
20973
+ parameters: {
20974
+ flags: {
20975
+ localOnly: {
20976
+ kind: "boolean",
20977
+ brief: "Clear local credentials without revoking the server token",
20978
+ default: false
20979
+ }
20980
+ }
20981
+ },
20944
20982
  docs: {
20945
20983
  brief: "Log out and remove stored credentials"
20946
20984
  }
@@ -20951,10 +20989,8 @@ async function runSetOrg() {
20951
20989
  Wt2("rudel set-org");
20952
20990
  const credentials = loadCredentials();
20953
20991
  if (!credentials) {
20954
- R2.error("Not authenticated.");
20955
20992
  Gt("Run `rudel login` first.");
20956
- process.exitCode = 1;
20957
- return;
20993
+ return new Error("Not authenticated.");
20958
20994
  }
20959
20995
  let orgs;
20960
20996
  if (credentials.authType === "api-key") {
@@ -20964,16 +21000,12 @@ async function runSetOrg() {
20964
21000
  try {
20965
21001
  orgs = await client2.listMyOrganizations();
20966
21002
  } catch {
20967
- R2.error("Failed to fetch organizations. Check your connection.");
20968
- process.exitCode = 1;
20969
- return;
21003
+ return new Error("Failed to fetch organizations. Check your connection.");
20970
21004
  }
20971
21005
  }
20972
21006
  if (orgs.length === 0) {
20973
- R2.error("No organizations found.");
20974
21007
  Gt("Create one at app.rudel.ai first.");
20975
- process.exitCode = 1;
20976
- return;
21008
+ return new Error("No organizations found.");
20977
21009
  }
20978
21010
  const cwd = process.cwd();
20979
21011
  const currentOrgId = await getProjectOrgId(cwd);
@@ -21147,9 +21179,7 @@ function validateNotSubagent(filename) {
21147
21179
  async function runInteractiveUpload(flags) {
21148
21180
  const credentials = loadCredentials();
21149
21181
  if (!credentials && !flags.dryRun) {
21150
- R2.error("Not authenticated. Run `rudel login` first.");
21151
- process.exitCode = 1;
21152
- return;
21182
+ return new Error("Not authenticated. Run `rudel login` first.");
21153
21183
  }
21154
21184
  Wt2("rudel upload");
21155
21185
  const spin = be();
@@ -21242,7 +21272,7 @@ async function runInteractiveUpload(flags) {
21242
21272
  Gt("Done!");
21243
21273
  }
21244
21274
  if (summary.failed > 0) {
21245
- process.exitCode = 1;
21275
+ return new Error(`${summary.failed} upload(s) failed.`);
21246
21276
  }
21247
21277
  }
21248
21278
  function getAdapterName(source) {
@@ -21254,26 +21284,18 @@ function sessionCountHint(count) {
21254
21284
  async function runSingleUpload(flags, session) {
21255
21285
  const write = (msg) => {
21256
21286
  process.stdout.write(`${msg}
21257
- `);
21258
- };
21259
- const writeError = (msg) => {
21260
- process.stderr.write(`${msg}
21261
21287
  `);
21262
21288
  };
21263
21289
  const credentials = loadCredentials();
21264
21290
  if (!credentials && !flags.dryRun) {
21265
- writeError("Error: Not authenticated. Run `rudel login` first.");
21266
- process.exitCode = 1;
21267
- return;
21291
+ return new Error("Not authenticated. Run `rudel login` first.");
21268
21292
  }
21269
21293
  write(`Resolving session: ${session}`);
21270
21294
  let sessionInfo;
21271
21295
  try {
21272
21296
  sessionInfo = await resolveSession(session);
21273
21297
  } catch (error) {
21274
- writeError(`Error: ${error instanceof Error ? error.message : String(error)}`);
21275
- process.exitCode = 1;
21276
- return;
21298
+ return error instanceof Error ? error : new Error(String(error));
21277
21299
  }
21278
21300
  write(`Found session at: ${sessionInfo.transcriptPath}`);
21279
21301
  const gitInfo = await getGitInfo(sessionInfo.projectPath);
@@ -21331,16 +21353,13 @@ async function runSingleUpload(flags, session) {
21331
21353
  if (result.success) {
21332
21354
  write("Upload successful!");
21333
21355
  } else {
21334
- writeError(`Upload failed: ${result.error}`);
21335
- process.exitCode = 1;
21356
+ return new Error(`Upload failed: ${result.error}`);
21336
21357
  }
21337
21358
  }
21338
21359
  async function runRetryUpload(flags) {
21339
21360
  const credentials = loadCredentials();
21340
21361
  if (!credentials) {
21341
- R2.error("Not authenticated. Run `rudel login` first.");
21342
- process.exitCode = 1;
21343
- return;
21362
+ return new Error("Not authenticated. Run `rudel login` first.");
21344
21363
  }
21345
21364
  Wt2("rudel upload --retry");
21346
21365
  const failures = await loadFailedUploads();
@@ -21403,7 +21422,7 @@ async function runRetryUpload(flags) {
21403
21422
  renderBatchSummary(summary);
21404
21423
  Gt("Done!");
21405
21424
  if (summary.failed > 0) {
21406
- process.exitCode = 1;
21425
+ return new Error(`${summary.failed} upload(s) failed.`);
21407
21426
  }
21408
21427
  }
21409
21428
  async function runUpload(flags, ...sessions) {
@@ -21487,11 +21506,9 @@ async function runWhoami() {
21487
21506
  if (!result.authenticated) {
21488
21507
  if (result.reason === "no_credentials") {
21489
21508
  R2.info("Not logged in. Run `rudel login` to authenticate.");
21490
- } else {
21491
- R2.error(result.message);
21492
- process.exitCode = 1;
21509
+ return;
21493
21510
  }
21494
- return;
21511
+ return new Error(result.message);
21495
21512
  }
21496
21513
  R2.info(`Logged in as ${result.user.name} (${result.user.email})`);
21497
21514
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rudel",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "description": "CLI for the Coding Agent Analytics Platform rudel.ai",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "build": "bun build src/bin/cli.ts --outdir dist --target node",
36
36
  "prepack": "bun run build",
37
37
  "test": "bun test",
38
- "test:integration": "bun test ./src/__tests__/api-upload.integration.ts ./src/__tests__/auth-verify.integration.ts ./src/__tests__/auth.e2e.integration.ts"
38
+ "test:integration": "bun test ./src/__tests__/api-upload.integration.ts ./src/__tests__/uploader-rate-limit.integration.ts ./src/__tests__/auth-verify.integration.ts ./src/__tests__/auth.e2e.integration.ts"
39
39
  },
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.0.1",