hillclimb 0.1.7 → 0.1.8

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +342 -291
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # hillclimb
2
2
 
3
- Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode) and upload them to a Hillclimb workspace.
3
+ Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode) and upload them to a Hillclimb project.
4
4
 
5
5
  ## Quickstart
6
6
 
package/dist/cli.js CHANGED
@@ -30,6 +30,29 @@ import os from "os";
30
30
  import path from "path";
31
31
  var CONFIG_DIR = path.join(os.homedir(), ".hillclimb");
32
32
  var CONFIG_PATH = path.join(CONFIG_DIR, "projects.json");
33
+ function normalizeProjectConfig(raw) {
34
+ if (!raw || typeof raw !== "object") return null;
35
+ const config = raw;
36
+ const projectId = config.projectId ?? config.workspaceId;
37
+ const projectSlug = config.projectSlug ?? config.workspaceSlug;
38
+ const projectName = config.projectName ?? config.workspaceName;
39
+ if (!config.apiBaseUrl || !projectId || !projectSlug || !projectName || !config.contributionTypeSlug || !config.contributionTypeName || typeof config.autoSubmit !== "boolean") {
40
+ return null;
41
+ }
42
+ return {
43
+ ...config,
44
+ apiBaseUrl: config.apiBaseUrl,
45
+ projectId,
46
+ projectSlug,
47
+ projectName,
48
+ workspaceId: config.workspaceId ?? projectId,
49
+ workspaceSlug: config.workspaceSlug ?? projectSlug,
50
+ workspaceName: config.workspaceName ?? projectName,
51
+ contributionTypeSlug: config.contributionTypeSlug,
52
+ contributionTypeName: config.contributionTypeName,
53
+ autoSubmit: config.autoSubmit
54
+ };
55
+ }
33
56
  function configDir() {
34
57
  return CONFIG_DIR;
35
58
  }
@@ -43,7 +66,12 @@ async function loadProjects() {
43
66
  if (!parsed.projects || typeof parsed.projects !== "object") {
44
67
  return { projects: {} };
45
68
  }
46
- return parsed;
69
+ const projects = {};
70
+ for (const [repoRoot, config] of Object.entries(parsed.projects)) {
71
+ const normalized = normalizeProjectConfig(config);
72
+ if (normalized) projects[repoRoot] = normalized;
73
+ }
74
+ return { projects };
47
75
  } catch {
48
76
  return { projects: {} };
49
77
  }
@@ -58,7 +86,9 @@ async function saveProjects(file) {
58
86
  }
59
87
  async function upsertProject(repoRoot, config) {
60
88
  const file = await loadProjects();
61
- file.projects[path.resolve(repoRoot)] = config;
89
+ const normalized = normalizeProjectConfig(config);
90
+ if (!normalized) throw new Error("Invalid project config.");
91
+ file.projects[path.resolve(repoRoot)] = normalized;
62
92
  await saveProjects(file);
63
93
  }
64
94
  async function findProjectForCwd(cwd) {
@@ -398,30 +428,51 @@ var PlatformClient = class {
398
428
  };
399
429
  }
400
430
  async listProjects() {
401
- const data = await this.request(
402
- "GET",
403
- "/api/v1/projects"
404
- );
405
- return data.workspaces ?? [];
431
+ const data = await this.request("GET", "/api/v1/projects");
432
+ return data.projects ?? data.workspaces ?? [];
406
433
  }
