hillclimb 0.1.6 → 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 +412 -336
  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(
@@ -501,6 +552,7 @@ import os2 from "os";
501
552
  import path5 from "path";
502
553
  var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
503
554
  var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
555
+ var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
504
556
  var TOOLS = [
505
557
  {
506
558
  tool: "claude",
@@ -508,7 +560,11 @@ var TOOLS = [
508
560
  settingsFile: ".claude/settings.local.json",
509
561
  format: "claude",
510
562
  events: [
511
- { eventName: "SessionEnd", command: HOOK_CMD("upload") },
563
+ {
564
+ eventName: "SessionEnd",
565
+ command: HOOK_CMD("upload"),
566
+ timeout: CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS
567
+ },
512
568
  { eventName: "SessionStart", command: GIT_TRACES_CMD("claude") },
513
569
  { eventName: "Stop", command: GIT_TRACES_CMD("claude") },
514
570
  { eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
@@ -593,11 +649,7 @@ function isDir(p7) {
593
649
  }
594
650
  function copilotChatDetect() {
595
651
  const home = os2.homedir();
596
- const suffix = path5.join(
597
- "User",
598
- "globalStorage",
599
- "github.copilot-chat"
600
- );
652
+ const suffix = path5.join("User", "globalStorage", "github.copilot-chat");
601
653
  const candidates = [
602
654
  path5.join(home, "Library", "Application Support", "Code", suffix),
603
655
  path5.join(home, ".config", "Code", suffix),
@@ -605,6 +657,10 @@ function copilotChatDetect() {
605
657
  ].filter((p7) => p7 !== null);
606
658
  return candidates.some(isDir);
607
659
  }
660
+ function ensureHooks(settings) {
661
+ if (!settings.hooks) settings.hooks = {};
662
+ return settings.hooks;
663
+ }
608
664
  function settingsPath(repoRoot, def) {
609
665
  return path5.join(repoRoot, def.settingsFile);
610
666
  }
@@ -624,31 +680,45 @@ async function writeJson(file, obj) {
624
680
  await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
625
681
  `);
626
682
  }
627
- function claudeHookPresent(matchers, command) {
683
+ function claudeHookPresent(matchers, command, options = {}) {
628
684
  return matchers.some(
629
- (m) => (m.hooks ?? []).some(
630
- (h) => h.type === "command" && h.command === command
631
- )
685
+ (m) => (m.hooks ?? []).some((h) => {
686
+ if (h.type !== "command" || h.command !== command) return false;
687
+ if (options.timeout !== void 0 && h.timeout !== options.timeout) {
688
+ return false;
689
+ }
690
+ return true;
691
+ })
632
692
  );
633
693
  }
634
- function claudeInstall(settings, eventName, command) {
635
- if (!settings.hooks) settings.hooks = {};
636
- if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
637
- const matchers = settings.hooks[eventName];
638
- if (claudeHookPresent(matchers, command)) return false;
694
+ function claudeInstall(settings, eventName, command, options = {}) {
695
+ const hooks = ensureHooks(settings);
696
+ if (!hooks[eventName]) hooks[eventName] = [];
697
+ const matchers = hooks[eventName];
698
+ for (const matcher of matchers) {
699
+ for (const hook of matcher.hooks ?? []) {
700
+ if (hook.type !== "command" || hook.command !== command) continue;
701
+ if (options.timeout === void 0 || hook.timeout === options.timeout) {
702
+ return false;
703
+ }
704
+ hook.timeout = options.timeout;
705
+ return true;
706
+ }
707
+ }
639
708
  matchers.push({
640
709
  matcher: "",
641
- hooks: [{ type: "command", command }]
710
+ hooks: [{ type: "command", command, ...options }]
642
711
  });
643
712
  return true;
644
713
  }
645
- function claudeCheck(settings, eventName, command) {
714
+ function claudeCheck(settings, eventName, command, options = {}) {
646
715
  const matchers = settings.hooks?.[eventName] ?? [];
647
- return claudeHookPresent(matchers, command);
716
+ return claudeHookPresent(matchers, command, options);
648
717
  }
649
718
  function claudeUninstall(settings, eventName, command) {
650
- const matchers = settings.hooks?.[eventName];
651
- if (!matchers || matchers.length === 0) return false;
719
+ const hooks = settings.hooks;
720
+ const matchers = hooks?.[eventName];
721
+ if (!hooks || !matchers || matchers.length === 0) return false;
652
722
  let removedAny = false;
653
723
  const next = [];
654
724
  for (const m of matchers) {
@@ -660,10 +730,10 @@ function claudeUninstall(settings, eventName, command) {
660
730
  }
661
731
  if (!removedAny) return false;
662
732
  if (next.length > 0) {
663
- settings.hooks[eventName] = next;
733
+ hooks[eventName] = next;
664
734
  } else {
665
- delete settings.hooks[eventName];
666
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
735
+ delete hooks[eventName];
736
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
667
737
  }
668
738
  return true;
669
739
  }
@@ -672,9 +742,9 @@ function cursorHookPresent(entries, command) {
672
742
  }
673
743
  function cursorInstall(settings, eventName, command) {
674
744
  if (!settings.version) settings.version = 1;
675
- if (!settings.hooks) settings.hooks = {};
676
- if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
677
- const entries = settings.hooks[eventName];
745
+ const hooks = ensureHooks(settings);
746
+ if (!hooks[eventName]) hooks[eventName] = [];
747
+ const entries = hooks[eventName];
678
748
  if (cursorHookPresent(entries, command)) return false;
679
749
  entries.push({ command });
680
750
  return true;
@@ -684,15 +754,16 @@ function cursorCheck(settings, eventName, command) {
684
754
  return cursorHookPresent(entries, command);
685
755
  }
686
756
  function cursorUninstall(settings, eventName, command) {
687
- const entries = settings.hooks?.[eventName];
688
- if (!entries || entries.length === 0) return false;
757
+ const hooks = settings.hooks;
758
+ const entries = hooks?.[eventName];
759
+ if (!hooks || !entries || entries.length === 0) return false;
689
760
  const next = entries.filter((e) => e.command !== command);
690
761
  if (next.length === entries.length) return false;
691
762
  if (next.length > 0) {
692
- settings.hooks[eventName] = next;
763
+ hooks[eventName] = next;
693
764
  } else {
694
- delete settings.hooks[eventName];
695
- if (Object.keys(settings.hooks).length === 0) {
765
+ delete hooks[eventName];
766
+ if (Object.keys(hooks).length === 0) {
696
767
  delete settings.hooks;
697
768
  delete settings.version;
698
769
  }
@@ -703,9 +774,9 @@ function copilotHookPresent(entries, command) {
703
774
  return entries.some((e) => e.type === "command" && e.command === command);
704
775
  }
705
776
  function copilotInstall(settings, eventName, command) {
706
- if (!settings.hooks) settings.hooks = {};
707
- if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
708
- const entries = settings.hooks[eventName];
777
+ const hooks = ensureHooks(settings);
778
+ if (!hooks[eventName]) hooks[eventName] = [];
779
+ const entries = hooks[eventName];
709
780
  if (copilotHookPresent(entries, command)) return false;
710
781
  entries.push({ type: "command", command });
711
782
  return true;
@@ -715,17 +786,18 @@ function copilotCheck(settings, eventName, command) {
715
786
  return copilotHookPresent(entries, command);
716
787
  }
717
788
  function copilotUninstall(settings, eventName, command) {
718
- const entries = settings.hooks?.[eventName];
719
- if (!entries || entries.length === 0) return false;
789
+ const hooks = settings.hooks;
790
+ const entries = hooks?.[eventName];
791
+ if (!hooks || !entries || entries.length === 0) return false;
720
792
  const next = entries.filter(
721
793
  (e) => !(e.type === "command" && e.command === command)
722
794
  );
723
795
  if (next.length === entries.length) return false;
724
796
  if (next.length > 0) {
725
- settings.hooks[eventName] = next;
797
+ hooks[eventName] = next;
726
798
  } else {
727
- delete settings.hooks[eventName];
728
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
799
+ delete hooks[eventName];
800
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
729
801
  }
730
802
  return true;
731
803
  }
@@ -897,24 +969,24 @@ async function opencodeCheck(file) {
897
969
  return false;
898
970
  }
899
971
  }
900
- function install(settings, format, eventName, command) {
972
+ function install(settings, format, eventName, command, options = {}) {
901
973
  switch (format) {
902
974
  case "cursor":
903
975
  return cursorInstall(settings, eventName, command);
904
976
  case "copilot":
905
977
  return copilotInstall(settings, eventName, command);
906
978
  default:
907
- return claudeInstall(settings, eventName, command);
979
+ return claudeInstall(settings, eventName, command, options);
908
980
  }
909
981
  }
910
- function check(settings, format, eventName, command) {
982
+ function check(settings, format, eventName, command, options = {}) {
911
983
  switch (format) {
912
984
  case "cursor":
913
985
  return cursorCheck(settings, eventName, command);
914
986
  case "copilot":
915
987
  return copilotCheck(settings, eventName, command);
916
988
  default:
917
- return claudeCheck(settings, eventName, command);
989
+ return claudeCheck(settings, eventName, command, options);
918
990
  }
919
991
  }
920
992
  function uninstall(settings, format, eventName, command) {
@@ -961,7 +1033,9 @@ async function installHooksForTool(repoRoot, def) {
961
1033
  mutated = true;
962
1034
  }
963
1035
  }
964
- const added = install(settings, def.format, evt.eventName, evt.command);
1036
+ const added = install(settings, def.format, evt.eventName, evt.command, {
1037
+ timeout: evt.timeout
1038
+ });
965
1039
  if (added) {
966
1040
  installed++;
967
1041
  mutated = true;
@@ -985,7 +1059,9 @@ async function checkHooksForTool(repoRoot, def) {
985
1059
  const settings = await readJson(file);
986
1060
  const missing = [];
987
1061
  for (const evt of def.events) {
988
- if (!check(settings, def.format, evt.eventName, evt.command)) {
1062
+ if (!check(settings, def.format, evt.eventName, evt.command, {
1063
+ timeout: evt.timeout
1064
+ })) {
989
1065
  missing.push(`${evt.eventName}:${evt.command}`);
990
1066
  }
991
1067
  }
@@ -1246,10 +1322,10 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
1246
1322
  }
1247
1323
  async function runInit(args = []) {
1248
1324
  const forceLogin = args.includes("--login");
1249
- const workspaceSlugFlag = parseStringFlag(args, "workspace");
1325
+ const projectSlugFlag = parseStringFlag(args, "project") ?? parseStringFlag(args, "workspace");
1250
1326
  appendLog(
1251
1327
  "info",
1252
- `init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, workspaceSlug=${workspaceSlugFlag ?? "<none>"})`
1328
+ `init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, projectSlug=${projectSlugFlag ?? "<none>"})`
1253
1329
  );
1254
1330
  p3.intro("hillclimb");
1255
1331
  const repo = detectRepoRoot();
@@ -1270,8 +1346,8 @@ async function runInit(args = []) {
1270
1346
  bootstrap = await tryReuseIdentity(apiBaseUrl, client, saved);
1271
1347
  }
1272
1348
  if (!bootstrap) {
1273
- let method = workspaceSlugFlag ? "web" : "email";
1274
- if (IS_TTY && !workspaceSlugFlag) {
1349
+ let method = projectSlugFlag ? "web" : "email";
1350
+ if (IS_TTY && !projectSlugFlag) {
1275
1351
  const selected = await p3.select({
1276
1352
  message: "How would you like to sign in?",
1277
1353
  options: [
@@ -1331,13 +1407,13 @@ async function runInit(args = []) {
1331
1407
  );
1332
1408
  }
1333
1409
  }
1334
- let workspace;
1335
- if (workspaceSlugFlag) {
1410
+ let project;
1411
+ if (projectSlugFlag) {
1336
1412
  const resolved = await withSpinner(
1337
- `Looking up project "${workspaceSlugFlag}"...`,
1413
+ `Looking up project "${projectSlugFlag}"...`,
1338
1414
  (w) => `Found project: ${w.name}`,
1339
- `Could not find project "${workspaceSlugFlag}".`,
1340
- () => client.getWorkspaceBySlug(workspaceSlugFlag)
1415
+ `Could not find project "${projectSlugFlag}".`,
1416
+ () => client.getProjectBySlug(projectSlugFlag)
1341
1417
  );
1342
1418
  if (resolved.status !== "active") {
1343
1419
  p3.log.error(
@@ -1346,21 +1422,21 @@ async function runInit(args = []) {
1346
1422
  p3.outro("Aborted.");
1347
1423
  process.exit(1);
1348
1424
  }
1349
- workspace = resolved;
1425
+ project = resolved;
1350
1426
  appendLog(
1351
1427
  "info",
1352
- `init: project pre-scoped (slug=${workspaceSlugFlag}, id=${resolved.id})`
1428
+ `init: project pre-scoped (slug=${projectSlugFlag}, id=${resolved.id})`
1353
1429
  );
1354
1430
  } else {
1355
- const workspaces = await withSpinner(
1431
+ const projects = await withSpinner(
1356
1432
  "Loading projects...",
1357
1433
  (ws) => `Found ${ws.length} project(s).`,
1358
1434
  "Could not list projects.",
1359
1435
  () => client.listProjects()
1360
1436
  );
1361
- const activeWorkspaces = workspaces.filter((w) => w.status === "active");
1362
- const workspaceChoices = activeWorkspaces.length > 0 ? activeWorkspaces : workspaces;
1363
- 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) {
1364
1440
  p3.log.error(
1365
1441
  "You do not have access to any projects. Create one in the web UI first."
1366
1442
  );
@@ -1368,44 +1444,44 @@ async function runInit(args = []) {
1368
1444
  process.exit(1);
1369
1445
  }
1370
1446
  let picked;
1371
- if (workspaceChoices.length === 1) {
1372
- picked = workspaceChoices[0];
1447
+ if (projectChoices.length === 1) {
1448
+ picked = projectChoices[0];
1373
1449
  p3.log.info(`Using project: ${picked.name}`);
1374
1450
  } else {
1375
- const selectedWorkspaceId = await p3.select({
1451
+ const selectedProjectId = await p3.select({
1376
1452
  message: "Select a project",
1377
- options: workspaceChoices.map((w) => ({
1453
+ options: projectChoices.map((w) => ({
1378
1454
  value: w.id,
1379
1455
  label: w.name,
1380
1456
  hint: `${w.slug} (${w.status})`
1381
1457
  }))
1382
1458
  });
1383
- if (p3.isCancel(selectedWorkspaceId)) {
1459
+ if (p3.isCancel(selectedProjectId)) {
1384
1460
  p3.cancel("Cancelled.");
1385
1461
  process.exit(0);
1386
1462
  }
1387
- picked = workspaceChoices.find((w) => w.id === selectedWorkspaceId);
1463
+ picked = projectChoices.find((w) => w.id === selectedProjectId);
1388
1464
  }
1389
1465
  if (!picked) {
1390
- p3.log.error("Invalid workspace selection.");
1466
+ p3.log.error("Invalid project selection.");
1391
1467
  p3.outro("Aborted.");
1392
1468
  process.exit(1);
1393
1469
  }
1394
- workspace = picked;
1470
+ project = picked;
1395
1471
  appendLog(
1396
1472
  "info",
1397
- `init: project selected (slug=${workspace.slug}, id=${workspace.id})`
1473
+ `init: project selected (slug=${project.slug}, id=${project.id})`
1398
1474
  );
1399
1475
  }
1400
1476
  const types = await withSpinner(
1401
1477
  "Loading contribution types...",
1402
1478
  (t) => `Found ${t.length} contribution type(s).`,
1403
1479
  "Could not list contribution types.",
1404
- () => client.listContributionTypes(workspace.id)
1480
+ () => client.listContributionTypes(project.id)
1405
1481
  );
1406
1482
  if (types.length === 0) {
1407
1483
  p3.log.error(
1408
- `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.`
1409
1485
  );
1410
1486
  p3.outro("Aborted.");
1411
1487
  process.exit(1);
@@ -1452,10 +1528,13 @@ async function runInit(args = []) {
1452
1528
  );
1453
1529
  await upsertProject(repoRoot, {
1454
1530
  apiBaseUrl,
1455
- organizationId: workspace.organizationId,
1456
- workspaceId: workspace.id,
1457
- workspaceSlug: workspace.slug,
1458
- 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,
1459
1538
  contributionTypeSlug: type.slug,
1460
1539
  contributionTypeName: type.name,
1461
1540
  autoSubmit: true
@@ -1520,10 +1599,10 @@ async function runInit(args = []) {
1520
1599
  }
1521
1600
  appendLog(
1522
1601
  "info",
1523
- `init: completed (project=${workspace.slug}, contributionType=${type.slug})`
1602
+ `init: completed (project=${project.slug}, contributionType=${type.slug})`
1524
1603
  );
1525
1604
  p3.outro(
1526
- `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}.`
1527
1606
  );
1528
1607
  }
1529
1608
 
@@ -1593,10 +1672,7 @@ async function runLogin(_args = []) {
1593
1672
  process.stdout.write("\r\x1B[K");
1594
1673
  }
1595
1674
  if (verified) {
1596
- appendLog(
1597
- "info",
1598
- `login: reused saved login (email=${verified.email})`
1599
- );
1675
+ appendLog("info", `login: reused saved login (email=${verified.email})`);
1600
1676
  row(CHECK, "Signed in", cyan(verified.email));
1601
1677
  const fresh = probe.getSessionCookie();
1602
1678
  if (fresh) {
@@ -1737,9 +1813,7 @@ async function runLogout(args = []) {
1737
1813
  footer("Nothing to do.");
1738
1814
  return;
1739
1815
  }
1740
- println(
1741
- ` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`
1742
- );
1816
+ println(` ${CHECK} ${url ? `Signed out from ${bold(url)}` : "Signed out"}`);
1743
1817
  footer(
1744
1818
  "Per-repo upload hooks still active.",
1745
1819
  "Re-run `npx hillclimb` to re-authenticate a repo."
@@ -1804,7 +1878,7 @@ async function runStatus(args = []) {
1804
1878
  } else {
1805
1879
  row(CROSS, "Login", dim(`couldn't reach ${config.apiBaseUrl}`));
1806
1880
  }
1807
- row(CHECK, "Project", bold(config.workspaceName));
1881
+ row(CHECK, "Project", bold(config.projectName));
1808
1882
  row(CHECK, "Contribution", bold(config.contributionTypeName));
1809
1883
  row(CHECK, "Repo", bold(repoName));
1810
1884
  let anyHookInstalled = false;
@@ -1845,8 +1919,8 @@ async function runStatus(args = []) {
1845
1919
  if (config.organizationId) {
1846
1920
  debugRow("Legacy org ID:", config.organizationId);
1847
1921
  }
1848
- const wsIdent = config.workspaceSlug === config.workspaceId ? config.workspaceId : `${config.workspaceSlug} / ${config.workspaceId}`;
1849
- debugRow("Project:", `${config.workspaceName} (${wsIdent})`);
1922
+ const projectIdent = config.projectSlug === config.projectId ? config.projectId : `${config.projectSlug} / ${config.projectId}`;
1923
+ debugRow("Project:", `${config.projectName} (${projectIdent})`);
1850
1924
  debugRow(
1851
1925
  "Contribution type:",
1852
1926
  `${config.contributionTypeName} (${config.contributionTypeSlug})`
@@ -10365,8 +10439,214 @@ var RedactMiddleware = class {
10365
10439
  // src/middleware/index.ts
10366
10440
  var middleware = [];
10367
10441
 
10368
- // src/normalizer/index.ts
10442
+ // src/middleware/secrets.ts
10443
+ import fs6 from "fs";
10369
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";
10370
10650
 
10371
10651
  // src/normalizer/claude.ts
10372
10652
  function stringify(value) {
@@ -10771,8 +11051,7 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
10771
11051
  finalExtra.service_tiers = [...serviceTiers].sort();
10772
11052
  if (cacheCreationSeen)
10773
11053
  finalExtra.total_cache_creation_input_tokens = cacheCreationTotal;
10774
- if (cacheReadSeen)
10775
- finalExtra.total_cache_read_input_tokens = cacheReadTotal;
11054
+ if (cacheReadSeen) finalExtra.total_cache_read_input_tokens = cacheReadTotal;
10776
11055
  const finalMetrics = {
10777
11056
  total_prompt_tokens: promptValues.length > 0 ? promptValues.reduce((a, b) => a + b, 0) : void 0,
10778
11057
  total_completion_tokens: completionValues.length > 0 ? completionValues.reduce((a, b) => a + b, 0) : void 0,
@@ -10830,7 +11109,8 @@ function convertEventToStep(event, stepId, defaultModelName) {
10830
11109
  };
10831
11110
  const observation = event.output !== void 0 ? { results: [observationResult] } : void 0;
10832
11111
  const extra = { ...event.extra ?? {} };
10833
- if (event.metadata !== void 0) extra.metadata = extra.metadata ?? event.metadata;
11112
+ if (event.metadata !== void 0)
11113
+ extra.metadata = extra.metadata ?? event.metadata;
10834
11114
  if (event.raw_arguments !== void 0)
10835
11115
  extra.raw_arguments = extra.raw_arguments ?? event.raw_arguments;
10836
11116
  if (event.status !== void 0) extra.status = extra.status ?? event.status;
@@ -10902,7 +11182,7 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
10902
11182
  const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
10903
11183
  const metaPayload = sessionMeta?.payload ?? {};
10904
11184
  const sid = sessionId ?? metaPayload.id ?? "";
10905
- let agentVersion = metaPayload.cli_version ?? "unknown";
11185
+ const agentVersion = metaPayload.cli_version ?? "unknown";
10906
11186
  const agentExtra = {};
10907
11187
  for (const key of ["originator", "cwd", "git", "instructions"]) {
10908
11188
  const value = metaPayload[key];
@@ -11020,7 +11300,6 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
11020
11300
  callInfo.timestamp = callInfo.timestamp ?? timestamp;
11021
11301
  normalizedEvents.push(callInfo);
11022
11302
  pendingReasoning = void 0;
11023
- continue;
11024
11303
  }
11025
11304
  }
11026
11305
  const steps = [];
@@ -11235,7 +11514,7 @@ var NormalizeMiddleware = class {
11235
11514
  if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
11236
11515
  const content = file.content ? file.content.toString("utf-8") : null;
11237
11516
  if (!content) continue;
11238
- const sessionId = file.metadata?.sessionId ?? path7.basename(file.absolutePath, ".jsonl");
11517
+ const sessionId = file.metadata?.sessionId ?? path8.basename(file.absolutePath, ".jsonl");
11239
11518
  try {
11240
11519
  const trajectory = normalizeContent(
11241
11520
  file.sourceName,
@@ -11248,10 +11527,7 @@ var NormalizeMiddleware = class {
11248
11527
  null,
11249
11528
  2
11250
11529
  );
11251
- const atifPath = file.absolutePath.replace(
11252
- /\.jsonl$/,
11253
- ".atif.json"
11254
- );
11530
+ const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
11255
11531
  newFiles.push({
11256
11532
  sourceName: file.sourceName,
11257
11533
  absolutePath: atifPath,
@@ -11266,212 +11542,6 @@ var NormalizeMiddleware = class {
11266
11542
  }
11267
11543
  };
11268
11544
 
11269
- // src/middleware/secrets.ts
11270
- import fs6 from "fs";
11271
- import path8 from "path";
11272
- var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
11273
- "true",
11274
- "false",
11275
- "null",
11276
- "undefined",
11277
- "yes",
11278
- "no",
11279
- "on",
11280
- "off",
11281
- "localhost",
11282
- "127.0.0.1",
11283
- "0.0.0.0",
11284
- "::1",
11285
- "development",
11286
- "production",
11287
- "staging",
11288
- "test"
11289
- ]);
11290
- var STRUCTURAL_VARS = /* @__PURE__ */ new Set([
11291
- "PATH",
11292
- "HOME",
11293
- "SHELL",
11294
- "USER",
11295
- "LOGNAME",
11296
- "LANG",
11297
- "TERM",
11298
- "PWD",
11299
- "OLDPWD",
11300
- "HOSTNAME",
11301
- "DISPLAY",
11302
- "EDITOR",
11303
- "VISUAL",
11304
- "PAGER",
11305
- "SHLVL",
11306
- "_",
11307
- "NODE_ENV"
11308
- ]);
11309
- var SENSITIVE_PATTERNS = [
11310
- /_TOKEN$/,
11311
- /_SECRET$/,
11312
- /_KEY$/,
11313
- /_PASSWORD$/,
11314
- /_API$/,
11315
- /_AUTH$/,
11316
- /_CREDENTIAL$/,
11317
- /_PASS$/,
11318
- /SECRET/,
11319
- /PASSWORD/,
11320
- /PRIVATE/
11321
- ];
11322
- var SENSITIVE_EXACT = /* @__PURE__ */ new Set([
11323
- "DATABASE_URL",
11324
- "REDIS_URL",
11325
- "MONGO_URI",
11326
- "AWS_ACCESS_KEY_ID",
11327
- "AWS_SECRET_ACCESS_KEY",
11328
- "SENTRY_DSN",
11329
- "SLACK_WEBHOOK_URL",
11330
- "STRIPE_SK"
11331
- ]);
11332
- function isSensitiveKey(key) {
11333
- if (SENSITIVE_EXACT.has(key)) return true;
11334
- return SENSITIVE_PATTERNS.some((pattern) => pattern.test(key));
11335
- }
11336
- function isStructuralVar(key) {
11337
- if (STRUCTURAL_VARS.has(key)) return true;
11338
- if (key.startsWith("XDG_")) return true;
11339
- return false;
11340
- }
11341
- function isUsableValue(value) {
11342
- if (value.length < 4) return false;
11343
- if (KNOWN_NON_SECRETS.has(value.toLowerCase())) return false;
11344
- if (/^\d+$/.test(value)) return false;
11345
- return true;
11346
- }
11347
- async function parseEnvFile(filePath) {
11348
- const values = [];
11349
- let content;
11350
- try {
11351
- content = await fs6.promises.readFile(filePath, "utf-8");
11352
- } catch {
11353
- return values;
11354
- }
11355
- for (const line of content.split("\n")) {
11356
- const trimmed = line.trim();
11357
- if (!trimmed || trimmed.startsWith("#")) continue;
11358
- const eqIndex = trimmed.indexOf("=");
11359
- if (eqIndex === -1) continue;
11360
- let value = trimmed.slice(eqIndex + 1).trim();
11361
- if (value.startsWith('"') || value.startsWith("'")) {
11362
- const quote = value[0];
11363
- let end = -1;
11364
- for (let i = 1; i < value.length; i++) {
11365
- if (value[i] === "\\" && i + 1 < value.length) {
11366
- i++;
11367
- continue;
11368
- }
11369
- if (value[i] === quote) {
11370
- end = i;
11371
- break;
11372
- }
11373
- }
11374
- if (end !== -1) {
11375
- value = value.slice(1, end).replace(/\\(.)/g, "$1");
11376
- } else {
11377
- value = value.slice(1);
11378
- }
11379
- } else {
11380
- const commentIndex = value.indexOf(" #");
11381
- if (commentIndex !== -1) {
11382
- value = value.slice(0, commentIndex);
11383
- }
11384
- value = value.trim();
11385
- }
11386
- if (!value) continue;
11387
- values.push(value);
11388
- }
11389
- return values;
11390
- }
11391
- function addWithVariants(set, value) {
11392
- set.add(value);
11393
- const encoded = encodeURIComponent(value);
11394
- if (encoded !== value) {
11395
- set.add(encoded);
11396
- }
11397
- if (value.includes("://")) {
11398
- try {
11399
- const url = new URL(value);
11400
- if (url.password) {
11401
- const rawPassword = url.password;
11402
- set.add(rawPassword);
11403
- const decodedPassword = decodeURIComponent(rawPassword);
11404
- if (decodedPassword !== rawPassword) {
11405
- set.add(decodedPassword);
11406
- }
11407
- const encodedPw = encodeURIComponent(decodedPassword);
11408
- if (encodedPw !== decodedPassword && encodedPw !== rawPassword) {
11409
- set.add(encodedPw);
11410
- }
11411
- }
11412
- } catch {
11413
- }
11414
- }
11415
- }
11416
- async function discoverEnvFiles(repoRoot) {
11417
- let entries;
11418
- try {
11419
- entries = await fs6.promises.readdir(repoRoot);
11420
- } catch {
11421
- return [];
11422
- }
11423
- const envFiles = [];
11424
- for (const name of entries) {
11425
- if (!name.startsWith(".env")) continue;
11426
- const filePath = path8.join(repoRoot, name);
11427
- try {
11428
- const stat = await fs6.promises.stat(filePath);
11429
- if (stat.isFile()) envFiles.push(name);
11430
- } catch {
11431
- }
11432
- }
11433
- return envFiles;
11434
- }
11435
- async function collectSecrets(repoRoot, envFiles, additionalFiles) {
11436
- const values = /* @__PURE__ */ new Set();
11437
- const sourceFiles = [];
11438
- let processEnvCount = 0;
11439
- let skippedCount = 0;
11440
- for (const filePath of envFiles) {
11441
- sourceFiles.push(filePath);
11442
- for (const value of await parseEnvFile(filePath)) {
11443
- if (isUsableValue(value)) {
11444
- addWithVariants(values, value);
11445
- } else {
11446
- skippedCount++;
11447
- }
11448
- }
11449
- }
11450
- for (const filePath of additionalFiles) {
11451
- const resolved = path8.resolve(repoRoot, filePath);
11452
- sourceFiles.push(resolved);
11453
- for (const value of await parseEnvFile(resolved)) {
11454
- if (isUsableValue(value)) {
11455
- addWithVariants(values, value);
11456
- } else {
11457
- skippedCount++;
11458
- }
11459
- }
11460
- }
11461
- for (const [key, value] of Object.entries(process.env)) {
11462
- if (!value) continue;
11463
- if (isStructuralVar(key)) continue;
11464
- if (!isSensitiveKey(key)) continue;
11465
- if (isUsableValue(value)) {
11466
- addWithVariants(values, value);
11467
- processEnvCount++;
11468
- } else {
11469
- skippedCount++;
11470
- }
11471
- }
11472
- return { values, sourceFiles, processEnvCount, skippedCount };
11473
- }
11474
-
11475
11545
  // src/outputs/platform.ts
11476
11546
  import { PassThrough } from "stream";
11477
11547
  import archiver from "archiver";
@@ -11532,14 +11602,14 @@ var PlatformUploadOutput = class {
11532
11602
  const buffer = await buildZipBuffer(group, selectedSources);
11533
11603
  const {
11534
11604
  client,
11535
- workspaceId,
11605
+ projectId,
11536
11606
  contributionTypeSlug,
11537
11607
  contributionTitle,
11538
11608
  contributionBody,
11539
11609
  zipFilename,
11540
11610
  autoSubmit
11541
11611
  } = this.opts;
11542
- const contribution = await client.createContribution(workspaceId, {
11612
+ const contribution = await client.createContribution(projectId, {
11543
11613
  contributionTypeSlug,
11544
11614
  title: contributionTitle,
11545
11615
  body: contributionBody
@@ -11549,7 +11619,10 @@ var PlatformUploadOutput = class {
11549
11619
  mimeType: "application/zip",
11550
11620
  sizeBytes: buffer.byteLength
11551
11621
  });
11552
- appendLog("info", `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`);
11622
+ appendLog(
11623
+ "info",
11624
+ `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
11625
+ );
11553
11626
  await client.uploadToPresignedUrl(
11554
11627
  presigned.presignedUrl,
11555
11628
  presigned.headers,
@@ -11821,7 +11894,7 @@ Uploaded: ${now.toISOString()}`;
11821
11894
  const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
11822
11895
  const output = new PlatformUploadOutput({
11823
11896
  client,
11824
- workspaceId: config.workspaceId,
11897
+ projectId: config.projectId,
11825
11898
  contributionTypeSlug: config.contributionTypeSlug,
11826
11899
  contributionTitle: title,
11827
11900
  contributionBody: body,
@@ -11835,7 +11908,7 @@ Uploaded: ${now.toISOString()}`;
11835
11908
  });
11836
11909
  appendLog(
11837
11910
  "info",
11838
- `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}`
11839
11912
  );
11840
11913
  } catch (err) {
11841
11914
  if (err instanceof PlatformError && err.status === 401) {
@@ -12436,7 +12509,10 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
12436
12509
  await fs10.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
12437
12510
  for (let i = 0; i < retries; i++) {
12438
12511
  try {
12439
- 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
+ );
12440
12516
  await fd.write(String(process.pid));
12441
12517
  await fd.close();
12442
12518
  return fd.fd;
@@ -12712,7 +12788,7 @@ async function handleStop(payload, tool) {
12712
12788
  const epochSeconds = formatEpochSeconds2(now);
12713
12789
  const shortId = state.sessionId.slice(0, 12);
12714
12790
  const contribution = await client.createContribution(
12715
- project.config.workspaceId,
12791
+ project.config.projectId,
12716
12792
  {
12717
12793
  contributionTypeSlug: GIT_TRACES_SLUG,
12718
12794
  title: `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`,
@@ -13773,12 +13849,12 @@ async function main() {
13773
13849
  case "-h":
13774
13850
  case "help": {
13775
13851
  process.stdout.write(
13776
- "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"
13777
13853
  );
13778
13854
  return;
13779
13855
  }
13780
13856
  default:
13781
- if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
13857
+ if (subcommand === "--login" || subcommand === "--project" || subcommand.startsWith("--project=") || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
13782
13858
  await runInit([subcommand, ...rest]);
13783
13859
  return;
13784
13860
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.1.6",
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",