@primitive.ai/prim 0.1.0-alpha.34 → 0.1.0-alpha.35

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
@@ -4,8 +4,9 @@ import {
4
4
  color,
5
5
  colorForArea,
6
6
  dim,
7
- stripAnsi
8
- } from "./chunk-4QJOQIY6.js";
7
+ stripAnsi,
8
+ stripControlChars
9
+ } from "./chunk-AAGJFO7C.js";
9
10
  import {
10
11
  checkAffectedDecisions,
11
12
  daemonOrDirectGet,
@@ -19,6 +20,11 @@ import {
19
20
  listFlushing,
20
21
  readMovesFromPath
21
22
  } from "./chunk-H4OR42TJ.js";
23
+ import {
24
+ activateRepoBestEffort,
25
+ gitToplevel,
26
+ setRepoActive
27
+ } from "./chunk-LUPD2JSH.js";
22
28
  import {
23
29
  HttpError,
24
30
  REFRESH_TOKEN_PATH,
@@ -36,12 +42,44 @@ import {
36
42
  } from "./chunk-UTKQTZHL.js";
37
43
 
38
44
  // src/index.ts
39
- import { readFileSync as readFileSync10 } from "fs";
40
- import { dirname as dirname7, resolve as resolve4 } from "path";
41
- import { fileURLToPath as fileURLToPath4 } from "url";
45
+ import { readFileSync as readFileSync12 } from "fs";
46
+ import { dirname as dirname9, resolve as resolve5 } from "path";
47
+ import { fileURLToPath as fileURLToPath5 } from "url";
42
48
  import { Command } from "commander";
43
49
  import updateNotifier from "update-notifier";
44
50
 
51
+ // src/output.ts
52
+ function printJson(data) {
53
+ console.log(JSON.stringify(data, null, 2));
54
+ }
55
+
56
+ // src/commands/activation.ts
57
+ function applyActivation(active) {
58
+ const root = gitToplevel();
59
+ if (!root) {
60
+ process.stderr.write(
61
+ `[prim] not a git repository \u2014 run \`prim ${active ? "enable" : "disable"}\` inside a repo
62
+ `
63
+ );
64
+ process.exit(1);
65
+ }
66
+ try {
67
+ setRepoActive(process.cwd(), active);
68
+ } catch (err) {
69
+ const detail = err instanceof Error ? err.message : String(err);
70
+ process.stderr.write(`[prim] failed to set prim.active: ${detail}
71
+ `);
72
+ process.exit(1);
73
+ }
74
+ process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
75
+ `);
76
+ printJson({ active, repo: root });
77
+ }
78
+ function registerActivationCommands(program2) {
79
+ program2.command("enable").description("Activate prim's hooks in this repo (git config prim.active=true)").action(() => applyActivation(true));
80
+ program2.command("disable").description("Mute prim's hooks in this repo (git config prim.active=false)").action(() => applyActivation(false));
81
+ }
82
+
45
83
  // src/commands/auth.ts
46
84
  import { exec } from "child_process";
47
85
  import { createHash, randomBytes } from "crypto";
@@ -50,16 +88,160 @@ import { createServer } from "http";
50
88
  import { platform } from "os";
51
89
  import { dirname } from "path";
52
90
 
53
- // src/output.ts
54
- function printJson(data) {
55
- console.log(JSON.stringify(data, null, 2));
56
- }
91
+ // src/commands/auth-pages.ts
92
+ var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="99.77 99.96 400.85 400.85" fill="currentColor">
93
+ <g>
94
+ <g>
95
+ <path d="M493.8,355.14c-9.16-6.69-36.52-22.75-55.41-31.94-20.92-10.19-59.12-26.94-71.36-30-1.09-.27-2.16-.41-3.19-.41-2.33,0-4.34.71-5.67,2-2.55,2.46-2.42,5.25-1.93,9.22.71,5.74,23.04,66.51,30.38,86.48l3.48,9.51c7.62,20.93,23.48,64.49,32.01,80.15,7.79,14.48,17.3,20.66,31.81,20.66h12.18c24.02,0,33.82-11.81,33.82-40.77l.26-87.05c0-11.21-2.06-14.69-6.38-17.85Z"/>
96
+ <path d="M405.18,113.07c-9.78,17.98-18.92,39.17-23.5,50.2-9.64,23.22-26.04,65.91-27.27,72.7l-.25,1.3c-.73,3.75-1.42,7.29.94,9.14,2.53,1.98,6.73.56,10.1-.58l1.08-.36c21.93-7.21,78.12-37.9,88.28-43.64l5.44-3.07c10.7-6.03,18.4-10.37,24.49-14.61,4.64-3.22,15.42-12.52,15.42-29.16l.21-22.17c.06-9.4-2.84-17.4-8.38-23.14-6.1-6.31-14.99-9.51-26.42-9.51l-40.49.02c-9.2,0-16.92,7.9-19.63,12.87Z"/>
97
+ </g>
98
+ <g>
99
+ <g>
100
+ <path d="M387.33,496.91c2.46-3.03,3.5-7.67,2.72-12.11-6.97-39.55-19.41-89.79-32.47-131.1-1.06-3.34-2.05-6.66-3.04-9.96l-.06-.2c-3.23-10.76-6.28-21.4-10.88-31.03l-.49-1.04c-1.48-3.17-3.01-6.45-7.55-6.7-.11,0-.23,0-.34,0-3.77,0-6.44,3.79-8.56,7.49-7.52,13.16-14.35,28.66-20.96,43.2h0s-2.58,5.69-2.58,5.69c-8.72,19.11-18.88,41.95-28.04,66.45l-9.41,25.16c-3.78,10.1-7.23,20.71-10.55,32.42-1.12,3.96-.67,7.72,1.29,10.57,1.63,2.36,4.86,4.91,8.13,4.91l115.07-.11c3.14,0,5.82-1.25,7.74-3.62Z"/>
101
+ </g>
102
+ <path d="M361.69,100.04h-86.95c-3.21-.01-5.95,1.24-7.71,3.52-1.81,2.34-2.41,5.61-1.65,8.96,1.6,7.02,15.48,35.97,21.37,48.1,7.81,16.07,27.58,54.75,38.81,71.76.32.48.65,1.04.99,1.62,1.73,2.95,3.48,5.91,7.12,5.56,3.84-.36,5.3-3.97,6.48-6.86.21-.51.4-.98.58-1.38,1.86-4.1,13.67-40.82,20.64-65.31,7.2-25.27,12.18-47.95,12.84-53.03.52-4.03-.14-7.16-1.97-9.31-2.05-2.4-5.59-3.62-10.55-3.62Z"/>
103
+ </g>
104
+ </g>
105
+ <g>
106
+ <path d="M298.79,268.76c-7.3-3.09-39.86-11.68-96.8-25.51-24.41-5.93-84.07-17.79-88.54-18.56-.63-.11-1.27-.16-1.9-.16-2.95,0-5.8,1.21-7.83,3.31-2.13,2.21-3.12,5.23-3.12,9.49v95.29c0,4.95.65,8.35,4.01,11.09,3.05,2.5,7.02,3.29,10.9,2.19,21.12-6.05,65.7-19.92,94.48-30.22,25.85-9.25,71.81-28.09,81.04-32.3,7.19-3.28,12.23-5.92,11.69-10.09-.32-2.51-2.32-3.85-3.94-4.54Z"/>
107
+ <path d="M447.09,228.98l-.66.27c-2.97,1.21-5.79,2.35-8.32,3.37-18.39,7.41-49.84,22.26-56.6,25.74l-1.74.89c-6.83,3.48-12.73,6.49-12.73,11.06,0,4.83,8.3,8.33,16.8,11.51.43.16,53.65,19.36,84.63,28.75l1.71.52c6.12,1.86,11.9,3.62,18.39,4.83,3.07.58,6.32-.29,8.68-2.31,2.18-1.87,3.37-4.51,3.37-7.44l-.03-84.37c0-2.55-1.04-4.89-2.92-6.58-1.78-1.6-4.21-2.52-6.66-2.52-.44,0-.88.03-1.32.09-4.06.56-27.25,9.98-42.59,16.21Z"/>
108
+ </g>
109
+ <g>
110
+ <path d="M199.41,487.9c11.63-15.23,42.48-59.57,55.71-81.4,25.25-41.68,55.4-93.54,58.65-100.9,2-4.51,1.9-6.61-.39-8.62-1.14-1.34-2.56-1.94-4.43-1.94-3.14,0-7.09,1.65-10.91,3.25l-.26.11c-.69.29-1.38.58-2.06.85-39.47,15.95-105.3,47.48-143.76,68.85l-4.64,2.57c-12.98,7.17-23.24,12.83-31.79,21.13-9.55,9.27-15.07,19.13-15.07,42.06l.1,27.6c.02,6.98.68,12.88,3.87,18.97,6.64,12.68,18.51,20.25,31.76,20.25l35.12-.14c15.31,0,22.31-5.06,28.12-12.66Z"/>
111
+ <g>
112
+ <path d="M100.86,166.89c0,11.82,6.85,17.37,8.95,18.79,5.81,3.92,37.07,18.76,68.66,30.25,28.43,10.35,107.26,35.67,125.35,36.95,2.87.22,5.62-.95,7.19-3.02,1.62-2.14,2.06-5.3,1.12-8.07-2.26-6.66-26.14-42.52-48.34-75.61-13.95-20.79-36.51-52.08-40.14-56.03l-.59-.64c-3.62-3.97-8.57-9.4-18.67-9.4l-72.02-.15c-17.35,0-30.26,13-31.39,31.62-.58,9.4-.44,15.56-.29,22.68l-.92.05.92-.02c.08,3.76.16,7.65.16,12.59Z"/>
113
+ </g>
114
+ </g>
115
+ </svg>`;
116
+ var WORDMARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="115 160 770 175" fill="currentColor">
117
+ <g>
118
+ <path d="M368.76,202.52h-35.98v96.31h12.86v-37.53h23.11c18.6,0,31.08-12.16,31.08-30.28,0-14.19-9.62-28.51-31.08-28.51ZM369.29,249.41h-23.66v-34.95h23.66c10.57,0,17.41,6.52,17.41,16.59,0,11.15-6.82,18.36-17.41,18.36Z"/>
119
+ <path d="M421.81,237.49v.02l-1.19-11.96h-10.37v73.28h12.74v-38.41c0-14.42,8.48-22.69,23.23-22.69h3.18v-12.6h-3.18c-11.23,0-19.45,4.17-24.42,12.36Z"/>
120
+ <rect x="463.25" y="225.55" width="12.74" height="73.28"/>
121
+ <path d="M576.17,223.94h-.02c-10.47,0-20.27,5.19-25.39,13.35-4.13-8.4-12.52-13.35-22.71-13.35-9.66,0-18.66,4.31-23.98,11.37l-.95-9.74h-10.37v73.28h12.74v-44.35c.14-11.48,8.44-19.19,20.66-19.19,9.14,0,14.84,5.8,14.84,15.1v48.44h12.6v-44.35c.14-11.48,8.44-19.19,20.66-19.19,9.14,0,14.84,5.8,14.84,15.1v48.44h12.74v-48.84c0-15.36-10.55-26.07-25.67-26.07Z"/>
122
+ <rect x="617.38" y="225.55" width="12.74" height="73.28"/>
123
+ <path d="M669.43,204.95h-12.72v20.62h-13.29v10.97h13.29v41.11c0,13.67,7.87,21.2,22.15,21.2h8.74v-10.97h-8.74c-6.24,0-9.42-3.44-9.42-10.23v-41.11h18.16v-10.97h-18.16v-20.62Z"/>
124
+ <rect x="702.48" y="225.55" width="12.74" height="73.28"/>
125
+ <polygon points="762.68 282.97 739.67 226.03 739.47 225.55 725.66 225.55 756.22 298.85 769 298.85 799.56 225.55 785.75 225.55 762.68 282.97"/>
126
+ <path d="M868.79,260.98l-.02-.02c-.4-23.19-12.99-37.04-33.66-37.04-23.88,0-34.75,19.97-34.75,38.53,0,22.29,14.84,37.87,36.1,37.87,16.39,0,28.67-9.46,32.03-24.7l.2-.91h-12.44l-.16.52c-2.84,8.92-9.8,13.85-19.61,13.85-13.49,0-22.39-9.26-23.33-24.24h55.65v-3.86ZM813.91,254c2.42-11.82,10.29-18.82,21.18-18.82s18.02,6.85,20.25,18.82h-41.43Z"/>
127
+ <path d="M469.7,199.68c-4.79,0-8.42,3.56-8.42,8.29s3.7,8.42,8.42,8.42,8.29-3.62,8.29-8.42-3.56-8.29-8.29-8.29Z"/>
128
+ <path d="M623.83,199.68c-4.79,0-8.42,3.56-8.42,8.29s3.7,8.42,8.42,8.42,8.29-3.62,8.29-8.42-3.56-8.29-8.29-8.29Z"/>
129
+ <path d="M708.91,199.68c-4.79,0-8.42,3.56-8.42,8.29s3.7,8.42,8.42,8.42,8.29-3.62,8.29-8.42-3.56-8.29-8.29-8.29Z"/>
130
+ </g>
131
+ <g>
132
+ <path d="M131.47,199.69c0,4.45,2.3,6.33,3.48,7.07,2.23,1.41,13.96,7.06,25.86,11.39,9.28,3.38,32.82,10.99,43.37,13.27,1.62.35,5.22,1.11,6.55-.5,1.33-1.61-.5-4.95-1.58-6.74-3.27-5.43-9.92-15.39-16.21-24.76-5.25-7.82-15.34-21.33-15.34-21.33-1.36-1.49-3.23-3.54-7.03-3.54l-28.33-.05c-5.96,0-10.78,4.83-10.78,10.78,0,0,0,.08,0,.22h0v14.08h0v.11Z"/>
133
+ <path d="M232.64,284.23c-2.77-10.24-9.15-29.14-9.73-30.42-.47-1.05-1.23-2.04-2.68-2.04-1.63,0-2.49,1.43-3.28,2.82-2.83,4.95-5.41,10.79-7.89,16.26h0s-.97,2.14-.97,2.14c-3.29,7.19-7.11,15.79-10.56,25.01l-3.54,9.47c-1.42,3.8-2.72,7.79-3.97,12.2-.42,1.49-.25,2.9.49,3.98.61.89,1.83,1.85,3.06,1.85l43.34-.04c1.18,0,2.19-.47,2.91-1.36.93-1.14,1.32-2.89,1.03-4.56-1.85-10.48-4.91-23.22-8.19-35.31Z"/>
134
+ <path d="M132.98,266.34c1.15.94,2.65,1.24,4.11.82,7.95-2.28,24.75-7.5,35.59-11.38,9.74-3.48,27.05-10.57,30.52-12.16,2.71-1.23,4.61-2.23,4.4-3.8-.12-.94-.87-1.45-1.48-1.71-2.75-1.16-15.01-4.4-36.46-9.6-9.2-2.23-32.27-7.05-34.06-7.05-1.11,0-2.19.45-2.95,1.25-.8.83-1.18,1.97-1.18,3.57v35.87c0,1.86.25,3.14,1.51,4.18Z"/>
135
+ <path d="M281.35,217.96c-.67-.6-1.59-.95-2.51-.95-.17,0-.33.01-.5.03-1.53.21-18.47,7.09-19.42,7.47-6.93,2.79-18.77,8.38-21.32,9.69,0,0-5.45,2.56-5.45,4.5,0,1.82,3.13,3.14,6.33,4.33.16.06,20.21,7.29,31.88,10.82l.64.2c2.3.7,4.48,1.36,6.93,1.82,1.16.22,2.38-.11,3.27-.87.82-.7,1.27-1.7,1.27-2.8v-31.76c-.01-.96-.4-1.84-1.11-2.48Z"/>
136
+ <path d="M280.06,270.67c-3.45-2.52-13.76-8.56-20.87-12.02-7.88-3.84-22.27-10.14-26.88-11.29-.41-.1-.81-.15-1.2-.15-.88,0-1.64.27-2.14.75-.96.93-.91,1.98-.73,3.47.27,2.16,8.68,25.04,11.44,32.55,0,0,10.15,27.85,13.37,33.75,2.93,5.45,6.52,7.77,11.98,7.77h6.64c5.96,0,10.78-4.83,10.78-10.78v-37.34c0-4.22-.77-5.53-2.4-6.72Z"/>
137
+ <path d="M227.72,228.15c-.03,1.15.92,2.07,2.06,2.09.01,0,.03,0,.04,0,.02,0,.03,0,.05-.01,1.23-.04,2.63-.5,2.63-.5,8.06-2.65,37.66-19.95,44.22-24.37,1.72-1.16,5.75-4.6,5.75-10.72v-9.37c0-5.95-4.83-10.78-10.78-10.78h-16.9c-3.38,0-6.22,2.91-7.22,4.74-3.59,6.61-6.96,14.39-8.64,18.45-3.55,8.53-8.86,22.45-10.79,28.55-.13.41-.41,1.49-.42,1.91Z"/>
138
+ <path d="M201.91,197.31c2.94,6.05,10.39,20.61,14.62,27.01.47.73,1.52,2.65,3.09,2.63,1.58-.02,2.58-2.92,2.63-3.04.7-1.54,5.09-14.96,7.72-24.17,2.71-9.51,4.54-17.99,4.89-20.37.24-1.6-.05-2.7-.74-3.51-.77-.9-2.11-1.36-3.97-1.36h-32.75c-1.21,0-2.24.47-2.9,1.33-.68.88-.91,2.11-.62,3.37.6,2.64,5.83,13.54,8.05,18.1Z"/>
139
+ <path d="M212.24,250.35c0-.96-.84-1.83-1.76-1.97-1.67-.24-3.97.79-5.47,1.31-14.87,6-39.66,17.87-54.15,25.92l-1.75.97c-4.89,2.7-8.75,4.83-11.97,7.96-3.6,3.49-5.68,7.2-5.68,15.83v14.35c0,5.95,4.83,10.78,10.78,10.78l15.9-.03c5.77,0,8.4-1.91,10.59-4.76,4.38-5.73,16-22.42,20.98-30.64,9.51-15.69,20.57-34.99,21.93-37.7.51-1.03.58-1.49.6-2.01Z"/>
140
+ </g>
141
+ </svg>`;
142
+ var FAVICON_HREF = `data:image/svg+xml;base64,${Buffer.from(FAVICON_SVG).toString("base64")}`;
143
+ function renderPage(o) {
144
+ return `<!doctype html>
145
+ <html lang="en">
146
+ <head>
147
+ <meta charset="utf-8" />
148
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
149
+ <title>${o.docTitle}</title>
150
+ <link rel="icon" type="image/svg+xml" href="${FAVICON_HREF}" />
151
+ <style>
152
+ :root {
153
+ --inter-orange: #ff4f00;
154
+ --light-bg: #f5f5f5;
155
+ --black: #333333;
156
+ --hairline: rgba(0, 0, 0, 0.16);
157
+ }
158
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
159
+ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
160
+ body {
161
+ min-height: 100vh;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 32px;
166
+ background-color: var(--light-bg);
167
+ color: var(--black);
168
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
169
+ line-height: 1.5;
170
+ overflow: hidden;
171
+ }
172
+ /* The brand frame: the homepage's inset rounded hero canvas, as a hairline. */
173
+ .frame {
174
+ position: fixed;
175
+ inset: 16px;
176
+ border: 1px solid var(--hairline);
177
+ border-radius: 24px;
178
+ pointer-events: none;
179
+ z-index: 1;
180
+ }
181
+ .card { position: relative; z-index: 2; width: 100%; max-width: 460px; text-align: center; }
182
+
183
+ .eyebrow {
184
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
185
+ font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase;
186
+ color: var(--inter-orange);
187
+ }
188
+ .title {
189
+ font-size: 30px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.1;
190
+ margin: 14px 0 10px;
191
+ }
192
+ .subtitle {
193
+ font-size: 15px; color: rgba(0, 0, 0, 0.55);
194
+ }
195
+ .mark { margin-top: 40px; }
196
+ .mark svg { height: 22px; width: auto; color: rgba(0, 0, 0, 0.38); }
197
+
198
+ @media (max-width: 460px) {
199
+ .title { font-size: 26px; }
200
+ .frame { inset: 10px; border-radius: 18px; }
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <div class="frame" aria-hidden="true"></div>
206
+ <main class="card">
207
+ <p class="eyebrow">${o.eyebrow}</p>
208
+ <h1 class="title">${o.title}</h1>
209
+ <p class="subtitle">${o.detail}</p>
210
+
211
+ <div class="mark" aria-hidden="true">
212
+ ${WORDMARK_SVG}
213
+ </div>
214
+ </main>
215
+ </body>
216
+ </html>`;
217
+ }
218
+ var SUCCESS_HTML = renderPage({
219
+ docTitle: "Primitive \u2014 Authenticated",
220
+ eyebrow: "Authenticated",
221
+ title: "Authentication successful!",
222
+ detail: "You can close this tab."
223
+ });
224
+ var FAILURE_HTML = renderPage({
225
+ docTitle: "Primitive \u2014 Authentication failed",
226
+ eyebrow: "Error",
227
+ title: "Authentication failed.",
228
+ detail: "Return to your terminal for details."
229
+ });
230
+ var STATE_MISMATCH_HTML = renderPage({
231
+ docTitle: "Primitive \u2014 Authentication failed",
232
+ eyebrow: "Error",
233
+ title: "State mismatch. Authentication failed.",
234
+ detail: "Return to your terminal for details."
235
+ });
57
236
 
58
237
  // src/commands/auth.ts
59
238
  var FILE_MODE = 384;
60
239
  var LOCALHOST = "127.0.0.1";
61
240
  var CALLBACK_PORT = 19876;
62
241
  var CALLBACK_TIMEOUT_MS = 12e4;
242
+ var EXIT_OK = 0;
243
+ var EXIT_FAIL = 1;
244
+ var HTML_HEADERS = { "Content-Type": "text/html; charset=utf-8" };
63
245
  var BASE64_PLUS_RE = /\+/g;
64
246
  var BASE64_SLASH_RE = /\//g;
65
247
  var BASE64_PAD_RE = /=+$/;
@@ -83,6 +265,55 @@ function saveToken(token) {
83
265
  }
84
266
  writeFileSync(TOKEN_FILE_PATH, token, { mode: FILE_MODE });
85
267
  }
268
+ function resolveCallbackPage(params, expectedState) {
269
+ if (params.get("state") !== expectedState) {
270
+ return {
271
+ status: 400,
272
+ html: STATE_MISMATCH_HTML,
273
+ result: {
274
+ authenticated: false,
275
+ error: "state_mismatch",
276
+ detail: "state mismatch on the OAuth callback"
277
+ }
278
+ };
279
+ }
280
+ const providerError = params.get("error");
281
+ if (providerError) {
282
+ return {
283
+ status: 400,
284
+ html: FAILURE_HTML,
285
+ result: {
286
+ authenticated: false,
287
+ error: providerError,
288
+ detail: params.get("error_description") ?? void 0
289
+ }
290
+ };
291
+ }
292
+ const code = params.get("code");
293
+ if (!code) {
294
+ return {
295
+ status: 400,
296
+ html: FAILURE_HTML,
297
+ result: {
298
+ authenticated: false,
299
+ error: "no_code",
300
+ detail: "No authorization code received"
301
+ }
302
+ };
303
+ }
304
+ return { status: 200, html: SUCCESS_HTML, result: { authenticated: true, code } };
305
+ }
306
+ function reportFailure(error, detail) {
307
+ const human = detail ? `${stripControlChars(error)}: ${stripControlChars(detail)}` : stripControlChars(error);
308
+ console.error(`Authentication failed: ${human}`);
309
+ console.log(JSON.stringify({ authenticated: false, error, detail }));
310
+ process.exitCode = EXIT_FAIL;
311
+ }
312
+ function reportSuccess() {
313
+ console.error(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
314
+ console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
315
+ process.exitCode = EXIT_OK;
316
+ }
86
317
  function registerAuthCommands(program2) {
87
318
  const auth = program2.command("auth").description("Manage CLI authentication");
88
319
  auth.command("login").description("Authenticate via browser (WorkOS OAuth)").action(async () => {
@@ -92,15 +323,23 @@ function registerAuthCommands(program2) {
92
323
  const res = await fetch(`${siteUrl}/mcp/config`);
93
324
  config = await res.json();
94
325
  } catch {
95
- console.error("Failed to fetch MCP config. Is the Convex backend running?");
96
- process.exit(1);
326
+ reportFailure(
327
+ "config_fetch_failed",
328
+ "Failed to fetch MCP config. Is the Convex backend running?"
329
+ );
330
+ return;
97
331
  }
98
332
  if (!config.authorization_server || !config.client_id) {
99
- console.error("MCP broker is not configured on the server.");
100
- process.exit(1);
333
+ reportFailure("broker_not_configured", "MCP broker is not configured on the server.");
334
+ return;
101
335
  }
102
336
  const { verifier, challenge } = generatePkce();
103
337
  const state = base64url(randomBytes(16));
338
+ const redirectUri = `http://${LOCALHOST}:${CALLBACK_PORT}/callback`;
339
+ let settle;
340
+ const outcome = new Promise((resolve6) => {
341
+ settle = resolve6;
342
+ });
104
343
  const server = createServer((req, res) => {
105
344
  const url = new URL(req.url ?? "/", `http://${LOCALHOST}`);
106
345
  if (url.pathname !== "/callback") {
@@ -108,42 +347,35 @@ function registerAuthCommands(program2) {
108
347
  res.end("Not found");
109
348
  return;
110
349
  }
111
- const code = url.searchParams.get("code");
112
- const returnedState = url.searchParams.get("state");
113
- if (returnedState !== state) {
114
- res.writeHead(400, { "Content-Type": "text/html" });
115
- res.end("<h1>State mismatch. Authentication failed.</h1>");
116
- server.close();
117
- process.exit(1);
118
- }
119
- if (!code) {
120
- const error = url.searchParams.get("error_description") ?? "No authorization code received";
121
- res.writeHead(400, { "Content-Type": "text/html" });
122
- res.end(`<h1>Authentication failed: ${error}</h1>`);
123
- server.close();
124
- process.exit(1);
125
- }
126
- res.writeHead(200, { "Content-Type": "text/html" });
127
- res.end("<h1>Authentication successful!</h1><p>You can close this tab.</p>");
128
- exchangeCode(siteUrl, code, verifier, `http://${LOCALHOST}:${port}/callback`).then((token) => {
129
- saveToken(token);
130
- console.error(`Authenticated! Token saved to ${TOKEN_FILE_PATH}`);
131
- console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
132
- server.close();
133
- process.exit(0);
134
- }).catch((err) => {
135
- console.error("Token exchange failed:", err);
136
- server.close();
137
- process.exit(1);
138
- });
350
+ const page = resolveCallbackPage(url.searchParams, state);
351
+ res.writeHead(page.status, HTML_HEADERS);
352
+ res.end(page.html, () => settle(page.result));
139
353
  });
140
- const port = await new Promise((resolve5) => {
141
- server.listen(CALLBACK_PORT, LOCALHOST, () => {
142
- const addr = server.address();
143
- resolve5(typeof addr === "object" && addr ? addr.port : 0);
354
+ try {
355
+ await new Promise((resolve6, reject) => {
356
+ const onError = (err) => reject(err);
357
+ server.once("error", onError);
358
+ server.listen(CALLBACK_PORT, LOCALHOST, () => {
359
+ server.removeListener("error", onError);
360
+ resolve6();
361
+ });
144
362
  });
363
+ } catch (err) {
364
+ if (err instanceof Error && err.code === "EADDRINUSE") {
365
+ reportFailure(
366
+ "callback_port_in_use",
367
+ `Port ${CALLBACK_PORT} is in use \u2014 another 'prim auth login' may be running. Close it and retry.`
368
+ );
369
+ } else {
370
+ reportFailure(
371
+ "callback_bind_failed",
372
+ `Could not start the local callback server: ${err instanceof Error ? err.message : String(err)}`
373
+ );
374
+ }
375
+ return;
376
+ }
377
+ server.on("error", () => {
145
378
  });
146
- const redirectUri = `http://${LOCALHOST}:${port}/callback`;
147
379
  const authorizeUrl = config.authorization_endpoint ?? "https://api.workos.com/user_management/authorize";
148
380
  const authUrl = new URL(authorizeUrl);
149
381
  authUrl.searchParams.set("client_id", config.client_id);
@@ -160,11 +392,24 @@ function registerAuthCommands(program2) {
160
392
  ${authUrl.toString()}
161
393
  `);
162
394
  console.error("Waiting for callback...");
163
- setTimeout(() => {
164
- console.error("Authentication timed out.");
165
- server.close();
166
- process.exit(1);
395
+ const timer = setTimeout(() => {
396
+ settle({ authenticated: false, error: "timeout", detail: "Authentication timed out." });
167
397
  }, CALLBACK_TIMEOUT_MS);
398
+ timer.unref();
399
+ const result = await outcome;
400
+ clearTimeout(timer);
401
+ server.close();
402
+ if (!result.authenticated) {
403
+ reportFailure(result.error, result.detail);
404
+ return;
405
+ }
406
+ try {
407
+ const token = await exchangeCode(siteUrl, result.code, verifier, redirectUri);
408
+ saveToken(token);
409
+ reportSuccess();
410
+ } catch (err) {
411
+ reportFailure("token_exchange_failed", err instanceof Error ? err.message : String(err));
412
+ }
168
413
  });
169
414
  auth.command("set-token <token>").description("Save a bearer token for authenticated CLI calls").action((token) => {
170
415
  saveToken(token);
@@ -282,7 +527,6 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
282
527
  }
283
528
 
284
529
  // src/commands/claude-install.ts
285
- import { execSync } from "child_process";
286
530
  import {
287
531
  closeSync,
288
532
  existsSync as existsSync3,
@@ -376,14 +620,7 @@ var PRIM_BINS = [
376
620
  var JSON_INDENT = 2;
377
621
  var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
378
622
  function projectRoot() {
379
- try {
380
- return execSync("git rev-parse --show-toplevel", {
381
- encoding: "utf-8",
382
- stdio: ["ignore", "pipe", "ignore"]
383
- }).trim();
384
- } catch {
385
- return process.cwd();
386
- }
623
+ return gitToplevel() ?? process.cwd();
387
624
  }
388
625
  var projectScopePath = () => join2(projectRoot(), ".claude", "settings.json");
389
626
  var CAPTURE_EVENTS = [
@@ -592,6 +829,9 @@ function performInstall(scope, force) {
592
829
  if (changed) {
593
830
  atomicWrite(path, after);
594
831
  }
832
+ if (scope === "project") {
833
+ activateRepoBestEffort(process.cwd());
834
+ }
595
835
  return {
596
836
  scope,
597
837
  path,
@@ -788,6 +1028,9 @@ function performInstall2(scope, force) {
788
1028
  if (changed) {
789
1029
  atomicWrite(path, after);
790
1030
  }
1031
+ if (scope === "project") {
1032
+ activateRepoBestEffort(process.cwd());
1033
+ }
791
1034
  return resultFor(scope, path, after, changed);
792
1035
  }
793
1036
  function performUninstall2(scope) {
@@ -891,7 +1134,7 @@ var STATUS_PROBE_TIMEOUT_MS = 500;
891
1134
  var READY_TIMEOUT_MS = 5e3;
892
1135
  var READY_POLL_MS = 100;
893
1136
  var READY_PROBE_TIMEOUT_MS = 250;
894
- var EXIT_OK = 0;
1137
+ var EXIT_OK2 = 0;
895
1138
  var EXIT_NOT_RUNNING = 2;
896
1139
  var EXIT_BOOTING = 3;
897
1140
  function readPidfile() {
@@ -924,8 +1167,8 @@ function clearStaleArtifacts() {
924
1167
  }
925
1168
  }
926
1169
  function sleep(ms) {
927
- return new Promise((resolve5) => {
928
- const timer = setTimeout(resolve5, ms);
1170
+ return new Promise((resolve6) => {
1171
+ const timer = setTimeout(resolve6, ms);
929
1172
  timer.unref();
930
1173
  });
931
1174
  }
@@ -1051,9 +1294,9 @@ function classifyStatus(pidAlive, responding, snapshot, pid) {
1051
1294
  };
1052
1295
  }
1053
1296
  if (!snapshot) {
1054
- return { json: { running: true, responding: true }, exitCode: EXIT_OK };
1297
+ return { json: { running: true, responding: true }, exitCode: EXIT_OK2 };
1055
1298
  }
1056
- return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK };
1299
+ return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
1057
1300
  }
1058
1301
  async function daemonStatus() {
1059
1302
  const pid = readPidfile();
@@ -1080,7 +1323,7 @@ async function daemonStatus() {
1080
1323
  );
1081
1324
  }
1082
1325
  console.log(JSON.stringify(json, null, 2));
1083
- if (exitCode !== EXIT_OK && !process.exitCode) {
1326
+ if (exitCode !== EXIT_OK2 && !process.exitCode) {
1084
1327
  process.exitCode = exitCode;
1085
1328
  }
1086
1329
  }
@@ -2385,9 +2628,10 @@ function registerHermesCommands(program2) {
2385
2628
  }
2386
2629
 
2387
2630
  // src/commands/hooks.ts
2388
- import { execSync as execSync2 } from "child_process";
2631
+ import { execFileSync } from "child_process";
2389
2632
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2390
- import { resolve } from "path";
2633
+ import { homedir as homedir5 } from "os";
2634
+ import { dirname as dirname5, join as join6, resolve } from "path";
2391
2635
  import { Option } from "commander";
2392
2636
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
2393
2637
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
@@ -2400,18 +2644,25 @@ function blockMarkers(spec) {
2400
2644
  }
2401
2645
  var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
2402
2646
  var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
2647
+ var PRIM_MANAGED_MARK = "prim-managed-hook";
2648
+ var PRIM_CREATED_MARK = "prim-created-hook";
2403
2649
  function hookShim(binName) {
2404
2650
  return `if command -v ${binName} >/dev/null 2>&1; then
2405
- ${binName}
2651
+ ${binName} || true
2406
2652
  elif [ -f "./node_modules/.bin/${binName}" ]; then
2407
- ./node_modules/.bin/${binName}
2653
+ ./node_modules/.bin/${binName} || true
2408
2654
  else
2409
2655
  npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
2410
2656
  fi`;
2411
2657
  }
2658
+ function gatedShim(binName) {
2659
+ return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
2660
+ ${hookShim(binName)}
2661
+ fi`;
2662
+ }
2412
2663
  function dotGitScript(spec) {
2413
2664
  return `#!/bin/sh
2414
- # prim ${spec.hookName} hook \u2014 installed by: prim hooks install
2665
+ # prim ${spec.hookName} hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
2415
2666
 
2416
2667
  ${hookShim(spec.binName)}
2417
2668
  `;
@@ -2422,10 +2673,35 @@ function huskyBlock(spec) {
2422
2673
  ${hookShim(spec.binName)}
2423
2674
  ${end}`;
2424
2675
  }
2676
+ function gatedBlock(spec) {
2677
+ const { start, end } = blockMarkers(spec);
2678
+ return `${start}
2679
+ ${gatedShim(spec.binName)}
2680
+ ${end}`;
2681
+ }
2682
+ function mergePrimBlock(hookPath, block, binName) {
2683
+ if (existsSync7(hookPath)) {
2684
+ const existing = readFileSync6(hookPath, "utf-8");
2685
+ if (containsPrimHook(existing, binName)) return false;
2686
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
2687
+ writeFileSync4(hookPath, `${existing}${separator}${block}
2688
+ `, { mode: 493 });
2689
+ return true;
2690
+ }
2691
+ mkdirSync5(dirname5(hookPath), { recursive: true });
2692
+ writeFileSync4(hookPath, `#!/bin/sh
2693
+ # ${PRIM_CREATED_MARK}
2694
+
2695
+ ${block}
2696
+ `, { mode: 493 });
2697
+ return true;
2698
+ }
2425
2699
  function getGitRoot() {
2426
- return execSync2("git rev-parse --show-toplevel", {
2427
- encoding: "utf-8"
2428
- }).trim();
2700
+ const root = gitToplevel();
2701
+ if (root === null) {
2702
+ throw new Error("not a git repository (run inside a repo, or use --scope user)");
2703
+ }
2704
+ return root;
2429
2705
  }
2430
2706
  function detectHusky(gitRoot) {
2431
2707
  const huskyDir = resolve(gitRoot, ".husky");
@@ -2462,25 +2738,13 @@ async function askConfirmation(question) {
2462
2738
  }
2463
2739
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
2464
2740
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
2465
- if (existsSync7(hookPath)) {
2466
- const existing = readFileSync6(hookPath, "utf-8");
2467
- if (containsPrimHook(existing, spec.binName)) {
2468
- console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
2469
- return;
2470
- }
2471
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
2472
- writeFileSync4(hookPath, `${existing}${separator}${huskyBlock(spec)}
2473
- `, {
2474
- mode: 493
2475
- });
2741
+ const existed = existsSync7(hookPath);
2742
+ const wrote = mergePrimBlock(hookPath, huskyBlock(spec), spec.binName);
2743
+ if (!wrote) {
2744
+ console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
2745
+ } else if (existed) {
2476
2746
  console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
2477
2747
  } else {
2478
- writeFileSync4(hookPath, `#!/bin/sh
2479
-
2480
- ${huskyBlock(spec)}
2481
- `, {
2482
- mode: 493
2483
- });
2484
2748
  console.log(`Created .husky/${spec.hookName} with prim hook block.`);
2485
2749
  }
2486
2750
  }
@@ -2503,6 +2767,161 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2503
2767
  writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
2504
2768
  console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
2505
2769
  }
2770
+ var PRIM_GIT_HOOKS_DIR = join6(homedir5(), ".config", "prim", "git-hooks");
2771
+ function expandTilde(p) {
2772
+ return p.startsWith("~/") ? join6(homedir5(), p.slice(2)) : p;
2773
+ }
2774
+ function isOurHooksDir(value) {
2775
+ return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
2776
+ }
2777
+ function gitConfigGet(level) {
2778
+ try {
2779
+ return execFileSync("git", ["config", level, "--get", "core.hooksPath"], {
2780
+ encoding: "utf-8",
2781
+ stdio: ["ignore", "pipe", "ignore"]
2782
+ }).trim();
2783
+ } catch {
2784
+ return "";
2785
+ }
2786
+ }
2787
+ var PASSTHROUGH_HOOKS = [
2788
+ "applypatch-msg",
2789
+ "pre-applypatch",
2790
+ "post-applypatch",
2791
+ "pre-merge-commit",
2792
+ "prepare-commit-msg",
2793
+ "commit-msg",
2794
+ "pre-rebase",
2795
+ "post-checkout",
2796
+ "post-merge",
2797
+ "pre-push",
2798
+ "post-rewrite",
2799
+ "pre-auto-gc",
2800
+ "push-to-checkout",
2801
+ "sendemail-validate",
2802
+ "reference-transaction",
2803
+ "post-index-change"
2804
+ // (fsmonitor-watchman is intentionally omitted — it's driven by core.fsmonitor
2805
+ // with a query protocol, not a lifecycle event, so a bare exec stub is wrong.)
2806
+ ];
2807
+ function globalHookScript(spec) {
2808
+ const chainExit = spec.hookName === "pre-commit" ? "|| exit $?" : "|| true";
2809
+ return `#!/bin/sh
2810
+ # prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
2811
+ # Install/uninstall: prim hooks install|uninstall --scope user
2812
+ # Runs prim only where activated \u2014 'prim enable' (this repo) or
2813
+ # 'git config --global prim.active true' (every repo). Chains to the repo's own
2814
+ # hook regardless, so inactive repos are unaffected.
2815
+ ${gatedShim(spec.binName)}
2816
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
2817
+ repo_hook="$common_dir/hooks/${spec.hookName}"
2818
+ if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null; then
2819
+ "$repo_hook" "$@" ${chainExit}
2820
+ fi
2821
+ exit 0
2822
+ `;
2823
+ }
2824
+ function passThroughScript(hookName) {
2825
+ return `#!/bin/sh
2826
+ # prim pass-through hook (core.hooksPath) \u2014 managed by prim; do not edit.
2827
+ common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
2828
+ repo_hook="$common_dir/hooks/${hookName}"
2829
+ [ -x "$repo_hook" ] && exec "$repo_hook" "$@"
2830
+ exit 0
2831
+ `;
2832
+ }
2833
+ function ownedHookNames() {
2834
+ return [...HOOKS.map((s) => s.hookName), ...PASSTHROUGH_HOOKS];
2835
+ }
2836
+ function writeOwnHooks() {
2837
+ if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
2838
+ mkdirSync5(PRIM_GIT_HOOKS_DIR, { recursive: true });
2839
+ }
2840
+ for (const spec of HOOKS) {
2841
+ writeFileSync4(resolve(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
2842
+ mode: 493
2843
+ });
2844
+ }
2845
+ for (const name of PASSTHROUGH_HOOKS) {
2846
+ writeFileSync4(resolve(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
2847
+ }
2848
+ }
2849
+ function appendPrimBlock(hookPath, spec) {
2850
+ mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
2851
+ }
2852
+ function stripPrimBlock(hookPath, spec) {
2853
+ if (!existsSync7(hookPath)) return;
2854
+ const existing = readFileSync6(hookPath, "utf-8");
2855
+ const primCreated = existing.includes(PRIM_CREATED_MARK);
2856
+ const { start, end } = blockMarkers(spec);
2857
+ const s = existing.indexOf(start);
2858
+ const e = existing.indexOf(end);
2859
+ if (s === -1 || e === -1) return;
2860
+ const out = (existing.slice(0, s) + existing.slice(e + end.length)).replace(/\n{2,}$/, "\n");
2861
+ const remainder = out.replaceAll("#!/bin/sh", "").replaceAll(`# ${PRIM_CREATED_MARK}`, "").trim();
2862
+ if (primCreated && remainder === "") {
2863
+ unlinkSync2(hookPath);
2864
+ return;
2865
+ }
2866
+ writeFileSync4(hookPath, out, { mode: 493 });
2867
+ }
2868
+ function installGlobalHooks(opts = {}) {
2869
+ const global = gitConfigGet("--global");
2870
+ if (global === "") {
2871
+ const system = gitConfigGet("--system");
2872
+ if (system !== "" && !isOurHooksDir(system)) {
2873
+ if (!opts.force) {
2874
+ console.error(
2875
+ `[prim] system core.hooksPath is set to ${system}; a --global set would override it, and prim chains only to .git/hooks (not a system dir), so those hooks would stop firing. Skipping \u2014 re-run with --force to override, or run per-repo \`prim hooks install\`.`
2876
+ );
2877
+ return false;
2878
+ }
2879
+ console.error(
2880
+ `[prim] --force: overriding system core.hooksPath ${system}; its hooks will no longer fire (prim chains only to .git/hooks).`
2881
+ );
2882
+ }
2883
+ writeOwnHooks();
2884
+ execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR]);
2885
+ console.log(
2886
+ `Installed prim global git hooks; set core.hooksPath to ${PRIM_GIT_HOOKS_DIR}. Repos are opt-in: run \`prim enable\` in each repo to capture, or \`git config --global prim.active true\` for all.`
2887
+ );
2888
+ return true;
2889
+ }
2890
+ if (isOurHooksDir(global)) {
2891
+ writeOwnHooks();
2892
+ console.log(`Prim global git hooks already active (${PRIM_GIT_HOOKS_DIR}); refreshed scripts.`);
2893
+ return true;
2894
+ }
2895
+ const dir = expandTilde(global);
2896
+ for (const spec of HOOKS) {
2897
+ appendPrimBlock(resolve(dir, spec.hookName), spec);
2898
+ }
2899
+ console.log(
2900
+ `Appended prim hooks into existing core.hooksPath dir ${global} (pointer unchanged).`
2901
+ );
2902
+ return true;
2903
+ }
2904
+ function uninstallGlobalHooks() {
2905
+ const global = gitConfigGet("--global");
2906
+ if (isOurHooksDir(global)) {
2907
+ for (const name of ownedHookNames()) {
2908
+ const p = resolve(PRIM_GIT_HOOKS_DIR, name);
2909
+ if (existsSync7(p)) unlinkSync2(p);
2910
+ }
2911
+ execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"]);
2912
+ console.log("Removed prim global git hooks and unset core.hooksPath.");
2913
+ return;
2914
+ }
2915
+ if (global !== "") {
2916
+ const dir = expandTilde(global);
2917
+ for (const spec of HOOKS) {
2918
+ stripPrimBlock(resolve(dir, spec.hookName), spec);
2919
+ }
2920
+ console.log(`Removed the prim block from ${global} (left the dir and core.hooksPath intact).`);
2921
+ return;
2922
+ }
2923
+ console.log("No prim global git hooks found.");
2924
+ }
2506
2925
  function installHooks(gitRoot, target) {
2507
2926
  for (const spec of HOOKS) {
2508
2927
  if (target === "husky") {
@@ -2511,6 +2930,7 @@ function installHooks(gitRoot, target) {
2511
2930
  installToDotGit(gitRoot, spec);
2512
2931
  }
2513
2932
  }
2933
+ activateRepoBestEffort(gitRoot);
2514
2934
  }
2515
2935
  function registerHooksCommands(program2) {
2516
2936
  const hooks = program2.command("hooks").description("Manage git hooks");
@@ -2521,36 +2941,58 @@ function registerHooksCommands(program2) {
2521
2941
  "husky",
2522
2942
  "git-hooks"
2523
2943
  ])
2524
- ).action(async (opts, command) => {
2525
- const globals = command.optsWithGlobals();
2526
- const nonInteractive = Boolean(
2527
- globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
2528
- );
2529
- const gitRoot = getGitRoot();
2530
- if (opts.target === "husky") return installHooks(gitRoot, "husky");
2531
- if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
2532
- if (detectHusky(gitRoot)) {
2533
- if (globals.yes) return installHooks(gitRoot, "husky");
2534
- if (nonInteractive) {
2535
- throw new Error(
2536
- "--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
2537
- );
2944
+ ).addOption(
2945
+ new Option(
2946
+ "--scope <scope>",
2947
+ "project (default, this repo) or user (a global core.hooksPath capturing every repo)"
2948
+ ).choices(["project", "user"])
2949
+ ).option("--force", "with --scope user, override a system-level core.hooksPath").action(
2950
+ async (opts, command) => {
2951
+ if (opts.scope === "user") {
2952
+ installGlobalHooks({ force: opts.force });
2953
+ return;
2538
2954
  }
2539
- if (!process.stdin.isTTY) {
2540
- console.error(
2541
- "Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
2542
- );
2543
- } else if (await askConfirmation(
2544
- "Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
2545
- )) {
2546
- return installHooks(gitRoot, "husky");
2547
- } else {
2548
- console.log("Falling back to .git/hooks install.");
2955
+ const globals = command.optsWithGlobals();
2956
+ const nonInteractive = Boolean(
2957
+ globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
2958
+ );
2959
+ const gitRoot = getGitRoot();
2960
+ if (opts.target === "husky") return installHooks(gitRoot, "husky");
2961
+ if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
2962
+ if (detectHusky(gitRoot)) {
2963
+ if (globals.yes) return installHooks(gitRoot, "husky");
2964
+ if (nonInteractive) {
2965
+ throw new Error(
2966
+ "--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
2967
+ );
2968
+ }
2969
+ if (!process.stdin.isTTY) {
2970
+ console.error(
2971
+ "Note: Husky detected but stdin is not a TTY \u2014 falling back to .git/hooks. Pass --yes for Husky or --non-interactive to fail fast."
2972
+ );
2973
+ } else if (await askConfirmation(
2974
+ "Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
2975
+ )) {
2976
+ return installHooks(gitRoot, "husky");
2977
+ } else {
2978
+ console.log("Falling back to .git/hooks install.");
2979
+ }
2549
2980
  }
2981
+ installHooks(gitRoot, "git-hooks");
2982
+ }
2983
+ );
2984
+ hooks.command("uninstall").description(
2985
+ "Remove the prim git hooks (.git/hooks, or the global core.hooksPath with --scope user)"
2986
+ ).addOption(
2987
+ new Option(
2988
+ "--scope <scope>",
2989
+ "project (default, this repo) or user (global core.hooksPath)"
2990
+ ).choices(["project", "user"])
2991
+ ).action((opts) => {
2992
+ if (opts.scope === "user") {
2993
+ uninstallGlobalHooks();
2994
+ return;
2550
2995
  }
2551
- installHooks(gitRoot, "git-hooks");
2552
- });
2553
- hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
2554
2996
  const gitRoot = getGitRoot();
2555
2997
  for (const spec of HOOKS) {
2556
2998
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
@@ -2570,7 +3012,7 @@ function registerHooksCommands(program2) {
2570
3012
 
2571
3013
  // src/commands/moves.ts
2572
3014
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
2573
- import { join as join6 } from "path";
3015
+ import { join as join7 } from "path";
2574
3016
 
2575
3017
  // src/flusher.ts
2576
3018
  import { renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
@@ -2729,18 +3171,18 @@ function registerMovesCommands(program2) {
2729
3171
  }
2730
3172
  });
2731
3173
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
2732
- const dir = join6(process.cwd(), ".prim");
3174
+ const dir = join7(process.cwd(), ".prim");
2733
3175
  if (!existsSync8(dir)) {
2734
3176
  mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
2735
3177
  }
2736
- const file = join6(process.cwd(), WORKSPACE_FILE);
3178
+ const file = join7(process.cwd(), WORKSPACE_FILE);
2737
3179
  writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
2738
3180
  mode: FILE_MODE2
2739
3181
  });
2740
3182
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
2741
3183
  });
2742
3184
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
2743
- const file = join6(process.cwd(), WORKSPACE_FILE);
3185
+ const file = join7(process.cwd(), WORKSPACE_FILE);
2744
3186
  if (!existsSync8(file)) {
2745
3187
  console.log("[prim] no workspace binding in cwd");
2746
3188
  return;
@@ -2751,7 +3193,7 @@ function registerMovesCommands(program2) {
2751
3193
  }
2752
3194
 
2753
3195
  // src/commands/reconcile.ts
2754
- var EXIT_OK2 = 0;
3196
+ var EXIT_OK3 = 0;
2755
3197
  var EXIT_USAGE2 = 2;
2756
3198
  var EXIT_SERVER = 3;
2757
3199
  var HTTP_CLIENT_ERROR_MIN = 400;
@@ -2814,7 +3256,7 @@ async function performReconcile(idOrShortId, opts = {}) {
2814
3256
  `
2815
3257
  );
2816
3258
  console.log(JSON.stringify(response, null, 2));
2817
- process.exitCode = EXIT_OK2;
3259
+ process.exitCode = EXIT_OK3;
2818
3260
  return;
2819
3261
  }
2820
3262
  process.stderr.write("[prim] reconcile: malformed server response\n");
@@ -2841,7 +3283,7 @@ import {
2841
3283
  unlinkSync as unlinkSync5,
2842
3284
  writeFileSync as writeFileSync6
2843
3285
  } from "fs";
2844
- import { join as join7 } from "path";
3286
+ import { join as join8 } from "path";
2845
3287
  var DIR_MODE2 = 448;
2846
3288
  var FILE_MODE3 = 384;
2847
3289
  function ensureDir() {
@@ -2850,7 +3292,7 @@ function ensureDir() {
2850
3292
  }
2851
3293
  }
2852
3294
  function markerPath(sessionId) {
2853
- return join7(SESSIONS_DIR, `${sessionId}.json`);
3295
+ return join8(SESSIONS_DIR, `${sessionId}.json`);
2854
3296
  }
2855
3297
  function registerSessionCommands(program2) {
2856
3298
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -2878,7 +3320,7 @@ function registerSessionCommands(program2) {
2878
3320
  for (const f of files) {
2879
3321
  const sessionId = f.replace(/\.json$/, "");
2880
3322
  try {
2881
- const m = JSON.parse(readFileSync7(join7(SESSIONS_DIR, f), "utf-8"));
3323
+ const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
2882
3324
  console.log(`${sessionId} org=${m.orgId}`);
2883
3325
  } catch {
2884
3326
  }
@@ -2897,6 +3339,8 @@ function registerSessionCommands(program2) {
2897
3339
 
2898
3340
  // src/commands/setup.ts
2899
3341
  import { spawnSync } from "child_process";
3342
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
3343
+ import { join as join9 } from "path";
2900
3344
  var EXIT_INCOMPLETE = 1;
2901
3345
  var EXIT_USAGE3 = 2;
2902
3346
  var SESSION_LABELS = {
@@ -2923,11 +3367,57 @@ function planSetupSteps(opts) {
2923
3367
  required: false
2924
3368
  });
2925
3369
  }
2926
- steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
2927
- const skillArgs = ["skill", "install", "--agent", opts.agent];
3370
+ steps.push({
3371
+ key: "hooks",
3372
+ label: "Git hooks",
3373
+ args: ["hooks", "install", ...scopeArgs],
3374
+ required: true
3375
+ });
3376
+ const skillArgs = ["skill", "install", "--agent", opts.agent, ...scopeArgs];
2928
3377
  steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
3378
+ if (opts.scope === "user") {
3379
+ steps.push({ key: "enable", label: "Activate this repo", args: ["enable"], required: false });
3380
+ }
3381
+ return steps;
3382
+ }
3383
+ var CONFLICT_SESSION = "session";
3384
+ var CONFLICT_HOOKS = "hooks";
3385
+ var CONFLICT_SKILL = "skill";
3386
+ function planCleanupUninstalls(agent, conflicts) {
3387
+ const steps = [];
3388
+ if (conflicts.includes(CONFLICT_SESSION) && agent !== "hermes") {
3389
+ steps.push([agent, "uninstall", "--scope", "project"]);
3390
+ }
3391
+ if (conflicts.includes(CONFLICT_HOOKS)) steps.push(["hooks", "uninstall"]);
3392
+ if (conflicts.includes(CONFLICT_SKILL)) steps.push(["skill", "uninstall", "--agent", agent]);
2929
3393
  return steps;
2930
3394
  }
3395
+ function detectProjectConflicts(agent, run) {
3396
+ const conflicts = [];
3397
+ if (agent !== "hermes") {
3398
+ try {
3399
+ const parsed = JSON.parse(run([agent, "status"], true).stdout || "{}");
3400
+ if (parsed.project?.gate || parsed.project?.capture) conflicts.push(CONFLICT_SESSION);
3401
+ } catch {
3402
+ }
3403
+ }
3404
+ try {
3405
+ const root = gitToplevel();
3406
+ const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
3407
+ if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
3408
+ conflicts.push(CONFLICT_HOOKS);
3409
+ }
3410
+ } catch {
3411
+ }
3412
+ try {
3413
+ const parsed = JSON.parse(
3414
+ run(["skill", "status", "--agent", agent, "--json"], true).stdout || "{}"
3415
+ );
3416
+ if (parsed.installed) conflicts.push(CONFLICT_SKILL);
3417
+ } catch {
3418
+ }
3419
+ return conflicts;
3420
+ }
2931
3421
  function detectAgent(env) {
2932
3422
  if (env.HERMES_INTERACTIVE) {
2933
3423
  return "hermes";
@@ -2943,7 +3433,14 @@ function resolveAgent(agentFlag, env) {
2943
3433
  function registerSetupCommand(program2) {
2944
3434
  program2.command("setup").description(
2945
3435
  "Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
2946
- ).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option("--scope <scope>", "project or user (session integration)", "project").option("--no-daemon", "skip starting the companion daemon").action((opts) => {
3436
+ ).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option(
3437
+ "--scope <scope>",
3438
+ "user (default \u2014 install once for every repo) or project (this repo only)",
3439
+ "user"
3440
+ ).option(
3441
+ "--migrate",
3442
+ "with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
3443
+ ).option("--no-daemon", "skip starting the companion daemon").action((opts) => {
2947
3444
  const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
2948
3445
  if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
2949
3446
  process.stderr.write(
@@ -2962,7 +3459,7 @@ function registerSetupCommand(program2) {
2962
3459
  const self = process.argv[1];
2963
3460
  const run = (args, capture = false) => {
2964
3461
  const r = spawnSync(process.execPath, [self, ...args], {
2965
- stdio: capture ? ["inherit", "pipe", "inherit"] : "inherit",
3462
+ stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
2966
3463
  encoding: "utf-8"
2967
3464
  });
2968
3465
  return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
@@ -2999,6 +3496,18 @@ function registerSetupCommand(program2) {
2999
3496
  const { code } = run(step.args);
3000
3497
  results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
3001
3498
  }
3499
+ if (scope === "user") {
3500
+ const conflicts = detectProjectConflicts(agent, run);
3501
+ if (conflicts.length > 0 && opts.migrate) {
3502
+ note(`migrate \xB7 removing project-scoped config (${conflicts.join(", ")})\u2026`);
3503
+ const ok = planCleanupUninstalls(agent, conflicts).map((args) => run(args).code === 0).every(Boolean);
3504
+ results.migrate = ok ? "ok" : "failed";
3505
+ } else if (conflicts.length > 0) {
3506
+ note(
3507
+ `migrate \xB7 project-scoped prim config present in this repo (${conflicts.join(", ")}) \u2014 it will double-fire with the user-scope install. Re-run \`prim setup --migrate\` to remove it.`
3508
+ );
3509
+ }
3510
+ }
3002
3511
  note("welcome");
3003
3512
  results.welcome = run(["welcome"]).code === 0 ? "ok" : "failed";
3004
3513
  const failed = Object.entries(results).filter(([, v]) => v === "failed").map(([k]) => k);
@@ -3013,17 +3522,116 @@ function registerSetupCommand(program2) {
3013
3522
  // src/commands/skill.ts
3014
3523
  import {
3015
3524
  closeSync as closeSync4,
3016
- existsSync as existsSync10,
3525
+ existsSync as existsSync12,
3017
3526
  fsyncSync as fsyncSync3,
3018
3527
  openSync as openSync4,
3019
- readFileSync as readFileSync8,
3528
+ readFileSync as readFileSync10,
3020
3529
  renameSync as renameSync4,
3021
3530
  writeFileSync as writeFileSync7
3022
3531
  } from "fs";
3023
- import { dirname as dirname5, resolve as resolve2 } from "path";
3024
- import { fileURLToPath as fileURLToPath2 } from "url";
3532
+ import { homedir as homedir7 } from "os";
3533
+ import { dirname as dirname7, join as join11, resolve as resolve3 } from "path";
3534
+ import { fileURLToPath as fileURLToPath3 } from "url";
3025
3535
  import { createPatch } from "diff";
3026
- var __dirname = dirname5(fileURLToPath2(import.meta.url));
3536
+
3537
+ // src/commands/claude-plugin.ts
3538
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
3539
+ import { homedir as homedir6 } from "os";
3540
+ import { dirname as dirname6, join as join10, resolve as resolve2 } from "path";
3541
+ import { fileURLToPath as fileURLToPath2 } from "url";
3542
+ var __dirname = dirname6(fileURLToPath2(import.meta.url));
3543
+ var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
3544
+ function resolvePluginDir(cwd, scope) {
3545
+ if (scope && scope !== "user" && scope !== "project") {
3546
+ console.error(`Unknown --scope "${scope}" (expected user or project)`);
3547
+ return null;
3548
+ }
3549
+ const base = scope === "user" ? join10(homedir6(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
3550
+ return join10(base, "skills", "prim");
3551
+ }
3552
+ function packageVersion() {
3553
+ let dir = __dirname;
3554
+ while (dir !== dirname6(dir)) {
3555
+ const p = resolve2(dir, "package.json");
3556
+ if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
3557
+ dir = dirname6(dir);
3558
+ }
3559
+ return "0.0.0";
3560
+ }
3561
+ function pluginManifest() {
3562
+ const manifest = { name: "prim", description: PLUGIN_DESCRIPTION, version: packageVersion() };
3563
+ return `${JSON.stringify(manifest, null, 2)}
3564
+ `;
3565
+ }
3566
+ function removeDirIfEmpty(dir) {
3567
+ if (existsSync11(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
3568
+ }
3569
+ function pluginPaths(dir) {
3570
+ return {
3571
+ manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
3572
+ skillPath: join10(dir, "SKILL.md")
3573
+ };
3574
+ }
3575
+ function installClaudePlugin(cwd, opts) {
3576
+ const dir = resolvePluginDir(cwd, opts.scope);
3577
+ if (dir === null) return 1;
3578
+ const { manifestPath, skillPath } = pluginPaths(dir);
3579
+ const manifest = pluginManifest();
3580
+ const skill = loadSkill();
3581
+ const manifestCurrent = existsSync11(manifestPath) ? readFileSync9(manifestPath, "utf-8") : null;
3582
+ const skillCurrent = existsSync11(skillPath) ? readFileSync9(skillPath, "utf-8") : null;
3583
+ if (manifestCurrent === manifest && skillCurrent === skill) {
3584
+ console.log("No changes \u2014 prim skill plugin already up to date.");
3585
+ return 0;
3586
+ }
3587
+ if (opts.dryRun) {
3588
+ console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
3589
+ return 0;
3590
+ }
3591
+ mkdirSync8(join10(dir, ".claude-plugin"), { recursive: true });
3592
+ atomicWrite2(manifestPath, manifest);
3593
+ atomicWrite2(skillPath, skill);
3594
+ console.log(`Installed prim skill plugin at ${dir}`);
3595
+ console.log("Restart Claude Code or run /reload-plugins to load it.");
3596
+ if (opts.scope !== "user") {
3597
+ console.log("Project-scope plugins load only when Claude Code launches from this directory.");
3598
+ }
3599
+ return 0;
3600
+ }
3601
+ function uninstallClaudePlugin(cwd, opts) {
3602
+ const dir = resolvePluginDir(cwd, opts.scope);
3603
+ if (dir === null) return 1;
3604
+ const { manifestPath, skillPath } = pluginPaths(dir);
3605
+ if (!existsSync11(manifestPath) && !existsSync11(skillPath)) {
3606
+ console.log(`prim skill plugin not present at ${dir}`);
3607
+ return 0;
3608
+ }
3609
+ rmSync3(manifestPath, { force: true });
3610
+ rmSync3(skillPath, { force: true });
3611
+ removeDirIfEmpty(join10(dir, ".claude-plugin"));
3612
+ removeDirIfEmpty(dir);
3613
+ console.log(`Removed prim skill plugin from ${dir}`);
3614
+ return 0;
3615
+ }
3616
+ function statusClaudePlugin(cwd, opts) {
3617
+ const dir = resolvePluginDir(cwd, opts.scope);
3618
+ if (dir === null) return 1;
3619
+ const { skillPath } = pluginPaths(dir);
3620
+ const installed = existsSync11(skillPath);
3621
+ if (opts.json) {
3622
+ printJson({ installed, target: dir });
3623
+ return installed ? 0 : 1;
3624
+ }
3625
+ if (installed) {
3626
+ console.log(`prim skill plugin installed at ${dir}`);
3627
+ return 0;
3628
+ }
3629
+ console.log(`No prim skill plugin at ${dir}`);
3630
+ return 1;
3631
+ }
3632
+
3633
+ // src/commands/skill.ts
3634
+ var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
3027
3635
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
3028
3636
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
3029
3637
  var TARGET_CANDIDATES = [
@@ -3040,17 +3648,25 @@ var AGENT_TARGET = {
3040
3648
  codex: "AGENTS.md",
3041
3649
  hermes: ".hermes.md"
3042
3650
  };
3651
+ function userTargetFor(agent) {
3652
+ if (agent === "claude") return join11(homedir7(), ".claude", "CLAUDE.md");
3653
+ if (agent === "codex") return join11(homedir7(), ".codex", "AGENTS.md");
3654
+ if (agent === "hermes") {
3655
+ return join11(process.env.HERMES_HOME ?? join11(homedir7(), ".hermes"), ".hermes.md");
3656
+ }
3657
+ return null;
3658
+ }
3043
3659
  function loadSkill() {
3044
- let dir = __dirname;
3045
- while (dir !== dirname5(dir)) {
3046
- const p = resolve2(dir, "SKILL.md");
3047
- if (existsSync10(p)) return readFileSync8(p, "utf-8");
3048
- dir = dirname5(dir);
3660
+ let dir = __dirname2;
3661
+ while (dir !== dirname7(dir)) {
3662
+ const p = resolve3(dir, "SKILL.md");
3663
+ if (existsSync12(p)) return readFileSync10(p, "utf-8");
3664
+ dir = dirname7(dir);
3049
3665
  }
3050
3666
  throw new Error("SKILL.md not found in package");
3051
3667
  }
3052
3668
  function detectTargets(cwd) {
3053
- return TARGET_CANDIDATES.filter((p) => existsSync10(resolve2(cwd, p)));
3669
+ return TARGET_CANDIDATES.filter((p) => existsSync12(resolve3(cwd, p)));
3054
3670
  }
3055
3671
  function detectNewline(content) {
3056
3672
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -3088,24 +3704,39 @@ function atomicWrite2(target, content) {
3088
3704
  renameSync4(tmp, target);
3089
3705
  }
3090
3706
  function resolveTarget(cwd, opts) {
3091
- if (opts.target) return resolve2(cwd, opts.target);
3707
+ if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
3708
+ console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
3709
+ return null;
3710
+ }
3711
+ if (opts.target) return resolve3(cwd, opts.target);
3712
+ if (opts.scope === "user") {
3713
+ if (!opts.agent) {
3714
+ console.error("--scope user requires --agent (claude, codex, or hermes)");
3715
+ return null;
3716
+ }
3717
+ const userTarget = userTargetFor(opts.agent);
3718
+ if (userTarget) return userTarget;
3719
+ console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
3720
+ return null;
3721
+ }
3092
3722
  if (opts.agent) {
3093
3723
  const mapped = AGENT_TARGET[opts.agent];
3094
- if (typeof mapped === "string") return resolve2(cwd, mapped);
3724
+ if (typeof mapped === "string") return resolve3(cwd, mapped);
3095
3725
  console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
3096
3726
  return null;
3097
3727
  }
3098
3728
  const matches = detectTargets(cwd);
3099
- if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
3100
- if (matches.length === 1) return resolve2(cwd, matches[0]);
3729
+ if (matches.length === 0) return resolve3(cwd, DEFAULT_TARGET);
3730
+ if (matches.length === 1) return resolve3(cwd, matches[0]);
3101
3731
  console.error("Multiple rules files detected. Use --target to disambiguate:");
3102
3732
  for (const m of matches) console.error(` ${m}`);
3103
3733
  return null;
3104
3734
  }
3105
3735
  function runInstall(cwd, opts) {
3736
+ if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
3106
3737
  const target = resolveTarget(cwd, opts);
3107
3738
  if (target === null) return 1;
3108
- const existing = existsSync10(target) ? readFileSync8(target, "utf-8") : "";
3739
+ const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
3109
3740
  const eol = existing ? detectNewline(existing) : "\n";
3110
3741
  const block = composeBlock(loadSkill(), eol);
3111
3742
  const next = applyBlock(existing, block, eol);
@@ -3122,13 +3753,14 @@ function runInstall(cwd, opts) {
3122
3753
  return 0;
3123
3754
  }
3124
3755
  function runUninstall(cwd, opts) {
3756
+ if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
3125
3757
  const target = resolveTarget(cwd, opts);
3126
3758
  if (target === null) return 1;
3127
- if (!existsSync10(target)) {
3759
+ if (!existsSync12(target)) {
3128
3760
  console.log(`Skill block not present at ${target}`);
3129
3761
  return 0;
3130
3762
  }
3131
- const existing = readFileSync8(target, "utf-8");
3763
+ const existing = readFileSync10(target, "utf-8");
3132
3764
  const next = removeBlock(existing);
3133
3765
  if (next === null) {
3134
3766
  console.log(`Skill block not present at ${target}`);
@@ -3139,12 +3771,13 @@ function runUninstall(cwd, opts) {
3139
3771
  return 0;
3140
3772
  }
3141
3773
  function runStatus(cwd, opts) {
3774
+ if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
3142
3775
  const target = resolveTarget(cwd, opts);
3143
3776
  if (target === null) return 1;
3144
- const fileExists = existsSync10(target);
3777
+ const fileExists = existsSync12(target);
3145
3778
  let installed = false;
3146
3779
  if (fileExists) {
3147
- const content = readFileSync8(target, "utf-8");
3780
+ const content = readFileSync10(target, "utf-8");
3148
3781
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
3149
3782
  }
3150
3783
  if (opts.json) {
@@ -3164,7 +3797,7 @@ function runStatus(cwd, opts) {
3164
3797
  }
3165
3798
  function registerSkillCommands(program2) {
3166
3799
  const skill = program2.command("skill").description("Manage the prim skill in your project rules file");
3167
- skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
3800
+ skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
3168
3801
  try {
3169
3802
  process.exit(runInstall(process.cwd(), opts));
3170
3803
  } catch (err) {
@@ -3172,27 +3805,27 @@ function registerSkillCommands(program2) {
3172
3805
  process.exit(2);
3173
3806
  }
3174
3807
  });
3175
- skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").action((opts) => {
3808
+ skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
3176
3809
  process.exit(runUninstall(process.cwd(), opts));
3177
3810
  });
3178
- skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--json", "Output as JSON").action((opts) => {
3811
+ skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
3179
3812
  process.exit(runStatus(process.cwd(), opts));
3180
3813
  });
3181
3814
  }
3182
3815
 
3183
3816
  // src/commands/statusline.ts
3184
- import { readFileSync as readFileSync9 } from "fs";
3185
- import { dirname as dirname6, resolve as resolve3 } from "path";
3186
- import { fileURLToPath as fileURLToPath3 } from "url";
3817
+ import { readFileSync as readFileSync11 } from "fs";
3818
+ import { dirname as dirname8, resolve as resolve4 } from "path";
3819
+ import { fileURLToPath as fileURLToPath4 } from "url";
3187
3820
  var STATUSLINE_TIMEOUT_MS = 200;
3188
3821
  var STATUSLINE_NAME_CAP = 3;
3189
3822
  function readPackageVersion() {
3190
3823
  try {
3191
- const here = dirname6(fileURLToPath3(import.meta.url));
3192
- const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
3824
+ const here = dirname8(fileURLToPath4(import.meta.url));
3825
+ const candidates = [resolve4(here, "../../package.json"), resolve4(here, "../package.json")];
3193
3826
  for (const path of candidates) {
3194
3827
  try {
3195
- const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
3828
+ const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
3196
3829
  if (pkg2.version) {
3197
3830
  return pkg2.version;
3198
3831
  }
@@ -3257,6 +3890,7 @@ var REVERSE_PROMPT_LINES = [
3257
3890
  "to focus on those goals?"
3258
3891
  ];
3259
3892
  var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
3893
+ var REVERSE_PROMPT_TEMPLATE = "We're interested in learning your current goals, as well as what you're not focusing on to achieve those goals. We found the following:\n\n$FOUND_GOALS\n\nHow do these look to you? Would you like to change these goals?";
3260
3894
  var CALLOUT_TITLE = "Your turn";
3261
3895
  var CALLOUT_INDENT = " ";
3262
3896
  function ruledQuestion(lines) {
@@ -3343,6 +3977,7 @@ function welcomeJson(state) {
3343
3977
  welcomed: true,
3344
3978
  org: "seed",
3345
3979
  reversePrompt: REVERSE_PROMPT,
3980
+ reversePromptTemplate: REVERSE_PROMPT_TEMPLATE,
3346
3981
  recent: state.recent
3347
3982
  };
3348
3983
  }
@@ -3359,8 +3994,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
3359
3994
  }
3360
3995
 
3361
3996
  // src/index.ts
3362
- var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
3363
- var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
3997
+ var __dirname3 = dirname9(fileURLToPath5(import.meta.url));
3998
+ var pkg = JSON.parse(readFileSync12(resolve5(__dirname3, "../package.json"), "utf-8"));
3364
3999
  updateNotifier({ pkg }).notify();
3365
4000
  var program = new Command();
3366
4001
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -3369,6 +4004,7 @@ program.name("prim").description("CLI for Primitive's decision graph").version(p
3369
4004
  );
3370
4005
  registerAuthCommands(program);
3371
4006
  registerHooksCommands(program);
4007
+ registerActivationCommands(program);
3372
4008
  registerSkillCommands(program);
3373
4009
  registerMovesCommands(program);
3374
4010
  registerSessionCommands(program);