runtape 0.8.0 → 0.9.0

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.
@@ -12,6 +12,15 @@ var CliAugment = z.object({
12
12
  sequence: z.number().int().nonnegative(),
13
13
  agent_tool_use_id: z.string().min(1).optional()
14
14
  });
15
+ var GitContext = z.object({
16
+ repo: z.string().min(1),
17
+ remote_url: z.string().min(1).optional(),
18
+ remote_host: z.string().min(1).optional(),
19
+ remote_slug: z.string().min(1).optional(),
20
+ branch: z.string().min(1).optional(),
21
+ commit_sha: z.string().length(40).optional(),
22
+ is_dirty: z.boolean().optional()
23
+ });
15
24
  var SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
16
25
  type: z.literal("session_start"),
17
26
  source: z.string(),
@@ -19,7 +28,13 @@ var SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
19
28
  // this in on SessionStart (outermost git repo basename, falling back to
20
29
  // nearest package.json name, finally to basename(cwd)). Optional so older
21
30
  // CLIs that never emit it remain compatible.
22
- project_name: z.string().min(1).optional()
31
+ project_name: z.string().min(1).optional(),
32
+ // Snapshot of the git state when Claude Code started: repo, branch, HEAD
33
+ // SHA, dirty bit, remote slug for deep-linking. Captured once per session
34
+ // — branch/SHA at runtime drift is acceptable; we anchor "what code was
35
+ // this run started against?". Optional so non-git sessions still ingest
36
+ // cleanly.
37
+ git_context: GitContext.optional()
23
38
  });
24
39
  var UserPromptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
25
40
  type: z.literal("user_prompt"),
@@ -95,6 +110,7 @@ var IngestionResponse = z.object({
95
110
  });
96
111
 
