hillclimb 0.1.0 → 0.1.2

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 +8 -16
  2. package/dist/cli.js +89 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,32 +1,24 @@
1
1
  # hillclimb
2
2
 
3
- CLI to extract AI coding-tool session logs (Claude Code, Cursor, Codex, opencode) and upload them to a Hillclimb workspace. Runs entirely via `npx` — no global install required.
3
+ Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode) and upload them to a Hillclimb workspace.
4
4
 
5
5
  ## Quickstart
6
6
 
7
7
  ```sh
8
- npx hillclimb login # sign in (opens your browser)
9
- npx hillclimb init # connect the current repo; installs tool-native auto-upload hooks
10
- npx hillclimb status # show login + hook status for this repo
8
+ npx hillclimb init
11
9
  ```
12
10
 
13
- Once a repo is initialized, every subsequent coding session in that repo auto-uploads its transcript when the session ends.
11
+ Run in any git repo. Sessions auto-upload when they end.
14
12
 
15
- ## Interactive export
13
+ ```sh
14
+ npx hillclimb status
15
+ ```
16
16
 
17
- Run `npx hillclimb` with no arguments inside a repo to pick a source, time range, and output format for a one-off export (zip to `~/Downloads`).
17
+ Shows login and hook status for the current repo.
18
18
 
19
19
  ## Hooks
20
20
 
21
- `hillclimb init` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`) and installs upload + git-trace hooks for each. All hooks invoke `npx hillclimb …` so upgrading is just clearing the npx cache the package never needs to be on `PATH`.
22
-
23
- ## Configuration
24
-
25
- - `~/.hillclimb/identity.json` — saved login (cookie), `0600`
26
- - `~/.hillclimb/projects.json` — per-repo workspace binding, `0600`
27
- - `~/.hillclimb/logs/<date>.log` — local upload log (not transmitted)
28
-
29
- Override the API host via `HILLCLIMB_API_URL`.
21
+ `hillclimb init` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`) and installs upload + git-trace hooks for each. You might want to gitignore the respective directories for the tools you use in your repo.
30
22
 
31
23
  ## License
32
24
 
package/dist/cli.js CHANGED
@@ -11941,7 +11941,7 @@ function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
11941
11941
  return "branch-switch";
11942
11942
  }
11943
11943
  }