407
- async getWorkspaceBySlug(slug) {
408
- const data = await this.request(
409
- "GET",
410
- `/api/v1/workspaces/${encodeURIComponent(slug)}`
411
- );
412
- return data.workspace;
434
+ async getProjectBySlug(slug) {
435
+ try {
436
+ const data = await this.request("GET", `/api/v1/projects/by-slug/${encodeURIComponent(slug)}`);
437
+ const project = data.project ?? data.workspace;
438
+ if (!project) throw new PlatformError("Project response was empty.");
439
+ return project;
440
+ } catch (err) {
441
+ if (!(err instanceof PlatformError) || err.status !== 404) throw err;
442
+ const data = await this.request(
443
+ "GET",
444
+ `/api/v1/workspaces/${encodeURIComponent(slug)}`
445
+ );
446
+ return data.workspace;
447
+ }
413
448
  }
414
- async listContributionTypes(workspaceId) {
415
- const data = await this.request("GET", `/api/v1/workspaces/${workspaceId}/contribution-types`);
416
- return data.contributionTypes ?? [];
449
+ async listContributionTypes(projectId) {
450
+ try {
451
+ const data = await this.request("GET", `/api/v1/projects/${projectId}/contribution-types`);
452
+ return data.contributionTypes ?? [];
453
+ } catch (err) {
454
+ if (!(err instanceof PlatformError) || err.status !== 404) throw err;
455
+ const data = await this.request("GET", `/api/v1/workspaces/${projectId}/contribution-types`);
456
+ return data.contributionTypes ?? [];
457
+ }
417
458
  }
418
- async createContribution(workspaceId, input) {
419
- const data = await this.request(
420
- "POST",
421
- `/api/v1/workspaces/${workspaceId}/contributions`,
422
- input
423
- );
424
- return data.contribution;
459
+ async createContribution(projectId, input) {
460
+ try {
461
+ const data = await this.request(
462
+ "POST",
463
+ `/api/v1/projects/${projectId}/contributions`,
464
+ input
465
+ );
466
+ return data.contribution;
467
+ } catch (err) {
468
+ if (!(err instanceof PlatformError) || err.status !== 404) throw err;
469
+ const data = await this.request(
470
+ "POST",
471
+ `/api/v1/workspaces/${projectId}/contributions`,
472
+ input
473
+ );
474
+ return data.contribution;
475
+ }
425
476
  }
426
477
  async createUpload(contributionId, input) {
427
478
  return this.request(
@@ -1271,10 +1322,10 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
1271
1322
  }
1272
1323
  async function runInit(args = []) {
1273
1324
  const forceLogin = args.includes("--login");
1274
- const workspaceSlugFlag = parseStringFlag(args, "workspace");
1325
+ const projectSlugFlag = parseStringFlag(args, "project") ?? parseStringFlag(args, "workspace");
1275
1326
  appendLog(
1276
1327
  "info",
1277
- `init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, workspaceSlug=${workspaceSlugFlag ?? "<none>"})`
1328
+ `init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, projectSlug=${projectSlugFlag ?? "<none>"})`
1278
1329
  );
1279
1330
  p3.intro("hillclimb");
1280
1331
  const repo = detectRepoRoot();
@@ -1295,8 +1346,8 @@ async function runInit(args = []) {
1295
1346
  bootstrap = await tryReuseIdentity(apiBaseUrl, client, saved);
1296
1347
  }
1297
1348
  if (!bootstrap) {
1298
- let method = workspaceSlugFlag ? "web" : "email";
1299
- if (IS_TTY && !workspaceSlugFlag) {
1349
+ let method = projectSlugFlag ? "web" : "email";
1350
+ if (IS_TTY && !projectSlugFlag) {
1300
1351
  const selected = await p3.select({
1301
1352
  message: "How would you like to sign in?",
1302
1353
  options: [
@@ -1356,13 +1407,13 @@ async function runInit(args = []) {
1356
1407
  );
1357
1408
  }
1358
1409
  }
1359
- let workspace;
1360
- if (workspaceSlugFlag) {
1410
+ let project;
1411
+ if (projectSlugFlag) {
1361
1412
  const resolved = await withSpinner(
1362
- `Looking up project "${workspaceSlugFlag}"...`,
1413
+ `Looking up project "${projectSlugFlag}"...`,
1363
1414
  (w) => `Found project: ${w.name}`,
1364
- `Could not find project "${workspaceSlugFlag}".`,
1365
- () => client.getWorkspaceBySlug(workspaceSlugFlag)
1415
+ `Could not find project "${projectSlugFlag}".`,
1416
+ () => client.getProjectBySlug(projectSlugFlag)
1366
1417
  );
1367
1418
  if (resolved.status !== "active") {
1368
1419
  p3.log.error(
@@ -1371,21 +1422,21 @@ async function runInit(args = []) {
1371
1422
  p3.outro("Aborted.");
1372
1423
  process.exit(1);
1373
1424
  }
1374
- workspace = resolved;
1425
+ project = resolved;
1375
1426
  appendLog(
1376
1427
  "info",
1377
- `init: project pre-scoped (slug=${workspaceSlugFlag}, id=${resolved.id})`
1428
+ `init: project pre-scoped (slug=${projectSlugFlag}, id=${resolved.id})`
1378
1429
  );
1379
1430
  } else {
1380
- const workspaces = await withSpinner(
1431
+ const projects = await withSpinner(
1381
1432
  "Loading projects...",
1382
1433
  (ws) => `Found ${ws.length} project(s).`,
1383
1434
  "Could not list projects.",
1384
1435
  () => client.listProjects()
1385
1436
  );
1386
- const activeWorkspaces = workspaces.filter((w) => w.status === "active");
1387
- const workspaceChoices = activeWorkspaces.length > 0 ? activeWorkspaces : workspaces;
1388
- if (workspaceChoices.length === 0) {
1437
+ const activeProjects = projects.filter((w) => w.status === "active");
1438
+ const projectChoices = activeProjects.length > 0 ? activeProjects : projects;
1439
+ if (projectChoices.length === 0) {
1389
1440
  p3.log.error(
1390
1441
  "You do not have access to any projects. Create one in the web UI first."
1391
1442
  );
@@ -1393,44 +1444,44 @@ async function runInit(args = []) {
1393
1444
  process.exit(1);
1394
1445
  }
1395
1446
  let picked;
1396
- if (workspaceChoices.length === 1) {
1397
- picked = workspaceChoices[0];
1447
+ if (projectChoices.length === 1) {
1448
+ picked = projectChoices[0];
1398
1449
  p3.log.info(`Using project: ${picked.name}`);
1399
1450
  } else {
1400
- const selectedWorkspaceId = await p3.select({
1451
+ const selectedProjectId = await p3.select({
1401
1452
  message: "Select a project",
1402
- options: workspaceChoices.map((w) => ({
1453
+ options: projectChoices.map((w) => ({
1403
1454
  value: w.id,
1404
1455
  label: w.name,
1405
1456
  hint: `${w.slug} (${w.status})`
1406
1457
  }))
1407
1458
  });
1408
- if (p3.isCancel(selectedWorkspaceId)) {
1459
+ if (p3.isCancel(selectedProjectId)) {
1409
1460
  p3.cancel("Cancelled.");
1410
1461
  process.exit(0);
1411
1462
  }
1412
- picked = workspaceChoices.find((w) => w.id === selectedWorkspaceId);
1463
+ picked = projectChoices.find((w) => w.id === selectedProjectId);
1413
1464
  }
1414
1465
  if (!picked) {
1415
- p3.log.error("Invalid workspace selection.");
1466
+ p3.log.error("Invalid project selection.");
1416
1467
  p3.outro("Aborted.");
1417
1468
  process.exit(1);
1418
1469
  }
1419
- workspace = picked;
1470
+ project = picked;
1420
1471
  appendLog(
1421
1472
  "info",
1422
- `init: project selected (slug=${workspace.slug}, id=${workspace.id})`
1473
+ `init: project selected (slug=${project.slug}, id=${project.id})`
1423
1474
  );
1424
1475
  }
1425
1476
  const types = await withSpinner(
1426
1477
  "Loading contribution types...",
1427
1478
  (t) => `Found ${t.length} contribution type(s).`,
1428
1479
  "Could not list contribution types.",
1429
- () => client.listContributionTypes(workspace.id)
1480
+ () => client.listContributionTypes(project.id)
1430
1481
  );
1431
1482
  if (types.length === 0) {
1432
1483
  p3.log.error(
1433
- `Project "${workspace.name}" has no contribution types. Create one in the web UI first.`
1484
+ `Project "${project.name}" has no contribution types. Create one in the web UI first.`
1434
1485
  );
1435
1486
  p3.outro("Aborted.");
1436
1487
  process.exit(1);
@@ -1477,10 +1528,13 @@ async function runInit(args = []) {
1477
1528
  );
1478
1529
  await upsertProject(repoRoot, {
1479
1530
  apiBaseUrl,
1480
- organizationId: workspace.organizationId,
1481
- workspaceId: workspace.id,
1482
- workspaceSlug: workspace.slug,
1483
- workspaceName: workspace.name,
1531
+ organizationId: project.organizationId,
1532
+ projectId: project.id,
1533
+ projectSlug: project.slug,
1534
+ projectName: project.name,
1535
+ workspaceId: project.id,
1536
+ workspaceSlug: project.slug,
1537
+ workspaceName: project.name,
1484
1538
  contributionTypeSlug: type.slug,
1485
1539
  contributionTypeName: type.name,
1486
1540
  autoSubmit: true
@@ -1545,10 +1599,10 @@ async function runInit(args = []) {
1545
1599
  }
1546
1600
  appendLog(
1547
1601
  "info",
1548
- `init: completed (project=${workspace.slug}, contributionType=${type.slug})`
1602
+ `init: completed (project=${project.slug}, contributionType=${type.slug})`
1549
1603
  );
1550
1604
  p3.outro(
1551
- `Done. Your next coding session in this repo will upload automatically to ${workspace.name}.`
1605
+ `Done. Your next coding session in this repo will upload automatically to ${project.name}.`
1552
1606
  );
1553
1607
  }
1554
1608
 
@@ -1618,10 +1672,7 @@ async function runLogin(_args = []) {
1618
1672
  process.stdout.write("\r\x1B[K");
1619
1673
  }
1620
1674
  if (verified) {
1621
- appendLog(
1622
- "info",
1623
- `login: reused saved login (email=${verified.email})`
1624
- );
1675
+ appendLog("info", `login: reused saved login (email=${verified.email})`);
1625
1676
  row(CHECK, "Signed in", cyan(verified.email));
1626
1677
  const fresh = probe.getSessionCookie();
1627
1678
  if (fresh) {
@@ -1762,9 +1813,7 @@ async function runLogout(args = []) {
1762
1813
  footer("Nothing to do.");
1763
1814
  return;
1764
1815
  }
1765
- println(
1766
- ` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`
1767
- );
1816
+ println(` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`);
1768
1817
  footer(
1769
1818
  "Per-repo upload hooks still active.",
1770
1819
  "Re-run `npx hillclimb` to re-authenticate a repo."
@@ -1829,7 +1878,7 @@ async function runStatus(args = []) {
1829
1878
  } else {
1830
1879
  row(CROSS, "Login", dim(`couldn't reach ${config.apiBaseUrl}`));
1831
1880
  }
1832
- row(CHECK, "Project", bold(config.workspaceName));
1881
+ row(CHECK, "Project", bold(config.projectName));
1833
1882
  row(CHECK, "Contribution", bold(config.contributionTypeName));
1834
1883
  row(CHECK, "Repo", bold(repoName));
1835
1884
  let anyHookInstalled = false;
@@ -1870,8 +1919,8 @@ async function runStatus(args = []) {
1870
1919
  if (config.organizationId) {
1871
1920
  debugRow("Legacy org ID:", config.organizationId);
1872
1921
  }
1873
- const wsIdent = config.workspaceSlug === config.workspaceId ? config.workspaceId : `${config.workspaceSlug} / ${config.workspaceId}`;
1874
- debugRow("Project:", `${config.workspaceName} (${wsIdent})`);
1922
+ const projectIdent = config.projectSlug === config.projectId ? config.projectId : `${config.projectSlug} / ${config.projectId}`;
1923
+ debugRow("Project:", `${config.projectName} (${projectIdent})`);
1875
1924
  debugRow(
1876
1925
  "Contribution type:",
1877
1926
  `${config.contributionTypeName} (${config.contributionTypeSlug})`
@@ -10390,8 +10439,214 @@ var RedactMiddleware = class {
10390
10439
  // src/middleware/index.ts
10391
10440
  var middleware = [];
10392
10441
 
10393
- // src/normalizer/index.ts
10442
+ // src/middleware/secrets.ts
10443
+ import fs6 from "fs";
10394
10444
  import path7 from "path";
10445
+ var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
10446
+ "true",
10447
+ "false",
10448
+ "null",
10449
+ "undefined",
10450
+ "yes",
10451
+ "no",
10452
+ "on",
10453
+ "off",
10454
+ "localhost",
10455
+ "127.0.0.1",
10456
+ "0.0.0.0",
10457
+ "::1",
10458
+ "development",
10459
+ "production",
10460
+ "staging",
10461
+ "test"
10462
+ ]);
10463
+ var STRUCTURAL_VARS = /* @__PURE__ */ new Set([
10464
+ "PATH",
10465
+ "HOME",
10466
+ "SHELL",
10467
+ "USER",
10468
+ "LOGNAME",
10469
+ "LANG",
10470
+ "TERM",
10471
+ "PWD",
10472
+ "OLDPWD",
10473
+ "HOSTNAME",
10474
+ "DISPLAY",
10475
+ "EDITOR",
10476
+ "VISUAL",
10477
+ "PAGER",
10478
+ "SHLVL",
10479
+ "_",
10480
+ "NODE_ENV"
10481
+ ]);
10482
+ var SENSITIVE_PATTERNS = [
10483
+ /_TOKEN$/,
10484
+ /_SECRET$/,
10485
+ /_KEY$/,
10486
+ /_PASSWORD$/,
10487
+ /_API$/,
10488
+ /_AUTH$/,
10489
+ /_CREDENTIAL$/,
10490
+ /_PASS$/,
10491
+ /SECRET/,
10492
+ /PASSWORD/,
10493
+ /PRIVATE/
10494
+ ];
10495
+ var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
10496
+ "DATABASE_URL",
10497
+ "REDIS_URL",
10498
+ "MONGO_URI",
10499
+ "AWS_ACCESS_KEY_ID",
10500
+ "AWS_SECRET_ACCESS_KEY",
10501
+ "SENTRY_DSN",
10502
+ "SLACK_WEBHOOK_URL",
10503
+ "STRIPE_SK"
10504
+ ]);
10505
+ function isSensitiveKey(key) {
10506
+ if (SENSITIVE_EXACT.has(key)) return true;
10507
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
10508
+ }
10509
+ function isStructuralVar(key) {
10510
+ if (STRUCTURAL_VARS.has(key)) return true;
10511
+ if (key.startsWith("XDG_")) return true;
10512
+ return false;
10513
+ }
10514
+ function isUsableValue(value) {
10515
+ if (value.length < 4) return false;
10516
+ if (KNOWN_NON_SECRETS.has(value.toLowerCase())) return false;
10517
+ if (/^\d+$/.test(value)) return false;
10518
+ return true;
10519
+ }
10520
+ async function parseEnvFile(filePath) {
10521
+ const values = [];
10522
+ let content;
10523
+ try {
10524
+ content = await fs6.promises.readFile(filePath, "utf-8");
10525
+ } catch {
10526
+ return values;
10527
+ }
10528
+ for (const line of content.split("\n")) {
10529
+ const trimmed = line.trim();
10530
+ if (!trimmed || trimmed.startsWith("#")) continue;
10531
+ const eqIndex = trimmed.indexOf("=");
10532
+ if (eqIndex === -1) continue;
10533
+ let value = trimmed.slice(eqIndex + 1).trim();
10534
+ if (value.startsWith('"') || value.startsWith("'")) {
10535
+ const quote = value[0];
10536
+ let end = -1;
10537
+ for (let i = 1; i < value.length; i++) {
10538
+ if (value[i] === "\\" && i + 1 < value.length) {
10539
+ i++;
10540
+ continue;
10541
+ }
10542
+ if (value[i] === quote) {
10543
+ end = i;
10544
+ break;
10545
+ }
10546
+ }
10547
+ if (end !== -1) {
10548
+ value = value.slice(1, end).replace(/\\(.)/g, "$1");
10549
+ } else {
10550
+ value = value.slice(1);
10551
+ }
10552
+ } else {
10553
+ const commentIndex = value.indexOf(" #");
10554
+ if (commentIndex !== -1) {
10555
+ value = value.slice(0, commentIndex);
10556
+ }
10557
+ value = value.trim();
10558
+ }
10559
+ if (!value) continue;
10560
+ values.push(value);
10561
+ }
10562
+ return values;
10563
+ }
10564
+ function addWithVariants(set, value) {
10565
+ set.add(value);
10566
+ const encoded = encodeURIComponent(value);
10567
+ if (encoded !== value) {
10568
+ set.add(encoded);
10569
+ }
10570
+ if (value.includes("://")) {
10571
+ try {
10572
+ const url = new URL(value);
10573
+ if (url.password) {
10574
+ const rawPassword = url.password;
10575
+ set.add(rawPassword);
10576
+ const decodedPassword = decodeURIComponent(rawPassword);
10577
+ if (decodedPassword !== rawPassword) {
10578
+ set.add(decodedPassword);
10579
+ }
10580
+ const encodedPw = encodeURIComponent(decodedPassword);
10581
+ if (encodedPw !== decodedPassword && encodedPw !== rawPassword) {
10582
+ set.add(encodedPw);
10583
+ }
10584
+ }
10585
+ } catch {
10586
+ }
10587
+ }
10588
+ }
10589
+ async function discoverEnvFiles(repoRoot) {
10590
+ let entries;
10591
+ try {
10592
+ entries = await fs6.promises.readdir(repoRoot);
10593
+ } catch {
10594
+ return [];
10595
+ }
10596
+ const envFiles = [];
10597
+ for (const name of entries) {
10598
+ if (!name.startsWith(".env")) continue;
10599
+ const filePath = path7.join(repoRoot, name);
10600
+ try {
10601
+ const stat = await fs6.promises.stat(filePath);
10602
+ if (stat.isFile()) envFiles.push(name);
10603
+ } catch {
10604
+ }
10605
+ }
10606
+ return envFiles;
10607
+ }
10608
+ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10609
+ const values = /* @__PURE__ */ new Set();
10610
+ const sourceFiles = [];
10611
+ let processEnvCount = 0;
10612
+ let skippedCount = 0;
10613
+ for (const filePath of envFiles) {
10614
+ sourceFiles.push(filePath);
10615
+ for (const value of await parseEnvFile(filePath)) {
10616
+ if (isUsableValue(value)) {
10617
+ addWithVariants(values, value);
10618
+ } else {
10619
+ skippedCount++;
10620
+ }
10621
+ }
10622
+ }
10623
+ for (const filePath of additionalFiles) {
10624
+ const resolved = path7.resolve(repoRoot, filePath);
10625
+ sourceFiles.push(resolved);
10626
+ for (const value of await parseEnvFile(resolved)) {
10627
+ if (isUsableValue(value)) {
10628
+ addWithVariants(values, value);
10629
+ } else {
10630
+ skippedCount++;
10631
+ }
10632
+ }
10633
+ }
10634
+ for (const [key, value] of Object.entries(process.env)) {
10635
+ if (!value) continue;
10636
+ if (isStructuralVar(key)) continue;
10637
+ if (!isSensitiveKey(key)) continue;
10638
+ if (isUsableValue(value)) {
10639
+ addWithVariants(values, value);
10640
+ processEnvCount++;
10641
+ } else {
10642
+ skippedCount++;
10643
+ }
10644
+ }
10645
+ return { values, sourceFiles, processEnvCount, skippedCount };
10646
+ }
10647
+
10648
+ // src/normalizer/index.ts
10649
+ import path8 from "path";
10395
10650
 
10396
10651
  // src/normalizer/claude.ts
10397
10652
  function stringify(value) {
@@ -10796,8 +11051,7 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
10796
11051
  finalExtra.service_tiers = [...serviceTiers].sort();
10797
11052
  if (cacheCreationSeen)
10798
11053
  finalExtra.total_cache_creation_input_tokens = cacheCreationTotal;
10799
- if (cacheReadSeen)
10800
- finalExtra.total_cache_read_input_tokens = cacheReadTotal;
11054
+ if (cacheReadSeen) finalExtra.total_cache_read_input_tokens = cacheReadTotal;
10801
11055
  const finalMetrics = {
10802
11056
  total_prompt_tokens: promptValues.length > 0 ? promptValues.reduce((a, b) => a + b, 0) : void 0,
10803
11057
  total_completion_tokens: completionValues.length > 0 ? completionValues.reduce((a, b) => a + b, 0) : void 0,
@@ -10855,7 +11109,8 @@ function convertEventToStep(event, stepId, defaultModelName) {
10855
11109
  };
10856
11110
  const observation = event.output !== void 0 ? { results: [observationResult] } : void 0;
10857
11111
  const extra = { ...event.extra ?? {} };
10858
- if (event.metadata !== void 0) extra.metadata = extra.metadata ?? event.metadata;
11112
+ if (event.metadata !== void 0)
11113
+ extra.metadata = extra.metadata ?? event.metadata;
10859
11114
  if (event.raw_arguments !== void 0)
10860
11115
  extra.raw_arguments = extra.raw_arguments ?? event.raw_arguments;
10861
11116
  if (event.status !== void 0) extra.status = extra.status ?? event.status;
@@ -10927,7 +11182,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
10927
11182
  const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
10928
11183
  const metaPayload = sessionMeta?.payload ?? {};
10929
11184
  const sid = sessionId ?? metaPayload.id ?? "";
10930
- let agentVersion = metaPayload.cli_version ?? "unknown";
11185
+ const agentVersion = metaPayload.cli_version ?? "unknown";
10931
11186
  const agentExtra = {};
10932
11187
  for (const key of ["originator", "cwd", "git", "instructions"]) {
10933
11188
  const value = metaPayload[key];
@@ -11045,7 +11300,6 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
11045
11300
  callInfo.timestamp = callInfo.timestamp ?? timestamp;
11046
11301
  normalizedEvents.push(callInfo);
11047
11302
  pendingReasoning = void 0;
11048
- continue;
11049
11303
  }
11050
11304
  }
11051
11305
  const steps = [];
@@ -11260,7 +11514,7 @@ var NormalizeMiddleware = class {
11260
11514
  if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
11261
11515
  const content = file.content ? file.content.toString("utf-8") : null;
11262
11516
  if (!content) continue;
11263
- const sessionId = file.metadata?.sessionId ?? path7.basename(file.absolutePath, ".jsonl");
11517
+ const sessionId = file.metadata?.sessionId ?? path8.basename(file.absolutePath, ".jsonl");
11264
11518
  try {
11265
11519
  const trajectory = normalizeContent(
11266
11520
  file.sourceName,
@@ -11273,10 +11527,7 @@ var NormalizeMiddleware = class {
11273
11527
  null,
11274
11528
  2
11275
11529
  );
11276
- const atifPath = file.absolutePath.replace(
11277
- /\.jsonl$/,
11278
- ".atif.json"
11279
- );
11530
+ const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
11280
11531
  newFiles.push({
11281
11532
  sourceName: file.sourceName,
11282
11533
  absolutePath: atifPath,
@@ -11291,212 +11542,6 @@ var NormalizeMiddleware = class {
11291
11542
  }
11292
11543
  };
11293
11544
 
11294
- // src/middleware/secrets.ts
11295
- import fs6 from "fs";
11296
- import path8 from "path";
11297
- var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
11298
- "true",
11299
- "false",
11300
- "null",
11301
- "undefined",
11302
- "yes",
11303
- "no",
11304
- "on",
11305
- "off",
11306
- "localhost",
11307
- "127.0.0.1",
11308
- "0.0.0.0",
11309
- "::1",
11310
- "development",
11311
- "production",
11312
- "staging",
11313
- "test"
11314
- ]);
11315
- var STRUCTURAL_VARS = /* @__PURE__ */ new Set([
11316
- "PATH",
11317
- "HOME",
11318
- "SHELL",
11319
- "USER",
11320
- "LOGNAME",
11321
- "LANG",
11322
- "TERM",
11323
- "PWD",
11324
- "OLDPWD",
11325
- "HOSTNAME",
11326
- "DISPLAY",
11327
- "EDITOR",
11328
- "VISUAL",
11329
- "PAGER",
11330
- "SHLVL",
11331
- "_",
11332
- "NODE_ENV"
11333
- ]);
11334
- var SENSITIVE_PATTERNS = [
11335
- /_TOKEN$/,
11336
- /_SECRET$/,
11337
- /_KEY$/,
11338
- /_PASSWORD$/,
11339
- /_API$/,
11340
- /_AUTH$/,
11341
- /_CREDENTIAL$/,
11342
- /_PASS$/,
11343
- /SECRET/,
11344
- /PASSWORD/,
11345
- /PRIVATE/
11346
- ];
11347
- var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
11348
- "DATABASE_URL",
11349
- "REDIS_URL",
11350
- "MONGO_URI",
11351
- "AWS_ACCESS_KEY_ID",
11352
- "AWS_SECRET_ACCESS_KEY",
11353
- "SENTRY_DSN",
11354
- "SLACK_WEBHOOK_URL",
11355
- "STRIPE_SK"
11356
- ]);
11357
- function isSensitiveKey(key) {
11358
- if (SENSITIVE_EXACT.has(key)) return true;
11359
- return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
11360
- }
11361
- function isStructuralVar(key) {
11362
- if (STRUCTURAL_VARS.has(key)) return true;
11363
- if (key.startsWith("XDG_")) return true;
11364
- return false;
11365
- }
11366
- function isUsableValue(value) {
11367
- if (value.length < 4) return false;
11368
- if (KNOWN_NON_SECRETS.has(value.toLowerCase())) return false;
11369
- if (/^\d+$/.test(value)) return false;
11370
- return true;
11371
- }
11372
- async function parseEnvFile(filePath) {
11373
- const values = [];
11374
- let content;
11375
- try {
11376
- content = await fs6.promises.readFile(filePath, "utf-8");
11377
- } catch {
11378
- return values;
11379
- }
11380
- for (const line of content.split("\n")) {
11381
- const trimmed = line.trim();
11382
- if (!trimmed || trimmed.startsWith("#")) continue;
11383
- const eqIndex = trimmed.indexOf("=");
11384
- if (eqIndex === -1) continue;
11385
- let value = trimmed.slice(eqIndex + 1).trim();
11386
- if (value.startsWith('"') || value.startsWith("'")) {
11387
- const quote = value[0];
11388
- let end = -1;
11389
- for (let i = 1; i < value.length; i++) {
11390
- if (value[i] === "\\" && i + 1 < value.length) {
11391
- i++;
11392
- continue;
11393
- }
11394
- if (value[i] === quote) {
11395
- end = i;
11396
- break;
11397
- }
11398
- }
11399
- if (end !== -1) {
11400
- value = value.slice(1, end).replace(/\\(.)/g, "$1");
11401
- } else {
11402
- value = value.slice(1);
11403
- }
11404
- } else {
11405
- const commentIndex = value.indexOf(" #");
11406
- if (commentIndex !== -1) {
11407
- value = value.slice(0, commentIndex);
11408
- }
11409
- value = value.trim();
11410
- }
11411
- if (!value) continue;
11412
- values.push(value);
11413
- }
11414
- return values;
11415
- }
11416
- function addWithVariants(set, value) {
11417
- set.add(value);
11418
- const encoded = encodeURIComponent(value);
11419
- if (encoded !== value) {
11420
- set.add(encoded);
11421
- }
11422
- if (value.includes("://")) {
11423
- try {
11424
- const url = new URL(value);
11425
- if (url.password) {
11426
- const rawPassword = url.password;
11427
- set.add(rawPassword);
11428
- const decodedPassword = decodeURIComponent(rawPassword);
11429
- if (decodedPassword !== rawPassword) {
11430
- set.add(decodedPassword);
11431
- }
11432
- const encodedPw = encodeURIComponent(decodedPassword);
11433
- if (encodedPw !== decodedPassword && encodedPw !== rawPassword) {
11434
- set.add(encodedPw);
11435
- }
11436
- }
11437
- } catch {
11438
- }
11439
- }
11440
- }
11441
- async function discoverEnvFiles(repoRoot) {
11442
- let entries;
11443
- try {
11444
- entries = await fs6.promises.readdir(repoRoot);
11445
- } catch {
11446
- return [];
11447
- }
11448
- const envFiles = [];
11449
- for (const name of entries) {
11450
- if (!name.startsWith(".env")) continue;
11451
- const filePath = path8.join(repoRoot, name);
11452
- try {
11453
- const stat = await fs6.promises.stat(filePath);
11454
- if (stat.isFile()) envFiles.push(name);
11455
- } catch {
11456
- }
11457
- }
11458
- return envFiles;
11459
- }
11460
- async function collectSecrets(repoRoot, envFiles, additionalFiles) {
11461
- const values = /* @__PURE__ */ new Set();
11462
- const sourceFiles = [];
11463
- let processEnvCount = 0;
11464
- let skippedCount = 0;
11465
- for (const filePath of envFiles) {
11466
- sourceFiles.push(filePath);
11467
- for (const value of await parseEnvFile(filePath)) {
11468
- if (isUsableValue(value)) {
11469
- addWithVariants(values, value);
11470
- } else {
11471
- skippedCount++;
11472
- }
11473
- }
11474
- }
11475
- for (const filePath of additionalFiles) {
11476
- const resolved = path8.resolve(repoRoot, filePath);
11477
- sourceFiles.push(resolved);
11478
- for (const value of await parseEnvFile(resolved)) {
11479
- if (isUsableValue(value)) {
11480
- addWithVariants(values, value);
11481
- } else {
11482
- skippedCount++;
11483
- }
11484
- }
11485
- }
11486
- for (const [key, value] of Object.entries(process.env)) {
11487
- if (!value) continue;
11488
- if (isStructuralVar(key)) continue;
11489
- if (!isSensitiveKey(key)) continue;
11490
- if (isUsableValue(value)) {
11491
- addWithVariants(values, value);
11492
- processEnvCount++;
11493
- } else {
11494
- skippedCount++;
11495
- }
11496
- }
11497
- return { values, sourceFiles, processEnvCount, skippedCount };
11498
- }
11499
-
11500
11545
  // src/outputs/platform.ts
11501
11546
  import { PassThrough } from "stream";
11502
11547
  import archiver from "archiver";
@@ -11557,14 +11602,14 @@ var PlatformUploadOutput = class {
11557
11602
  const buffer = await buildZipBuffer(group, selectedSources);
11558
11603
  const {
11559
11604
  client,
11560
- workspaceId,
11605
+ projectId,
11561
11606
  contributionTypeSlug,
11562
11607
  contributionTitle,
11563
11608
  contributionBody,
11564
11609
  zipFilename,
11565
11610
  autoSubmit
11566
11611
  } = this.opts;
11567
- const contribution = await client.createContribution(workspaceId, {
11612
+ const contribution = await client.createContribution(projectId, {
11568
11613
  contributionTypeSlug,
11569
11614
  title: contributionTitle,
11570
11615
  body: contributionBody
@@ -11574,7 +11619,10 @@ var PlatformUploadOutput = class {
11574
11619
  mimeType: "application/zip",
11575
11620
  sizeBytes: buffer.byteLength
11576
11621
  });
11577
- appendLog("info", `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`);
11622
+ appendLog(
11623
+ "info",
11624
+ `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
11625
+ );
11578
11626
  await client.uploadToPresignedUrl(
11579
11627
  presigned.presignedUrl,
11580
11628
  presigned.headers,
@@ -11846,7 +11894,7 @@ Uploaded: ${now.toISOString()}`;
11846
11894
  const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
11847
11895
  const output = new PlatformUploadOutput({
11848
11896
  client,
11849
- workspaceId: config.workspaceId,
11897
+ projectId: config.projectId,
11850
11898
  contributionTypeSlug: config.contributionTypeSlug,
11851
11899
  contributionTitle: title,
11852
11900
  contributionBody: body,
@@ -11860,7 +11908,7 @@ Uploaded: ${now.toISOString()}`;
11860
11908
  });
11861
11909
  appendLog(
11862
11910
  "info",
11863
- `Uploaded session ${sessionId} to workspace ${config.workspaceSlug} (${config.workspaceId}) as contribution ${contributionId}`
11911
+ `Uploaded session ${sessionId} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId}`
11864
11912
  );
11865
11913
  } catch (err) {
11866
11914
  if (err instanceof PlatformError && err.status === 401) {
@@ -12461,7 +12509,10 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
12461
12509
  await fs10.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
12462
12510
  for (let i = 0; i < retries; i++) {
12463
12511
  try {
12464
- const fd = await fs10.promises.open(lockPath, fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY);
12512
+ const fd = await fs10.promises.open(
12513
+ lockPath,
12514
+ fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
12515
+ );
12465
12516
  await fd.write(String(process.pid));
12466
12517
  await fd.close();
12467
12518
  return fd.fd;
@@ -12737,7 +12788,7 @@ async function handleStop(payload, tool) {
12737
12788
  const epochSeconds = formatEpochSeconds2(now);
12738
12789
  const shortId = state.sessionId.slice(0, 12);
12739
12790
  const contribution = await client.createContribution(
12740
- project.config.workspaceId,
12791
+ project.config.projectId,
12741
12792
  {
12742
12793
  contributionTypeSlug: GIT_TRACES_SLUG,
12743
12794
  title: `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`,
@@ -13798,12 +13849,12 @@ async function main() {
13798
13849
  case "-h":
13799
13850
  case "help": {
13800
13851
  process.stdout.write(
13801
- "Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Configure this repo for automatic upload\n init Configure this repo for automatic upload (--login forces fresh sign-in)\n export Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n status Show whether you're signed in and this repo is connected (pass --debug for details)\n upload Hook entry point \u2014 reads JSON payload from stdin and uploads one session\n help Show this message\n"
13852
+ "Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Configure this repo for automatic upload\n init Configure this repo for automatic upload (--project scopes setup, --login forces fresh sign-in)\n export Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n status Show whether you're signed in and this repo is connected (pass --debug for details)\n upload Hook entry point \u2014 reads JSON payload from stdin and uploads one session\n help Show this message\n"
13802
13853
  );
13803
13854
  return;
13804
13855
  }
13805
13856
  default:
13806
- if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
13857
+ if (subcommand === "--login" || subcommand === "--project" || subcommand.startsWith("--project=") || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
13807
13858
  await runInit([subcommand, ...rest]);
13808
13859
  return;
13809
13860
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",