97
112
  export {
113
+ GitContext,
98
114
  SessionStartEvent,
99
115
  UserPromptEvent,
100
116
  ToolAttemptEvent,
@@ -107,4 +123,4 @@ export {
107
123
  IngestionRequest,
108
124
  IngestionResponse
109
125
  };
110
- //# sourceMappingURL=chunk-54VJGDD2.js.map
126
+ //# sourceMappingURL=chunk-4AEXXI4C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { z } from 'zod';\n\n// Every Claude Code hook event carries these envelope fields.\nconst ClaudeHookBase = z.object({\n session_id: z.string().min(1),\n transcript_path: z.string().min(1),\n cwd: z.string().min(1),\n hook_event_name: z.string().min(1),\n permission_mode: z.string().optional(),\n});\n\n// CLI-augmented envelope adds monotonic ordering + wall-clock timestamp.\n// agent_tool_use_id is set when the CLI is synthesizing events from a\n// subagent transcript — it carries the parent Agent step's tool_use_id so\n// the server can resolve parent_step_id deterministically instead of\n// relying on the temporal open-stack heuristic. Optional so the field is\n// absent for normal top-level events.\nconst CliAugment = z.object({\n wall_ts: z.string().datetime(),\n sequence: z.number().int().nonnegative(),\n agent_tool_use_id: z.string().min(1).optional(),\n});\n\n// Git context attached to SessionStart when the cwd is inside a working tree.\n// All inner fields are optional individually — a session in a detached HEAD\n// repo can carry repo + commit_sha but no branch, and a freshly init'd repo\n// can carry repo + branch but no remote_url. The CLI never throws on\n// detection failure; absence of git_context just means \"no repo detected\n// here\" (e.g. /tmp scratch session).\nexport const GitContext = z.object({\n repo: z.string().min(1),\n remote_url: z.string().min(1).optional(),\n remote_host: z.string().min(1).optional(),\n remote_slug: z.string().min(1).optional(),\n branch: z.string().min(1).optional(),\n commit_sha: z.string().length(40).optional(),\n is_dirty: z.boolean().optional(),\n});\n\nexport const SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_start'),\n source: z.string(),\n // Best-effort project label derived from the session's cwd. The CLI fills\n // this in on SessionStart (outermost git repo basename, falling back to\n // nearest package.json name, finally to basename(cwd)). Optional so older\n // CLIs that never emit it remain compatible.\n project_name: z.string().min(1).optional(),\n // Snapshot of the git state when Claude Code started: repo, branch, HEAD\n // SHA, dirty bit, remote slug for deep-linking. Captured once per session\n // — branch/SHA at runtime drift is acceptable; we anchor \"what code was\n // this run started against?\". Optional so non-git sessions still ingest\n // cleanly.\n git_context: GitContext.optional(),\n});\n\nexport const UserPromptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('user_prompt'),\n prompt: z.string(),\n});\n\nexport const ToolAttemptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('tool_attempt'),\n tool_name: z.string().min(1),\n tool_input: z.unknown(),\n tool_use_id: z.string().min(1),\n});\n\nexport const ToolCallEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('tool_call'),\n tool_name: z.string().min(1),\n tool_input: z.unknown(),\n tool_response: z.unknown(),\n tool_use_id: z.string().min(1),\n duration_ms: z.number().nonnegative(),\n // Set by the CLI when the tool_response signals an error (Bash non-zero\n // exit, Edit/Write rejection, tool_response.is_error, etc.). Optional so\n // older CLI versions stay forward-compatible.\n is_error: z.boolean().optional(),\n error_message: z.string().optional(),\n});\n\n// Emitted by the CLI on Stop / PostToolUse after scanning the Claude Code\n// transcript JSONL. One event per assistant message we haven't seen yet,\n// keyed by message_uuid so re-deliveries dedupe at the server. Carries the\n// model identifier and token usage for that turn — the only place either\n// datum is available in Claude Code's emit chain.\nexport const AssistantTurnEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('assistant_turn'),\n message_uuid: z.string().min(1),\n model: z.string().min(1),\n input_tokens: z.number().int().nonnegative(),\n output_tokens: z.number().int().nonnegative(),\n cache_read_tokens: z.number().int().nonnegative().default(0),\n cache_creation_tokens: z.number().int().nonnegative().default(0),\n text: z.string().optional(),\n});\n\nexport const SubagentEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('subagent_end'),\n agent_id: z.string().min(1),\n agent_type: z.string().min(1),\n agent_transcript_path: z.string().min(1),\n last_assistant_message: z.string(),\n stop_hook_active: z.boolean().optional(),\n});\n\n// Stop hook (fires every turn). Despite the literal name 'session_end', this\n// is \"turn end\" semantically. Kept for backward compatibility with CLI <= 0.3.x.\n// New CLIs still emit this; the server interprets it as \"run is idle\".\nexport const SessionEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_end'),\n last_assistant_message: z.string().optional(),\n stop_hook_active: z.boolean().optional(),\n});\n\n// SessionEnd hook (fires once when Claude Code actually closes the session).\n// Server uses this to promote the run from 'idle' to 'ended'.\nexport const SessionCloseEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_close'),\n reason: z.string().optional(),\n});\n\nexport const RuntapeEvent = z.discriminatedUnion('type', [\n SessionStartEvent,\n UserPromptEvent,\n ToolAttemptEvent,\n ToolCallEvent,\n AssistantTurnEvent,\n SubagentEndEvent,\n SessionEndEvent,\n SessionCloseEvent,\n]);\n\nexport type RuntapeEvent = z.infer<typeof RuntapeEvent>;\n\n// POST /v1/events body — a batch of up to 500 events. The cap MUST match the\n// CLI flusher's BATCH_MAX (src/lib/flusher.ts). They diverged silently from\n// 0.5.0–0.7.0 (flusher = 500, schema = 100), causing every overflow batch to\n// 400 with \"too_big\" → flusher's poison-drop path nuked them, taking out\n// Stop hooks and any other events that piled up with them. Keep these two\n// numbers in lockstep — if you raise one, raise the other.\nexport const IngestionRequest = z.object({\n events: z.array(RuntapeEvent).min(1).max(500),\n});\n\nexport type IngestionRequest = z.infer<typeof IngestionRequest>;\n\n// Response shape (server returns 200 on accepted, 400 on Zod failure, 401 on bad auth).\nexport const IngestionResponse = z.object({\n accepted: z.number().int().nonnegative(),\n errors: z\n .array(\n z.object({\n index: z.number().int().nonnegative(),\n reason: z.string(),\n }),\n )\n .default([]),\n});\n\nexport type IngestionResponse = z.infer<typeof IngestionResponse>;\n"],"mappings":";AAAA,SAAS,SAAS;AAGlB,IAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACrB,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,iBAAiB,EAAE,OAAO,EAAE,SAAS;AACvC,CAAC;AAQD,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAChD,CAAC;AAQM,IAAM,aAAa,EAAE,OAAO;AAAA,EACjC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACtB,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACvC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACnC,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,SAAS;AAAA,EAC3C,UAAU,EAAE,QAAQ,EAAE,SAAS;AACjC,CAAC;AAEM,IAAM,oBAAoB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC9E,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,QAAQ,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,EAKjB,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzC,aAAa,WAAW,SAAS;AACnC,CAAC;AAEM,IAAM,kBAAkB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC5E,MAAM,EAAE,QAAQ,aAAa;AAAA,EAC7B,QAAQ,EAAE,OAAO;AACnB,CAAC;AAEM,IAAM,mBAAmB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC7E,MAAM,EAAE,QAAQ,cAAc;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,YAAY,EAAE,QAAQ;AAAA,EACtB,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAC/B,CAAC;AAEM,IAAM,gBAAgB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC1E,MAAM,EAAE,QAAQ,WAAW;AAAA,EAC3B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,YAAY,EAAE,QAAQ;AAAA,EACtB,eAAe,EAAE,QAAQ;AAAA,EACzB,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,YAAY;AAAA;AAAA;AAAA;AAAA,EAIpC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,eAAe,EAAE,OAAO,EAAE,SAAS;AACrC,CAAC;AAOM,IAAM,qBAAqB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC/E,MAAM,EAAE,QAAQ,gBAAgB;AAAA,EAChC,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC3C,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC5C,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,EAC3D,uBAAuB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,EAC/D,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC;AAEM,IAAM,mBAAmB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC7E,MAAM,EAAE,QAAQ,cAAc;AAAA,EAC9B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,uBAAuB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvC,wBAAwB,EAAE,OAAO;AAAA,EACjC,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AACzC,CAAC;AAKM,IAAM,kBAAkB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC5E,MAAM,EAAE,QAAQ,aAAa;AAAA,EAC7B,wBAAwB,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5C,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AACzC,CAAC;AAIM,IAAM,oBAAoB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC9E,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAC9B,CAAC;AAEM,IAAM,eAAe,EAAE,mBAAmB,QAAQ;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAUM,IAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,QAAQ,EAAE,MAAM,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAC9C,CAAC;AAKM,IAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,QAAQ,EACL;AAAA,IACC,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,MACpC,QAAQ,EAAE,OAAO;AAAA,IACnB,CAAC;AAAA,EACH,EACC,QAAQ,CAAC,CAAC;AACf,CAAC;","names":[]}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RuntapeEvent
3
- } from "./chunk-54VJGDD2.js";
3
+ } from "./chunk-4AEXXI4C.js";
4
4
 
