@primitive.ai/prim 0.1.0-alpha.63 → 0.1.0-alpha.65

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.
package/README.md CHANGED
@@ -71,12 +71,13 @@ so the agent runs it directly — instead of fetching a doc and executing a comm
71
71
  it, which an auto-mode safety classifier blocks as untrusted-origin code:
72
72
 
73
73
  ```text
74
- Install the Primitive CLI: run `npx --yes @primitive.ai/prim@latest setup` and surface its output. It's an agent-first CLI — drive it yourself, don't hand the command back to me. The only thing I'll do is click "Authorize" in the browser if it asks. When it finishes, show me the welcome.
74
+ Install the Primitive CLI and activate passive decision capture for this repository: run `npx --yes @primitive.ai/prim@latest setup` and surface its output. Drive it yourself; I'll only click Authorize in the browser. This request authorizes setup's built-in activation of this repository and an idempotent retry of that step, but not activation in another repository or creation of a Decision. If enable or health fails, surface the actual error; do not claim fresh activation approval is required. Then show me the welcome.
75
75
  ```
76
76
 
77
- That single command runs the whole install in one process (auth → session hooks →
78
- daemon → git hooks → skill → welcome). The detailed step-by-step the agent follows —
79
- and the manual fallback — live in [`setup.md`](./setup.md).
77
+ That single command runs the whole install and current-repository activation in one
78
+ process (auth → session hooks → daemon → git hooks → skill → welcome). The detailed
79
+ step-by-step the agent follows — and the manual fallback — live in
80
+ [`setup.md`](./setup.md).
80
81
 
81
82
  ## Commands
82
83
 
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-UVIWZWM6.js";
4
4
  import {
5
5
  isValidRepoSyncId
6
- } from "./chunk-XTNMSS4C.js";
6
+ } from "./chunk-TDRXHBEX.js";
7
7
 
8
8
  // src/hooks/prim-hook-core.ts
9
9
  import { createHash, randomUUID } from "crypto";
@@ -15,15 +15,61 @@ var AGENT_ENVELOPE_VERSION = 3;
15
15
  var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
16
16
 
17
17
  // src/hooks/prim-hook-core.ts
18
+ var CORRELATED_TOOL_EVENTS = /* @__PURE__ */ new Set([
19
+ "PostToolUse",
20
+ "PostToolUseFailure",
21
+ "post_tool_call",
22
+ "post_approval_response"
23
+ ]);
18
24
  function postToolInvocationId(parsed, agent) {
19
- if (parsed.hook_event_name !== "PostToolUse") return void 0;
25
+ if (typeof parsed.hook_event_name !== "string" || !CORRELATED_TOOL_EVENTS.has(parsed.hook_event_name)) {
26
+ return void 0;
27
+ }
20
28
  const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
21
29
  return typeof raw === "string" && raw.length > 0 ? raw : void 0;
22
30
  }
31
+ function lowerString(value) {
32
+ return typeof value === "string" ? value.toLowerCase() : void 0;
33
+ }
34
+ function toolOutcomeFor(parsed, agent) {
35
+ const event = parsed.hook_event_name;
36
+ if (agent === "claude_code") {
37
+ if (event === "PostToolUseFailure") {
38
+ return parsed.is_interrupt === true ? "interrupted" : "failed";
39
+ }
40
+ return event === "PostToolUse" ? "succeeded" : void 0;
41
+ }
42
+ if (agent === "codex") {
43
+ return event === "PostToolUse" ? "returned" : void 0;
44
+ }
45
+ const extra = parsed.extra;
46
+ if (event === "post_approval_response") {
47
+ const choice = lowerString(extra?.choice);
48
+ return (choice === "deny" || choice === "smart_deny" || choice === "timeout") && postToolInvocationId(parsed, agent) ? "prevented" : void 0;
49
+ }
50
+ if (event !== "PostToolUse" && event !== "post_tool_call") {
51
+ return void 0;
52
+ }
53
+ const status = lowerString(extra?.status);
54
+ if (status === "ok" || status === "success") {
55
+ return "succeeded";
56
+ }
57
+ if (status === "error" || status === "timeout") {
58
+ return "failed";
59
+ }
60
+ if (status === "cancelled") {
61
+ return "interrupted";
62
+ }
63
+ if (status === "blocked") {
64
+ return "prevented";
65
+ }
66
+ return "unknown";
67
+ }
23
68
  function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, repository, invocationId) {
24
69
  const sessionId = parsed.session_id ?? "";
25
70
  const eventType = parsed.hook_event_name ?? "unknown";
26
- const moveId = eventType === "PostToolUse" && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
71
+ const toolOutcome = toolOutcomeFor(parsed, agent);
72
+ const moveId = CORRELATED_TOOL_EVENTS.has(eventType) && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
27
73
  return {
28
74
  moveId,
29
75
  capturedAt: Date.now(),
@@ -45,7 +91,8 @@ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, reposito
45
91
  },
46
92
  envelopeVersion: AGENT_ENVELOPE_VERSION,
47
93
  producer: agent,
48
- ...invocationId ? { invocationId } : {}
94
+ ...invocationId ? { invocationId } : {},
95
+ ...toolOutcome ? { toolOutcome } : {}
49
96
  };
