@primitive.ai/prim 0.1.0-alpha.33 → 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
  }
@@ -1332,6 +1575,12 @@ function formatCascadeJson(result) {
1332
1575
  var RECENT_TIMEOUT_MS = 1e4;
1333
1576
  var defaultDeps2 = { getClient };
1334
1577
  async function fetchRecent(args, deps = defaultDeps2) {
1578
+ if (args.author !== void 0 && args.author.trim() === "") {
1579
+ return {
1580
+ decisions: [],
1581
+ unavailable: "--author must be a non-empty name"
1582
+ };
1583
+ }
1335
1584
  const params = new URLSearchParams();
1336
1585
  if (args.limit !== void 0) {
1337
1586
  params.set("limit", String(args.limit));
@@ -1339,6 +1588,9 @@ async function fetchRecent(args, deps = defaultDeps2) {
1339
1588
  if (args.since !== void 0) {
1340
1589
  params.set("since", args.since);
1341
1590
  }
1591
+ if (args.author !== void 0) {
1592
+ params.set("author", args.author);
1593
+ }
1342
1594
  try {
1343
1595
  const client = deps.getClient();
1344
1596
  const res = await daemonOrDirectGet(
@@ -1347,10 +1599,22 @@ async function fetchRecent(args, deps = defaultDeps2) {
1347
1599
  client,
1348
1600
  RECENT_TIMEOUT_MS
1349
1601
  );
1602
+ if (args.author !== void 0 && res.author === void 0 && res.unavailable === void 0) {
1603
+ return {
1604
+ decisions: [],
1605
+ unavailable: "--author requires a newer Primitive backend (no author echo in response); retry without --author for the team-wide feed"
1606
+ };
1607
+ }
1350
1608
  const result = { decisions: res.decisions };
1351
1609
  if (res.viewerHasDecisions !== void 0) {
1352
1610
  result.viewerHasDecisions = res.viewerHasDecisions;
1353
1611
  }
1612
+ if (res.author !== void 0) {
1613
+ result.author = res.author;
1614
+ }
1615
+ if (res.authorHasDecisions !== void 0) {
1616
+ result.authorHasDecisions = res.authorHasDecisions;
1617
+ }
1354
1618
  if (res.unavailable !== void 0) {
1355
1619
  result.unavailable = res.unavailable;
1356
1620
  }
@@ -1407,10 +1671,20 @@ function formatRecentHuman(result) {
1407
1671
  if (result.unavailable !== void 0) {
1408
1672
  return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
1409
1673
  }
1674
+ if (result.author !== void 0 && result.decisions.length === 0) {
1675
+ if (result.authorHasDecisions === true) {
1676
+ return `[prim] recent \xB7 ${result.author.name} \xB7 0 decisions in this window (older decisions exist \u2014 widen --since or raise --limit)`;
1677
+ }
1678
+ if (result.authorHasDecisions === false) {
1679
+ return `[prim] recent \xB7 ${result.author.name} \xB7 no feed-visible decisions yet (if unexpected, check prim setup/doctor on their machine and the repo they work in)`;
1680
+ }
1681
+ return `[prim] recent \xB7 ${result.author.name} \xB7 0 decisions`;
1682
+ }
1683
+ const label = result.author === void 0 ? "recent" : `recent \xB7 ${result.author.name}`;
1410
1684
  if (result.decisions.length === 0) {
1411
1685
  return "[prim] recent \xB7 0 decisions";
1412
1686
  }
1413
- const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
1687
+ const lines = [`[prim] ${label} \xB7 ${String(result.decisions.length)} decision(s)`];
1414
1688
  for (const row of result.decisions) {
1415
1689
  lines.push(formatRecentRow(row));
1416
1690
  }
@@ -1771,10 +2045,14 @@ function registerDecisionsCommands(program2) {
1771
2045
  decisions.command("recent").description("Show the team-wide chronological decision feed").option("--limit <n>", "Maximum number of rows to return (default 10)").option(
1772
2046
  "--since <duration>",
1773
2047
  "Lookback window \u2014 accepts `Nm`, `Nh`, `Nd` (minutes / hours / days) or absolute epoch ms"
2048
+ ).option(
2049
+ "--author <name>",
2050
+ `Filter to one teammate's decisions \u2014 feed name, "First Last", last name, username, email, or email local-part`
1774
2051
  ).action(async (opts) => {
1775
2052
  const result = await fetchRecent({
1776
2053
  limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
1777
- since: opts.since
2054
+ since: opts.since,
2055
+ author: opts.author
1778
2056
  });
1779
2057
  console.error(formatRecentHuman(result));
1780
2058
  console.log(formatRecentJson(result));
@@ -2350,9 +2628,10 @@ function registerHermesCommands(program2) {
2350
2628
  }
2351
2629
 
2352
2630
  // src/commands/hooks.ts
2353
- import { execSync as execSync2 } from "child_process";
2631
+ import { execFileSync } from "child_process";
2354
2632
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2355
- import { resolve } from "path";
2633
+ import { homedir as homedir5 } from "os";
2634
+ import { dirname as dirname5, join as join6, resolve } from "path";
2356
2635
  import { Option } from "commander";
2357
2636
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
2358
2637
  var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
@@ -2365,18 +2644,25 @@ function blockMarkers(spec) {
2365
2644
  }
2366
2645
  var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
2367
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";
2368
2649
  function hookShim(binName) {
2369
2650
  return `if command -v ${binName} >/dev/null 2>&1; then
2370
- ${binName}
2651
+ ${binName} || true
2371
2652
  elif [ -f "./node_modules/.bin/${binName}" ]; then
2372
- ./node_modules/.bin/${binName}
2653
+ ./node_modules/.bin/${binName} || true
2373
2654
  else
2374
2655
  npx --yes -p @primitive.ai/prim ${binName} 2>/dev/null || true
2375
2656
  fi`;
2376
2657
  }
2658
+ function gatedShim(binName) {
2659
+ return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
2660
+ ${hookShim(binName)}
2661
+ fi`;
2662
+ }
2377
2663
  function dotGitScript(spec) {
2378
2664
  return `#!/bin/sh
2379
- # prim ${spec.hookName} hook \u2014 installed by: prim hooks install
2665
+ # prim ${spec.hookName} hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
2380
2666
 
2381
2667
  ${hookShim(spec.binName)}
2382
2668
  `;
@@ -2387,10 +2673,35 @@ function huskyBlock(spec) {
2387
2673
  ${hookShim(spec.binName)}
2388
2674
  ${end}`;
2389
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
+ }
2390
2699
  function getGitRoot() {
2391
- return execSync2("git rev-parse --show-toplevel", {
2392
- encoding: "utf-8"
2393
- }).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;
2394
2705
  }
2395
2706
  function detectHusky(gitRoot) {
2396
2707
  const huskyDir = resolve(gitRoot, ".husky");
@@ -2427,25 +2738,13 @@ async function askConfirmation(question) {
2427
2738
  }
2428
2739
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
2429
2740
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
2430
- if (existsSync7(hookPath)) {
2431
- const existing = readFileSync6(hookPath, "utf-8");
2432
- if (containsPrimHook(existing, spec.binName)) {
2433
- console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
2434
- return;
2435
- }
2436
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
2437
- writeFileSync4(hookPath, `${existing}${separator}${huskyBlock(spec)}
2438
- `, {
2439
- mode: 493
2440
- });
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) {
2441
2746
  console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
2442
2747
  } else {
2443
- writeFileSync4(hookPath, `#!/bin/sh
2444
-
2445
- ${huskyBlock(spec)}
2446
- `, {
2447
- mode: 493
2448
- });
2449
2748
  console.log(`Created .husky/${spec.hookName} with prim hook block.`);
2450
2749
  }
2451
2750
  }
@@ -2468,6 +2767,161 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
2468
2767
  writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
2469
2768
  console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
2470
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
+ }
2471
2925
  function installHooks(gitRoot, target) {
2472
2926
  for (const spec of HOOKS) {
2473
2927
  if (target === "husky") {
@@ -2476,6 +2930,7 @@ function installHooks(gitRoot, target) {
2476
2930
  installToDotGit(gitRoot, spec);
2477
2931
  }
2478
2932
  }
2933
+ activateRepoBestEffort(gitRoot);
2479
2934
  }
2480
2935
  function registerHooksCommands(program2) {
2481
2936
  const hooks = program2.command("hooks").description("Manage git hooks");
@@ -2486,36 +2941,58 @@ function registerHooksCommands(program2) {
2486
2941
  "husky",
2487
2942
  "git-hooks"
2488
2943
  ])
2489
- ).action(async (opts, command) => {
2490
- const globals = command.optsWithGlobals();
2491
- const nonInteractive = Boolean(
2492
- globals.nonInteractive || process.env.CI || process.env.PRIM_NON_INTERACTIVE
2493
- );
2494
- const gitRoot = getGitRoot();
2495
- if (opts.target === "husky") return installHooks(gitRoot, "husky");
2496
- if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
2497
- if (detectHusky(gitRoot)) {
2498
- if (globals.yes) return installHooks(gitRoot, "husky");
2499
- if (nonInteractive) {
2500
- throw new Error(
2501
- "--non-interactive set, refusing to prompt for Husky-hook installation. Pass --yes to confirm or --target=git-hooks to choose."
2502
- );
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;
2503
2954
  }
2504
- if (!process.stdin.isTTY) {
2505
- console.error(
2506
- "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."
2507
- );
2508
- } else if (await askConfirmation(
2509
- "Husky detected. Install prim hooks into .husky/ instead of .git/hooks/?"
2510
- )) {
2511
- return installHooks(gitRoot, "husky");
2512
- } else {
2513
- 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
+ }
2514
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;
2515
2995
  }
2516
- installHooks(gitRoot, "git-hooks");
2517
- });
2518
- hooks.command("uninstall").description("Remove the prim git hooks (.git/hooks)").action(() => {
2519
2996
  const gitRoot = getGitRoot();
2520
2997
  for (const spec of HOOKS) {
2521
2998
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
@@ -2535,7 +3012,7 @@ function registerHooksCommands(program2) {
2535
3012
 
2536
3013
  // src/commands/moves.ts
2537
3014
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
2538
- import { join as join6 } from "path";
3015
+ import { join as join7 } from "path";
2539
3016
 
2540
3017
  // src/flusher.ts
2541
3018
  import { renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
@@ -2694,18 +3171,18 @@ function registerMovesCommands(program2) {
2694
3171
  }
2695
3172
  });
2696
3173
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
2697
- const dir = join6(process.cwd(), ".prim");
3174
+ const dir = join7(process.cwd(), ".prim");
2698
3175
  if (!existsSync8(dir)) {
2699
3176
  mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
2700
3177
  }
2701
- const file = join6(process.cwd(), WORKSPACE_FILE);
3178
+ const file = join7(process.cwd(), WORKSPACE_FILE);
2702
3179
  writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
2703
3180
  mode: FILE_MODE2
2704
3181
  });
2705
3182
  console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
2706
3183
  });
2707
3184
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
2708
- const file = join6(process.cwd(), WORKSPACE_FILE);
3185
+ const file = join7(process.cwd(), WORKSPACE_FILE);
2709
3186
  if (!existsSync8(file)) {
2710
3187
  console.log("[prim] no workspace binding in cwd");
2711
3188
  return;
@@ -2716,7 +3193,7 @@ function registerMovesCommands(program2) {
2716
3193
  }
2717
3194
 
2718
3195
  // src/commands/reconcile.ts
2719
- var EXIT_OK2 = 0;
3196
+ var EXIT_OK3 = 0;
2720
3197
  var EXIT_USAGE2 = 2;
2721
3198
  var EXIT_SERVER = 3;
2722
3199
  var HTTP_CLIENT_ERROR_MIN = 400;
@@ -2779,7 +3256,7 @@ async function performReconcile(idOrShortId, opts = {}) {
2779
3256
  `
2780
3257
  );
2781
3258
  console.log(JSON.stringify(response, null, 2));
2782
- process.exitCode = EXIT_OK2;
3259
+ process.exitCode = EXIT_OK3;
2783
3260
  return;
2784
3261
  }
2785
3262
  process.stderr.write("[prim] reconcile: malformed server response\n");
@@ -2806,7 +3283,7 @@ import {
2806
3283
  unlinkSync as unlinkSync5,
2807
3284
  writeFileSync as writeFileSync6
2808
3285
  } from "fs";
2809
- import { join as join7 } from "path";
3286
+ import { join as join8 } from "path";
2810
3287
  var DIR_MODE2 = 448;
2811
3288
  var FILE_MODE3 = 384;
2812
3289
  function ensureDir() {
@@ -2815,7 +3292,7 @@ function ensureDir() {
2815
3292
  }
2816
3293
  }
2817
3294
  function markerPath(sessionId) {
2818
- return join7(SESSIONS_DIR, `${sessionId}.json`);
3295
+ return join8(SESSIONS_DIR, `${sessionId}.json`);
2819
3296
  }
2820
3297
  function registerSessionCommands(program2) {
2821
3298
  const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
@@ -2843,7 +3320,7 @@ function registerSessionCommands(program2) {
2843
3320
  for (const f of files) {
2844
3321
  const sessionId = f.replace(/\.json$/, "");
2845
3322
  try {
2846
- const m = JSON.parse(readFileSync7(join7(SESSIONS_DIR, f), "utf-8"));
3323
+ const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
2847
3324
  console.log(`${sessionId} org=${m.orgId}`);
2848
3325
  } catch {
2849
3326
  }
@@ -2862,6 +3339,8 @@ function registerSessionCommands(program2) {
2862
3339
 
2863
3340
  // src/commands/setup.ts
2864
3341
  import { spawnSync } from "child_process";
3342
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
3343
+ import { join as join9 } from "path";
2865
3344
  var EXIT_INCOMPLETE = 1;
2866
3345
  var EXIT_USAGE3 = 2;
2867
3346
  var SESSION_LABELS = {
@@ -2888,11 +3367,57 @@ function planSetupSteps(opts) {
2888
3367
  required: false
2889
3368
  });
2890
3369
  }
2891
- steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
2892
- 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];
2893
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]);
2894
3393
  return steps;
2895
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
+ }
2896
3421
  function detectAgent(env) {
2897
3422
  if (env.HERMES_INTERACTIVE) {
2898
3423
  return "hermes";
@@ -2908,7 +3433,14 @@ function resolveAgent(agentFlag, env) {
2908
3433
  function registerSetupCommand(program2) {
2909
3434
  program2.command("setup").description(
2910
3435
  "Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
2911
- ).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) => {
2912
3444
  const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
2913
3445
  if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
2914
3446
  process.stderr.write(
@@ -2927,7 +3459,7 @@ function registerSetupCommand(program2) {
2927
3459
  const self = process.argv[1];
2928
3460
  const run = (args, capture = false) => {
2929
3461
  const r = spawnSync(process.execPath, [self, ...args], {
2930
- stdio: capture ? ["inherit", "pipe", "inherit"] : "inherit",
3462
+ stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
2931
3463
  encoding: "utf-8"
2932
3464
  });
2933
3465
  return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
@@ -2964,6 +3496,18 @@ function registerSetupCommand(program2) {
2964
3496
  const { code } = run(step.args);
2965
3497
  results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
2966
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
+ }
2967
3511
  note("welcome");
2968
3512
  results.welcome = run(["welcome"]).code === 0 ? "ok" : "failed";
2969
3513
  const failed = Object.entries(results).filter(([, v]) => v === "failed").map(([k]) => k);
@@ -2978,17 +3522,116 @@ function registerSetupCommand(program2) {
2978
3522
  // src/commands/skill.ts
2979
3523
  import {
2980
3524
  closeSync as closeSync4,
2981
- existsSync as existsSync10,
3525
+ existsSync as existsSync12,
2982
3526
  fsyncSync as fsyncSync3,
2983
3527
  openSync as openSync4,
2984
- readFileSync as readFileSync8,
3528
+ readFileSync as readFileSync10,
2985
3529
  renameSync as renameSync4,
2986
3530
  writeFileSync as writeFileSync7
2987
3531
  } from "fs";
2988
- import { dirname as dirname5, resolve as resolve2 } from "path";
2989
- 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";
2990
3535
  import { createPatch } from "diff";
2991
- 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));
2992
3635
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
2993
3636
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
2994
3637
  var TARGET_CANDIDATES = [
@@ -3005,17 +3648,25 @@ var AGENT_TARGET = {
3005
3648
  codex: "AGENTS.md",
3006
3649
  hermes: ".hermes.md"
3007
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
+ }
3008
3659
  function loadSkill() {
3009
- let dir = __dirname;
3010
- while (dir !== dirname5(dir)) {
3011
- const p = resolve2(dir, "SKILL.md");
3012
- if (existsSync10(p)) return readFileSync8(p, "utf-8");
3013
- 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);
3014
3665
  }
3015
3666
  throw new Error("SKILL.md not found in package");
3016
3667
  }
3017
3668
  function detectTargets(cwd) {
3018
- return TARGET_CANDIDATES.filter((p) => existsSync10(resolve2(cwd, p)));
3669
+ return TARGET_CANDIDATES.filter((p) => existsSync12(resolve3(cwd, p)));
3019
3670
  }
3020
3671
  function detectNewline(content) {
3021
3672
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -3053,24 +3704,39 @@ function atomicWrite2(target, content) {
3053
3704
  renameSync4(tmp, target);
3054
3705
  }
3055
3706
  function resolveTarget(cwd, opts) {
3056
- 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
+ }
3057
3722
  if (opts.agent) {
3058
3723
  const mapped = AGENT_TARGET[opts.agent];
3059
- if (typeof mapped === "string") return resolve2(cwd, mapped);
3724
+ if (typeof mapped === "string") return resolve3(cwd, mapped);
3060
3725
  console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
3061
3726
  return null;
3062
3727
  }
3063
3728
  const matches = detectTargets(cwd);
3064
- if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
3065
- 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]);
3066
3731
  console.error("Multiple rules files detected. Use --target to disambiguate:");
3067
3732
  for (const m of matches) console.error(` ${m}`);
3068
3733
  return null;
3069
3734
  }
3070
3735
  function runInstall(cwd, opts) {
3736
+ if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
3071
3737
  const target = resolveTarget(cwd, opts);
3072
3738
  if (target === null) return 1;
3073
- const existing = existsSync10(target) ? readFileSync8(target, "utf-8") : "";
3739
+ const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
3074
3740
  const eol = existing ? detectNewline(existing) : "\n";
3075
3741
  const block = composeBlock(loadSkill(), eol);
3076
3742
  const next = applyBlock(existing, block, eol);
@@ -3087,13 +3753,14 @@ function runInstall(cwd, opts) {
3087
3753
  return 0;
3088
3754
  }
3089
3755
  function runUninstall(cwd, opts) {
3756
+ if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
3090
3757
  const target = resolveTarget(cwd, opts);
3091
3758
  if (target === null) return 1;
3092
- if (!existsSync10(target)) {
3759
+ if (!existsSync12(target)) {
3093
3760
  console.log(`Skill block not present at ${target}`);
3094
3761
  return 0;
3095
3762
  }
3096
- const existing = readFileSync8(target, "utf-8");
3763
+ const existing = readFileSync10(target, "utf-8");
3097
3764
  const next = removeBlock(existing);
3098
3765
  if (next === null) {
3099
3766
  console.log(`Skill block not present at ${target}`);
@@ -3104,12 +3771,13 @@ function runUninstall(cwd, opts) {
3104
3771
  return 0;
3105
3772
  }
3106
3773
  function runStatus(cwd, opts) {
3774
+ if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
3107
3775
  const target = resolveTarget(cwd, opts);
3108
3776
  if (target === null) return 1;
3109
- const fileExists = existsSync10(target);
3777
+ const fileExists = existsSync12(target);
3110
3778
  let installed = false;
3111
3779
  if (fileExists) {
3112
- const content = readFileSync8(target, "utf-8");
3780
+ const content = readFileSync10(target, "utf-8");
3113
3781
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
3114
3782
  }
3115
3783
  if (opts.json) {
@@ -3129,7 +3797,7 @@ function runStatus(cwd, opts) {
3129
3797
  }
3130
3798
  function registerSkillCommands(program2) {
3131
3799
  const skill = program2.command("skill").description("Manage the prim skill in your project rules file");
3132
- 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) => {
3133
3801
  try {
3134
3802
  process.exit(runInstall(process.cwd(), opts));
3135
3803
  } catch (err) {
@@ -3137,27 +3805,27 @@ function registerSkillCommands(program2) {
3137
3805
  process.exit(2);
3138
3806
  }
3139
3807
  });
3140
- 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) => {
3141
3809
  process.exit(runUninstall(process.cwd(), opts));
3142
3810
  });
3143
- 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) => {
3144
3812
  process.exit(runStatus(process.cwd(), opts));
3145
3813
  });
3146
3814
  }
3147
3815
 
3148
3816
  // src/commands/statusline.ts
3149
- import { readFileSync as readFileSync9 } from "fs";
3150
- import { dirname as dirname6, resolve as resolve3 } from "path";
3151
- 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";
3152
3820
  var STATUSLINE_TIMEOUT_MS = 200;
3153
3821
  var STATUSLINE_NAME_CAP = 3;
3154
3822
  function readPackageVersion() {
3155
3823
  try {
3156
- const here = dirname6(fileURLToPath3(import.meta.url));
3157
- 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")];
3158
3826
  for (const path of candidates) {
3159
3827
  try {
3160
- const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
3828
+ const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
3161
3829
  if (pkg2.version) {
3162
3830
  return pkg2.version;
3163
3831
  }
@@ -3222,6 +3890,7 @@ var REVERSE_PROMPT_LINES = [
3222
3890
  "to focus on those goals?"
3223
3891
  ];
3224
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?";
3225
3894
  var CALLOUT_TITLE = "Your turn";
3226
3895
  var CALLOUT_INDENT = " ";
3227
3896
  function ruledQuestion(lines) {
@@ -3308,6 +3977,7 @@ function welcomeJson(state) {
3308
3977
  welcomed: true,
3309
3978
  org: "seed",
3310
3979
  reversePrompt: REVERSE_PROMPT,
3980
+ reversePromptTemplate: REVERSE_PROMPT_TEMPLATE,
3311
3981
  recent: state.recent
3312
3982
  };
3313
3983
  }
@@ -3324,8 +3994,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
3324
3994
  }
3325
3995
 
3326
3996
  // src/index.ts
3327
- var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
3328
- 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"));
3329
3999
  updateNotifier({ pkg }).notify();
3330
4000
  var program = new Command();
3331
4001
  program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -3334,6 +4004,7 @@ program.name("prim").description("CLI for Primitive's decision graph").version(p
3334
4004
  );
3335
4005
  registerAuthCommands(program);
3336
4006
  registerHooksCommands(program);
4007
+ registerActivationCommands(program);
3337
4008
  registerSkillCommands(program);
3338
4009
  registerMovesCommands(program);
3339
4010
  registerSessionCommands(program);