11944
- function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind) {
11944
+ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
11945
11945
  const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
11946
11946
  const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
11947
11947
  const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
@@ -11953,7 +11953,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
11953
11953
  return {
11954
11954
  sessionId,
11955
11955
  tool,
11956
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
11956
+ startedAt,
11957
11957
  cwd: repoRoot,
11958
11958
  patchFormat: "incremental",
11959
11959
  epoch,
@@ -12111,7 +12111,7 @@ import crypto from "crypto";
12111
12111
  import fs10 from "fs";
12112
12112
  import os7 from "os";
12113
12113
  import path13 from "path";
12114
- var CURRENT_SCHEMA_VERSION = 2;
12114
+ var CURRENT_SCHEMA_VERSION = 3;
12115
12115
  var STATE_DIR = path13.join(os7.homedir(), ".hillclimb", "git-traces");
12116
12116
  function stateFileForRepo(repoRoot, tool) {
12117
12117
  const hash = crypto.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
@@ -12237,7 +12237,16 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
12237
12237
  );
12238
12238
  appendLog("info", `git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`);
12239
12239
  }
12240
- async function openEpoch(params) {
12240
+ function pinEpochBaseline(repoRoot, sessionId, epoch) {
12241
+ const baselineSha = captureBaselineSha(repoRoot);
12242
+ pinRef(
12243
+ repoRoot,
12244
+ `refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`,
12245
+ baselineSha
12246
+ );
12247
+ return { baselineSha, headSha: captureHeadSha(repoRoot) };
12248
+ }
12249
+ async function uploadEpochBaseline(params) {
12241
12250
  const {
12242
12251
  repoRoot,
12243
12252
  client,
@@ -12245,16 +12254,12 @@ async function openEpoch(params) {
12245
12254
  sessionId,
12246
12255
  tool,
12247
12256
  epoch,
12257
+ baselineSha,
12248
12258
  prevHeadSha,
12249
- transitionKind
12259
+ transitionKind,
12260
+ startedAt
12250
12261
  } = params;
12251
- const baselineSha = captureBaselineSha(repoRoot);
12252
12262
  const prefix = epochPrefix(epoch);
12253
- pinRef(
12254
- repoRoot,
12255
- `refs/hillclimb/baseline/${sessionId}/${prefix}`,
12256
- baselineSha
12257
- );
12258
12263
  const metadata = buildBaselineMetadata(
12259
12264
  repoRoot,
12260
12265
  sessionId,
@@ -12263,7 +12268,8 @@ async function openEpoch(params) {
12263
12268
  CLI_VERSION,
12264
12269
  epoch,
12265
12270
  prevHeadSha,
12266
- transitionKind
12271
+ transitionKind,
12272
+ startedAt
12267
12273
  );
12268
12274
  let baselineTreeSha;
12269
12275
  let bundleBuffer;
@@ -12297,11 +12303,17 @@ async function openEpoch(params) {
12297
12303
  "application/x-git-bundle",
12298
12304
  bundleBuffer
12299
12305
  );
12300
- return {
12301
- baselineSha,
12302
- baselineTreeSha,
12303
- headSha: captureHeadSha(repoRoot)
12304
- };
12306
+ return { baselineTreeSha };
12307
+ }
12308
+ async function openEpoch(params) {
12309
+ const { baselineSha, headSha } = pinEpochBaseline(
12310
+ params.repoRoot,
12311
+ params.sessionId,
12312
+ params.epoch
12313
+ );
12314
+ const uploaded = await uploadEpochBaseline({ ...params, baselineSha });
12315
+ if (!uploaded) return null;
12316
+ return { baselineSha, baselineTreeSha: uploaded.baselineTreeSha, headSha };
12305
12317
  }
12306
12318
  async function initializeSession(cwd, tool, sessionId) {
12307
12319
  const project = await findProjectForCwd(cwd);
@@ -12309,57 +12321,25 @@ async function initializeSession(cwd, tool, sessionId) {
12309
12321
  appendLog("warn", "git-traces: no hillclimb project config found, skipping");
12310
12322
  return null;
12311
12323
  }
12312
- const { config } = project;
12313
- const identity = await loadIdentity(config.apiBaseUrl);
12314
- if (!identity) {
12315
- appendLog(
12316
- "warn",
12317
- `git-traces: no saved login for ${config.apiBaseUrl}, skipping`
12318
- );
12319
- return null;
12320
- }
12321
- const client = new PlatformClient(config.apiBaseUrl, identity.sessionCookie);
12322
- const toolLabel = TOOL_LABELS[tool] ?? "Claude";
12323
- const now = /* @__PURE__ */ new Date();
12324
- const shortId = sessionId.slice(0, 12);
12325
- const contribution = await client.createContribution(config.workspaceId, {
12326
- contributionTypeSlug: GIT_TRACES_SLUG,
12327
- title: `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp2(now)}`,
12328
- body: `Session ID: ${sessionId}
12329
- Tool: ${toolLabel}
12330
- Repo: ${cwd}
12331
- Uploaded: ${now.toISOString()}`
12332
- });
12333
- await client.submitContribution(contribution.id);
12334
- const artifacts = await openEpoch({
12335
- repoRoot: cwd,
12336
- client,
12337
- contributionId: contribution.id,
12338
- sessionId,
12339
- tool,
12340
- epoch: 1,
12341
- prevHeadSha: null,
12342
- transitionKind: "initial"
12343
- });
12344
- if (!artifacts) return null;
12324
+ const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
12345
12325
  const state = {
12346
12326
  schemaVersion: CURRENT_SCHEMA_VERSION,
12347
12327
  sessionId,
12348
- contributionId: contribution.id,
12349
- baselineSha: artifacts.baselineSha,
12350
- baselineTreeSha: artifacts.baselineTreeSha,
12351
- lastSnapshotSha: artifacts.baselineSha,
12352
- lastSnapshotTreeSha: artifacts.baselineTreeSha,
12328
+ contributionId: null,
12329
+ baselineSha,
12330
+ baselineTreeSha: null,
12331
+ lastSnapshotSha: baselineSha,
12332
+ lastSnapshotTreeSha: null,
12353
12333
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
12354
12334
  epoch: 1,
12355
12335
  turnCount: 0,
12356
- headSha: artifacts.headSha,
12336
+ headSha,
12357
12337
  repoRoot: cwd
12358
12338
  };
12359
12339
  await writeSessionState(state, tool);
12360
12340
  appendLog(
12361
12341
  "info",
12362
- `git-traces: session started (epoch=1, baseline=${artifacts.baselineSha.slice(0, 8)}, contribution=${contribution.id})`
12342
+ `git-traces: session pending (baseline=${baselineSha.slice(0, 8)}, awaiting first turn)`
12363
12343
  );
12364
12344
  return state;
12365
12345
  }
@@ -12417,6 +12397,53 @@ async function handleStop(payload, tool) {
12417
12397
  project.config.apiBaseUrl,
12418
12398
  identity.sessionCookie
12419
12399
  );
12400
+ if (state.contributionId === null) {
12401
+ const toolLabel = TOOL_LABELS[tool] ?? "Claude";
12402
+ const now = /* @__PURE__ */ new Date();
12403
+ const shortId = state.sessionId.slice(0, 12);
12404
+ const contribution = await client.createContribution(
12405
+ project.config.workspaceId,
12406
+ {
12407
+ contributionTypeSlug: GIT_TRACES_SLUG,
12408
+ title: `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp2(now)}`,
12409
+ body: `Session ID: ${state.sessionId}
12410
+ Tool: ${toolLabel}
12411
+ Repo: ${cwd}
12412
+ Uploaded: ${now.toISOString()}`
12413
+ }
12414
+ );
12415
+ await client.submitContribution(contribution.id);
12416
+ const uploaded = await uploadEpochBaseline({
12417
+ repoRoot: cwd,
12418
+ client,
12419
+ contributionId: contribution.id,
12420
+ sessionId: state.sessionId,
12421
+ tool,
12422
+ epoch: 1,
12423
+ baselineSha: state.baselineSha,
12424
+ prevHeadSha: null,
12425
+ transitionKind: "initial",
12426
+ startedAt: state.startedAt
12427
+ });
12428
+ if (!uploaded) return;
12429
+ state.contributionId = contribution.id;
12430
+ state.baselineTreeSha = uploaded.baselineTreeSha;
12431
+ state.lastSnapshotTreeSha = uploaded.baselineTreeSha;
12432
+ await writeSessionState(state, tool);
12433
+ appendLog(
12434
+ "info",
12435
+ `git-traces: session registered on first turn (epoch=1, baseline=${state.baselineSha.slice(0, 8)}, contribution=${contribution.id})`
12436
+ );
12437
+ }
12438
+ const contributionId = state.contributionId;
12439
+ const lastSnapshotTreeSha = state.lastSnapshotTreeSha;
12440
+ if (contributionId === null || lastSnapshotTreeSha === null) {
12441
+ appendLog(
12442
+ "error",
12443
+ "git-traces: invariant violation \u2014 session state missing contributionId or tree SHA after lazy init"
12444
+ );
12445
+ return;
12446
+ }
12420
12447
  const currentHeadSha = captureHeadSha(cwd);
12421
12448
  if (currentHeadSha !== state.headSha) {
12422
12449
  const transitionKind = detectTransitionKind(
@@ -12432,12 +12459,13 @@ async function handleStop(payload, tool) {
12432
12459
  const artifacts = await openEpoch({
12433
12460
  repoRoot: cwd,
12434
12461
  client,
12435
- contributionId: state.contributionId,
12462
+ contributionId,
12436
12463
  sessionId: state.sessionId,
12437
12464
  tool,
12438
12465
  epoch: nextEpoch,
12439
12466
  prevHeadSha: state.headSha || null,
12440
- transitionKind
12467
+ transitionKind,
12468
+ startedAt: state.startedAt
12441
12469
  });
12442
12470
  if (!artifacts) return;
12443
12471
  const next = {
@@ -12457,7 +12485,7 @@ async function handleStop(payload, tool) {
12457
12485
  const currentTreeSha = buildSnapshotTree(cwd, currentSha);
12458
12486
  const patchBuffer = createTreeDiffPatchGz(
12459
12487
  cwd,
12460
- state.lastSnapshotTreeSha,
12488
+ lastSnapshotTreeSha,
12461
12489
  currentTreeSha
12462
12490
  );
12463
12491
  if (!patchBuffer) {
@@ -12478,7 +12506,7 @@ async function handleStop(payload, tool) {
12478
12506
  const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
12479
12507
  await uploadFile(
12480
12508
  client,
12481
- state.contributionId,
12509
+ contributionId,
12482
12510
  filename,
12483
12511
  "application/gzip",
12484
12512
  patchBuffer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",