50
97
  }
51
98
  var MAX_CLASSIFIER_ID_CHARS = 256;
@@ -63,17 +110,18 @@ function commitAttributionFromEnvironment(env, workspaceId) {
63
110
  return candidates.length === 1 ? candidates[0] : void 0;
64
111
  }
65
112
  function toCommitMove(commit, cliVersion, cwd, context = {}) {
66
- const { repository, repoFullName, repoSyncId, workspaceId, attribution } = context;
113
+ const { repository, repoFullName, repoSyncId, workspaceId, attribution, capturedAt } = context;
67
114
  const validatedRepoSyncId = isValidRepoSyncId(repoSyncId) ? repoSyncId : void 0;
68
115
  const validatedWorkspaceId = workspaceId && isCanonicalWorkspaceId(workspaceId) ? workspaceId : void 0;
69
116
  const validatedAttribution = validatedWorkspaceId ? attribution : void 0;
117
+ const validatedCapturedAt = typeof capturedAt === "number" && Number.isSafeInteger(capturedAt) && capturedAt > 0 ? capturedAt : Date.now();
70
118
  const fullSha = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(commit.sha);
71
119
  const moveId = validatedRepoSyncId && validatedWorkspaceId && fullSha ? `commit:v2:${createHash("sha256").update(
72
120
  JSON.stringify(["commit:v2", validatedRepoSyncId, validatedWorkspaceId, commit.sha])
73
121
  ).digest("hex")}` : `commit:v2:unscoped:${randomUUID()}`;
74
122
  const base = {
75
123
  moveId,
76
- capturedAt: Date.now(),
124
+ capturedAt: validatedCapturedAt,
77
125
  sessionId: validatedAttribution?.sessionId ?? "",
78
126
  eventType: "git.commit",
79
127
  payload: {
@@ -110,6 +158,7 @@ function shouldFlushAfter(eventType) {
110
158
 
111
159
  export {
112
160
  postToolInvocationId,
161
+ toolOutcomeFor,
113
162
  toMove,
114
163
  commitAttributionFromEnvironment,
115
164
  toCommitMove,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  analyzeShellTargets,
3
3
  extractFileTargets
4
- } from "./chunk-GF5QII7T.js";
4
+ } from "./chunk-G5D3FF3D.js";
5
5
  import {
6
6
  canonicalRepositoryPath
7
- } from "./chunk-XTNMSS4C.js";
7
+ } from "./chunk-TDRXHBEX.js";
8
8
 
9
9
  // src/hooks/file-refs.ts
10
10
  var MAX_PREFLIGHT_FILE_TARGETS = 25;
@@ -26,7 +26,7 @@ function preserveHookFileMetadata(payload, resolution) {
26
26
  return { ...payload, primitive };
27
27
  }
28
28
  function resolveHookFileRefs(args) {
29
- const command = args.agent === "claude_code" && args.toolName === "Bash" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.command === "string" ? args.toolInput.command : void 0;
29
+ const command = (args.agent === "claude_code" || args.agent === "codex") && args.toolName === "Bash" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.command === "string" ? args.toolInput.command : void 0;
30
30
  const shell = command === void 0 ? void 0 : analyzeShellTargets(command);
31
31
  const shellMutation = shell?.mutation === "none" ? "none" : shell?.coverage === "complete" ? "resolved" : shell ? "unresolved" : void 0;
32
32
  const readPath = args.agent === "claude_code" && args.toolName === "Read" && args.toolInput && typeof args.toolInput === "object" && typeof args.toolInput.file_path === "string" ? [args.toolInput.file_path] : void 0;
@@ -1,10 +1,12 @@
1
1
  import {
2
+ clearRepoSyncId,
2
3
  gitToplevel,
3
4
  githubRepositoryFullName,
4
5
  isValidRepoSyncId,
5
6
  setRepoSyncId
6
- } from "./chunk-XTNMSS4C.js";
7
+ } from "./chunk-TDRXHBEX.js";
7
8
  import {
9
+ HttpError,
8
10
  getClient,
9
11
  withFileLock
10
12
  } from "./chunk-CPX3BUOC.js";
@@ -649,6 +651,7 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
649
651
  case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
650
652
  case "\${#prim_commit_sha}" in 40|64) ;; *) prim_commit_sha= ;; esac
