runtape 0.8.0 → 0.9.1

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,19 @@ 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
+ // true = HEAD commit reachable from at least one remote-tracking branch
24
+ // (safe to deep-link to GitHub). false = local-only. Absent when the
25
+ // CLI couldn't decide (no remotes / detached / git lookup failed).
26
+ commit_pushed: z.boolean().optional()
27
+ });
15
28
  var SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
16
29
  type: z.literal("session_start"),
17
30
  source: z.string(),
@@ -19,7 +32,13 @@ var SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
19
32
  // this in on SessionStart (outermost git repo basename, falling back to
20
33
  // nearest package.json name, finally to basename(cwd)). Optional so older
21
34
  // CLIs that never emit it remain compatible.
22
- project_name: z.string().min(1).optional()
35
+ project_name: z.string().min(1).optional(),
36
+ // Snapshot of the git state when Claude Code started: repo, branch, HEAD
37
+ // SHA, dirty bit, remote slug for deep-linking. Captured once per session
38
+ // — branch/SHA at runtime drift is acceptable; we anchor "what code was
39
+ // this run started against?". Optional so non-git sessions still ingest
40
+ // cleanly.
41
+ git_context: GitContext.optional()
23
42
  });
24
43
  var UserPromptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
25
44
  type: z.literal("user_prompt"),
@@ -95,6 +114,7 @@ var IngestionResponse = z.object({
95
114
  });
96
115
 
97
116
  export {
117
+ GitContext,
98
118
  SessionStartEvent,
99
119
  UserPromptEvent,
100
120
  ToolAttemptEvent,
@@ -107,4 +127,4 @@ export {
107
127
  IngestionRequest,
108
128
  IngestionResponse
109
129
  };
110
- //# sourceMappingURL=chunk-54VJGDD2.js.map
130
+ //# sourceMappingURL=chunk-Y52VWHZ3.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 // true = HEAD commit reachable from at least one remote-tracking branch\n // (safe to deep-link to GitHub). false = local-only. Absent when the\n // CLI couldn't decide (no remotes / detached / git lookup failed).\n commit_pushed: 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":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RuntapeEvent
3
- } from "./chunk-54VJGDD2.js";
3
+ } from "./chunk-Y52VWHZ3.js";
4
4
 
5
5
  // src/index.ts
6
6
  import { readFileSync } from "fs";
@@ -822,6 +822,90 @@ 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, remoteBranches] = 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
+ // List every remote-tracking branch that contains HEAD. Empty stdout
839
+ // means the commit only exists locally — linking to GitHub would 404.
840
+ // Uses --no-color so output is stable across user config.
841
+ git(["branch", "-r", "--contains", "HEAD", "--no-color"], cwd)
842
+ ]);
843
+ const ctx = { repo: basename2(repoRoot) };
844
+ if (remoteUrl) {
845
+ ctx.remote_url = remoteUrl;
846
+ const parsed = parseRemoteUrl(remoteUrl);
847
+ if (parsed) {
848
+ ctx.remote_host = parsed.host;
849
+ ctx.remote_slug = parsed.slug;
850
+ }
851
+ }
852
+ if (branchRaw && branchRaw !== "HEAD") {
853
+ ctx.branch = branchRaw;
854
+ }
855
+ if (sha && /^[0-9a-f]{40}$/.test(sha)) {
856
+ ctx.commit_sha = sha;
857
+ }
858
+ if (statusOut !== null) {
859
+ ctx.is_dirty = statusOut.length > 0;
860
+ }
861
+ if (remoteBranches !== null) {
862
+ ctx.commit_pushed = remoteBranches.length > 0;
863
+ }
864
+ return ctx;
865
+ }
866
+ function git(args, cwd) {
867
+ return new Promise((resolve3) => {
868
+ const child = execFile(
869
+ "git",
870
+ args,
871
+ {
872
+ cwd,
873
+ timeout: GIT_TIMEOUT_MS,
874
+ // 64KB ceiling — `git status --porcelain` on a huge dirty tree could
875
+ // theoretically grow, but we only care about empty-vs-not so we can
876
+ // cap aggressively. If we hit the cap the buffer truncates and we
877
+ // still get the right is_dirty signal.
878
+ maxBuffer: 64 * 1024,
879
+ windowsHide: true
880
+ },
881
+ (err, stdout) => {
882
+ if (err) return resolve3(null);
883
+ resolve3(stdout.toString().trim());
884
+ }
885
+ );
886
+ child.on("error", () => resolve3(null));
887
+ });
888
+ }
889
+ var REMOTE_PATTERNS = [
890
+ // git@host:owner/repo(.git)?
891
+ /^git@([^:]+):(.+?)(?:\.git)?$/,
892
+ // ssh://git@host/owner/repo(.git)?
893
+ /^ssh:\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/,
894
+ // https?://(user@)?host/owner/repo(.git)?
895
+ /^https?:\/\/(?:[^@/]+@)?([^/]+)\/(.+?)(?:\.git)?$/
896
+ ];
897
+ function parseRemoteUrl(url) {
898
+ for (const re of REMOTE_PATTERNS) {
899
+ const m = re.exec(url.trim());
900
+ if (m) {
901
+ const host = m[1].toLowerCase();
902
+ const slug = m[2].replace(/\.git$/, "");
903
+ if (slug.includes("/")) return { host, slug };
904
+ }
905
+ }
906
+ return null;
907
+ }
908
+
825
909
  // src/commands/push.ts
826
910
  function isDisabledByEnv() {
827
911
  const v = (process.env.RUNTAPE_DISABLE ?? "").trim().toLowerCase();
@@ -883,10 +967,16 @@ async function pushCommand(opts) {
883
967
  return 0;
884
968
  }
885
969
  if (result.event.type === "session_start" && cwd) {
886
- const projectName = await detectProjectName(cwd);
970
+ const [projectName, gitContext] = await Promise.all([
971
+ detectProjectName(cwd),
972
+ detectGitContext(cwd)
973
+ ]);
887
974
  if (projectName) {
888
975
  result.event.project_name = projectName;
889
976
  }
977
+ if (gitContext) {
978
+ result.event.git_context = gitContext;
979
+ }
890
980
  }
891
981
  await appendEvent(sessionId, result.event);
892
982
  if (opts.event === "PostToolUse" || opts.event === "Stop" || opts.event === "SubagentStop") {