repowisestage 0.0.65 → 0.0.66

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.
Files changed (2) hide show
  1. package/dist/bin/repowise.js +1488 -1388
  2. package/package.json +1 -1
@@ -22,1391 +22,1398 @@ var init_config_dir = __esm({
22
22
  }
23
23
  });
24
24
 
25
- // ../listener/dist/process-manager.js
26
- import { spawn } from "child_process";
27
- import { openSync, closeSync } from "fs";
28
- import { readFile as readFile6, writeFile as writeFile7, mkdir as mkdir7, unlink as unlink5 } from "fs/promises";
29
- import { homedir as homedir2 } from "os";
30
- import { join as join10 } from "path";
31
- import { createRequire } from "module";
32
- import { fileURLToPath } from "url";
33
- function repowiseDir() {
34
- return getConfigDir();
35
- }
36
- function pidPath() {
37
- return join10(repowiseDir(), "listener.pid");
38
- }
39
- function logDirPath() {
40
- return join10(repowiseDir(), "logs");
41
- }
42
- function resolveListenerCommand() {
43
- try {
44
- const require2 = createRequire(import.meta.url);
45
- const mainPath = require2.resolve("@repowise/listener/main");
46
- return { script: mainPath, args: [] };
47
- } catch {
48
- const bundlePath = fileURLToPath(import.meta.url);
49
- return { script: bundlePath, args: ["__listener"] };
50
- }
51
- }
52
- async function readPid() {
53
- try {
54
- const content = await readFile6(pidPath(), "utf-8");
55
- const pid = parseInt(content.trim(), 10);
56
- return Number.isNaN(pid) ? null : pid;
57
- } catch (err) {
58
- if (err.code === "ENOENT")
59
- return null;
60
- throw err;
61
- }
62
- }
63
- function isAlive(pid) {
64
- try {
65
- process.kill(pid, 0);
66
- return true;
67
- } catch {
68
- return false;
69
- }
70
- }
71
- async function startBackground() {
72
- if (await isRunning()) {
73
- const pid2 = await readPid();
74
- if (pid2)
75
- return pid2;
76
- }
77
- const logDir2 = logDirPath();
78
- await mkdir7(logDir2, { recursive: true });
79
- const cmd = resolveListenerCommand();
80
- const stdoutFd = openSync(join10(logDir2, "listener-stdout.log"), "a");
81
- const stderrFd = openSync(join10(logDir2, "listener-stderr.log"), "a");
82
- const child = spawn(process.execPath, [cmd.script, ...cmd.args], {
83
- detached: true,
84
- stdio: ["ignore", stdoutFd, stderrFd],
85
- cwd: homedir2(),
86
- env: { ...process.env }
87
- });
88
- child.unref();
89
- closeSync(stdoutFd);
90
- closeSync(stderrFd);
91
- const pid = child.pid;
92
- if (!pid)
93
- throw new Error("Failed to spawn listener process");
94
- await writeFile7(pidPath(), String(pid));
95
- return pid;
25
+ // ../../packages/shared/src/types/typed-resolution.ts
26
+ function typedResolutionKey(filePath, propertyName, line, column) {
27
+ return `${filePath}\0${propertyName}\0${line.toString()}\0${column.toString()}`;
96
28
  }
97
- async function stopProcess() {
98
- const pid = await readPid();
99
- if (pid === null)
100
- return;
101
- if (!isAlive(pid)) {
102
- await removePidFile();
103
- return;
104
- }
105
- try {
106
- process.kill(pid, "SIGTERM");
107
- } catch {
29
+ var TYPED_RESOLUTION_SCHEMA_VERSION, ACCEPTED_TYPED_RESOLUTION_SCHEMA_VERSIONS;
30
+ var init_typed_resolution = __esm({
31
+ "../../packages/shared/src/types/typed-resolution.ts"() {
32
+ "use strict";
33
+ TYPED_RESOLUTION_SCHEMA_VERSION = 2;
34
+ ACCEPTED_TYPED_RESOLUTION_SCHEMA_VERSIONS = [1, 2];
108
35
  }
109
- const deadline = Date.now() + 5e3;
110
- while (Date.now() < deadline && isAlive(pid)) {
111
- await new Promise((r) => setTimeout(r, 200));
36
+ });
37
+
38
+ // ../../packages/shared/src/types/billing.ts
39
+ var init_billing = __esm({
40
+ "../../packages/shared/src/types/billing.ts"() {
41
+ "use strict";
112
42
  }
113
- if (isAlive(pid)) {
114
- try {
115
- process.kill(pid, "SIGKILL");
116
- } catch {
117
- }
43
+ });
44
+
45
+ // ../../packages/shared/src/constants/platforms.ts
46
+ var init_platforms = __esm({
47
+ "../../packages/shared/src/constants/platforms.ts"() {
48
+ "use strict";
118
49
  }
119
- await removePidFile();
120
- }
121
- async function isRunning() {
122
- const pid = await readPid();
123
- if (pid === null)
124
- return false;
125
- return isAlive(pid);
50
+ });
51
+
52
+ // ../../packages/shared/src/constants/tiers.ts
53
+ function hasFeature(plan, feature) {
54
+ const features = TIER_FEATURES[plan] ?? TIER_FEATURES[SUBSCRIPTION_TIERS.FREE];
55
+ return features[feature] ?? false;
126
56
  }
127
- async function removePidFile() {
128
- try {
129
- await unlink5(pidPath());
130
- } catch {
131
- }
57
+ function agentInstructionsVariantForPlan(plan) {
58
+ return hasFeature(plan, "context") ? "full" : "graph";
132
59
  }
133
- var init_process_manager = __esm({
134
- "../listener/dist/process-manager.js"() {
60
+ var SUBSCRIPTION_TIERS, TIER_LIMITS, TIER_FEATURES;
61
+ var init_tiers = __esm({
62
+ "../../packages/shared/src/constants/tiers.ts"() {
135
63
  "use strict";
136
- init_config_dir();
64
+ SUBSCRIPTION_TIERS = {
65
+ FREE: "free",
66
+ // `starter` is the legacy $15 tier — kept as a migration alias (with its OLD limits)
67
+ // until existing Starter subs are price-swapped to Free (pricing v3). Removed after M7.
68
+ STARTER: "starter",
69
+ PRO: "pro",
70
+ TEAM: "team"
71
+ };
72
+ TIER_LIMITS = {
73
+ [SUBSCRIPTION_TIERS.FREE]: {
74
+ maxRepos: 1,
75
+ maxSeats: 1,
76
+ minSeats: 1,
77
+ maxSyncsPerMonth: 4,
78
+ // manual-only, hard cap (no overage)
79
+ maxConcurrentSyncs: 1,
80
+ freeFullScans: 0,
81
+ // graph-only: no LLM full scan
82
+ bedrockEndpoint: "public"
83
+ },
84
+ // Legacy alias — retains the OLD Starter limits until existing Starter subs are
85
+ // migrated to Free, then removed (M7). Do NOT reference for new logic.
86
+ [SUBSCRIPTION_TIERS.STARTER]: {
87
+ maxRepos: 1,
88
+ maxSeats: 1,
89
+ minSeats: 1,
90
+ maxSyncsPerMonth: 10,
91
+ maxConcurrentSyncs: 1,
92
+ freeFullScans: 1,
93
+ bedrockEndpoint: "public"
94
+ },
95
+ [SUBSCRIPTION_TIERS.PRO]: {
96
+ maxRepos: 1,
97
+ maxSeats: 1,
98
+ minSeats: 1,
99
+ maxSyncsPerMonth: 20,
100
+ maxConcurrentSyncs: 2,
101
+ freeFullScans: 1,
102
+ bedrockEndpoint: "public"
103
+ },
104
+ [SUBSCRIPTION_TIERS.TEAM]: {
105
+ maxRepos: 1,
106
+ maxSeats: 999999,
107
+ minSeats: 3,
108
+ maxSyncsPerMonth: 30,
109
+ maxConcurrentSyncs: 10,
110
+ freeFullScans: 1,
111
+ bedrockEndpoint: "public"
112
+ }
113
+ };
114
+ TIER_FEATURES = {
115
+ [SUBSCRIPTION_TIERS.FREE]: {
116
+ knowledgeGraph: true,
117
+ mcp: true,
118
+ context: false,
119
+ // LLM-generated narrative context docs
120
+ docs: false,
121
+ // RepoWise Docs
122
+ autoSync: false,
123
+ // auto-sync on merge (Free is manual-only)
124
+ overage: false
125
+ // pay-as-you-go syncs (Free is hard-capped)
126
+ },
127
+ // Legacy alias — full features until existing Starter subs migrate to Free.
128
+ [SUBSCRIPTION_TIERS.STARTER]: {
129
+ knowledgeGraph: true,
130
+ mcp: true,
131
+ context: true,
132
+ docs: true,
133
+ autoSync: true,
134
+ overage: true
135
+ },
136
+ [SUBSCRIPTION_TIERS.PRO]: {
137
+ knowledgeGraph: true,
138
+ mcp: true,
139
+ context: true,
140
+ docs: true,
141
+ autoSync: true,
142
+ overage: true
143
+ },
144
+ [SUBSCRIPTION_TIERS.TEAM]: {
145
+ knowledgeGraph: true,
146
+ mcp: true,
147
+ context: true,
148
+ docs: true,
149
+ autoSync: true,
150
+ overage: true
151
+ }
152
+ };
137
153
  }
138
154
  });
139
155
 
140
- // ../listener/dist/service-installer.js
141
- var service_installer_exports = {};
142
- __export(service_installer_exports, {
143
- install: () => install,
144
- isInstalled: () => isInstalled,
145
- isServiceRunning: () => isServiceRunning,
146
- stopService: () => stopService,
147
- uninstall: () => uninstall
148
- });
149
- import { execFile as execFile4 } from "child_process";
150
- import { writeFile as writeFile8, mkdir as mkdir8, unlink as unlink6 } from "fs/promises";
151
- import { homedir as homedir3 } from "os";
152
- import { join as join13 } from "path";
153
- function exec(cmd, args) {
154
- return new Promise((resolve5, reject) => {
155
- execFile4(cmd, args, (err, stdout) => {
156
- if (err) {
157
- reject(err);
158
- return;
159
- }
160
- resolve5(String(stdout ?? ""));
161
- });
162
- });
156
+ // ../../packages/shared/src/constants/roles.ts
157
+ function hasPermission(role, permission) {
158
+ return PERMISSIONS[role][permission];
163
159
  }
164
- function plistPath() {
165
- return join13(homedir3(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
160
+ function resolveRole(groups) {
161
+ for (const role of ROLE_PRIORITY) {
162
+ if (groups.includes(role)) return role;
163
+ }
164
+ return ROLES.MEMBER;
166
165
  }
167
- function logDir() {
168
- return join13(getConfigDir(), "logs");
169
- }
170
- function buildPlist() {
171
- const cmd = resolveListenerCommand();
172
- const logs = logDir();
173
- const programArgs = [process.execPath, cmd.script, ...cmd.args].map((a) => ` <string>${a}</string>`).join("\n");
174
- return `<?xml version="1.0" encoding="UTF-8"?>
175
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
176
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
177
- <plist version="1.0">
178
- <dict>
179
- <key>Label</key>
180
- <string>${PLIST_LABEL}</string>
181
- <key>ProgramArguments</key>
182
- <array>
183
- ${programArgs}
184
- </array>
185
- <key>RunAtLoad</key>
186
- <true/>
187
- <key>KeepAlive</key>
188
- <true/>
189
- <key>StandardOutPath</key>
190
- <string>${join13(logs, "listener-stdout.log")}</string>
191
- <key>StandardErrorPath</key>
192
- <string>${join13(logs, "listener-stderr.log")}</string>
193
- <key>ProcessType</key>
194
- <string>Background</string>
195
- <!--
196
- SoftResourceLimits: raise the open-file descriptor limit from the macOS
197
- default of 256 to 4096. When multiple LSP child processes (rust-analyzer,
198
- jdtls, etc.) share the listener's fd budget they can exhaust the default
199
- very quickly, causing EMFILE errors. 4096 matches the system-wide default
200
- set by /etc/launchd.conf on modern macOS and gives enough headroom for
201
- ~10 concurrent LSP servers each opening ~300 fds.
202
-
203
- MemoryLimit: intentionally NOT set here. The launchd MemoryLimit key
204
- (available since macOS 12.3) terminates the entire LaunchAgent when RSS
205
- exceeds the threshold \u2014 it cannot be scoped to individual child processes.
206
- Killing the listener to evict a memory-hungry rust-analyzer instance is
207
- undesirable. Per-child memory pressure is better handled by the LSP
208
- process manager (process-manager.ts) or by OS-level memory pressure
209
- signals (SIGTERM on macOS, OOM killer on Linux).
210
- -->
211
- <key>SoftResourceLimits</key>
212
- <dict>
213
- <key>NumberOfFiles</key>
214
- <integer>4096</integer>
215
- </dict>
216
- </dict>
217
- </plist>`;
218
- }
219
- async function darwinInstall() {
220
- await mkdir8(logDir(), { recursive: true });
221
- await mkdir8(join13(homedir3(), "Library", "LaunchAgents"), { recursive: true });
222
- try {
223
- await exec("launchctl", ["unload", plistPath()]);
224
- } catch {
225
- }
226
- await writeFile8(plistPath(), buildPlist());
227
- await exec("launchctl", ["load", plistPath()]);
228
- }
229
- async function darwinUninstall() {
230
- try {
231
- await exec("launchctl", ["unload", plistPath()]);
232
- } catch {
233
- }
234
- try {
235
- await unlink6(plistPath());
236
- } catch {
237
- }
238
- }
239
- async function darwinIsInstalled() {
240
- try {
241
- const stdout = await exec("launchctl", ["list"]);
242
- return stdout.includes(PLIST_LABEL);
243
- } catch {
244
- return false;
166
+ var ROLES, PERMISSIONS, ROLE_PRIORITY;
167
+ var init_roles = __esm({
168
+ "../../packages/shared/src/constants/roles.ts"() {
169
+ "use strict";
170
+ ROLES = {
171
+ OWNER: "owner",
172
+ ADMIN: "admin",
173
+ REPO_MANAGER: "repo_manager",
174
+ MEMBER: "member"
175
+ };
176
+ PERMISSIONS = {
177
+ [ROLES.OWNER]: {
178
+ connectRepos: true,
179
+ viewSync: true,
180
+ triggerRetry: true,
181
+ configWatcher: true,
182
+ inviteMembers: true,
183
+ manageBilling: true,
184
+ configAiTools: true,
185
+ manageTeam: true
186
+ },
187
+ [ROLES.ADMIN]: {
188
+ connectRepos: true,
189
+ viewSync: true,
190
+ triggerRetry: true,
191
+ configWatcher: true,
192
+ inviteMembers: true,
193
+ manageBilling: false,
194
+ configAiTools: true,
195
+ manageTeam: true
196
+ },
197
+ [ROLES.REPO_MANAGER]: {
198
+ connectRepos: true,
199
+ viewSync: true,
200
+ triggerRetry: true,
201
+ configWatcher: true,
202
+ inviteMembers: false,
203
+ manageBilling: false,
204
+ configAiTools: true,
205
+ manageTeam: false
206
+ },
207
+ [ROLES.MEMBER]: {
208
+ connectRepos: false,
209
+ viewSync: true,
210
+ triggerRetry: true,
211
+ configWatcher: false,
212
+ inviteMembers: false,
213
+ manageBilling: false,
214
+ configAiTools: true,
215
+ manageTeam: false
216
+ }
217
+ };
218
+ ROLE_PRIORITY = [
219
+ ROLES.OWNER,
220
+ ROLES.ADMIN,
221
+ ROLES.REPO_MANAGER,
222
+ ROLES.MEMBER
223
+ ];
245
224
  }
246
- }
247
- function unitPath() {
248
- return join13(homedir3(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
249
- }
250
- function buildUnit() {
251
- const cmd = resolveListenerCommand();
252
- const execStart = [process.execPath, cmd.script, ...cmd.args].join(" ");
253
- const logs = logDir();
254
- return `[Unit]
255
- Description=RepoWise Listener
256
- After=network-online.target
257
- Wants=network-online.target
258
-
259
- [Service]
260
- Type=simple
261
- ExecStart=${execStart}
262
- Restart=always
263
- RestartSec=10
264
- StandardOutput=append:${join13(logs, "listener-stdout.log")}
265
- StandardError=append:${join13(logs, "listener-stderr.log")}
225
+ });
266
226
 
267
- [Install]
268
- WantedBy=default.target`;
269
- }
270
- async function linuxInstall() {
271
- await mkdir8(logDir(), { recursive: true });
272
- await mkdir8(join13(homedir3(), ".config", "systemd", "user"), { recursive: true });
273
- await writeFile8(unitPath(), buildUnit());
274
- await exec("systemctl", ["--user", "daemon-reload"]);
275
- await exec("systemctl", ["--user", "enable", SYSTEMD_SERVICE]);
276
- await exec("systemctl", ["--user", "start", SYSTEMD_SERVICE]);
277
- }
278
- async function linuxUninstall() {
279
- try {
280
- await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
281
- } catch {
282
- }
283
- try {
284
- await exec("systemctl", ["--user", "disable", SYSTEMD_SERVICE]);
285
- } catch {
286
- }
287
- try {
288
- await unlink6(unitPath());
289
- } catch {
290
- }
291
- try {
292
- await exec("systemctl", ["--user", "daemon-reload"]);
293
- } catch {
294
- }
295
- }
296
- async function linuxIsInstalled() {
297
- try {
298
- const stdout = await exec("systemctl", ["--user", "is-enabled", SYSTEMD_SERVICE]);
299
- return stdout.trim() === "enabled";
300
- } catch {
301
- return false;
302
- }
303
- }
304
- async function win32Install() {
305
- await mkdir8(logDir(), { recursive: true });
306
- const cmd = resolveListenerCommand();
307
- const taskCmd = [process.execPath, cmd.script, ...cmd.args].map((a) => `"${a}"`).join(" ");
308
- await exec("schtasks", [
309
- "/create",
310
- "/tn",
311
- TASK_NAME,
312
- "/tr",
313
- taskCmd,
314
- "/sc",
315
- "onlogon",
316
- "/ru",
317
- process.env.USERNAME ?? "",
318
- "/f"
319
- ]);
320
- await exec("schtasks", ["/run", "/tn", TASK_NAME]);
321
- }
322
- async function win32Uninstall() {
323
- try {
324
- await exec("schtasks", ["/end", "/tn", TASK_NAME]);
325
- } catch {
326
- }
327
- try {
328
- await exec("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]);
329
- } catch {
330
- }
331
- }
332
- async function win32IsInstalled() {
333
- try {
334
- await exec("schtasks", ["/query", "/tn", TASK_NAME]);
335
- return true;
336
- } catch {
337
- return false;
338
- }
339
- }
340
- async function darwinStop() {
341
- try {
342
- await exec("launchctl", ["unload", plistPath()]);
343
- } catch {
344
- }
345
- }
346
- async function linuxStop() {
347
- await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
348
- }
349
- async function win32Stop() {
350
- await exec("schtasks", ["/end", "/tn", TASK_NAME]);
227
+ // ../../packages/shared/src/constants/lsp-versions.ts
228
+ function getCurrentPlatformKey(platform = process.platform, arch = process.arch) {
229
+ const platformMap = {
230
+ darwin: "darwin",
231
+ linux: "linux",
232
+ win32: "win32"
233
+ };
234
+ const archMap = {
235
+ x64: "x64",
236
+ arm64: "arm64"
237
+ };
238
+ const p = platformMap[platform];
239
+ const a = archMap[arch];
240
+ if (!p || !a) return null;
241
+ return `${p}-${a}`;
351
242
  }
352
- async function darwinIsServiceRunning() {
353
- try {
354
- const stdout = await exec("launchctl", ["list", PLIST_LABEL]);
355
- const pidMatch = stdout.match(/"PID"\s*=\s*(\d+)/);
356
- return pidMatch !== null && parseInt(pidMatch[1], 10) > 0;
357
- } catch {
358
- return false;
359
- }
360
- }
361
- async function linuxIsServiceRunning() {
362
- try {
363
- const stdout = await exec("systemctl", ["--user", "is-active", SYSTEMD_SERVICE]);
364
- return stdout.trim() === "active";
365
- } catch {
366
- return false;
367
- }
368
- }
369
- async function win32IsServiceRunning() {
370
- try {
371
- const stdout = await exec("schtasks", ["/query", "/tn", TASK_NAME, "/fo", "CSV"]);
372
- return stdout.includes("Running");
373
- } catch {
374
- return false;
375
- }
376
- }
377
- async function stopService() {
378
- switch (process.platform) {
379
- case "darwin":
380
- await darwinStop();
381
- break;
382
- case "linux":
383
- await linuxStop();
384
- break;
385
- case "win32":
386
- await win32Stop();
387
- break;
388
- default:
389
- throw new Error(`Unsupported platform: ${process.platform}`);
390
- }
391
- }
392
- async function install() {
393
- switch (process.platform) {
394
- case "darwin":
395
- await darwinInstall();
396
- break;
397
- case "linux":
398
- await linuxInstall();
399
- break;
400
- case "win32":
401
- await win32Install();
402
- break;
403
- default:
404
- throw new Error(`Unsupported platform: ${process.platform}`);
405
- }
406
- }
407
- async function uninstall() {
408
- switch (process.platform) {
409
- case "darwin":
410
- await darwinUninstall();
411
- break;
412
- case "linux":
413
- await linuxUninstall();
414
- break;
415
- case "win32":
416
- await win32Uninstall();
417
- break;
418
- default:
419
- throw new Error(`Unsupported platform: ${process.platform}`);
420
- }
421
- }
422
- async function isInstalled() {
423
- switch (process.platform) {
424
- case "darwin":
425
- return darwinIsInstalled();
426
- case "linux":
427
- return linuxIsInstalled();
428
- case "win32":
429
- return win32IsInstalled();
430
- default:
431
- return false;
432
- }
433
- }
434
- async function isServiceRunning() {
435
- switch (process.platform) {
436
- case "darwin":
437
- return darwinIsServiceRunning();
438
- case "linux":
439
- return linuxIsServiceRunning();
440
- case "win32":
441
- return win32IsServiceRunning();
442
- default:
443
- return false;
444
- }
445
- }
446
- var IS_STAGING, PLIST_LABEL, SYSTEMD_SERVICE, TASK_NAME;
447
- var init_service_installer = __esm({
448
- "../listener/dist/service-installer.js"() {
449
- "use strict";
450
- init_config_dir();
451
- init_process_manager();
452
- IS_STAGING = true ? true : false;
453
- PLIST_LABEL = IS_STAGING ? "com.repowise-staging.listener" : "com.repowise.listener";
454
- SYSTEMD_SERVICE = IS_STAGING ? "repowise-staging-listener" : "repowise-listener";
455
- TASK_NAME = IS_STAGING ? "RepoWise Staging Listener" : "RepoWise Listener";
456
- }
457
- });
458
-
459
- // ../listener/dist/lsp/registry.js
460
- function resolvePhpLspOverride(env = process.env, configOverrides) {
461
- const envChoice = env["PHP_LSP"];
462
- const configChoice = configOverrides?.["php"];
463
- const choice = envChoice ?? configChoice;
464
- if (choice !== "intelephense")
465
- return null;
466
- return {
467
- id: "intelephense",
468
- displayName: "intelephense (user-provided)",
469
- command: "intelephense",
470
- args: ["--stdio"],
471
- extensions: [".php"],
472
- lspLanguageId: "php",
473
- installHint: "Intelephense override is enabled \u2014 install it yourself: `npm i -g intelephense` (commercial licence required for premium features)."
474
- };
475
- }
476
- function getEffectiveConfig(lang, env = process.env, configOverrides) {
477
- if (lang === "php") {
478
- const override = resolvePhpLspOverride(env, configOverrides);
479
- if (override)
480
- return override;
481
- }
482
- return LSP_REGISTRY[lang]?.[0];
483
- }
484
- function getEffectiveConfigList(lang, env = process.env, configOverrides) {
485
- if (lang === "php") {
486
- const override = resolvePhpLspOverride(env, configOverrides);
487
- if (override)
488
- return [override];
489
- }
490
- return LSP_REGISTRY[lang] ?? [];
491
- }
492
- function detectLanguage(path) {
493
- const dot = path.lastIndexOf(".");
494
- if (dot < 0)
495
- return null;
496
- const ext = path.slice(dot).toLowerCase();
497
- for (const [lang, configs] of Object.entries(LSP_REGISTRY)) {
498
- if (configs.some((c) => c.extensions.includes(ext))) {
499
- return lang;
500
- }
501
- }
502
- return null;
503
- }
504
- async function probeServers(isAvailable, env = process.env, configOverrides) {
505
- const out = [];
506
- for (const lang of Object.keys(LSP_REGISTRY)) {
507
- const configs = getEffectiveConfigList(lang, env, configOverrides);
508
- const checkedCommands = configs.map((c) => c.command);
509
- let found = null;
510
- for (const config2 of configs) {
511
- if (await isAvailable(config2.command)) {
512
- found = config2;
513
- break;
514
- }
515
- }
516
- out.push({ language: lang, config: found, checkedCommands });
517
- }
518
- return out;
519
- }
520
- function getBuildSpec(language) {
521
- return BUILD_SPECS[language] ?? null;
522
- }
523
- var LSP_REGISTRY, BUILD_SPECS;
524
- var init_registry = __esm({
525
- "../listener/dist/lsp/registry.js"() {
243
+ var NATIVE_LSP_VERSIONS, COURSIER_LSPS, TOOLCHAIN_LSPS;
244
+ var init_lsp_versions = __esm({
245
+ "../../packages/shared/src/constants/lsp-versions.ts"() {
526
246
  "use strict";
527
- LSP_REGISTRY = {
528
- typescript: [
529
- {
530
- id: "typescript-language-server",
531
- displayName: "typescript-language-server",
532
- command: "typescript-language-server",
533
- args: ["--stdio"],
534
- extensions: [".ts", ".tsx", ".mts", ".cts"],
535
- lspLanguageId: "typescript",
536
- installHint: "npm i -g typescript typescript-language-server",
537
- npmPackage: "typescript-language-server"
538
- }
539
- ],
540
- javascript: [
541
- {
542
- id: "typescript-language-server-js",
543
- displayName: "typescript-language-server (for JS)",
544
- command: "typescript-language-server",
545
- args: ["--stdio"],
546
- extensions: [".js", ".jsx", ".mjs", ".cjs"],
547
- lspLanguageId: "javascript",
548
- installHint: "npm i -g typescript typescript-language-server",
549
- npmPackage: "typescript-language-server"
247
+ NATIVE_LSP_VERSIONS = {
248
+ "rust-analyzer": {
249
+ source: { kind: "github-release", repo: "rust-lang/rust-analyzer" },
250
+ version: "2026-05-11",
251
+ license: "MIT OR Apache-2.0",
252
+ assets: {
253
+ "darwin-arm64": {
254
+ filename: "rust-analyzer-aarch64-apple-darwin.gz",
255
+ sha256: "eeb6367cd2c6cc9217ae06cea2e0c7e9cf20cdac5e934a1b272b13c76ece5aee",
256
+ binaryPath: "rust-analyzer-aarch64-apple-darwin"
257
+ },
258
+ "darwin-x64": {
259
+ filename: "rust-analyzer-x86_64-apple-darwin.gz",
260
+ sha256: "718398822ec2f282bca36f6f109bbbb33746437e1a63946fe7fd3d84d502a67d",
261
+ binaryPath: "rust-analyzer-x86_64-apple-darwin"
262
+ },
263
+ "linux-x64": {
264
+ filename: "rust-analyzer-x86_64-unknown-linux-gnu.gz",
265
+ sha256: "ede62b8536a37c90d804850d0bed3623e3d6af90a1195e4930a1627e5d8ae33f",
266
+ binaryPath: "rust-analyzer-x86_64-unknown-linux-gnu"
267
+ },
268
+ "linux-arm64": {
269
+ filename: "rust-analyzer-aarch64-unknown-linux-gnu.gz",
270
+ sha256: "dbe8c4c0bed79f36133c9d0a0eb629709d91343e065a179cd4a5f8137817de1c",
271
+ binaryPath: "rust-analyzer-aarch64-unknown-linux-gnu"
272
+ },
273
+ "win32-x64": {
274
+ filename: "rust-analyzer-x86_64-pc-windows-msvc.zip",
275
+ sha256: "1600365f2d8b0e42a811eafb5e129b75b991ddf9d9955d19d8fc2f4f2b9c19ef",
276
+ binaryPath: "rust-analyzer.exe"
277
+ },
278
+ "win32-arm64": {
279
+ filename: "rust-analyzer-aarch64-pc-windows-msvc.zip",
280
+ sha256: "3cd63d31e9abfea89488eebfd2c29d41550af6cb6f6b32f52e455fb783215fd0",
281
+ binaryPath: "rust-analyzer.exe"
282
+ }
550
283
  }
551
- ],
552
- python: [
553
- {
554
- id: "pyright",
555
- displayName: "pyright",
556
- command: "pyright-langserver",
557
- args: ["--stdio"],
558
- extensions: [".py", ".pyi"],
559
- lspLanguageId: "python",
560
- installHint: "npm i -g pyright",
561
- npmPackage: "pyright",
562
- // Phase 7L: strict-mode Pyright. Default `basic` mode suppresses
563
- // definition/hover for un-annotated locals which kneecaps the
564
- // upgrade rate on real codebases. `useLibraryCodeForTypes`
565
- // unlocks stdlib member lookups, `autoSearchPaths` lets Pyright
566
- // find venv site-packages without per-project pyrightconfig.json,
567
- // and `diagnosticMode: 'workspace'` forces full-workspace symbol
568
- // materialization so the first `workspace/symbol` query doesn't
569
- // race the indexer.
570
- //
571
- // Disable `publishDiagnostics` — we don't render lint output
572
- // and the noise slows Pyright on large projects (plan §Python
573
- // risks).
574
- initializationOptions: {
575
- python: {
576
- analysis: {
577
- typeCheckingMode: "strict",
578
- useLibraryCodeForTypes: true,
579
- autoSearchPaths: true,
580
- diagnosticMode: "workspace"
581
- // Pyright honours either `disableLanguageServices` or
582
- // simply not subscribing to diagnostics; the conservative
583
- // path is to send the request handshake without
584
- // publishDiagnostics in our capabilities (already the
585
- // case in workspace-session.ts) and avoid extra knobs
586
- // here that would diverge from Pyright defaults.
587
- }
588
- }
284
+ },
285
+ clangd: {
286
+ source: { kind: "github-release", repo: "clangd/clangd" },
287
+ version: "18.1.3",
288
+ license: "Apache-2.0 WITH LLVM-exception",
289
+ assets: {
290
+ // clangd ships a single universal macOS binary covering both
291
+ // architectures — same filename for darwin-arm64 and darwin-x64.
292
+ "darwin-arm64": {
293
+ filename: "clangd-mac-18.1.3.zip",
294
+ sha256: "442c18d98671d8b5d811b25002ad1f2fd0b9efb07e7905a41502f1f1311dc566",
295
+ binaryPath: "clangd_18.1.3/bin/clangd"
296
+ },
297
+ "darwin-x64": {
298
+ filename: "clangd-mac-18.1.3.zip",
299
+ sha256: "442c18d98671d8b5d811b25002ad1f2fd0b9efb07e7905a41502f1f1311dc566",
300
+ binaryPath: "clangd_18.1.3/bin/clangd"
301
+ },
302
+ "linux-x64": {
303
+ filename: "clangd-linux-18.1.3.zip",
304
+ sha256: "775f6ac798d9c599258e3f4fa14d0af908b1801d38250e72a18fb907b28a1be6",
305
+ binaryPath: "clangd_18.1.3/bin/clangd"
306
+ },
307
+ "win32-x64": {
308
+ filename: "clangd-windows-18.1.3.zip",
309
+ sha256: "3d287d39bab29406f002ff80b939668e59e25ef72346d8f38826305016e45896",
310
+ binaryPath: "clangd_18.1.3/bin/clangd.exe"
589
311
  }
590
- },
591
- {
592
- id: "pylsp",
593
- displayName: "python-lsp-server",
594
- command: "pylsp",
595
- args: [],
596
- extensions: [".py", ".pyi"],
597
- lspLanguageId: "python",
598
- installHint: 'pipx install "python-lsp-server[all]"'
599
- }
600
- ],
601
- go: [
602
- {
603
- id: "gopls",
604
- displayName: "gopls",
605
- command: "gopls",
606
- // Modern gopls uses stdio by default; `serve` is the legacy
607
- // subcommand and no longer needed (review finding).
608
- args: [],
609
- extensions: [".go"],
610
- lspLanguageId: "go",
611
- installHint: "go install golang.org/x/tools/gopls@latest",
612
- toolchainInstallKey: "gopls"
613
312
  }
614
- ],
615
- rust: [
616
- {
617
- id: "rust-analyzer",
618
- displayName: "rust-analyzer",
619
- command: "rust-analyzer",
620
- args: [],
621
- extensions: [".rs"],
622
- lspLanguageId: "rust",
623
- nativeInstallKey: "rust-analyzer",
624
- // `rustup component add rust-analyzer` installs a rustup proxy
625
- // — the binary may not be on PATH directly. `brew install
626
- // rust-analyzer` or a release tarball is more reliable.
627
- installHint: "brew install rust-analyzer (or 'rustup component add rust-analyzer' and ensure it is on PATH)",
628
- // Phase 7L: rust-analyzer init options. `cargo.buildScripts.enable`
629
- // and `procMacro.enable` are critical — without them, half of
630
- // Serde's call graph is invisible (proc-macro-expanded code is
631
- // skipped and `derive(Serialize)` impl trait methods disappear).
632
- // `checkOnSave.enable: false` keeps the server quiet on workspace
633
- // edits since we don't render diagnostics.
634
- initializationOptions: {
635
- cargo: {
636
- buildScripts: { enable: true }
637
- },
638
- procMacro: { enable: true },
639
- checkOnSave: { enable: false }
313
+ },
314
+ // Coursier — single-file JVM launcher that we install once and then
315
+ // use to install any JVM-based LSP (metals, future kotlin LSPs, etc.).
316
+ // Tier 2 of the install plan: the binary itself is just another
317
+ // GitHub-release Tier 1 entry; the magic is what we DO with it after
318
+ // install (see coursier-installer.ts).
319
+ coursier: {
320
+ source: { kind: "github-release", repo: "coursier/coursier" },
321
+ version: "v2.1.24",
322
+ license: "Apache-2.0",
323
+ assets: {
324
+ "darwin-arm64": {
325
+ filename: "cs-aarch64-apple-darwin.gz",
326
+ sha256: "53a5728c2016118c8296fa7d5678ddfe122e22dc6c36deb554d771b3b9295b4f",
327
+ binaryPath: "cs"
328
+ },
329
+ "darwin-x64": {
330
+ filename: "cs-x86_64-apple-darwin.gz",
331
+ sha256: "33913cd6b61658035d9e6fe971e919cb0ef1f659aa7bff7deeded963a2d36385",
332
+ binaryPath: "cs"
333
+ },
334
+ "linux-x64": {
335
+ filename: "cs-x86_64-pc-linux.gz",
336
+ sha256: "d2c0572a17fb6146ea65349b59dd216b38beff60ae22bce6e549867c6ed2eda6",
337
+ binaryPath: "cs"
338
+ },
339
+ // coursier v2.1.24 ships no aarch64-linux build — Scala dev on
340
+ // ARM Linux falls back to the toolchain-installed Metals path.
341
+ "win32-x64": {
342
+ filename: "cs-x86_64-pc-win32.zip",
343
+ sha256: "c16b4f95b59fbe035cdda4353cfca78befcbbef5a20a3b151c679717c1504c9f",
344
+ binaryPath: "cs.exe"
640
345
  }
641
346
  }
642
- ],
643
- java: [
644
- // The `jdtls` binary exists when installed via Homebrew (which
645
- // ships a wrapper over the Eclipse JDT launcher) or the nvim
646
- // jdtls scripts. Raw Eclipse downloads require invoking
647
- // `java -jar org.eclipse.equinox.launcher_*.jar`; doctor needs
648
- // to surface that when the bare `jdtls` binary is absent.
649
- //
650
- // Phase 7L init options:
651
- // - autobuild disabled — per-edit work the listener doesn't
652
- // need; saves CPU + memory on the cold start path.
653
- // - completion disabled — same rationale.
654
- // - progressReportProvider true — needed so waitForIndex can
655
- // subscribe to "Importing projects" / "Building workspace"
656
- // progress and block receiver queries until end-of-progress.
657
- //
658
- // JVM args:
659
- // - `-Xmx2G` — JDTLS OOMs on real-world repos with the default
660
- // `-Xmx256m`. Without this the OOM kills the server inside
661
- // workspace import, which opens the Java per-repo circuit
662
- // breaker (plan §Java failure modes).
663
- // - `-data <repoSha-scoped workspace>` — added at spawn time by
664
- // workspace-session.ts so JDTLS skips the Maven/Gradle import
665
- // on subsequent runs. Watch pom.xml/build.gradle mtime to
666
- // invalidate stale incremental state.
667
- {
668
- id: "jdtls",
669
- displayName: "Eclipse JDT Language Server",
670
- command: "jdtls",
671
- args: ["--jvm-arg=-Xmx2G"],
672
- extensions: [".java"],
673
- lspLanguageId: "java",
674
- installHint: "brew install jdtls (see https://github.com/eclipse-jdtls/eclipse.jdt.ls)",
675
- nativeInstallKey: "jdtls",
676
- initializationOptions: {
677
- bundles: [],
678
- extendedClientCapabilities: {
679
- progressReportProvider: true,
680
- classFileContentsSupport: true
681
- },
682
- settings: {
683
- java: {
684
- autobuild: { enabled: false },
685
- completion: { enabled: false },
686
- // Don't render diagnostics — listener doesn't surface
687
- // Java lint output and the firehose slows JDTLS.
688
- errors: { incompleteClasspath: { severity: "ignore" } }
689
- }
690
- }
347
+ },
348
+ // Eclipse JDT Language Server — single cross-platform tarball
349
+ // hosted at download.eclipse.org. Ships as an Equinox launcher jar
350
+ // that must be invoked via `java -jar`, so the installer generates
351
+ // a `bin/jdtls` shim that selects the right config_{platform} dir.
352
+ // Requires JDK 17+ (jdtls 1.51 minimum).
353
+ jdtls: {
354
+ source: { kind: "direct-url" },
355
+ version: "1.51.0",
356
+ license: "EPL-2.0",
357
+ requiresJdk: 17,
358
+ assets: {
359
+ "darwin-arm64": {
360
+ url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
361
+ filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
362
+ sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
363
+ binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
364
+ launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
365
+ wrapperBinaryName: "jdtls"
366
+ },
367
+ "darwin-x64": {
368
+ url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
369
+ filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
370
+ sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
371
+ binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
372
+ launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
373
+ wrapperBinaryName: "jdtls"
374
+ },
375
+ "linux-x64": {
376
+ url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
377
+ filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
378
+ sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
379
+ binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
380
+ launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
381
+ wrapperBinaryName: "jdtls"
382
+ },
383
+ "linux-arm64": {
384
+ url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
385
+ filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
386
+ sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
387
+ binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
388
+ launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
389
+ wrapperBinaryName: "jdtls"
390
+ },
391
+ "win32-x64": {
392
+ url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
393
+ filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
394
+ sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
395
+ binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
396
+ launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
397
+ wrapperBinaryName: "jdtls"
691
398
  }
692
399
  }
693
- ],
694
- csharp: [
695
- // Issue #352 item #5 — csharp-ls listed FIRST. Both are MIT-OSS
696
- // and Roslyn-backed, but csharp-ls is significantly lighter (~10 MB
697
- // dotnet-tool install vs. OmniSharp's ~200 MB native binary). On
698
- // user machines with `dotnet` on PATH, csharp-ls is installable in
699
- // one command with no native-binary fetch. OmniSharp stays as the
700
- // installed-elsewhere fallback (Roslyn-build farms, JetBrains
701
- // Rider tool dirs, etc.).
702
- {
703
- id: "csharp-ls",
704
- displayName: "csharp-ls",
705
- command: "csharp-ls",
706
- args: [],
707
- extensions: [".cs"],
708
- lspLanguageId: "csharp",
709
- installHint: "dotnet tool install -g csharp-ls",
710
- toolchainInstallKey: "csharp-ls"
711
- },
712
- {
713
- id: "omnisharp",
714
- displayName: "OmniSharp",
715
- command: "omnisharp",
716
- args: ["-lsp"],
717
- extensions: [".cs"],
718
- lspLanguageId: "csharp",
719
- installHint: "https://github.com/OmniSharp/omnisharp-roslyn/releases"
720
- }
721
- ],
722
- php: [
723
- // Phase 7L++: Phpactor only. Phpactor is MIT-licensed, free, and
724
- // ships as a single phar via Composer. We DO NOT bundle
725
- // Intelephense — its premium tier is commercial and its community
726
- // licence is not redistributable. Customers who already own an
727
- // Intelephense licence can opt-in via the `PHP_LSP=intelephense`
728
- // environment variable, which the CLI swaps into the registry at
729
- // runtime (see apps/cli/src/commands/lsp.ts step 7). The escape
730
- // hatch requires the user to provision an `intelephense` binary
731
- // on PATH themselves.
732
- //
733
- // Confidence floor on PHP is 0.7 (see lsp-upgrade-strategy.ts).
734
- // Phpactor's static analysis is solid for PSR-4 resolvable calls
735
- // but stumbles on Laravel-style facades / magic accessors — those
736
- // surface as null definitions, recorded as `kind: 'definition'`
737
- // with `targets.length === 0` (no upgrade, telemetry preserved).
738
- {
739
- id: "phpactor",
740
- displayName: "phpactor",
741
- command: "phpactor",
742
- args: ["language-server"],
743
- extensions: [".php"],
744
- lspLanguageId: "php",
745
- installHint: "composer global require phpactor/phpactor (see https://phpactor.readthedocs.io/en/master/usage/standalone.html)",
746
- toolchainInstallKey: "phpactor"
747
- }
748
- ],
749
- ruby: [
750
- {
751
- id: "ruby-lsp",
752
- displayName: "ruby-lsp",
753
- command: "ruby-lsp",
754
- args: [],
755
- extensions: [".rb"],
756
- lspLanguageId: "ruby",
757
- installHint: "gem install ruby-lsp",
758
- toolchainInstallKey: "ruby-lsp"
759
- },
760
- {
761
- id: "solargraph",
762
- displayName: "solargraph",
763
- command: "solargraph",
764
- args: ["stdio"],
765
- extensions: [".rb"],
766
- lspLanguageId: "ruby",
767
- installHint: "gem install solargraph"
768
- }
769
- ],
770
- kotlin: [
771
- // Phase 7L++: fwcd Kotlin LSP. JVM cold-start is ~30s on the first
772
- // Gradle/Maven import — the language server materialises the
773
- // build's classpath before symbol lookups return. On mixed
774
- // Kotlin/Java repos the WorkspaceManager spawns BOTH `jdtls` and
775
- // `kotlin-language-server` — each owns its own file-type filter
776
- // and they coexist without contention. Per-language LRU eviction
777
- // (5-min idle, RSS soft-cap) prevents the two JVMs from pinning
778
- // memory on long-lived listeners.
779
- //
780
- // Future: when JetBrains' Kotlin LSP exits preview, drop a
781
- // higher-priority entry above this one with the JB binary; we
782
- // keep fwcd as the fallback because JB's LSP requires a JetBrains
783
- // account.
784
- {
785
- id: "kotlin-language-server",
786
- displayName: "kotlin-language-server (fwcd)",
787
- command: "kotlin-language-server",
788
- args: [],
789
- extensions: [".kt", ".kts"],
790
- lspLanguageId: "kotlin",
791
- installHint: "brew install kotlin-language-server (or see https://github.com/fwcd/kotlin-language-server)",
792
- nativeInstallKey: "kotlin-language-server"
793
- }
794
- ],
795
- swift: [
796
- {
797
- id: "sourcekit-lsp",
798
- displayName: "sourcekit-lsp",
799
- command: "sourcekit-lsp",
800
- args: [],
801
- extensions: [".swift"],
802
- lspLanguageId: "swift",
803
- installHint: "Bundled with Xcode Command Line Tools on macOS",
804
- toolchainInstallKey: "sourcekit-lsp",
805
- requiresBuild: true
806
- }
807
- ],
808
- dart: [
809
- // Phase 7L++: the official Dart Analysis Server ships inside the
810
- // Dart SDK as `dart language-server --protocol=lsp`. There's no
811
- // separate binary to install — installing Dart (or Flutter, which
812
- // bundles Dart) is sufficient. Confidence floor is 0.9: the Dart
813
- // analyzer is authoritative, derives types from sound null safety,
814
- // and is the same server that powers the official IDE plugins.
815
- // No `initializationOptions` — Dart's analyzer derives everything
816
- // from `pubspec.yaml` / `analysis_options.yaml` at the repo root.
817
- {
818
- id: "dart-language-server",
819
- displayName: "dart language-server",
820
- command: "dart",
821
- args: ["language-server", "--protocol=lsp"],
822
- extensions: [".dart"],
823
- lspLanguageId: "dart",
824
- installHint: "Install Dart SDK from https://dart.dev/get-dart (or Flutter from https://flutter.dev \u2014 bundles Dart).",
825
- toolchainInstallKey: "dart-language-server"
826
- }
827
- ],
828
- // Scala via Metals (Phase 7L+).
829
- //
830
- // Metals is the de-facto Scala language server. It coordinates with
831
- // a build server (Bloop by default; SBT/Mill/Gradle alternatives are
832
- // discovered from the project's build files) to obtain the project's
833
- // classpath, semantic database, and compiler diagnostics. The Bloop
834
- // daemon is a separate JVM process that Metals spawns on its first
835
- // `initialize`, then keeps warm; the listener's WorkspaceManager
836
- // pairs Bloop lifecycle to Metals' (see bloop-lifecycle.ts) so they
837
- // are spawned together on lazy `getOrOpen` and torn down together
838
- // on idle eviction.
839
- //
840
- // Install hint: `coursier install metals` — Coursier (`cs`) is the
841
- // canonical Scala launcher and is typically already on a Scala
842
- // developer's machine. The Coursier-managed `metals` shim resolves
843
- // Bloop on first use, so no separate Bloop install is required.
844
- // Macro-expanded code can mis-locate definitions (Scala 3 inline /
845
- // quote macros, scala-compat shims) — the per-language strategy
846
- // pins confidence at 0.85 to reflect that limitation.
847
- scala: [
848
- {
849
- id: "metals",
850
- displayName: "Metals (Scala)",
851
- command: "metals",
852
- args: [],
853
- extensions: [".scala", ".sc", ".sbt"],
854
- lspLanguageId: "scala",
855
- installHint: "coursier install metals (see https://scalameta.org/metals \u2014 Coursier-managed binary coordinates with Bloop automatically)",
856
- coursierInstallKey: "metals"
857
- }
858
- ],
859
- c: [
860
- {
861
- id: "clangd-c",
862
- displayName: "clangd",
863
- command: "clangd",
864
- // `--compile-commands-dir=.repowise` lets clangd pick up the
865
- // stub compile_commands.json the listener writes for repos
866
- // that don't ship one (header-only libs, single-file repos).
867
- // clangd still finds repo-root or build/ compile_commands.json
868
- // first when present — this is a fallback location, not an
869
- // override (task #1).
870
- args: ["--background-index", "--compile-commands-dir=.repowise"],
871
- extensions: [".c", ".h"],
872
- lspLanguageId: "c",
873
- installHint: "brew install llvm (includes clangd)",
874
- nativeInstallKey: "clangd"
875
- }
876
- ],
877
- cpp: [
878
- {
879
- id: "clangd-cpp",
880
- displayName: "clangd",
881
- command: "clangd",
882
- // `--compile-commands-dir=.repowise` lets clangd pick up the
883
- // stub compile_commands.json the listener writes for repos
884
- // that don't ship one (header-only libs, single-file repos).
885
- // clangd still finds repo-root or build/ compile_commands.json
886
- // first when present — this is a fallback location, not an
887
- // override (task #1).
888
- args: ["--background-index", "--compile-commands-dir=.repowise"],
889
- extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
890
- lspLanguageId: "cpp",
891
- installHint: "brew install llvm (includes clangd)",
892
- nativeInstallKey: "clangd"
893
- }
894
- ],
895
- vue: [
896
- {
897
- id: "vue-language-server",
898
- displayName: "Vue Language Server (volar)",
899
- command: "vue-language-server",
900
- args: ["--stdio"],
901
- extensions: [".vue"],
902
- lspLanguageId: "vue",
903
- installHint: "npm i -g @vue/language-server",
904
- npmPackage: "@vue/language-server"
905
- }
906
- ],
907
- svelte: [
908
- {
909
- id: "svelte-language-server",
910
- displayName: "svelte-language-server",
911
- command: "svelteserver",
912
- args: ["--stdio"],
913
- extensions: [".svelte"],
914
- lspLanguageId: "svelte",
915
- installHint: "npm i -g svelte-language-server",
916
- npmPackage: "svelte-language-server"
400
+ },
401
+ // fwcd/kotlin-language-server — single cross-platform zip on GitHub
402
+ // releases. Ships a `bin/kotlin-language-server` shell script + a
403
+ // matching `.bat` for Windows both invoke `java` internally, so
404
+ // we just chmod+x and symlink (no shim needed). Requires JDK 11+.
405
+ "kotlin-language-server": {
406
+ source: { kind: "github-release", repo: "fwcd/kotlin-language-server" },
407
+ version: "1.3.13",
408
+ license: "MIT",
409
+ requiresJdk: 11,
410
+ assets: {
411
+ "darwin-arm64": {
412
+ filename: "server.zip",
413
+ sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
414
+ binaryPath: "server/bin/kotlin-language-server"
415
+ },
416
+ "darwin-x64": {
417
+ filename: "server.zip",
418
+ sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
419
+ binaryPath: "server/bin/kotlin-language-server"
420
+ },
421
+ "linux-x64": {
422
+ filename: "server.zip",
423
+ sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
424
+ binaryPath: "server/bin/kotlin-language-server"
425
+ },
426
+ "linux-arm64": {
427
+ filename: "server.zip",
428
+ sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
429
+ binaryPath: "server/bin/kotlin-language-server"
430
+ },
431
+ "win32-x64": {
432
+ filename: "server.zip",
433
+ sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
434
+ binaryPath: "server/bin/kotlin-language-server.bat"
435
+ }
917
436
  }
918
- ]
437
+ }
919
438
  };
920
- BUILD_SPECS = {
921
- swift: {
922
- gateCommands: ["swift", "xcrun"],
923
- gateExecs: [{ command: "xcode-select", args: ["-p"] }],
924
- manifests: ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
925
- platform: "darwin",
926
- command: "swift",
927
- args: ["build"],
928
- indexStoreProbe: ".build/index/store"
439
+ COURSIER_LSPS = {
440
+ metals: {
441
+ appName: "metals",
442
+ displayName: "Metals (Scala)",
443
+ license: "Apache-2.0"
929
444
  }
930
445
  };
446
+ TOOLCHAIN_LSPS = {
447
+ gopls: {
448
+ toolchain: "go",
449
+ installArgs: ["install", "golang.org/x/tools/gopls@latest"],
450
+ binaryName: "gopls",
451
+ toolchainBinDir: "go/bin",
452
+ displayName: "gopls",
453
+ license: "BSD-3-Clause"
454
+ },
455
+ "ruby-lsp": {
456
+ toolchain: "gem",
457
+ installArgs: ["install", "--user-install", "ruby-lsp"],
458
+ binaryName: "ruby-lsp",
459
+ // gem userdir is version-dependent — resolved at install time.
460
+ displayName: "ruby-lsp",
461
+ license: "MIT"
462
+ },
463
+ "csharp-ls": {
464
+ toolchain: "dotnet",
465
+ installArgs: ["tool", "install", "-g", "csharp-ls"],
466
+ binaryName: "csharp-ls",
467
+ toolchainBinDir: ".dotnet/tools",
468
+ displayName: "csharp-ls",
469
+ license: "MIT"
470
+ },
471
+ phpactor: {
472
+ toolchain: "composer",
473
+ installArgs: ["global", "require", "phpactor/phpactor"],
474
+ binaryName: "phpactor",
475
+ toolchainBinDir: ".composer/vendor/bin",
476
+ displayName: "phpactor",
477
+ license: "MIT"
478
+ },
479
+ "dart-language-server": {
480
+ bundled: true,
481
+ binaryName: "dart",
482
+ displayName: "dart language-server (Dart SDK)",
483
+ license: "BSD-3-Clause"
484
+ },
485
+ "sourcekit-lsp": {
486
+ bundled: true,
487
+ binaryName: "sourcekit-lsp",
488
+ displayName: "sourcekit-lsp (Xcode CLT)",
489
+ license: "Apache-2.0"
490
+ }
491
+ };
492
+ }
493
+ });
494
+
495
+ // ../../packages/shared/src/index.ts
496
+ var init_src = __esm({
497
+ "../../packages/shared/src/index.ts"() {
498
+ "use strict";
499
+ init_typed_resolution();
500
+ init_billing();
501
+ init_platforms();
502
+ init_tiers();
503
+ init_roles();
504
+ init_lsp_versions();
505
+ }
506
+ });
507
+
508
+ // ../listener/dist/process-manager.js
509
+ import { spawn } from "child_process";
510
+ import { openSync, closeSync } from "fs";
511
+ import { readFile as readFile6, writeFile as writeFile7, mkdir as mkdir7, unlink as unlink5 } from "fs/promises";
512
+ import { homedir as homedir2 } from "os";
513
+ import { join as join10 } from "path";
514
+ import { createRequire } from "module";
515
+ import { fileURLToPath } from "url";
516
+ function repowiseDir() {
517
+ return getConfigDir();
518
+ }
519
+ function pidPath() {
520
+ return join10(repowiseDir(), "listener.pid");
521
+ }
522
+ function logDirPath() {
523
+ return join10(repowiseDir(), "logs");
524
+ }
525
+ function resolveListenerCommand() {
526
+ try {
527
+ const require2 = createRequire(import.meta.url);
528
+ const mainPath = require2.resolve("@repowise/listener/main");
529
+ return { script: mainPath, args: [] };
530
+ } catch {
531
+ const bundlePath = fileURLToPath(import.meta.url);
532
+ return { script: bundlePath, args: ["__listener"] };
533
+ }
534
+ }
535
+ async function readPid() {
536
+ try {
537
+ const content = await readFile6(pidPath(), "utf-8");
538
+ const pid = parseInt(content.trim(), 10);
539
+ return Number.isNaN(pid) ? null : pid;
540
+ } catch (err) {
541
+ if (err.code === "ENOENT")
542
+ return null;
543
+ throw err;
544
+ }
545
+ }
546
+ function isAlive(pid) {
547
+ try {
548
+ process.kill(pid, 0);
549
+ return true;
550
+ } catch {
551
+ return false;
552
+ }
553
+ }
554
+ async function startBackground() {
555
+ if (await isRunning()) {
556
+ const pid2 = await readPid();
557
+ if (pid2)
558
+ return pid2;
559
+ }
560
+ const logDir2 = logDirPath();
561
+ await mkdir7(logDir2, { recursive: true });
562
+ const cmd = resolveListenerCommand();
563
+ const stdoutFd = openSync(join10(logDir2, "listener-stdout.log"), "a");
564
+ const stderrFd = openSync(join10(logDir2, "listener-stderr.log"), "a");
565
+ const child = spawn(process.execPath, [cmd.script, ...cmd.args], {
566
+ detached: true,
567
+ stdio: ["ignore", stdoutFd, stderrFd],
568
+ cwd: homedir2(),
569
+ env: { ...process.env }
570
+ });
571
+ child.unref();
572
+ closeSync(stdoutFd);
573
+ closeSync(stderrFd);
574
+ const pid = child.pid;
575
+ if (!pid)
576
+ throw new Error("Failed to spawn listener process");
577
+ await writeFile7(pidPath(), String(pid));
578
+ return pid;
579
+ }
580
+ async function stopProcess() {
581
+ const pid = await readPid();
582
+ if (pid === null)
583
+ return;
584
+ if (!isAlive(pid)) {
585
+ await removePidFile();
586
+ return;
587
+ }
588
+ try {
589
+ process.kill(pid, "SIGTERM");
590
+ } catch {
591
+ }
592
+ const deadline = Date.now() + 5e3;
593
+ while (Date.now() < deadline && isAlive(pid)) {
594
+ await new Promise((r) => setTimeout(r, 200));
595
+ }
596
+ if (isAlive(pid)) {
597
+ try {
598
+ process.kill(pid, "SIGKILL");
599
+ } catch {
600
+ }
601
+ }
602
+ await removePidFile();
603
+ }
604
+ async function isRunning() {
605
+ const pid = await readPid();
606
+ if (pid === null)
607
+ return false;
608
+ return isAlive(pid);
609
+ }
610
+ async function removePidFile() {
611
+ try {
612
+ await unlink5(pidPath());
613
+ } catch {
614
+ }
615
+ }
616
+ var init_process_manager = __esm({
617
+ "../listener/dist/process-manager.js"() {
618
+ "use strict";
619
+ init_config_dir();
620
+ }
621
+ });
622
+
623
+ // ../listener/dist/service-installer.js
624
+ var service_installer_exports = {};
625
+ __export(service_installer_exports, {
626
+ install: () => install,
627
+ isInstalled: () => isInstalled,
628
+ isServiceRunning: () => isServiceRunning,
629
+ stopService: () => stopService,
630
+ uninstall: () => uninstall
631
+ });
632
+ import { execFile as execFile4 } from "child_process";
633
+ import { writeFile as writeFile8, mkdir as mkdir8, unlink as unlink6 } from "fs/promises";
634
+ import { homedir as homedir3 } from "os";
635
+ import { join as join13 } from "path";
636
+ function exec(cmd, args) {
637
+ return new Promise((resolve5, reject) => {
638
+ execFile4(cmd, args, (err, stdout) => {
639
+ if (err) {
640
+ reject(err);
641
+ return;
642
+ }
643
+ resolve5(String(stdout ?? ""));
644
+ });
645
+ });
646
+ }
647
+ function plistPath() {
648
+ return join13(homedir3(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
649
+ }
650
+ function logDir() {
651
+ return join13(getConfigDir(), "logs");
652
+ }
653
+ function buildPlist() {
654
+ const cmd = resolveListenerCommand();
655
+ const logs = logDir();
656
+ const programArgs = [process.execPath, cmd.script, ...cmd.args].map((a) => ` <string>${a}</string>`).join("\n");
657
+ return `<?xml version="1.0" encoding="UTF-8"?>
658
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
659
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
660
+ <plist version="1.0">
661
+ <dict>
662
+ <key>Label</key>
663
+ <string>${PLIST_LABEL}</string>
664
+ <key>ProgramArguments</key>
665
+ <array>
666
+ ${programArgs}
667
+ </array>
668
+ <key>RunAtLoad</key>
669
+ <true/>
670
+ <key>KeepAlive</key>
671
+ <true/>
672
+ <key>StandardOutPath</key>
673
+ <string>${join13(logs, "listener-stdout.log")}</string>
674
+ <key>StandardErrorPath</key>
675
+ <string>${join13(logs, "listener-stderr.log")}</string>
676
+ <key>ProcessType</key>
677
+ <string>Background</string>
678
+ <!--
679
+ SoftResourceLimits: raise the open-file descriptor limit from the macOS
680
+ default of 256 to 4096. When multiple LSP child processes (rust-analyzer,
681
+ jdtls, etc.) share the listener's fd budget they can exhaust the default
682
+ very quickly, causing EMFILE errors. 4096 matches the system-wide default
683
+ set by /etc/launchd.conf on modern macOS and gives enough headroom for
684
+ ~10 concurrent LSP servers each opening ~300 fds.
685
+
686
+ MemoryLimit: intentionally NOT set here. The launchd MemoryLimit key
687
+ (available since macOS 12.3) terminates the entire LaunchAgent when RSS
688
+ exceeds the threshold \u2014 it cannot be scoped to individual child processes.
689
+ Killing the listener to evict a memory-hungry rust-analyzer instance is
690
+ undesirable. Per-child memory pressure is better handled by the LSP
691
+ process manager (process-manager.ts) or by OS-level memory pressure
692
+ signals (SIGTERM on macOS, OOM killer on Linux).
693
+ -->
694
+ <key>SoftResourceLimits</key>
695
+ <dict>
696
+ <key>NumberOfFiles</key>
697
+ <integer>4096</integer>
698
+ </dict>
699
+ </dict>
700
+ </plist>`;
701
+ }
702
+ async function darwinInstall() {
703
+ await mkdir8(logDir(), { recursive: true });
704
+ await mkdir8(join13(homedir3(), "Library", "LaunchAgents"), { recursive: true });
705
+ try {
706
+ await exec("launchctl", ["unload", plistPath()]);
707
+ } catch {
708
+ }
709
+ await writeFile8(plistPath(), buildPlist());
710
+ await exec("launchctl", ["load", plistPath()]);
711
+ }
712
+ async function darwinUninstall() {
713
+ try {
714
+ await exec("launchctl", ["unload", plistPath()]);
715
+ } catch {
716
+ }
717
+ try {
718
+ await unlink6(plistPath());
719
+ } catch {
720
+ }
721
+ }
722
+ async function darwinIsInstalled() {
723
+ try {
724
+ const stdout = await exec("launchctl", ["list"]);
725
+ return stdout.includes(PLIST_LABEL);
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+ function unitPath() {
731
+ return join13(homedir3(), ".config", "systemd", "user", `${SYSTEMD_SERVICE}.service`);
732
+ }
733
+ function buildUnit() {
734
+ const cmd = resolveListenerCommand();
735
+ const execStart = [process.execPath, cmd.script, ...cmd.args].join(" ");
736
+ const logs = logDir();
737
+ return `[Unit]
738
+ Description=RepoWise Listener
739
+ After=network-online.target
740
+ Wants=network-online.target
741
+
742
+ [Service]
743
+ Type=simple
744
+ ExecStart=${execStart}
745
+ Restart=always
746
+ RestartSec=10
747
+ StandardOutput=append:${join13(logs, "listener-stdout.log")}
748
+ StandardError=append:${join13(logs, "listener-stderr.log")}
749
+
750
+ [Install]
751
+ WantedBy=default.target`;
752
+ }
753
+ async function linuxInstall() {
754
+ await mkdir8(logDir(), { recursive: true });
755
+ await mkdir8(join13(homedir3(), ".config", "systemd", "user"), { recursive: true });
756
+ await writeFile8(unitPath(), buildUnit());
757
+ await exec("systemctl", ["--user", "daemon-reload"]);
758
+ await exec("systemctl", ["--user", "enable", SYSTEMD_SERVICE]);
759
+ await exec("systemctl", ["--user", "start", SYSTEMD_SERVICE]);
760
+ }
761
+ async function linuxUninstall() {
762
+ try {
763
+ await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
764
+ } catch {
765
+ }
766
+ try {
767
+ await exec("systemctl", ["--user", "disable", SYSTEMD_SERVICE]);
768
+ } catch {
769
+ }
770
+ try {
771
+ await unlink6(unitPath());
772
+ } catch {
773
+ }
774
+ try {
775
+ await exec("systemctl", ["--user", "daemon-reload"]);
776
+ } catch {
777
+ }
778
+ }
779
+ async function linuxIsInstalled() {
780
+ try {
781
+ const stdout = await exec("systemctl", ["--user", "is-enabled", SYSTEMD_SERVICE]);
782
+ return stdout.trim() === "enabled";
783
+ } catch {
784
+ return false;
785
+ }
786
+ }
787
+ async function win32Install() {
788
+ await mkdir8(logDir(), { recursive: true });
789
+ const cmd = resolveListenerCommand();
790
+ const taskCmd = [process.execPath, cmd.script, ...cmd.args].map((a) => `"${a}"`).join(" ");
791
+ await exec("schtasks", [
792
+ "/create",
793
+ "/tn",
794
+ TASK_NAME,
795
+ "/tr",
796
+ taskCmd,
797
+ "/sc",
798
+ "onlogon",
799
+ "/ru",
800
+ process.env.USERNAME ?? "",
801
+ "/f"
802
+ ]);
803
+ await exec("schtasks", ["/run", "/tn", TASK_NAME]);
804
+ }
805
+ async function win32Uninstall() {
806
+ try {
807
+ await exec("schtasks", ["/end", "/tn", TASK_NAME]);
808
+ } catch {
809
+ }
810
+ try {
811
+ await exec("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]);
812
+ } catch {
813
+ }
814
+ }
815
+ async function win32IsInstalled() {
816
+ try {
817
+ await exec("schtasks", ["/query", "/tn", TASK_NAME]);
818
+ return true;
819
+ } catch {
820
+ return false;
821
+ }
822
+ }
823
+ async function darwinStop() {
824
+ try {
825
+ await exec("launchctl", ["unload", plistPath()]);
826
+ } catch {
827
+ }
828
+ }
829
+ async function linuxStop() {
830
+ await exec("systemctl", ["--user", "stop", SYSTEMD_SERVICE]);
831
+ }
832
+ async function win32Stop() {
833
+ await exec("schtasks", ["/end", "/tn", TASK_NAME]);
834
+ }
835
+ async function darwinIsServiceRunning() {
836
+ try {
837
+ const stdout = await exec("launchctl", ["list", PLIST_LABEL]);
838
+ const pidMatch = stdout.match(/"PID"\s*=\s*(\d+)/);
839
+ return pidMatch !== null && parseInt(pidMatch[1], 10) > 0;
840
+ } catch {
841
+ return false;
842
+ }
843
+ }
844
+ async function linuxIsServiceRunning() {
845
+ try {
846
+ const stdout = await exec("systemctl", ["--user", "is-active", SYSTEMD_SERVICE]);
847
+ return stdout.trim() === "active";
848
+ } catch {
849
+ return false;
931
850
  }
932
- });
933
-
934
- // ../../packages/shared/src/types/typed-resolution.ts
935
- function typedResolutionKey(filePath, propertyName, line, column) {
936
- return `${filePath}\0${propertyName}\0${line.toString()}\0${column.toString()}`;
937
851
  }
938
- var TYPED_RESOLUTION_SCHEMA_VERSION, ACCEPTED_TYPED_RESOLUTION_SCHEMA_VERSIONS;
939
- var init_typed_resolution = __esm({
940
- "../../packages/shared/src/types/typed-resolution.ts"() {
941
- "use strict";
942
- TYPED_RESOLUTION_SCHEMA_VERSION = 2;
943
- ACCEPTED_TYPED_RESOLUTION_SCHEMA_VERSIONS = [1, 2];
852
+ async function win32IsServiceRunning() {
853
+ try {
854
+ const stdout = await exec("schtasks", ["/query", "/tn", TASK_NAME, "/fo", "CSV"]);
855
+ return stdout.includes("Running");
856
+ } catch {
857
+ return false;
944
858
  }
945
- });
946
-
947
- // ../../packages/shared/src/types/billing.ts
948
- var init_billing = __esm({
949
- "../../packages/shared/src/types/billing.ts"() {
950
- "use strict";
859
+ }
860
+ async function stopService() {
861
+ switch (process.platform) {
862
+ case "darwin":
863
+ await darwinStop();
864
+ break;
865
+ case "linux":
866
+ await linuxStop();
867
+ break;
868
+ case "win32":
869
+ await win32Stop();
870
+ break;
871
+ default:
872
+ throw new Error(`Unsupported platform: ${process.platform}`);
951
873
  }
952
- });
953
-
954
- // ../../packages/shared/src/constants/platforms.ts
955
- var init_platforms = __esm({
956
- "../../packages/shared/src/constants/platforms.ts"() {
957
- "use strict";
874
+ }
875
+ async function install() {
876
+ switch (process.platform) {
877
+ case "darwin":
878
+ await darwinInstall();
879
+ break;
880
+ case "linux":
881
+ await linuxInstall();
882
+ break;
883
+ case "win32":
884
+ await win32Install();
885
+ break;
886
+ default:
887
+ throw new Error(`Unsupported platform: ${process.platform}`);
958
888
  }
959
- });
960
-
961
- // ../../packages/shared/src/constants/tiers.ts
962
- var SUBSCRIPTION_TIERS, TIER_LIMITS, TIER_FEATURES;
963
- var init_tiers = __esm({
964
- "../../packages/shared/src/constants/tiers.ts"() {
965
- "use strict";
966
- SUBSCRIPTION_TIERS = {
967
- FREE: "free",
968
- // `starter` is the legacy $15 tier — kept as a migration alias (with its OLD limits)
969
- // until existing Starter subs are price-swapped to Free (pricing v3). Removed after M7.
970
- STARTER: "starter",
971
- PRO: "pro",
972
- TEAM: "team"
973
- };
974
- TIER_LIMITS = {
975
- [SUBSCRIPTION_TIERS.FREE]: {
976
- maxRepos: 1,
977
- maxSeats: 1,
978
- minSeats: 1,
979
- maxSyncsPerMonth: 4,
980
- // manual-only, hard cap (no overage)
981
- maxConcurrentSyncs: 1,
982
- freeFullScans: 0,
983
- // graph-only: no LLM full scan
984
- bedrockEndpoint: "public"
985
- },
986
- // Legacy alias — retains the OLD Starter limits until existing Starter subs are
987
- // migrated to Free, then removed (M7). Do NOT reference for new logic.
988
- [SUBSCRIPTION_TIERS.STARTER]: {
989
- maxRepos: 1,
990
- maxSeats: 1,
991
- minSeats: 1,
992
- maxSyncsPerMonth: 10,
993
- maxConcurrentSyncs: 1,
994
- freeFullScans: 1,
995
- bedrockEndpoint: "public"
996
- },
997
- [SUBSCRIPTION_TIERS.PRO]: {
998
- maxRepos: 1,
999
- maxSeats: 1,
1000
- minSeats: 1,
1001
- maxSyncsPerMonth: 20,
1002
- maxConcurrentSyncs: 2,
1003
- freeFullScans: 1,
1004
- bedrockEndpoint: "public"
1005
- },
1006
- [SUBSCRIPTION_TIERS.TEAM]: {
1007
- maxRepos: 1,
1008
- maxSeats: 999999,
1009
- minSeats: 3,
1010
- maxSyncsPerMonth: 30,
1011
- maxConcurrentSyncs: 10,
1012
- freeFullScans: 1,
1013
- bedrockEndpoint: "public"
1014
- }
1015
- };
1016
- TIER_FEATURES = {
1017
- [SUBSCRIPTION_TIERS.FREE]: {
1018
- knowledgeGraph: true,
1019
- mcp: true,
1020
- context: false,
1021
- // LLM-generated narrative context docs
1022
- docs: false,
1023
- // RepoWise Docs
1024
- autoSync: false,
1025
- // auto-sync on merge (Free is manual-only)
1026
- overage: false
1027
- // pay-as-you-go syncs (Free is hard-capped)
1028
- },
1029
- // Legacy alias — full features until existing Starter subs migrate to Free.
1030
- [SUBSCRIPTION_TIERS.STARTER]: {
1031
- knowledgeGraph: true,
1032
- mcp: true,
1033
- context: true,
1034
- docs: true,
1035
- autoSync: true,
1036
- overage: true
1037
- },
1038
- [SUBSCRIPTION_TIERS.PRO]: {
1039
- knowledgeGraph: true,
1040
- mcp: true,
1041
- context: true,
1042
- docs: true,
1043
- autoSync: true,
1044
- overage: true
1045
- },
1046
- [SUBSCRIPTION_TIERS.TEAM]: {
1047
- knowledgeGraph: true,
1048
- mcp: true,
1049
- context: true,
1050
- docs: true,
1051
- autoSync: true,
1052
- overage: true
1053
- }
1054
- };
889
+ }
890
+ async function uninstall() {
891
+ switch (process.platform) {
892
+ case "darwin":
893
+ await darwinUninstall();
894
+ break;
895
+ case "linux":
896
+ await linuxUninstall();
897
+ break;
898
+ case "win32":
899
+ await win32Uninstall();
900
+ break;
901
+ default:
902
+ throw new Error(`Unsupported platform: ${process.platform}`);
1055
903
  }
1056
- });
1057
-
1058
- // ../../packages/shared/src/constants/roles.ts
1059
- function hasPermission(role, permission) {
1060
- return PERMISSIONS[role][permission];
1061
904
  }
1062
- function resolveRole(groups) {
1063
- for (const role of ROLE_PRIORITY) {
1064
- if (groups.includes(role)) return role;
905
+ async function isInstalled() {
906
+ switch (process.platform) {
907
+ case "darwin":
908
+ return darwinIsInstalled();
909
+ case "linux":
910
+ return linuxIsInstalled();
911
+ case "win32":
912
+ return win32IsInstalled();
913
+ default:
914
+ return false;
1065
915
  }
1066
- return ROLES.MEMBER;
1067
916
  }
1068
- var ROLES, PERMISSIONS, ROLE_PRIORITY;
1069
- var init_roles = __esm({
1070
- "../../packages/shared/src/constants/roles.ts"() {
1071
- "use strict";
1072
- ROLES = {
1073
- OWNER: "owner",
1074
- ADMIN: "admin",
1075
- REPO_MANAGER: "repo_manager",
1076
- MEMBER: "member"
1077
- };
1078
- PERMISSIONS = {
1079
- [ROLES.OWNER]: {
1080
- connectRepos: true,
1081
- viewSync: true,
1082
- triggerRetry: true,
1083
- configWatcher: true,
1084
- inviteMembers: true,
1085
- manageBilling: true,
1086
- configAiTools: true,
1087
- manageTeam: true
1088
- },
1089
- [ROLES.ADMIN]: {
1090
- connectRepos: true,
1091
- viewSync: true,
1092
- triggerRetry: true,
1093
- configWatcher: true,
1094
- inviteMembers: true,
1095
- manageBilling: false,
1096
- configAiTools: true,
1097
- manageTeam: true
1098
- },
1099
- [ROLES.REPO_MANAGER]: {
1100
- connectRepos: true,
1101
- viewSync: true,
1102
- triggerRetry: true,
1103
- configWatcher: true,
1104
- inviteMembers: false,
1105
- manageBilling: false,
1106
- configAiTools: true,
1107
- manageTeam: false
1108
- },
1109
- [ROLES.MEMBER]: {
1110
- connectRepos: false,
1111
- viewSync: true,
1112
- triggerRetry: true,
1113
- configWatcher: false,
1114
- inviteMembers: false,
1115
- manageBilling: false,
1116
- configAiTools: true,
1117
- manageTeam: false
1118
- }
1119
- };
1120
- ROLE_PRIORITY = [
1121
- ROLES.OWNER,
1122
- ROLES.ADMIN,
1123
- ROLES.REPO_MANAGER,
1124
- ROLES.MEMBER
1125
- ];
917
+ async function isServiceRunning() {
918
+ switch (process.platform) {
919
+ case "darwin":
920
+ return darwinIsServiceRunning();
921
+ case "linux":
922
+ return linuxIsServiceRunning();
923
+ case "win32":
924
+ return win32IsServiceRunning();
925
+ default:
926
+ return false;
927
+ }
928
+ }
929
+ var IS_STAGING, PLIST_LABEL, SYSTEMD_SERVICE, TASK_NAME;
930
+ var init_service_installer = __esm({
931
+ "../listener/dist/service-installer.js"() {
932
+ "use strict";
933
+ init_config_dir();
934
+ init_process_manager();
935
+ IS_STAGING = true ? true : false;
936
+ PLIST_LABEL = IS_STAGING ? "com.repowise-staging.listener" : "com.repowise.listener";
937
+ SYSTEMD_SERVICE = IS_STAGING ? "repowise-staging-listener" : "repowise-listener";
938
+ TASK_NAME = IS_STAGING ? "RepoWise Staging Listener" : "RepoWise Listener";
1126
939
  }
1127
940
  });
1128
941
 
1129
- // ../../packages/shared/src/constants/lsp-versions.ts
1130
- function getCurrentPlatformKey(platform = process.platform, arch = process.arch) {
1131
- const platformMap = {
1132
- darwin: "darwin",
1133
- linux: "linux",
1134
- win32: "win32"
1135
- };
1136
- const archMap = {
1137
- x64: "x64",
1138
- arm64: "arm64"
942
+ // ../listener/dist/lsp/registry.js
943
+ function resolvePhpLspOverride(env = process.env, configOverrides) {
944
+ const envChoice = env["PHP_LSP"];
945
+ const configChoice = configOverrides?.["php"];
946
+ const choice = envChoice ?? configChoice;
947
+ if (choice !== "intelephense")
948
+ return null;
949
+ return {
950
+ id: "intelephense",
951
+ displayName: "intelephense (user-provided)",
952
+ command: "intelephense",
953
+ args: ["--stdio"],
954
+ extensions: [".php"],
955
+ lspLanguageId: "php",
956
+ installHint: "Intelephense override is enabled \u2014 install it yourself: `npm i -g intelephense` (commercial licence required for premium features)."
1139
957
  };
1140
- const p = platformMap[platform];
1141
- const a = archMap[arch];
1142
- if (!p || !a) return null;
1143
- return `${p}-${a}`;
1144
958
  }
1145
- var NATIVE_LSP_VERSIONS, COURSIER_LSPS, TOOLCHAIN_LSPS;
1146
- var init_lsp_versions = __esm({
1147
- "../../packages/shared/src/constants/lsp-versions.ts"() {
959
+ function getEffectiveConfig(lang, env = process.env, configOverrides) {
960
+ if (lang === "php") {
961
+ const override = resolvePhpLspOverride(env, configOverrides);
962
+ if (override)
963
+ return override;
964
+ }
965
+ return LSP_REGISTRY[lang]?.[0];
966
+ }
967
+ function getEffectiveConfigList(lang, env = process.env, configOverrides) {
968
+ if (lang === "php") {
969
+ const override = resolvePhpLspOverride(env, configOverrides);
970
+ if (override)
971
+ return [override];
972
+ }
973
+ return LSP_REGISTRY[lang] ?? [];
974
+ }
975
+ function detectLanguage(path) {
976
+ const dot = path.lastIndexOf(".");
977
+ if (dot < 0)
978
+ return null;
979
+ const ext = path.slice(dot).toLowerCase();
980
+ for (const [lang, configs] of Object.entries(LSP_REGISTRY)) {
981
+ if (configs.some((c) => c.extensions.includes(ext))) {
982
+ return lang;
983
+ }
984
+ }
985
+ return null;
986
+ }
987
+ async function probeServers(isAvailable, env = process.env, configOverrides) {
988
+ const out = [];
989
+ for (const lang of Object.keys(LSP_REGISTRY)) {
990
+ const configs = getEffectiveConfigList(lang, env, configOverrides);
991
+ const checkedCommands = configs.map((c) => c.command);
992
+ let found = null;
993
+ for (const config2 of configs) {
994
+ if (await isAvailable(config2.command)) {
995
+ found = config2;
996
+ break;
997
+ }
998
+ }
999
+ out.push({ language: lang, config: found, checkedCommands });
1000
+ }
1001
+ return out;
1002
+ }
1003
+ function getBuildSpec(language) {
1004
+ return BUILD_SPECS[language] ?? null;
1005
+ }
1006
+ var LSP_REGISTRY, BUILD_SPECS;
1007
+ var init_registry = __esm({
1008
+ "../listener/dist/lsp/registry.js"() {
1148
1009
  "use strict";
1149
- NATIVE_LSP_VERSIONS = {
1150
- "rust-analyzer": {
1151
- source: { kind: "github-release", repo: "rust-lang/rust-analyzer" },
1152
- version: "2026-05-11",
1153
- license: "MIT OR Apache-2.0",
1154
- assets: {
1155
- "darwin-arm64": {
1156
- filename: "rust-analyzer-aarch64-apple-darwin.gz",
1157
- sha256: "eeb6367cd2c6cc9217ae06cea2e0c7e9cf20cdac5e934a1b272b13c76ece5aee",
1158
- binaryPath: "rust-analyzer-aarch64-apple-darwin"
1159
- },
1160
- "darwin-x64": {
1161
- filename: "rust-analyzer-x86_64-apple-darwin.gz",
1162
- sha256: "718398822ec2f282bca36f6f109bbbb33746437e1a63946fe7fd3d84d502a67d",
1163
- binaryPath: "rust-analyzer-x86_64-apple-darwin"
1164
- },
1165
- "linux-x64": {
1166
- filename: "rust-analyzer-x86_64-unknown-linux-gnu.gz",
1167
- sha256: "ede62b8536a37c90d804850d0bed3623e3d6af90a1195e4930a1627e5d8ae33f",
1168
- binaryPath: "rust-analyzer-x86_64-unknown-linux-gnu"
1169
- },
1170
- "linux-arm64": {
1171
- filename: "rust-analyzer-aarch64-unknown-linux-gnu.gz",
1172
- sha256: "dbe8c4c0bed79f36133c9d0a0eb629709d91343e065a179cd4a5f8137817de1c",
1173
- binaryPath: "rust-analyzer-aarch64-unknown-linux-gnu"
1174
- },
1175
- "win32-x64": {
1176
- filename: "rust-analyzer-x86_64-pc-windows-msvc.zip",
1177
- sha256: "1600365f2d8b0e42a811eafb5e129b75b991ddf9d9955d19d8fc2f4f2b9c19ef",
1178
- binaryPath: "rust-analyzer.exe"
1179
- },
1180
- "win32-arm64": {
1181
- filename: "rust-analyzer-aarch64-pc-windows-msvc.zip",
1182
- sha256: "3cd63d31e9abfea89488eebfd2c29d41550af6cb6f6b32f52e455fb783215fd0",
1183
- binaryPath: "rust-analyzer.exe"
1010
+ LSP_REGISTRY = {
1011
+ typescript: [
1012
+ {
1013
+ id: "typescript-language-server",
1014
+ displayName: "typescript-language-server",
1015
+ command: "typescript-language-server",
1016
+ args: ["--stdio"],
1017
+ extensions: [".ts", ".tsx", ".mts", ".cts"],
1018
+ lspLanguageId: "typescript",
1019
+ installHint: "npm i -g typescript typescript-language-server",
1020
+ npmPackage: "typescript-language-server"
1021
+ }
1022
+ ],
1023
+ javascript: [
1024
+ {
1025
+ id: "typescript-language-server-js",
1026
+ displayName: "typescript-language-server (for JS)",
1027
+ command: "typescript-language-server",
1028
+ args: ["--stdio"],
1029
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
1030
+ lspLanguageId: "javascript",
1031
+ installHint: "npm i -g typescript typescript-language-server",
1032
+ npmPackage: "typescript-language-server"
1033
+ }
1034
+ ],
1035
+ python: [
1036
+ {
1037
+ id: "pyright",
1038
+ displayName: "pyright",
1039
+ command: "pyright-langserver",
1040
+ args: ["--stdio"],
1041
+ extensions: [".py", ".pyi"],
1042
+ lspLanguageId: "python",
1043
+ installHint: "npm i -g pyright",
1044
+ npmPackage: "pyright",
1045
+ // Phase 7L: strict-mode Pyright. Default `basic` mode suppresses
1046
+ // definition/hover for un-annotated locals which kneecaps the
1047
+ // upgrade rate on real codebases. `useLibraryCodeForTypes`
1048
+ // unlocks stdlib member lookups, `autoSearchPaths` lets Pyright
1049
+ // find venv site-packages without per-project pyrightconfig.json,
1050
+ // and `diagnosticMode: 'workspace'` forces full-workspace symbol
1051
+ // materialization so the first `workspace/symbol` query doesn't
1052
+ // race the indexer.
1053
+ //
1054
+ // Disable `publishDiagnostics` — we don't render lint output
1055
+ // and the noise slows Pyright on large projects (plan §Python
1056
+ // risks).
1057
+ initializationOptions: {
1058
+ python: {
1059
+ analysis: {
1060
+ typeCheckingMode: "strict",
1061
+ useLibraryCodeForTypes: true,
1062
+ autoSearchPaths: true,
1063
+ diagnosticMode: "workspace"
1064
+ // Pyright honours either `disableLanguageServices` or
1065
+ // simply not subscribing to diagnostics; the conservative
1066
+ // path is to send the request handshake without
1067
+ // publishDiagnostics in our capabilities (already the
1068
+ // case in workspace-session.ts) and avoid extra knobs
1069
+ // here that would diverge from Pyright defaults.
1070
+ }
1071
+ }
1184
1072
  }
1073
+ },
1074
+ {
1075
+ id: "pylsp",
1076
+ displayName: "python-lsp-server",
1077
+ command: "pylsp",
1078
+ args: [],
1079
+ extensions: [".py", ".pyi"],
1080
+ lspLanguageId: "python",
1081
+ installHint: 'pipx install "python-lsp-server[all]"'
1185
1082
  }
1186
- },
1187
- clangd: {
1188
- source: { kind: "github-release", repo: "clangd/clangd" },
1189
- version: "18.1.3",
1190
- license: "Apache-2.0 WITH LLVM-exception",
1191
- assets: {
1192
- // clangd ships a single universal macOS binary covering both
1193
- // architectures same filename for darwin-arm64 and darwin-x64.
1194
- "darwin-arm64": {
1195
- filename: "clangd-mac-18.1.3.zip",
1196
- sha256: "442c18d98671d8b5d811b25002ad1f2fd0b9efb07e7905a41502f1f1311dc566",
1197
- binaryPath: "clangd_18.1.3/bin/clangd"
1198
- },
1199
- "darwin-x64": {
1200
- filename: "clangd-mac-18.1.3.zip",
1201
- sha256: "442c18d98671d8b5d811b25002ad1f2fd0b9efb07e7905a41502f1f1311dc566",
1202
- binaryPath: "clangd_18.1.3/bin/clangd"
1203
- },
1204
- "linux-x64": {
1205
- filename: "clangd-linux-18.1.3.zip",
1206
- sha256: "775f6ac798d9c599258e3f4fa14d0af908b1801d38250e72a18fb907b28a1be6",
1207
- binaryPath: "clangd_18.1.3/bin/clangd"
1208
- },
1209
- "win32-x64": {
1210
- filename: "clangd-windows-18.1.3.zip",
1211
- sha256: "3d287d39bab29406f002ff80b939668e59e25ef72346d8f38826305016e45896",
1212
- binaryPath: "clangd_18.1.3/bin/clangd.exe"
1083
+ ],
1084
+ go: [
1085
+ {
1086
+ id: "gopls",
1087
+ displayName: "gopls",
1088
+ command: "gopls",
1089
+ // Modern gopls uses stdio by default; `serve` is the legacy
1090
+ // subcommand and no longer needed (review finding).
1091
+ args: [],
1092
+ extensions: [".go"],
1093
+ lspLanguageId: "go",
1094
+ installHint: "go install golang.org/x/tools/gopls@latest",
1095
+ toolchainInstallKey: "gopls"
1096
+ }
1097
+ ],
1098
+ rust: [
1099
+ {
1100
+ id: "rust-analyzer",
1101
+ displayName: "rust-analyzer",
1102
+ command: "rust-analyzer",
1103
+ args: [],
1104
+ extensions: [".rs"],
1105
+ lspLanguageId: "rust",
1106
+ nativeInstallKey: "rust-analyzer",
1107
+ // `rustup component add rust-analyzer` installs a rustup proxy
1108
+ // — the binary may not be on PATH directly. `brew install
1109
+ // rust-analyzer` or a release tarball is more reliable.
1110
+ installHint: "brew install rust-analyzer (or 'rustup component add rust-analyzer' and ensure it is on PATH)",
1111
+ // Phase 7L: rust-analyzer init options. `cargo.buildScripts.enable`
1112
+ // and `procMacro.enable` are critical — without them, half of
1113
+ // Serde's call graph is invisible (proc-macro-expanded code is
1114
+ // skipped and `derive(Serialize)` impl trait methods disappear).
1115
+ // `checkOnSave.enable: false` keeps the server quiet on workspace
1116
+ // edits since we don't render diagnostics.
1117
+ initializationOptions: {
1118
+ cargo: {
1119
+ buildScripts: { enable: true }
1120
+ },
1121
+ procMacro: { enable: true },
1122
+ checkOnSave: { enable: false }
1213
1123
  }
1214
1124
  }
1215
- },
1216
- // Coursier — single-file JVM launcher that we install once and then
1217
- // use to install any JVM-based LSP (metals, future kotlin LSPs, etc.).
1218
- // Tier 2 of the install plan: the binary itself is just another
1219
- // GitHub-release Tier 1 entry; the magic is what we DO with it after
1220
- // install (see coursier-installer.ts).
1221
- coursier: {
1222
- source: { kind: "github-release", repo: "coursier/coursier" },
1223
- version: "v2.1.24",
1224
- license: "Apache-2.0",
1225
- assets: {
1226
- "darwin-arm64": {
1227
- filename: "cs-aarch64-apple-darwin.gz",
1228
- sha256: "53a5728c2016118c8296fa7d5678ddfe122e22dc6c36deb554d771b3b9295b4f",
1229
- binaryPath: "cs"
1230
- },
1231
- "darwin-x64": {
1232
- filename: "cs-x86_64-apple-darwin.gz",
1233
- sha256: "33913cd6b61658035d9e6fe971e919cb0ef1f659aa7bff7deeded963a2d36385",
1234
- binaryPath: "cs"
1235
- },
1236
- "linux-x64": {
1237
- filename: "cs-x86_64-pc-linux.gz",
1238
- sha256: "d2c0572a17fb6146ea65349b59dd216b38beff60ae22bce6e549867c6ed2eda6",
1239
- binaryPath: "cs"
1240
- },
1241
- // coursier v2.1.24 ships no aarch64-linux build — Scala dev on
1242
- // ARM Linux falls back to the toolchain-installed Metals path.
1243
- "win32-x64": {
1244
- filename: "cs-x86_64-pc-win32.zip",
1245
- sha256: "c16b4f95b59fbe035cdda4353cfca78befcbbef5a20a3b151c679717c1504c9f",
1246
- binaryPath: "cs.exe"
1125
+ ],
1126
+ java: [
1127
+ // The `jdtls` binary exists when installed via Homebrew (which
1128
+ // ships a wrapper over the Eclipse JDT launcher) or the nvim
1129
+ // jdtls scripts. Raw Eclipse downloads require invoking
1130
+ // `java -jar org.eclipse.equinox.launcher_*.jar`; doctor needs
1131
+ // to surface that when the bare `jdtls` binary is absent.
1132
+ //
1133
+ // Phase 7L init options:
1134
+ // - autobuild disabled — per-edit work the listener doesn't
1135
+ // need; saves CPU + memory on the cold start path.
1136
+ // - completion disabled — same rationale.
1137
+ // - progressReportProvider true — needed so waitForIndex can
1138
+ // subscribe to "Importing projects" / "Building workspace"
1139
+ // progress and block receiver queries until end-of-progress.
1140
+ //
1141
+ // JVM args:
1142
+ // - `-Xmx2G` — JDTLS OOMs on real-world repos with the default
1143
+ // `-Xmx256m`. Without this the OOM kills the server inside
1144
+ // workspace import, which opens the Java per-repo circuit
1145
+ // breaker (plan §Java failure modes).
1146
+ // - `-data <repoSha-scoped workspace>` — added at spawn time by
1147
+ // workspace-session.ts so JDTLS skips the Maven/Gradle import
1148
+ // on subsequent runs. Watch pom.xml/build.gradle mtime to
1149
+ // invalidate stale incremental state.
1150
+ {
1151
+ id: "jdtls",
1152
+ displayName: "Eclipse JDT Language Server",
1153
+ command: "jdtls",
1154
+ args: ["--jvm-arg=-Xmx2G"],
1155
+ extensions: [".java"],
1156
+ lspLanguageId: "java",
1157
+ installHint: "brew install jdtls (see https://github.com/eclipse-jdtls/eclipse.jdt.ls)",
1158
+ nativeInstallKey: "jdtls",
1159
+ initializationOptions: {
1160
+ bundles: [],
1161
+ extendedClientCapabilities: {
1162
+ progressReportProvider: true,
1163
+ classFileContentsSupport: true
1164
+ },
1165
+ settings: {
1166
+ java: {
1167
+ autobuild: { enabled: false },
1168
+ completion: { enabled: false },
1169
+ // Don't render diagnostics — listener doesn't surface
1170
+ // Java lint output and the firehose slows JDTLS.
1171
+ errors: { incompleteClasspath: { severity: "ignore" } }
1172
+ }
1173
+ }
1247
1174
  }
1248
1175
  }
1249
- },
1250
- // Eclipse JDT Language Server — single cross-platform tarball
1251
- // hosted at download.eclipse.org. Ships as an Equinox launcher jar
1252
- // that must be invoked via `java -jar`, so the installer generates
1253
- // a `bin/jdtls` shim that selects the right config_{platform} dir.
1254
- // Requires JDK 17+ (jdtls 1.51 minimum).
1255
- jdtls: {
1256
- source: { kind: "direct-url" },
1257
- version: "1.51.0",
1258
- license: "EPL-2.0",
1259
- requiresJdk: 17,
1260
- assets: {
1261
- "darwin-arm64": {
1262
- url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
1263
- filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
1264
- sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
1265
- binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
1266
- launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
1267
- wrapperBinaryName: "jdtls"
1268
- },
1269
- "darwin-x64": {
1270
- url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
1271
- filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
1272
- sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
1273
- binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
1274
- launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
1275
- wrapperBinaryName: "jdtls"
1276
- },
1277
- "linux-x64": {
1278
- url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
1279
- filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
1280
- sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
1281
- binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
1282
- launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
1283
- wrapperBinaryName: "jdtls"
1284
- },
1285
- "linux-arm64": {
1286
- url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
1287
- filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
1288
- sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
1289
- binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
1290
- launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
1291
- wrapperBinaryName: "jdtls"
1292
- },
1293
- "win32-x64": {
1294
- url: "https://download.eclipse.org/jdtls/milestones/1.51.0/jdt-language-server-1.51.0-202510022025.tar.gz",
1295
- filename: "jdt-language-server-1.51.0-202510022025.tar.gz",
1296
- sha256: "8a59372117881bf5bdc0220f2254472846b88137c058f344b00a7d41427745a1",
1297
- binaryPath: "plugins/org.eclipse.equinox.launcher_*.jar",
1298
- launcherJarGlob: "plugins/org.eclipse.equinox.launcher_*.jar",
1299
- wrapperBinaryName: "jdtls"
1300
- }
1176
+ ],
1177
+ csharp: [
1178
+ // Issue #352 item #5 csharp-ls listed FIRST. Both are MIT-OSS
1179
+ // and Roslyn-backed, but csharp-ls is significantly lighter (~10 MB
1180
+ // dotnet-tool install vs. OmniSharp's ~200 MB native binary). On
1181
+ // user machines with `dotnet` on PATH, csharp-ls is installable in
1182
+ // one command with no native-binary fetch. OmniSharp stays as the
1183
+ // installed-elsewhere fallback (Roslyn-build farms, JetBrains
1184
+ // Rider tool dirs, etc.).
1185
+ {
1186
+ id: "csharp-ls",
1187
+ displayName: "csharp-ls",
1188
+ command: "csharp-ls",
1189
+ args: [],
1190
+ extensions: [".cs"],
1191
+ lspLanguageId: "csharp",
1192
+ installHint: "dotnet tool install -g csharp-ls",
1193
+ toolchainInstallKey: "csharp-ls"
1194
+ },
1195
+ {
1196
+ id: "omnisharp",
1197
+ displayName: "OmniSharp",
1198
+ command: "omnisharp",
1199
+ args: ["-lsp"],
1200
+ extensions: [".cs"],
1201
+ lspLanguageId: "csharp",
1202
+ installHint: "https://github.com/OmniSharp/omnisharp-roslyn/releases"
1203
+ }
1204
+ ],
1205
+ php: [
1206
+ // Phase 7L++: Phpactor only. Phpactor is MIT-licensed, free, and
1207
+ // ships as a single phar via Composer. We DO NOT bundle
1208
+ // Intelephense — its premium tier is commercial and its community
1209
+ // licence is not redistributable. Customers who already own an
1210
+ // Intelephense licence can opt-in via the `PHP_LSP=intelephense`
1211
+ // environment variable, which the CLI swaps into the registry at
1212
+ // runtime (see apps/cli/src/commands/lsp.ts step 7). The escape
1213
+ // hatch requires the user to provision an `intelephense` binary
1214
+ // on PATH themselves.
1215
+ //
1216
+ // Confidence floor on PHP is 0.7 (see lsp-upgrade-strategy.ts).
1217
+ // Phpactor's static analysis is solid for PSR-4 resolvable calls
1218
+ // but stumbles on Laravel-style facades / magic accessors — those
1219
+ // surface as null definitions, recorded as `kind: 'definition'`
1220
+ // with `targets.length === 0` (no upgrade, telemetry preserved).
1221
+ {
1222
+ id: "phpactor",
1223
+ displayName: "phpactor",
1224
+ command: "phpactor",
1225
+ args: ["language-server"],
1226
+ extensions: [".php"],
1227
+ lspLanguageId: "php",
1228
+ installHint: "composer global require phpactor/phpactor (see https://phpactor.readthedocs.io/en/master/usage/standalone.html)",
1229
+ toolchainInstallKey: "phpactor"
1230
+ }
1231
+ ],
1232
+ ruby: [
1233
+ {
1234
+ id: "ruby-lsp",
1235
+ displayName: "ruby-lsp",
1236
+ command: "ruby-lsp",
1237
+ args: [],
1238
+ extensions: [".rb"],
1239
+ lspLanguageId: "ruby",
1240
+ installHint: "gem install ruby-lsp",
1241
+ toolchainInstallKey: "ruby-lsp"
1242
+ },
1243
+ {
1244
+ id: "solargraph",
1245
+ displayName: "solargraph",
1246
+ command: "solargraph",
1247
+ args: ["stdio"],
1248
+ extensions: [".rb"],
1249
+ lspLanguageId: "ruby",
1250
+ installHint: "gem install solargraph"
1251
+ }
1252
+ ],
1253
+ kotlin: [
1254
+ // Phase 7L++: fwcd Kotlin LSP. JVM cold-start is ~30s on the first
1255
+ // Gradle/Maven import — the language server materialises the
1256
+ // build's classpath before symbol lookups return. On mixed
1257
+ // Kotlin/Java repos the WorkspaceManager spawns BOTH `jdtls` and
1258
+ // `kotlin-language-server` — each owns its own file-type filter
1259
+ // and they coexist without contention. Per-language LRU eviction
1260
+ // (5-min idle, RSS soft-cap) prevents the two JVMs from pinning
1261
+ // memory on long-lived listeners.
1262
+ //
1263
+ // Future: when JetBrains' Kotlin LSP exits preview, drop a
1264
+ // higher-priority entry above this one with the JB binary; we
1265
+ // keep fwcd as the fallback because JB's LSP requires a JetBrains
1266
+ // account.
1267
+ {
1268
+ id: "kotlin-language-server",
1269
+ displayName: "kotlin-language-server (fwcd)",
1270
+ command: "kotlin-language-server",
1271
+ args: [],
1272
+ extensions: [".kt", ".kts"],
1273
+ lspLanguageId: "kotlin",
1274
+ installHint: "brew install kotlin-language-server (or see https://github.com/fwcd/kotlin-language-server)",
1275
+ nativeInstallKey: "kotlin-language-server"
1276
+ }
1277
+ ],
1278
+ swift: [
1279
+ {
1280
+ id: "sourcekit-lsp",
1281
+ displayName: "sourcekit-lsp",
1282
+ command: "sourcekit-lsp",
1283
+ args: [],
1284
+ extensions: [".swift"],
1285
+ lspLanguageId: "swift",
1286
+ installHint: "Bundled with Xcode Command Line Tools on macOS",
1287
+ toolchainInstallKey: "sourcekit-lsp",
1288
+ requiresBuild: true
1289
+ }
1290
+ ],
1291
+ dart: [
1292
+ // Phase 7L++: the official Dart Analysis Server ships inside the
1293
+ // Dart SDK as `dart language-server --protocol=lsp`. There's no
1294
+ // separate binary to install — installing Dart (or Flutter, which
1295
+ // bundles Dart) is sufficient. Confidence floor is 0.9: the Dart
1296
+ // analyzer is authoritative, derives types from sound null safety,
1297
+ // and is the same server that powers the official IDE plugins.
1298
+ // No `initializationOptions` — Dart's analyzer derives everything
1299
+ // from `pubspec.yaml` / `analysis_options.yaml` at the repo root.
1300
+ {
1301
+ id: "dart-language-server",
1302
+ displayName: "dart language-server",
1303
+ command: "dart",
1304
+ args: ["language-server", "--protocol=lsp"],
1305
+ extensions: [".dart"],
1306
+ lspLanguageId: "dart",
1307
+ installHint: "Install Dart SDK from https://dart.dev/get-dart (or Flutter from https://flutter.dev \u2014 bundles Dart).",
1308
+ toolchainInstallKey: "dart-language-server"
1309
+ }
1310
+ ],
1311
+ // Scala via Metals (Phase 7L+).
1312
+ //
1313
+ // Metals is the de-facto Scala language server. It coordinates with
1314
+ // a build server (Bloop by default; SBT/Mill/Gradle alternatives are
1315
+ // discovered from the project's build files) to obtain the project's
1316
+ // classpath, semantic database, and compiler diagnostics. The Bloop
1317
+ // daemon is a separate JVM process that Metals spawns on its first
1318
+ // `initialize`, then keeps warm; the listener's WorkspaceManager
1319
+ // pairs Bloop lifecycle to Metals' (see bloop-lifecycle.ts) so they
1320
+ // are spawned together on lazy `getOrOpen` and torn down together
1321
+ // on idle eviction.
1322
+ //
1323
+ // Install hint: `coursier install metals` — Coursier (`cs`) is the
1324
+ // canonical Scala launcher and is typically already on a Scala
1325
+ // developer's machine. The Coursier-managed `metals` shim resolves
1326
+ // Bloop on first use, so no separate Bloop install is required.
1327
+ // Macro-expanded code can mis-locate definitions (Scala 3 inline /
1328
+ // quote macros, scala-compat shims) — the per-language strategy
1329
+ // pins confidence at 0.85 to reflect that limitation.
1330
+ scala: [
1331
+ {
1332
+ id: "metals",
1333
+ displayName: "Metals (Scala)",
1334
+ command: "metals",
1335
+ args: [],
1336
+ extensions: [".scala", ".sc", ".sbt"],
1337
+ lspLanguageId: "scala",
1338
+ installHint: "coursier install metals (see https://scalameta.org/metals \u2014 Coursier-managed binary coordinates with Bloop automatically)",
1339
+ coursierInstallKey: "metals"
1340
+ }
1341
+ ],
1342
+ c: [
1343
+ {
1344
+ id: "clangd-c",
1345
+ displayName: "clangd",
1346
+ command: "clangd",
1347
+ // `--compile-commands-dir=.repowise` lets clangd pick up the
1348
+ // stub compile_commands.json the listener writes for repos
1349
+ // that don't ship one (header-only libs, single-file repos).
1350
+ // clangd still finds repo-root or build/ compile_commands.json
1351
+ // first when present — this is a fallback location, not an
1352
+ // override (task #1).
1353
+ args: ["--background-index", "--compile-commands-dir=.repowise"],
1354
+ extensions: [".c", ".h"],
1355
+ lspLanguageId: "c",
1356
+ installHint: "brew install llvm (includes clangd)",
1357
+ nativeInstallKey: "clangd"
1301
1358
  }
1302
- },
1303
- // fwcd/kotlin-language-server — single cross-platform zip on GitHub
1304
- // releases. Ships a `bin/kotlin-language-server` shell script + a
1305
- // matching `.bat` for Windows — both invoke `java` internally, so
1306
- // we just chmod+x and symlink (no shim needed). Requires JDK 11+.
1307
- "kotlin-language-server": {
1308
- source: { kind: "github-release", repo: "fwcd/kotlin-language-server" },
1309
- version: "1.3.13",
1310
- license: "MIT",
1311
- requiresJdk: 11,
1312
- assets: {
1313
- "darwin-arm64": {
1314
- filename: "server.zip",
1315
- sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
1316
- binaryPath: "server/bin/kotlin-language-server"
1317
- },
1318
- "darwin-x64": {
1319
- filename: "server.zip",
1320
- sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
1321
- binaryPath: "server/bin/kotlin-language-server"
1322
- },
1323
- "linux-x64": {
1324
- filename: "server.zip",
1325
- sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
1326
- binaryPath: "server/bin/kotlin-language-server"
1327
- },
1328
- "linux-arm64": {
1329
- filename: "server.zip",
1330
- sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
1331
- binaryPath: "server/bin/kotlin-language-server"
1332
- },
1333
- "win32-x64": {
1334
- filename: "server.zip",
1335
- sha256: "4fe7d71d087b307c7869036171bd9d8c6a4284cd7c25b89098b0a24eb2d9b6d2",
1336
- binaryPath: "server/bin/kotlin-language-server.bat"
1337
- }
1359
+ ],
1360
+ cpp: [
1361
+ {
1362
+ id: "clangd-cpp",
1363
+ displayName: "clangd",
1364
+ command: "clangd",
1365
+ // `--compile-commands-dir=.repowise` lets clangd pick up the
1366
+ // stub compile_commands.json the listener writes for repos
1367
+ // that don't ship one (header-only libs, single-file repos).
1368
+ // clangd still finds repo-root or build/ compile_commands.json
1369
+ // first when present — this is a fallback location, not an
1370
+ // override (task #1).
1371
+ args: ["--background-index", "--compile-commands-dir=.repowise"],
1372
+ extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
1373
+ lspLanguageId: "cpp",
1374
+ installHint: "brew install llvm (includes clangd)",
1375
+ nativeInstallKey: "clangd"
1338
1376
  }
1339
- }
1340
- };
1341
- COURSIER_LSPS = {
1342
- metals: {
1343
- appName: "metals",
1344
- displayName: "Metals (Scala)",
1345
- license: "Apache-2.0"
1346
- }
1377
+ ],
1378
+ vue: [
1379
+ {
1380
+ id: "vue-language-server",
1381
+ displayName: "Vue Language Server (volar)",
1382
+ command: "vue-language-server",
1383
+ args: ["--stdio"],
1384
+ extensions: [".vue"],
1385
+ lspLanguageId: "vue",
1386
+ installHint: "npm i -g @vue/language-server",
1387
+ npmPackage: "@vue/language-server"
1388
+ }
1389
+ ],
1390
+ svelte: [
1391
+ {
1392
+ id: "svelte-language-server",
1393
+ displayName: "svelte-language-server",
1394
+ command: "svelteserver",
1395
+ args: ["--stdio"],
1396
+ extensions: [".svelte"],
1397
+ lspLanguageId: "svelte",
1398
+ installHint: "npm i -g svelte-language-server",
1399
+ npmPackage: "svelte-language-server"
1400
+ }
1401
+ ]
1347
1402
  };
1348
- TOOLCHAIN_LSPS = {
1349
- gopls: {
1350
- toolchain: "go",
1351
- installArgs: ["install", "golang.org/x/tools/gopls@latest"],
1352
- binaryName: "gopls",
1353
- toolchainBinDir: "go/bin",
1354
- displayName: "gopls",
1355
- license: "BSD-3-Clause"
1356
- },
1357
- "ruby-lsp": {
1358
- toolchain: "gem",
1359
- installArgs: ["install", "--user-install", "ruby-lsp"],
1360
- binaryName: "ruby-lsp",
1361
- // gem userdir is version-dependent — resolved at install time.
1362
- displayName: "ruby-lsp",
1363
- license: "MIT"
1364
- },
1365
- "csharp-ls": {
1366
- toolchain: "dotnet",
1367
- installArgs: ["tool", "install", "-g", "csharp-ls"],
1368
- binaryName: "csharp-ls",
1369
- toolchainBinDir: ".dotnet/tools",
1370
- displayName: "csharp-ls",
1371
- license: "MIT"
1372
- },
1373
- phpactor: {
1374
- toolchain: "composer",
1375
- installArgs: ["global", "require", "phpactor/phpactor"],
1376
- binaryName: "phpactor",
1377
- toolchainBinDir: ".composer/vendor/bin",
1378
- displayName: "phpactor",
1379
- license: "MIT"
1380
- },
1381
- "dart-language-server": {
1382
- bundled: true,
1383
- binaryName: "dart",
1384
- displayName: "dart language-server (Dart SDK)",
1385
- license: "BSD-3-Clause"
1386
- },
1387
- "sourcekit-lsp": {
1388
- bundled: true,
1389
- binaryName: "sourcekit-lsp",
1390
- displayName: "sourcekit-lsp (Xcode CLT)",
1391
- license: "Apache-2.0"
1403
+ BUILD_SPECS = {
1404
+ swift: {
1405
+ gateCommands: ["swift", "xcrun"],
1406
+ gateExecs: [{ command: "xcode-select", args: ["-p"] }],
1407
+ manifests: ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
1408
+ platform: "darwin",
1409
+ command: "swift",
1410
+ args: ["build"],
1411
+ indexStoreProbe: ".build/index/store"
1392
1412
  }
1393
1413
  };
1394
1414
  }
1395
1415
  });
1396
1416
 
1397
- // ../../packages/shared/src/index.ts
1398
- var init_src = __esm({
1399
- "../../packages/shared/src/index.ts"() {
1400
- "use strict";
1401
- init_typed_resolution();
1402
- init_billing();
1403
- init_platforms();
1404
- init_tiers();
1405
- init_roles();
1406
- init_lsp_versions();
1407
- }
1408
- });
1409
-
1410
1417
  // ../listener/dist/lsp/jdk-probe.js
1411
1418
  import { spawn as spawn3 } from "child_process";
1412
1419
  function parseJavaMajorVersion(stderr) {
@@ -3105,17 +3112,53 @@ Treat RepoWise as your default starting point for every task that touches this c
3105
3112
 
3106
3113
  6. **Don't edit \`repowise-context/\` files manually.** They're regenerated by RepoWise on every sync. Manual edits will be overwritten. Update source code; let the next sync regenerate.
3107
3114
 
3108
- 7. **Mark every response that used RepoWise.** When your work used any RepoWise resource \u2014 a file under \`repowise-context/\`, or a RepoWise MCP tool \u2014 begin your response with a single tag line:
3115
+ 7. **Mark a response with \`_Powered by RepoWise._\` \u2014 and ONLY when you actually used RepoWise this turn.** Begin the response with a single tag line:
3109
3116
 
3110
3117
  _Powered by RepoWise._
3111
3118
 
3112
- That's it \u2014 one short line, no longer preamble. The host's tool-call UI already shows RepoWise MCP calls visibly; this tag adds the same surface for context-file reads. If you didn't use RepoWise on this turn, omit the tag \u2014 never fake it.
3119
+ ONLY on a turn where you genuinely used a RepoWise resource THIS turn: a RepoWise MCP/LSP tool call, OR a read of a \`repowise-context/\` file. One short line, no longer preamble. The host's tool-call UI already shows MCP/LSP calls visibly; this tag adds the same surface for the otherwise-invisible context-file reads. If the turn did not actually use RepoWise, omit the tag entirely \u2014 never add it out of habit, never fake it.
3113
3120
 
3114
3121
  8. **MCP unavailable vs. a RepoWise miss \u2014 handle them differently.** If RepoWise MCP is entirely unavailable (no \`.mcp.json\` configured, listener not running), tell the user. But if RepoWise IS available and simply does not cover a specific question, just continue with Tier 3 \u2014 do NOT announce that "RepoWise doesn't have it." That fallback is normal and expected; the system records it on its own to improve coverage.
3115
3122
  ${END_MARKER}`;
3116
- function injectAgentInstructions(content) {
3123
+ var AI_AGENT_USAGE_INSTRUCTIONS_GRAPH = `${START_MARKER}
3124
+ ## How AI Agents Should Use This Context
3125
+
3126
+ RepoWise provides a live, pre-analyzed **knowledge graph** of this codebase through its MCP server \u2014 symbol resolution, callers, references, call graphs, refactor impact, dependency graphs, and type info \u2014 plus language-server (LSP) intelligence. Use it to navigate and reason about the code structurally instead of guessing from raw text.
3127
+
3128
+ Treat RepoWise as your default starting point for every task that touches this codebase, not a Q&A reference.
3129
+
3130
+ **Rules for using RepoWise:**
3131
+
3132
+ 1. **Always start with RepoWise.** Before any task \u2014 questions, planning, coding, debugging, refactoring, tests, code review \u2014 resolve the relevant symbols and their blast radius via RepoWise FIRST. Skipping it leads to wrong assumptions, duplicated abstractions, and missed call sites.
3133
+
3134
+ 2. **Use the two-tier hierarchy in this order:**
3135
+ - **Tier 1 \u2014 RepoWise MCP + LSP** for all structural queries: symbol resolution, callers, references, call graphs, refactor impact, dependency graphs, type info (\`find_symbol\`, \`get_impact\`, \`find_callers\`, \`find_references\`, \`lsp_definition\`, \`lsp_references\`, \`lsp_hover\`, etc. \u2014 the server is named "RepoWise MCP for <repo>", so hosts that prefix tool names expose them accordingly). Each tool's description tells you when to use it.
3136
+ - **Tier 2 \u2014 Native code search** (\`read_file\` / \`grep\` / \`glob\` on raw source) to read implementation bodies and for anything the graph does not cover (recent commits, in-flight branches, exact strings). An enforcement hook may DENY a native search until you have used a RepoWise MCP/LSP tool this turn \u2014 that is expected, not an error: query RepoWise first, then retry the same search and it will proceed.
3137
+
3138
+ 3. **Batch tool calls in parallel.** Whenever you need multiple independent lookups, invoke all relevant tools SIMULTANEOUSLY rather than sequentially (e.g. confirm a symbol's shape AND its callers in one turn). Only chain serially when one tool's output is genuinely the input to another (e.g. \`find_symbol\` \u2192 then \`lsp_definition\` on the match).
3139
+
3140
+ 4. **Apply per-task patterns** (always batch the gathering step):
3141
+ - Planning / refactoring \u2192 \`get_impact\`, \`get_deps\`, \`find_references\` to size the blast radius first.
3142
+ - Implementing \u2192 \`find_symbol\`, \`lsp_definition\`, \`lsp_hover\` to land on the exact definition and its type.
3143
+ - Debugging \u2192 \`find_callers\`, \`get_call_graph\` to trace the path.
3144
+ - Writing tests \u2192 \`find_tests_for_symbol\` to find existing coverage.
3145
+
3146
+ 5. **Propagate to subagents.** When delegating work, explicitly instruct the sub-task to use the RepoWise MCP/LSP tools FIRST. Subagents don't inherit parent instructions automatically.
3147
+
3148
+ 6. **Mark a response with \`_Powered by RepoWise._\` \u2014 and ONLY when you actually used RepoWise this turn.** Begin the response with a single tag line:
3149
+
3150
+ _Powered by RepoWise._
3151
+
3152
+ ONLY on a turn where you genuinely called a RepoWise MCP/LSP tool. One short line, no longer preamble. The host's tool-call UI already shows MCP/LSP calls visibly, so this only confirms what the turn did \u2014 if the turn did not call a RepoWise tool, omit the tag entirely; never add it out of habit, never fake it.
3153
+
3154
+ 7. **MCP unavailable vs. a RepoWise miss \u2014 handle them differently.** If RepoWise MCP is entirely unavailable (no \`.mcp.json\` configured, listener not running), tell the user. But if RepoWise IS available and simply does not cover a specific question, just continue with native search \u2014 do NOT announce that "RepoWise doesn't have it." That fallback is normal and expected; the system records it on its own to improve coverage.
3155
+ ${END_MARKER}`;
3156
+ function agentInstructionsFor(variant = "full") {
3157
+ return variant === "graph" ? AI_AGENT_USAGE_INSTRUCTIONS_GRAPH : AI_AGENT_USAGE_INSTRUCTIONS;
3158
+ }
3159
+ function injectAgentInstructions(content, variant = "full") {
3117
3160
  const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
3118
- const canonicalBlock = AI_AGENT_USAGE_INSTRUCTIONS.replace(/\n/g, lineEnding);
3161
+ const canonicalBlock = agentInstructionsFor(variant).replace(/\n/g, lineEnding);
3119
3162
  const startIdx = content.indexOf(START_MARKER);
3120
3163
  const endIdx = content.indexOf(END_MARKER);
3121
3164
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -3159,14 +3202,20 @@ function claudeToolPrefix(repoName) {
3159
3202
  }
3160
3203
  var GRAPH_TOOLS = "find_symbol, get_symbol, get_impact, find_callers, find_references, batch_query";
3161
3204
  var LSP_TOOLS = "lsp_definition, lsp_references, lsp_hover";
3162
- function buildRepoWiseFirstDirective(repoName, contextFolder, style = "generic") {
3205
+ function buildRepoWiseFirstDirective(repoName, contextFolder, style = "generic", variant = "full") {
3163
3206
  const repo = sanitizeForShellText(repoName);
3164
3207
  const folder = sanitizeForShellText(contextFolder);
3165
3208
  const toolNaming = style === "claude" ? `${claudeToolPrefix(repo)}<tool>, e.g. ${claudeToolPrefix(repo)}find_symbol` : `the "${mcpServerName(repo)}" MCP server`;
3209
+ if (variant === "graph") {
3210
+ return `RepoWise-first: before any codebase task, query the RepoWise knowledge graph first. Tier 1: RepoWise MCP graph tools (${toolNaming}; tools: ${GRAPH_TOOLS}) and LSP tools (${LSP_TOOLS}) for symbol resolution and blast radius. Tier 2: native file search only when the graph does not cover the question. If these tools are deferred in your environment, load them via tool search before calling.`;
3211
+ }
3166
3212
  return `RepoWise-first: before any codebase task, gather context from RepoWise first. Tier 1: RepoWise MCP graph tools (${toolNaming}; tools: ${GRAPH_TOOLS}) and LSP tools (${LSP_TOOLS}) for symbol resolution and blast radius. Tier 2: ${folder}/ files, starting with ${folder}/project-overview.md. Tier 3: native file search only when tiers 1-2 do not cover the question. If these tools are deferred in your environment, load them via tool search before calling.`;
3167
3213
  }
3168
- function buildSubagentDirective(contextFolder) {
3214
+ function buildSubagentDirective(contextFolder, variant = "full") {
3169
3215
  const folder = sanitizeForShellText(contextFolder);
3216
+ if (variant === "graph") {
3217
+ return `IMPORTANT: Use the RepoWise MCP/LSP tools (find_symbol, get_impact, find_callers, lsp_definition) to resolve symbols and blast radius before performing any work.`;
3218
+ }
3170
3219
  return `IMPORTANT: Read ${folder}/project-overview.md before performing any work. This file maps every context file to its domain.`;
3171
3220
  }
3172
3221
  var GATE_SCRIPT_REL = ".claude/repowise-gate.cjs";
@@ -3175,9 +3224,10 @@ var GATE_MAX_DENIES = 2;
3175
3224
  function buildGateHookCommand() {
3176
3225
  return `node "$CLAUDE_PROJECT_DIR/${GATE_SCRIPT_REL}" ${REPOWISE_MANAGED_COMMENT}`;
3177
3226
  }
3178
- function buildGateScript(contextFolder) {
3227
+ function buildGateScript(contextFolder, variant = "full") {
3179
3228
  const folder = contextFolder.replace(/[^A-Za-z0-9_./-]/g, "");
3180
- return GATE_SCRIPT_TEMPLATE.replace(/__CONTEXT_FOLDER__/g, folder).replace(/__MAX_DENIES__/g, String(GATE_MAX_DENIES));
3229
+ const denyReason = variant === "graph" ? "RepoWise-first: this codebase has RepoWise. Query its MCP tools (find_symbol/get_impact/lsp_*) to answer this before native search. Retry this search and it will be allowed." : "RepoWise-first: this codebase has RepoWise. Use Tier 1 (RepoWise MCP tools find_symbol/get_impact/lsp_*) or Tier 2 (" + folder + "/ docs) to answer this before native search. Retry this search and it will be allowed.";
3230
+ return GATE_SCRIPT_TEMPLATE.replace(/__CONTEXT_FOLDER__/g, folder).replace(/__MAX_DENIES__/g, String(GATE_MAX_DENIES)).replace(/__DENY_REASON__/g, () => denyReason);
3181
3231
  }
3182
3232
  var GATE_SCRIPT_TEMPLATE = [
3183
3233
  "#!/usr/bin/env node",
@@ -3216,7 +3266,7 @@ var GATE_SCRIPT_TEMPLATE = [
3216
3266
  "if (state.consulted) { logMiss(); process.exit(0); }",
3217
3267
  "if ((state.denies || 0) >= MAX_DENIES) { process.exit(0); }",
3218
3268
  "state.denies = (state.denies || 0) + 1; writeState(state);",
3219
- 'var reason = "RepoWise-first: this codebase has RepoWise. Use Tier 1 (RepoWise MCP tools find_symbol/get_impact/lsp_*) or Tier 2 (" + CONTEXT_FOLDER + "/ docs) to answer this before native search. Retry this search and it will be allowed.";',
3269
+ 'var reason = "__DENY_REASON__";',
3220
3270
  'process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }));',
3221
3271
  "process.exit(0);",
3222
3272
  ""
@@ -3325,7 +3375,7 @@ function mergeClaudeHookSettings(existingContent, params) {
3325
3375
  hooks: [
3326
3376
  {
3327
3377
  type: "command",
3328
- command: buildHookEchoCommand("SubagentStart", buildSubagentDirective(params.contextFolder))
3378
+ command: buildHookEchoCommand("SubagentStart", buildSubagentDirective(params.contextFolder, params.variant))
3329
3379
  }
3330
3380
  ]
3331
3381
  },
@@ -3333,7 +3383,7 @@ function mergeClaudeHookSettings(existingContent, params) {
3333
3383
  hooks: [
3334
3384
  {
3335
3385
  type: "command",
3336
- command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "claude"))
3386
+ command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "claude", params.variant))
3337
3387
  },
3338
3388
  // Re-arm the per-question gate (silent; the echo above carries the reminder).
3339
3389
  { type: "command", command: buildGateHookCommand() }
@@ -3358,7 +3408,7 @@ function mergeGeminiHookSettings(existingContent, params) {
3358
3408
  name: "repowise-first",
3359
3409
  command: buildPlainJsonEchoCommand({
3360
3410
  hookSpecificOutput: {
3361
- additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3411
+ additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3362
3412
  }
3363
3413
  })
3364
3414
  }
@@ -3375,7 +3425,7 @@ function mergeCodexHookSettings(existingContent, params) {
3375
3425
  hooks: [
3376
3426
  {
3377
3427
  type: "command",
3378
- command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder))
3428
+ command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant))
3379
3429
  }
3380
3430
  ]
3381
3431
  }
@@ -3386,7 +3436,7 @@ function removeCodexHookSettings(existingContent) {
3386
3436
  }
3387
3437
  function buildCopilotHooksFile(params) {
3388
3438
  const command = buildPlainJsonEchoCommand({
3389
- additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3439
+ additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3390
3440
  });
3391
3441
  return JSON.stringify({
3392
3442
  version: 1,
@@ -3397,7 +3447,7 @@ function buildCopilotHooksFile(params) {
3397
3447
  }
3398
3448
  function buildClineHookScript(params) {
3399
3449
  const command = buildPlainJsonEchoCommand({
3400
- contextModification: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3450
+ contextModification: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3401
3451
  });
3402
3452
  return [
3403
3453
  "#!/bin/sh",
@@ -3482,7 +3532,7 @@ async function applyGitignoreChanges(repoRoot, changes) {
3482
3532
  await writeFile(path, next, "utf-8");
3483
3533
  }
3484
3534
  }
3485
- async function writeClaudeHooksToRepo(repoRoot, contextFolder) {
3535
+ async function writeClaudeHooksToRepo(repoRoot, contextFolder, variant = "full") {
3486
3536
  const repoName = basename(repoRoot);
3487
3537
  const projectTracked = await isGitTracked(repoRoot, CLAUDE_PROJECT_SETTINGS);
3488
3538
  if (projectTracked) {
@@ -3506,14 +3556,14 @@ async function writeClaudeHooksToRepo(repoRoot, contextFolder) {
3506
3556
  existing = await readFile(targetPath, "utf-8");
3507
3557
  } catch {
3508
3558
  }
3509
- const merged = mergeClaudeHookSettings(existing, { repoName, contextFolder });
3559
+ const merged = mergeClaudeHookSettings(existing, { repoName, contextFolder, variant });
3510
3560
  let status2 = "unchanged";
3511
3561
  if (merged !== existing) {
3512
3562
  await mkdir(join2(repoRoot, ".claude"), { recursive: true });
3513
3563
  await writeFile(targetPath, merged, "utf-8");
3514
3564
  status2 = "written";
3515
3565
  }
3516
- const gateScript = buildGateScript(contextFolder);
3566
+ const gateScript = buildGateScript(contextFolder, variant);
3517
3567
  const gatePath = join2(repoRoot, GATE_SCRIPT_REL);
3518
3568
  let gateExisting = null;
3519
3569
  try {
@@ -3559,8 +3609,8 @@ async function removeClaudeHooksFromRepo(repoRoot) {
3559
3609
  }
3560
3610
 
3561
3611
  // ../../packages/shared/dist/lib/ai-tools.js
3562
- var AI_TOOLS_REFERENCE_REV = 3;
3563
- var AGENT_INSTRUCTIONS_HASH = createHash("sha256").update(AI_AGENT_USAGE_INSTRUCTIONS).digest("hex").slice(0, 12);
3612
+ var AI_TOOLS_REFERENCE_REV = 4;
3613
+ var AGENT_INSTRUCTIONS_HASH = createHash("sha256").update(AI_AGENT_USAGE_INSTRUCTIONS).update(" ").update(AI_AGENT_USAGE_INSTRUCTIONS_GRAPH).digest("hex").slice(0, 12);
3564
3614
  var AI_TOOL_CONFIG = {
3565
3615
  cursor: {
3566
3616
  label: "Cursor",
@@ -3702,7 +3752,7 @@ function sanitizeRepoName(name) {
3702
3752
  function fileDescriptionFromName(fileName) {
3703
3753
  return fileName.replace(/\.md$/, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3704
3754
  }
3705
- function generateReference(tool, repoName, contextFolder, contextFiles) {
3755
+ function generateReference(tool, repoName, contextFolder, contextFiles, variant = "full") {
3706
3756
  const config2 = AI_TOOL_CONFIG[tool];
3707
3757
  const safeName = sanitizeRepoName(repoName);
3708
3758
  const fileLines = contextFiles.map((f) => {
@@ -3714,19 +3764,21 @@ function generateReference(tool, repoName, contextFolder, contextFiles) {
3714
3764
  const hasFiles = fileLines.length > 0;
3715
3765
  const mcpName = repoName.replace(/`/g, "");
3716
3766
  const mcpNamingNote = tool === "claude-code" ? `**RepoWise MCP tool naming in this repo:** tools are exposed as \`${claudeToolPrefix(mcpName)}<tool>\`, e.g. \`${claudeToolPrefix(mcpName)}find_symbol\`, \`${claudeToolPrefix(mcpName)}get_impact\`, \`${claudeToolPrefix(mcpName)}lsp_definition\`.` : `**RepoWise MCP server for this repo:** "${mcpServerName(mcpName)}" \u2014 graph tools: find_symbol, get_symbol, get_impact, find_callers, find_references, batch_query; LSP tools: lsp_definition, lsp_references, lsp_hover.`;
3767
+ const isGraph = variant === "graph";
3768
+ const introLine = isGraph ? `This repository has a RepoWise knowledge graph (MCP + LSP) available to AI tools.` : `This repository has AI-optimized context files generated by RepoWise.`;
3717
3769
  const contentLines = [
3718
3770
  `## Project Context \u2014 ${safeName}`,
3719
3771
  "",
3720
- `This repository has AI-optimized context files generated by RepoWise.`,
3721
- "",
3722
- AI_AGENT_USAGE_INSTRUCTIONS,
3772
+ introLine,
3723
3773
  "",
3724
- `**Start here:** \`${contextFolder}/project-overview.md\` \u2014 the routing document that maps every context file to its domain.`,
3725
- "",
3726
- mcpNamingNote,
3774
+ agentInstructionsFor(variant),
3727
3775
  ""
3728
3776
  ];
3729
- if (hasFiles) {
3777
+ if (!isGraph) {
3778
+ contentLines.push(`**Start here:** \`${contextFolder}/project-overview.md\` \u2014 the routing document that maps every context file to its domain.`, "");
3779
+ }
3780
+ contentLines.push(mcpNamingNote, "");
3781
+ if (!isGraph && hasFiles) {
3730
3782
  contentLines.push(`**Context File Routing Map:**`, "", ...fileLines.map((f) => `- \`${f.path}\` \u2014 ${f.desc}`), "", `> Additional context files may exist beyond this list. Check \`project-overview.md\` for the complete index.`);
3731
3783
  }
3732
3784
  if (config2.owned) {
@@ -3739,14 +3791,14 @@ function generateReference(tool, repoName, contextFolder, contextFiles) {
3739
3791
  }
3740
3792
  return [config2.markerStart, "", ...contentLines, "", config2.markerEnd].join("\n");
3741
3793
  }
3742
- async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
3794
+ async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant = "full") {
3743
3795
  const config2 = AI_TOOL_CONFIG[tool];
3744
3796
  const fullPath = join3(repoRoot, config2.filePath);
3745
3797
  const dir = dirname(fullPath);
3746
3798
  if (dir !== repoRoot) {
3747
3799
  await mkdir2(dir, { recursive: true });
3748
3800
  }
3749
- const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles);
3801
+ const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles, variant);
3750
3802
  if (config2.owned) {
3751
3803
  let created2 = true;
3752
3804
  try {
@@ -3809,7 +3861,7 @@ async function removeToolConfig(repoRoot, tool) {
3809
3861
  const content = (before + after).replace(/\n{3,}/g, "\n\n").trim() + "\n";
3810
3862
  await writeFile2(fullPath, content, "utf-8");
3811
3863
  }
3812
- async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
3864
+ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant = "full") {
3813
3865
  const config2 = AI_TOOL_CONFIG[tool];
3814
3866
  if (!config2.legacyFilePath)
3815
3867
  return { migrated: false, legacyRemoved: false };
@@ -3819,11 +3871,11 @@ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contex
3819
3871
  const s = await stat(legacyPath);
3820
3872
  legacyIsFile = s.isFile();
3821
3873
  } catch {
3822
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3874
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3823
3875
  return { migrated: false, legacyRemoved: false };
3824
3876
  }
3825
3877
  if (!legacyIsFile) {
3826
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3878
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3827
3879
  return { migrated: false, legacyRemoved: false };
3828
3880
  }
3829
3881
  const legacyContent = await readFile2(legacyPath, "utf-8");
@@ -3853,7 +3905,7 @@ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contex
3853
3905
  } else {
3854
3906
  await unlink2(legacyPath);
3855
3907
  }
3856
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3908
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3857
3909
  return { migrated: true, legacyRemoved: cleaned.length === 0 };
3858
3910
  }
3859
3911
  async function fileExists(path) {
@@ -3929,6 +3981,9 @@ async function scanLocalContextFiles(repoRoot, contextFolder) {
3929
3981
  }
3930
3982
  }
3931
3983
 
3984
+ // ../listener/dist/main.js
3985
+ init_src();
3986
+
3932
3987
  // ../listener/dist/lib/config.js
3933
3988
  init_config_dir();
3934
3989
  import { readFile as readFile3, writeFile as writeFile3, rename, unlink as unlink3, mkdir as mkdir3, chmod, open } from "fs/promises";
@@ -3964,7 +4019,8 @@ async function getListenerConfig() {
3964
4019
  noAutoUpdate: raw.noAutoUpdate ?? false,
3965
4020
  lspEnabled: raw.lspEnabled ?? true,
3966
4021
  lspAutoWarm: raw.lspAutoWarm ?? true,
3967
- ...lspOverrides ? { lspOverrides } : {}
4022
+ ...lspOverrides ? { lspOverrides } : {},
4023
+ ...typeof raw.graphOnly === "boolean" ? { graphOnly: raw.graphOnly } : {}
3968
4024
  };
3969
4025
  } catch {
3970
4026
  return {
@@ -4004,7 +4060,10 @@ async function saveListenerConfig(config2) {
4004
4060
  // the listener round-trips them here unchanged.
4005
4061
  lspEnabled: config2.lspEnabled,
4006
4062
  lspAutoWarm: config2.lspAutoWarm,
4007
- ...config2.lspOverrides ? { lspOverrides: config2.lspOverrides } : {}
4063
+ ...config2.lspOverrides ? { lspOverrides: config2.lspOverrides } : {},
4064
+ // Persist the tier signal when set so a listener-driven plan-change save
4065
+ // round-trips it; absent ⇒ `...raw` preserves any CLI-written value.
4066
+ ...config2.graphOnly !== void 0 ? { graphOnly: config2.graphOnly } : {}
4008
4067
  };
4009
4068
  const tmpPath = configPath + ".tmp";
4010
4069
  try {
@@ -8015,7 +8074,7 @@ var claudeCodeHooksWriter = {
8015
8074
  return await fileExists3(join24(repoRoot, "CLAUDE.md")) || await fileExists3(join24(home, ".claude.json"));
8016
8075
  },
8017
8076
  async write(ctx) {
8018
- const result = await writeClaudeHooksToRepo(ctx.repoRoot, ctx.contextFolder ?? "repowise-context");
8077
+ const result = await writeClaudeHooksToRepo(ctx.repoRoot, ctx.contextFolder ?? "repowise-context", ctx.variant);
8019
8078
  return { status: result.status, path: join24(ctx.repoRoot, result.relPath) };
8020
8079
  },
8021
8080
  async remove(ctx) {
@@ -8085,7 +8144,8 @@ var clineHooksWriter = {
8085
8144
  const path = join26(ctx.repoRoot, SCRIPT_REL);
8086
8145
  const next = buildClineHookScript({
8087
8146
  repoName: ctx.repoName,
8088
- contextFolder: ctx.contextFolder ?? "repowise-context"
8147
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8148
+ variant: ctx.variant
8089
8149
  });
8090
8150
  const existing = await readOrNull(path);
8091
8151
  if (existing !== null && !existing.includes(OWNERSHIP_MARKER)) {
@@ -8197,7 +8257,8 @@ var codexHooksWriter = {
8197
8257
  const existing = await readOrNull2(path);
8198
8258
  const merged = mergeCodexHookSettings(existing, {
8199
8259
  repoName: ctx.repoName,
8200
- contextFolder: ctx.contextFolder ?? "repowise-context"
8260
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8261
+ variant: ctx.variant
8201
8262
  });
8202
8263
  if (merged === existing) {
8203
8264
  return { status: "unchanged", path };
@@ -8283,7 +8344,8 @@ var copilotHooksWriter = {
8283
8344
  const path = join30(ctx.repoRoot, HOOKS_REL2);
8284
8345
  const next = buildCopilotHooksFile({
8285
8346
  repoName: ctx.repoName,
8286
- contextFolder: ctx.contextFolder ?? "repowise-context"
8347
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8348
+ variant: ctx.variant
8287
8349
  });
8288
8350
  const existing = await readOrNull3(path);
8289
8351
  if (await isGitTracked(ctx.repoRoot, HOOKS_REL2)) {
@@ -8422,7 +8484,8 @@ var geminiHooksWriter = {
8422
8484
  const existing = await readOrNull4(path);
8423
8485
  const merged = mergeGeminiHookSettings(existing, {
8424
8486
  repoName: ctx.repoName,
8425
- contextFolder: ctx.contextFolder ?? "repowise-context"
8487
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8488
+ variant: ctx.variant
8426
8489
  });
8427
8490
  if (merged === existing) {
8428
8491
  return { status: "unchanged", path };
@@ -8587,7 +8650,8 @@ async function runAutoConfig(opts) {
8587
8650
  shimCmd: opts.shimCmd,
8588
8651
  home,
8589
8652
  contextFolder: opts.contextFolder,
8590
- selectedTools: opts.selectedTools
8653
+ selectedTools: opts.selectedTools,
8654
+ variant: opts.variant
8591
8655
  });
8592
8656
  results.push({ tool: writer.tool, detected: true, outcome });
8593
8657
  } catch (err) {
@@ -10233,7 +10297,8 @@ async function readRawToolConfig() {
10233
10297
  const raw = JSON.parse(data);
10234
10298
  return {
10235
10299
  aiTools: Array.isArray(raw["aiTools"]) ? raw["aiTools"] : void 0,
10236
- contextFolder: typeof raw["contextFolder"] === "string" ? raw["contextFolder"] : void 0
10300
+ contextFolder: typeof raw["contextFolder"] === "string" ? raw["contextFolder"] : void 0,
10301
+ graphOnly: typeof raw["graphOnly"] === "boolean" ? raw["graphOnly"] : void 0
10237
10302
  };
10238
10303
  } catch {
10239
10304
  return {};
@@ -10254,12 +10319,14 @@ async function updateToolConfigsForRepo(localPath, config2, state, repoId) {
10254
10319
  }
10255
10320
  await migrateRooToKilo(localPath).catch(() => {
10256
10321
  });
10322
+ const variant = config2.graphOnly ? "graph" : "full";
10257
10323
  const contextFiles = await scanLocalContextFiles(localPath, contextFolder);
10258
- if (contextFiles.length === 0)
10324
+ if (contextFiles.length === 0 && variant === "full")
10259
10325
  return;
10260
10326
  const hash = JSON.stringify({
10261
10327
  rev: AI_TOOLS_REFERENCE_REV,
10262
10328
  block: AGENT_INSTRUCTIONS_HASH,
10329
+ variant,
10263
10330
  tools,
10264
10331
  files: contextFiles.map((f) => f.fileName)
10265
10332
  });
@@ -10274,13 +10341,13 @@ async function updateToolConfigsForRepo(localPath, config2, state, repoId) {
10274
10341
  continue;
10275
10342
  written.add(toolConfig.filePath);
10276
10343
  if (toolConfig.legacyFilePath) {
10277
- await migrateToolConfig(localPath, tool, repoName, contextFolder, contextFiles);
10344
+ await migrateToolConfig(localPath, tool, repoName, contextFolder, contextFiles, variant);
10278
10345
  } else {
10279
- await updateToolConfig(localPath, tool, repoName, contextFolder, contextFiles);
10346
+ await updateToolConfig(localPath, tool, repoName, contextFolder, contextFiles, variant);
10280
10347
  }
10281
10348
  }
10282
10349
  if (!written.has("AGENTS.md")) {
10283
- await updateToolConfig(localPath, "codex", repoName, contextFolder, contextFiles);
10350
+ await updateToolConfig(localPath, "codex", repoName, contextFolder, contextFiles, variant);
10284
10351
  }
10285
10352
  if (!state.repos[repoId]) {
10286
10353
  state.repos[repoId] = { lastSyncTimestamp: "", lastSyncCommitSha: null };
@@ -10599,7 +10666,8 @@ async function checkStaleContext(repos, state, groups) {
10599
10666
  }
10600
10667
  async function reconcileMcpConfigs(repos, packageName) {
10601
10668
  const shimCmd = packageName;
10602
- const { contextFolder, aiTools } = await readRawToolConfig();
10669
+ const { contextFolder, aiTools, graphOnly } = await readRawToolConfig();
10670
+ const variant = graphOnly ? "graph" : "full";
10603
10671
  for (const repo of repos) {
10604
10672
  if (!repo.localPath)
10605
10673
  continue;
@@ -10616,7 +10684,8 @@ async function reconcileMcpConfigs(repos, packageName) {
10616
10684
  repoId: repo.repoId,
10617
10685
  shimCmd,
10618
10686
  contextFolder,
10619
- selectedTools: aiTools
10687
+ selectedTools: aiTools,
10688
+ variant
10620
10689
  });
10621
10690
  const written = results.filter((r) => r.detected && r.outcome?.status === "written");
10622
10691
  if (written.length > 0) {
@@ -10628,6 +10697,8 @@ async function reconcileMcpConfigs(repos, packageName) {
10628
10697
  }
10629
10698
  }
10630
10699
  async function reconcileAgentInstructions(repos) {
10700
+ const { graphOnly } = await readRawToolConfig();
10701
+ const variant = graphOnly ? "graph" : "full";
10631
10702
  for (const repo of repos) {
10632
10703
  const path = join40(repo.localPath, "repowise-context", "project-overview.md");
10633
10704
  let content;
@@ -10641,7 +10712,7 @@ async function reconcileAgentInstructions(repos) {
10641
10712
  console.warn(`[reconcile] read failed for ${repo.repoId}:`, err instanceof Error ? err.message : String(err));
10642
10713
  continue;
10643
10714
  }
10644
- const injected = injectAgentInstructions(content);
10715
+ const injected = injectAgentInstructions(content, variant);
10645
10716
  if (injected === content)
10646
10717
  continue;
10647
10718
  try {
@@ -11075,6 +11146,22 @@ async function startListener() {
11075
11146
  if (response.latestCliVersion && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/.test(response.latestCliVersion)) {
11076
11147
  latestCliVersion = response.latestCliVersion;
11077
11148
  }
11149
+ if (typeof response.plan === "string") {
11150
+ const planGraphOnly = agentInstructionsVariantForPlan(response.plan) === "graph";
11151
+ const currentGraphOnly = config2.graphOnly ?? false;
11152
+ if (currentGraphOnly !== planGraphOnly) {
11153
+ console.log(`[tier] Plan change detected (plan=${response.plan}); graphOnly ${String(currentGraphOnly)} \u2192 ${String(planGraphOnly)} \u2014 re-reconciling`);
11154
+ try {
11155
+ await saveListenerConfig({ ...config2, graphOnly: planGraphOnly });
11156
+ config2.graphOnly = planGraphOnly;
11157
+ await reconcileToolConfigs(config2.repos, state);
11158
+ await reconcileAgentInstructions(config2.repos);
11159
+ await reconcileMcpConfigs(config2.repos, packageName);
11160
+ } catch (err) {
11161
+ console.warn("[tier] Plan-change reconcile failed:", err instanceof Error ? err.message : String(err));
11162
+ }
11163
+ }
11164
+ }
11078
11165
  if (shouldReconcile) {
11079
11166
  try {
11080
11167
  const freshConfig = await getListenerConfig();
@@ -13117,7 +13204,7 @@ async function create() {
13117
13204
  let graphOnly = false;
13118
13205
  try {
13119
13206
  const usage = await apiRequest("/v1/account/usage");
13120
- graphOnly = usage.plan === "free";
13207
+ graphOnly = agentInstructionsVariantForPlan(usage.plan ?? "free") === "graph";
13121
13208
  } catch {
13122
13209
  }
13123
13210
  let pollAttempts = 0;
@@ -13369,19 +13456,28 @@ Files are stored on our servers (not in git). Retry when online.`
13369
13456
  spinner.start("Configuring AI tools...");
13370
13457
  const results = [];
13371
13458
  const written = /* @__PURE__ */ new Set();
13459
+ const toolVariant = graphOnly ? "graph" : "full";
13372
13460
  for (const tool of tools) {
13373
13461
  const config2 = AI_TOOL_CONFIG[tool];
13374
13462
  if (written.has(config2.filePath)) continue;
13375
13463
  written.add(config2.filePath);
13376
13464
  if (config2.legacyFilePath) {
13377
- await migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
13465
+ await migrateToolConfig(
13466
+ repoRoot,
13467
+ tool,
13468
+ repoName,
13469
+ contextFolder,
13470
+ contextFiles,
13471
+ toolVariant
13472
+ );
13378
13473
  }
13379
13474
  const { created: wasCreated } = await updateToolConfig(
13380
13475
  repoRoot,
13381
13476
  tool,
13382
13477
  repoName,
13383
13478
  contextFolder,
13384
- contextFiles
13479
+ contextFiles,
13480
+ toolVariant
13385
13481
  );
13386
13482
  const action = wasCreated ? "Created" : "Updated";
13387
13483
  results.push(` ${action} ${config2.filePath}`);
@@ -13392,12 +13488,13 @@ Files are stored on our servers (not in git). Retry when online.`
13392
13488
  "codex",
13393
13489
  repoName,
13394
13490
  contextFolder,
13395
- contextFiles
13491
+ contextFiles,
13492
+ toolVariant
13396
13493
  );
13397
13494
  results.push(` ${wasCreated ? "Created" : "Updated"} AGENTS.md`);
13398
13495
  }
13399
13496
  if (tools.includes("claude-code")) {
13400
- const { relPath } = await writeClaudeHooksToRepo(repoRoot, contextFolder);
13497
+ const { relPath } = await writeClaudeHooksToRepo(repoRoot, contextFolder, toolVariant);
13401
13498
  results.push(` Configured ${relPath} (RepoWise-first + SubagentStart hooks, local-only)`);
13402
13499
  }
13403
13500
  spinner.succeed("AI tools configured");
@@ -13416,7 +13513,7 @@ Files are stored on our servers (not in git). Retry when online.`
13416
13513
  })
13417
13514
  );
13418
13515
  }
13419
- const configUpdate = { aiTools: tools, contextFolder };
13516
+ const configUpdate = { aiTools: tools, contextFolder, graphOnly };
13420
13517
  if (updatedRepos.length > 0) configUpdate.repos = updatedRepos;
13421
13518
  await mergeAndSaveConfig(configUpdate);
13422
13519
  let listenerRunning = false;
@@ -14230,7 +14327,8 @@ async function sync() {
14230
14327
  const aiTools = (existingConfig.aiTools ?? []).map((t) => t === "roo-code" ? "kilo" : t).filter((t, i, arr) => arr.indexOf(t) === i);
14231
14328
  if (aiTools.length > 0) {
14232
14329
  const contextFiles = await scanLocalContextFiles(repoRoot, DEFAULT_CONTEXT_FOLDER3);
14233
- if (contextFiles.length > 0) {
14330
+ const toolVariant = existingConfig.graphOnly ? "graph" : "full";
14331
+ if (toolVariant === "graph" || contextFiles.length > 0) {
14234
14332
  const written = /* @__PURE__ */ new Set();
14235
14333
  for (const tool of aiTools) {
14236
14334
  const config2 = AI_TOOL_CONFIG[tool];
@@ -14241,7 +14339,8 @@ async function sync() {
14241
14339
  tool,
14242
14340
  repoName,
14243
14341
  DEFAULT_CONTEXT_FOLDER3,
14244
- contextFiles
14342
+ contextFiles,
14343
+ toolVariant
14245
14344
  );
14246
14345
  }
14247
14346
  if (!written.has("AGENTS.md")) {
@@ -14250,7 +14349,8 @@ async function sync() {
14250
14349
  "codex",
14251
14350
  repoName,
14252
14351
  DEFAULT_CONTEXT_FOLDER3,
14253
- contextFiles
14352
+ contextFiles,
14353
+ toolVariant
14254
14354
  );
14255
14355
  }
14256
14356
  }