651
653
  if [ -n "$prim_commit_sha" ]; then
654
+ prim_commit_observed_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_commit_observed_file=
652
655
  prim_commit_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_commit_branch=
653
656
  prim_cache_dir="\${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin"
654
657
  prim_post_commit_ran=0
@@ -656,18 +659,23 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
656
659
  prim_node=$(cat "$prim_cache_dir/node")
657
660
  prim_entry=$(cat "$prim_cache_dir/prim-post-commit")
658
661
  if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
659
- ( trap '' HUP; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch"; "$prim_node" "$prim_entry" ) </dev/null >/dev/null 2>&1 &
662
+ ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; "$prim_node" "$prim_entry" ) </dev/null >/dev/null 2>&1 &
660
663
  prim_post_commit_ran=1
661
664
  fi
662
665
  fi
663
666
  if [ "$prim_post_commit_ran" -eq 0 ]; then
664
667
  if [ -x "./node_modules/.bin/prim-post-commit" ]; then
665
- ( trap '' HUP; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch"; ./node_modules/.bin/prim-post-commit ) </dev/null >/dev/null 2>&1 &
668
+ ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; ./node_modules/.bin/prim-post-commit ) </dev/null >/dev/null 2>&1 &
669
+ prim_post_commit_ran=1
666
670
  elif command -v npx >/dev/null 2>&1; then
667
- ( trap '' HUP; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch"; npx --yes -p @primitive.ai/prim@latest prim-post-commit ) </dev/null >/dev/null 2>&1 &
671
+ ( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; npx --yes -p @primitive.ai/prim@latest prim-post-commit ) </dev/null >/dev/null 2>&1 &
672
+ prim_post_commit_ran=1
668
673
  fi
669
674
  fi
670
- unset prim_cache_dir prim_post_commit_ran prim_node prim_entry prim_commit_branch
675
+ if [ "$prim_post_commit_ran" -eq 0 ] && [ -n "$prim_commit_observed_file" ]; then
676
+ rm -f "$prim_commit_observed_file"
677
+ fi
678
+ unset prim_cache_dir prim_post_commit_ran prim_node prim_entry prim_commit_branch prim_commit_observed_file
671
679
  fi
672
680
  unset prim_commit_sha
673
681
  fi
@@ -1211,21 +1219,37 @@ function uninstallPostCommitHookAtPath(hookPath) {
1211
1219
  }
1212
1220
 
1213
1221
  // src/lib/repository-binding.ts
1214
- async function bindRepository(root, options) {
1222
+ async function resolveRepositoryBinding(root, options) {
1215
1223
  const repositoryFullName = githubRepositoryFullName(root);
1216
1224
  if (!repositoryFullName) {
1217
1225
  throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
1218
1226
  }
1219
- const response = await getClient().post(
1220
- "/api/cli/repositories/bind",
1221
- { repositoryFullName },
1222
- options
1223
- );
1227
+ let response;
1228
+ try {
1229
+ response = await getClient().post(
1230
+ "/api/cli/repositories/bind",
1231
+ { repositoryFullName },
1232
+ options
1233
+ );
1234
+ } catch (error) {
1235
+ if (error instanceof HttpError && error.status === 404) {
1236
+ return { status: "pending", repositoryFullName };
1237
+ }
1238
+ throw error;
1239
+ }
1224
1240
  if (!isValidRepoSyncId(response.repoSyncId)) {
1225
1241
  throw new Error("server returned no repository binding");
1226
1242
  }
1227
- setRepoSyncId(root, response.repoSyncId);
1228
- return { repoSyncId: response.repoSyncId, repositoryFullName };
1243
+ return { status: "connected", repoSyncId: response.repoSyncId, repositoryFullName };
1244
+ }
1245
+ async function bindRepository(root, options) {
1246
+ const binding = await resolveRepositoryBinding(root, options);
1247
+ if (binding.status === "connected") {
1248
+ setRepoSyncId(root, binding.repoSyncId);
1249
+ } else {
1250
+ clearRepoSyncId(root);
1251
+ }
1252
+ return binding;
1229
1253
  }
1230
1254
 
1231
1255
  export {
@@ -1235,6 +1259,7 @@ export {
1235
1259
  inspectEffectivePostCommitHook,
1236
1260
  uninstallProjectPostCommitHook,
1237
1261
  uninstallPostCommitHookAtPath,
1262
+ resolveRepositoryBinding,
1238
1263
  bindRepository,
1239
1264
  printJson,
1240
1265
  compareSemver,