@primitive.ai/prim 0.1.0-alpha.56 → 0.1.0-alpha.57

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/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  import {
26
26
  printJson,
27
27
  registerSkillCommands
28
- } from "./chunk-ROL7SDXX.js";
28
+ } from "./chunk-GQURYENF.js";
29
29
  import {
30
30
  fetchFeedbackCapability
31
31
  } from "./chunk-WELBNODJ.js";
@@ -36,16 +36,21 @@ import {
36
36
  binFile,
37
37
  commandMatchesBin,
38
38
  detachedHookShimCommand,
39
- hookShimCommand,
39
+ packageVersion,
40
+ pinnedHookCommand,
40
41
  warmBinCache
41
- } from "./chunk-HSZPTVKU.js";
42
+ } from "./chunk-YIMUIPMD.js";
42
43
  import {
43
- activateRepoBestEffort,
44
+ canonicalGitRoot,
45
+ canonicalRepositoryPath,
44
46
  decisionIngestionStatus,
45
47
  gitToplevel,
48
+ githubRepositoryFullName,
46
49
  isRepoActiveForCapture,
47
- setRepoActive
48
- } from "./chunk-R5ZJRSLV.js";
50
+ repoSyncId,
51
+ setRepoActive,
52
+ setRepoSyncId
53
+ } from "./chunk-LDQZASIF.js";
49
54
  import {
50
55
  HttpError,
51
56
  REFRESH_TOKEN_PATH,
@@ -74,7 +79,7 @@ import { Command } from "commander";
74
79
  import updateNotifier from "update-notifier";
75
80
 
76
81
  // src/commands/activation.ts
77
- function applyActivation(active) {
82
+ async function applyActivation(active) {
78
83
  const root = gitToplevel();
79
84
  if (!root) {
80
85
  process.stderr.write(
@@ -84,16 +89,33 @@ function applyActivation(active) {
84
89
  process.exit(1);
85
90
  }
86
91
  try {
87
- setRepoActive(process.cwd(), active);
92
+ let bound;
93
+ if (active) {
94
+ const repositoryFullName = githubRepositoryFullName(root);
95
+ if (!repositoryFullName) {
96
+ throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
97
+ }
98
+ const response = await getClient().post("/api/cli/repositories/bind", {
99
+ repositoryFullName
100
+ });
101
+ if (typeof response.repoSyncId !== "string" || response.repoSyncId.length === 0) {
102
+ throw new Error("server returned no repository binding");
103
+ }
104
+ setRepoSyncId(root, response.repoSyncId);
105
+ bound = { repoSyncId: response.repoSyncId, repositoryFullName };
106
+ }
107
+ setRepoActive(root, active);
108
+ process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
109
+ `);
110
+ printJson({ active, repo: root, ...bound });
88
111
  } catch (err) {
89
112
  const detail = err instanceof Error ? err.message : String(err);
90
- process.stderr.write(`[prim] failed to set prim.active: ${detail}
91
- `);
113
+ process.stderr.write(
114
+ `[prim] failed to ${active ? "bind and enable" : "disable"} prim: ${detail}
115
+ `
116
+ );
92
117
  process.exit(1);
93
118
  }
94
- process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
95
- `);
96
- printJson({ active, repo: root });
97
119
  }
98
120
  function registerActivationCommands(program2) {
99
121
  program2.command("enable").description("Activate prim's hooks in this repo (git config prim.active=true)").action(() => applyActivation(true));
@@ -1068,7 +1090,7 @@ var SESSION_START_BIN = "prim-session-start";
1068
1090
  var SESSION_END_BIN = "prim-session-end";
1069
1091
  var STATUSLINE_BIN = "prim";
1070
1092
  var STATUSLINE_ARGS = "statusline";
1071
- var STATUSLINE_COMMAND = hookShimCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
1093
+ var STATUSLINE_COMMAND = pinnedHookCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
1072
1094
  var STATUSLINE_REFRESH_SECONDS = 5;
1073
1095
  var PRIM_BINS = [
1074
1096
  CAPTURE_BIN,
@@ -1092,8 +1114,13 @@ var CAPTURE_EVENTS = [
1092
1114
  "SessionEnd",
1093
1115
  "SubagentStop"
1094
1116
  ];
1095
- function makeRegistration(event, matcher, bin, args = "", opts = {}) {
1096
- return { event, matcher, bin, command: hookShimCommand(bin, args, opts) };
1117
+ function makeRegistration(event, matcher, bin, args = "") {
1118
+ return {
1119
+ event,
1120
+ matcher,
1121
+ bin,
1122
+ command: pinnedHookCommand(bin, args)
1123
+ };
1097
1124
  }
1098
1125
  function makeDetachedRegistration(event, matcher, bin, args = "") {
1099
1126
  return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
@@ -1102,11 +1129,9 @@ var REGISTRATIONS = [
1102
1129
  ...CAPTURE_EVENTS.map(
1103
1130
  (event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
1104
1131
  ),
1105
- makeRegistration("PreToolUse", "Edit|Write|MultiEdit", GATE_BIN),
1106
- makeRegistration("PostToolUse", "Edit|Write|MultiEdit", POST_TOOL_USE_BIN),
1107
- // Bare ladder (no branch-0): SessionStart must re-resolve @latest each session
1108
- // to refresh the bin cache the other hooks read. See lib/bin-cache.ts.
1109
- makeRegistration("SessionStart", "*", SESSION_START_BIN, "", { cacheRead: false }),
1132
+ makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
1133
+ makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
1134
+ makeRegistration("SessionStart", "*", SESSION_START_BIN),
1110
1135
  makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
1111
1136
  ];
1112
1137
  var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
@@ -1347,9 +1372,6 @@ function performInstall(scope, force) {
1347
1372
  if (changed) {
1348
1373
  atomicWrite2(path, after);
1349
1374
  }
1350
- if (scope === "project") {
1351
- activateRepoBestEffort(process.cwd());
1352
- }
1353
1375
  return {
1354
1376
  scope,
1355
1377
  path,
@@ -1507,9 +1529,7 @@ var CODEX_REGISTRATIONS = [
1507
1529
  ...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN2, CODEX_ARGS)),
1508
1530
  makeRegistration("PreToolUse", "apply_patch", GATE_BIN2, CODEX_ARGS),
1509
1531
  makeRegistration("PostToolUse", "apply_patch", POST_TOOL_USE_BIN2, CODEX_ARGS),
1510
- // Bare ladder (no branch-0): SessionStart re-resolves @latest each session to
1511
- // refresh the bin cache the other hooks read. See lib/bin-cache.ts.
1512
- makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS, { cacheRead: false })
1532
+ makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
1513
1533
  ];
1514
1534
  var USER_SCOPE_PATH2 = join3(homedir3(), ".codex", "hooks.json");
1515
1535
  var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
@@ -1562,9 +1582,6 @@ function performInstall2(scope, force) {
1562
1582
  if (changed) {
1563
1583
  atomicWrite2(path, after);
1564
1584
  }
1565
- if (scope === "project") {
1566
- activateRepoBestEffort(process.cwd());
1567
- }
1568
1585
  return resultFor(scope, path, after, changed);
1569
1586
  }
1570
1587
  function performUninstall2(scope) {
@@ -2775,7 +2792,9 @@ function toRequestBody(request) {
2775
2792
  alternatives: request.alternatives,
2776
2793
  confidence: request.confidence,
2777
2794
  reversibility: request.reversibility,
2778
- files: request.files
2795
+ files: request.files,
2796
+ protocolVersion: request.protocolVersion,
2797
+ repoSyncId: request.repoSyncId
2779
2798
  };
2780
2799
  const body = {};
2781
2800
  for (const [key, value] of Object.entries(candidate)) {
@@ -3115,10 +3134,29 @@ function registerDecisionsCommands(program2) {
3115
3134
  ).option("--kind <kind>", "change | exploration | task_execution | unclear (default change)").option("--rationale <text>", "Why the decision was made").option(
3116
3135
  "--area <area>",
3117
3136
  "Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
3118
- ).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option(
3119
- "--files <paths>",
3120
- "Comma-separated repo-relative paths this decision governs (the files Conflict Gates would check \u2014 not currently enabled)"
3121
- ).action(async (opts, command) => {
3137
+ ).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option("--files <paths>", "Comma-separated exact repo-relative paths this decision governs").action(async (opts, command) => {
3138
+ const requestedFiles = splitList(opts.files);
3139
+ let explicitScope = {};
3140
+ if (requestedFiles.length > 0) {
3141
+ const binding = repoSyncId(process.cwd());
3142
+ const root = canonicalGitRoot(process.cwd());
3143
+ const canonical = requestedFiles.map(
3144
+ (path) => canonicalRepositoryPath(path, root ?? process.cwd(), root)
3145
+ );
3146
+ if (!binding || canonical.some((path) => !path)) {
3147
+ console.error(
3148
+ "[prim] scoped decision rejected: run `prim enable` and provide exact in-repository file paths"
3149
+ );
3150
+ console.log(JSON.stringify({ ok: false, error: "invalid_repository_scope" }, null, 2));
3151
+ process.exitCode = EXIT_USAGE;
3152
+ return;
3153
+ }
3154
+ explicitScope = {
3155
+ protocolVersion: 3,
3156
+ repoSyncId: binding,
3157
+ files: canonical
3158
+ };
3159
+ }
3122
3160
  if (!isRepoActiveForCapture(process.cwd())) {
3123
3161
  const globals = command.optsWithGlobals();
3124
3162
  const nonInteractive = isNonInteractive(globals);
@@ -3141,7 +3179,7 @@ function registerDecisionsCommands(program2) {
3141
3179
  alternatives: splitList(opts.alternatives),
3142
3180
  confidence: opts.confidence,
3143
3181
  reversibility: opts.reversibility,
3144
- files: splitList(opts.files)
3182
+ ...explicitScope
3145
3183
  };
3146
3184
  try {
3147
3185
  const outcome = await fetchCreate(request);
@@ -3562,13 +3600,19 @@ var HERMES_ARGS = "--agent hermes";
3562
3600
  var EDIT_MATCHER = "write_file|patch";
3563
3601
  var JSON_INDENT3 = 2;
3564
3602
  var SHIM_MODE = 493;
3565
- var PRIM_BINS3 = [
3566
- CAPTURE_BIN3,
3567
- GATE_BIN3,
3568
- POST_TOOL_USE_BIN3,
3569
- SESSION_START_BIN3,
3570
- SESSION_END_BIN2
3571
- ];
3603
+ var PRIM_BINS3 = [CAPTURE_BIN3, GATE_BIN3, POST_TOOL_USE_BIN3, SESSION_START_BIN3, SESSION_END_BIN2];
3604
+ var PINNED_VERSION = packageVersion();
3605
+ if (!PINNED_VERSION) throw new Error("cannot determine Primitive package version");
3606
+ var sh = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
3607
+ var PINNED_NODE = sh(process.execPath);
3608
+ var PINNED_CASES = PRIM_BINS3.map((bin) => {
3609
+ const file = binFile(bin);
3610
+ if (!file) throw new Error(`cannot locate Primitive hook binary: ${bin}`);
3611
+ return ` ${bin})
3612
+ if [ -x ${PINNED_NODE} ] && [ -f ${sh(file)} ]; then exec ${PINNED_NODE} ${sh(file)} "$@"; fi
3613
+ exec npx --yes -p @primitive.ai/prim@${PINNED_VERSION} "$bin" "$@"
3614
+ ;;`;
3615
+ }).join("\n");
3572
3616
  function hermesHome() {
3573
3617
  return process.env.HERMES_HOME ?? join5(homedir5(), ".hermes");
3574
3618
  }
@@ -3580,17 +3624,13 @@ function shimPath() {
3580
3624
  }
3581
3625
  var SHIM_SCRIPT = `#!/bin/sh
3582
3626
  # prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
3583
- # with shell=False, so the PATH \u2192 local node_modules \u2192 npx @latest resolution a
3584
- # shell does for the Claude Code / Codex installs lives here instead.
3627
+ # with shell=False, so exact package entrypoints are selected here.
3585
3628
  bin="$1"
3586
3629
  shift
3587
- if command -v "$bin" >/dev/null 2>&1; then
3588
- exec "$bin" "$@"
3589
- fi
3590
- if [ -x "./node_modules/.bin/$bin" ]; then
3591
- exec "./node_modules/.bin/$bin" "$@"
3592
- fi
3593
- exec npx --yes -p @primitive.ai/prim@latest "$bin" "$@"
3630
+ case "$bin" in
3631
+ ${PINNED_CASES}
3632
+ esac
3633
+ exit 64
3594
3634
  `;
3595
3635
  function commandFor(bin) {
3596
3636
  return `"${shimPath()}" ${bin} ${HERMES_ARGS}`;
@@ -3899,13 +3939,7 @@ var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
3899
3939
  var PRIM_MANAGED_MARK = "prim-managed-hook";
3900
3940
  var PRIM_CREATED_MARK = "prim-created-hook";
3901
3941
  function hookShim(binName) {
3902
- return `if command -v ${binName} >/dev/null 2>&1; then
3903
- ${binName} || true
3904
- elif [ -f "./node_modules/.bin/${binName}" ]; then
3905
- ./node_modules/.bin/${binName} || true
3906
- else
3907
- npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
3908
- fi`;
3942
+ return `{ ${pinnedHookCommand(binName)}; } || true`;
3909
3943
  }
3910
3944
  function gatedShim(binName) {
3911
3945
  return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
@@ -4170,7 +4204,6 @@ function installHooks(gitRoot, target) {
4170
4204
  installToDotGit(gitRoot, spec);
4171
4205
  }
4172
4206
  }
4173
- activateRepoBestEffort(gitRoot);
4174
4207
  }
4175
4208
  function registerHooksCommands(program2) {
4176
4209
  const hooks = program2.command("hooks").description("Manage git hooks");
@@ -4712,9 +4745,12 @@ function planSetupSteps(opts) {
4712
4745
  });
4713
4746
  const skillArgs = ["skill", "install", "--agent", opts.agent, ...scopeArgs];
4714
4747
  steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
4715
- if (opts.scope === "user") {
4716
- steps.push({ key: "enable", label: "Activate this repo", args: ["enable"], required: false });
4717
- }
4748
+ steps.push({
4749
+ key: "enable",
4750
+ label: "Bind and activate this repo",
4751
+ args: ["enable"],
4752
+ required: true
4753
+ });
4718
4754
  return steps;
4719
4755
  }
4720
4756
  var CONFLICT_SESSION = "session";
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/commands/statusline.ts
4
4
  import { readFileSync as readFileSync5 } from "fs";
5
- import { dirname as dirname4, resolve as resolve2 } from "path";
5
+ import { dirname as dirname5, resolve as resolve3 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // src/client.ts
@@ -63,7 +63,7 @@ function daemonRequest(method, params = {}, opts = {}) {
63
63
  const id = nextRequestId++;
64
64
  const request = { id, method, params };
65
65
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
66
- return new Promise((resolve3) => {
66
+ return new Promise((resolve4) => {
67
67
  let settled = false;
68
68
  let socket;
69
69
  let buffer = "";
@@ -78,7 +78,7 @@ function daemonRequest(method, params = {}, opts = {}) {
78
78
  } catch {
79
79
  }
80
80
  }
81
- resolve3(value);
81
+ resolve4(value);
82
82
  };
83
83
  const timer = setTimeout(() => settle(null), timeoutMs);
84
84
  timer.unref();
@@ -140,6 +140,8 @@ import { join as join4 } from "path";
140
140
 
141
141
  // src/lib/git.ts
142
142
  import { execFileSync } from "child_process";
143
+ import { lstatSync, realpathSync } from "fs";
144
+ import { basename, dirname as dirname3, isAbsolute, relative, resolve as resolve2, sep } from "path";
143
145
  function gitToplevel(cwd) {
144
146
  try {
145
147
  return execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -202,7 +204,7 @@ import { join as join6 } from "path";
202
204
 
203
205
  // src/lib/bin-path.ts
204
206
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
205
- import { dirname as dirname3, isAbsolute, join as join5 } from "path";
207
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5 } from "path";
206
208
  import { fileURLToPath } from "url";
207
209
  var PKG_NAME = "@primitive.ai/prim";
208
210
  var NPX_FALLBACK = `npx --yes -p ${PKG_NAME}@latest`;
@@ -271,13 +273,13 @@ var STATUSLINE_TIMEOUT_MS = 200;
271
273
  var STATUSLINE_NAME_CAP = 3;
272
274
  function readPackageVersion() {
273
275
  try {
274
- const here = dirname4(fileURLToPath2(import.meta.url));
276
+ const here = dirname5(fileURLToPath2(import.meta.url));
275
277
  const candidates = [
276
278
  // The supervised runtime stages a tiny manifest beside the standalone
277
279
  // statusline bundle, so it remains versioned without loading the package.
278
- resolve2(here, "manifest.json"),
279
- resolve2(here, "../../package.json"),
280
- resolve2(here, "../package.json")
280
+ resolve3(here, "manifest.json"),
281
+ resolve3(here, "../../package.json"),
282
+ resolve3(here, "../package.json")
281
283
  ];
282
284
  for (const path of candidates) {
283
285
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.56",
3
+ "version": "0.1.0-alpha.57",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,6 +62,7 @@
62
62
  "dependencies": {
63
63
  "commander": "^12.1.0",
64
64
  "diff": "^5.2.0",
65
+ "unbash": "4.0.3",
65
66
  "update-notifier": "^7.3.1",
66
67
  "yaml": "^2.9.0"
67
68
  },
@@ -1,82 +0,0 @@
1
- // src/lib/activation.ts
2
- import { execFileSync as execFileSync2 } from "child_process";
3
- import { existsSync, readFileSync } from "fs";
4
- import { homedir } from "os";
5
- import { join } from "path";
6
-
7
- // src/lib/git.ts
8
- import { execFileSync } from "child_process";
9
- function gitToplevel(cwd) {
10
- try {
11
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
12
- cwd,
13
- encoding: "utf-8",
14
- stdio: ["ignore", "pipe", "ignore"]
15
- }).trim();
16
- } catch {
17
- return null;
18
- }
19
- }
20
-
21
- // src/lib/activation.ts
22
- var PRIM_ACTIVE_KEY = "prim.active";
23
- var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
24
- var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
25
- function repoActiveFlag(cwd) {
26
- try {
27
- const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
28
- cwd,
29
- encoding: "utf-8",
30
- stdio: ["ignore", "pipe", "ignore"]
31
- }).trim();
32
- if (value === "true") return "true";
33
- if (value === "false") return "false";
34
- return void 0;
35
- } catch {
36
- return void 0;
37
- }
38
- }
39
- function setRepoActive(cwd, active) {
40
- execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
41
- cwd,
42
- stdio: ["ignore", "ignore", "pipe"]
43
- });
44
- }
45
- function activateRepoBestEffort(cwd) {
46
- try {
47
- setRepoActive(cwd, true);
48
- } catch {
49
- }
50
- }
51
- function hasProjectPrimInstall(cwd) {
52
- const root = gitToplevel(cwd);
53
- if (root === null || root === homedir()) return false;
54
- for (const rel of PROJECT_INSTALL_FILES) {
55
- const path = join(root, rel);
56
- if (existsSync(path)) {
57
- try {
58
- const content = readFileSync(path, "utf-8");
59
- if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
60
- } catch {
61
- }
62
- }
63
- }
64
- return false;
65
- }
66
- function isRepoActiveForCapture(cwd) {
67
- const flag = repoActiveFlag(cwd);
68
- if (flag === "true") return true;
69
- if (flag === "false") return false;
70
- return hasProjectPrimInstall(cwd);
71
- }
72
- function decisionIngestionStatus(cwd) {
73
- return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
74
- }
75
-
76
- export {
77
- gitToplevel,
78
- setRepoActive,
79
- activateRepoBestEffort,
80
- isRepoActiveForCapture,
81
- decisionIngestionStatus
82
- };