5
5
  // src/index.ts
6
6
  import { readFileSync } from "fs";
@@ -822,6 +822,83 @@ async function findNearestPackageName(cwd) {
822
822
  return null;
823
823
  }
824
824
 
825
+ // src/lib/git-context.ts
826
+ import { execFile } from "child_process";
827
+ import { basename as basename2 } from "path";
828
+ var GIT_TIMEOUT_MS = 500;
829
+ async function detectGitContext(cwd) {
830
+ const repoRoot = await git(["rev-parse", "--show-toplevel"], cwd);
831
+ if (!repoRoot) return null;
832
+ const [remoteUrl, branchRaw, sha, statusOut] = await Promise.all([
833
+ git(["config", "--get", "remote.origin.url"], cwd),
834
+ git(["rev-parse", "--abbrev-ref", "HEAD"], cwd),
835
+ git(["rev-parse", "HEAD"], cwd),
836
+ // --porcelain emits one line per changed entry; empty output = clean.
837
+ git(["status", "--porcelain"], cwd)
838
+ ]);
839
+ const ctx = { repo: basename2(repoRoot) };
840
+ if (remoteUrl) {
841
+ ctx.remote_url = remoteUrl;
842
+ const parsed = parseRemoteUrl(remoteUrl);
843
+ if (parsed) {
844
+ ctx.remote_host = parsed.host;
845
+ ctx.remote_slug = parsed.slug;
846
+ }
847
+ }
848
+ if (branchRaw && branchRaw !== "HEAD") {
849
+ ctx.branch = branchRaw;
850
+ }
851
+ if (sha && /^[0-9a-f]{40}$/.test(sha)) {
852
+ ctx.commit_sha = sha;
853
+ }
854
+ if (statusOut !== null) {
855
+ ctx.is_dirty = statusOut.length > 0;
856
+ }
857
+ return ctx;
858
+ }
859
+ function git(args, cwd) {
860
+ return new Promise((resolve3) => {
861
+ const child = execFile(
862
+ "git",
863
+ args,
864
+ {
865
+ cwd,
866
+ timeout: GIT_TIMEOUT_MS,
867
+ // 64KB ceiling — `git status --porcelain` on a huge dirty tree could
868
+ // theoretically grow, but we only care about empty-vs-not so we can
869
+ // cap aggressively. If we hit the cap the buffer truncates and we
870
+ // still get the right is_dirty signal.
871
+ maxBuffer: 64 * 1024,
872
+ windowsHide: true
873
+ },
874
+ (err, stdout) => {
875
+ if (err) return resolve3(null);
876
+ resolve3(stdout.toString().trim());
877
+ }
878
+ );
879
+ child.on("error", () => resolve3(null));
880
+ });
881
+ }
882
+ var REMOTE_PATTERNS = [
883
+ // git@host:owner/repo(.git)?
884
+ /^git@([^:]+):(.+?)(?:\.git)?$/,
885
+ // ssh://git@host/owner/repo(.git)?
886
+ /^ssh:\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/,
887
+ // https?://(user@)?host/owner/repo(.git)?
888
+ /^https?:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/
889
+ ];
890
+ function parseRemoteUrl(url) {
891
+ for (const re of REMOTE_PATTERNS) {
892
+ const m = re.exec(url.trim());
893
+ if (m) {
894
+ const host = m[1].toLowerCase();
895
+ const slug = m[2].replace(/\.git$/, "");
896
+ if (slug.includes("/")) return { host, slug };
897
+ }
898
+ }
899
+ return null;
900
+ }
901
+
825
902
  // src/commands/push.ts
826
903
  function isDisabledByEnv() {
827
904
  const v = (process.env.RUNTAPE_DISABLE ?? "").trim().toLowerCase();
@@ -883,10 +960,16 @@ async function pushCommand(opts) {
883
960
  return 0;
884
961
  }
885
962
  if (result.event.type === "session_start" && cwd) {
886
- const projectName = await detectProjectName(cwd);
963
+ const [projectName, gitContext] = await Promise.all([
964
+ detectProjectName(cwd),
965
+ detectGitContext(cwd)
966
+ ]);
887
967
  if (projectName) {
888
968
  result.event.project_name = projectName;
889
969
  }
970
+ if (gitContext) {
971
+ result.event.git_context = gitContext;
972
+ }
890
973
  }
891
974
  await appendEvent(sessionId, result.event);
892
975
  if (opts.event === "PostToolUse" || opts.event === "Stop" || opts.event === "SubagentStop") {