repowisestage 0.0.64 → 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 +1532 -1394
  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;
850
+ }
851
+ }
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;
858
+ }
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}`);
931
873
  }
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
874
  }
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];
944
- }
945
- });
946
-
947
- // ../../packages/shared/src/types/billing.ts
948
- var init_billing = __esm({
949
- "../../packages/shared/src/types/billing.ts"() {
950
- "use strict";
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}`);
951
888
  }
952
- });
953
-
954
- // ../../packages/shared/src/constants/platforms.ts
955
- var init_platforms = __esm({
956
- "../../packages/shared/src/constants/platforms.ts"() {
957
- "use strict";
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}`);
958
903
  }
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
- };
904
+ }
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;
1055
915
  }
1056
- });
1057
-
1058
- // ../../packages/shared/src/constants/roles.ts
1059
- function hasPermission(role, permission) {
1060
- return PERMISSIONS[role][permission];
1061
916
  }
1062
- function resolveRole(groups) {
1063
- for (const role of ROLE_PRIORITY) {
1064
- if (groups.includes(role)) return role;
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;
1065
927
  }
1066
- return ROLES.MEMBER;
1067
928
  }
1068
- var ROLES, PERMISSIONS, ROLE_PRIORITY;
1069
- var init_roles = __esm({
1070
- "../../packages/shared/src/constants/roles.ts"() {
929
+ var IS_STAGING, PLIST_LABEL, SYSTEMD_SERVICE, TASK_NAME;
930
+ var init_service_installer = __esm({
931
+ "../listener/dist/service-installer.js"() {
1071
932
  "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
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";
939
+ }
940
+ });
941
+
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)."
957
+ };
958
+ }
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;
1118
997
  }
1119
- };
1120
- ROLE_PRIORITY = [
1121
- ROLES.OWNER,
1122
- ROLES.ADMIN,
1123
- ROLES.REPO_MANAGER,
1124
- ROLES.MEMBER
1125
- ];
998
+ }
999
+ out.push({ language: lang, config: found, checkedCommands });
1126
1000
  }
1127
- });
1128
-
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"
1139
- };
1140
- const p = platformMap[platform];
1141
- const a = archMap[arch];
1142
- if (!p || !a) return null;
1143
- return `${p}-${a}`;
1001
+ return out;
1144
1002
  }
1145
- var NATIVE_LSP_VERSIONS, COURSIER_LSPS, TOOLCHAIN_LSPS;
1146
- var init_lsp_versions = __esm({
1147
- "../../packages/shared/src/constants/lsp-versions.ts"() {
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
+ }
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]"'
1082
+ }
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 }
1123
+ }
1124
+ }
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
+ }
1184
1174
  }
1185
1175
  }
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"
1213
- }
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"
1214
1203
  }
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"
1247
- }
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"
1248
1358
  }
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
- }
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"
1301
1376
  }
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
- }
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"
1338
1388
  }
1339
- }
1340
- };
1341
- COURSIER_LSPS = {
1342
- metals: {
1343
- appName: "metals",
1344
- displayName: "Metals (Scala)",
1345
- license: "Apache-2.0"
1346
- }
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) {
@@ -2426,7 +2433,8 @@ __export(sidecar_client_exports, {
2426
2433
  uploadSidecar: () => uploadSidecar
2427
2434
  });
2428
2435
  async function uploadSidecar(req, fetchImpl = fetch) {
2429
- const url = `${req.apiUrl.replace(/\/$/, "")}/v1/repos/${encodeURIComponent(req.repoId)}/typed-resolution`;
2436
+ const base = req.apiUrl.replace(/\/$/, "");
2437
+ const repoSeg = encodeURIComponent(req.repoId);
2430
2438
  const body = JSON.stringify({ ...req.sidecar, commitSha: req.commitSha });
2431
2439
  const byteLength = Buffer.byteLength(body, "utf8");
2432
2440
  if (byteLength > UPLOAD_BODY_BYTE_LIMIT) {
@@ -2439,6 +2447,43 @@ async function uploadSidecar(req, fetchImpl = fetch) {
2439
2447
  error: msg
2440
2448
  };
2441
2449
  }
2450
+ const urlRes = await fetchImpl(`${base}/v1/repos/${repoSeg}/typed-resolution/upload-url`, {
2451
+ method: "POST",
2452
+ headers: {
2453
+ "content-type": "application/json",
2454
+ authorization: `Bearer ${req.authToken}`
2455
+ },
2456
+ body: JSON.stringify({ commitSha: req.commitSha })
2457
+ });
2458
+ if (urlRes.status === 404) {
2459
+ return uploadSidecarLegacy(req, body, fetchImpl);
2460
+ }
2461
+ if (!urlRes.ok) {
2462
+ const text = await urlRes.text().catch(() => "");
2463
+ throw new Error(`upload-url request failed: HTTP ${urlRes.status.toString()} ${text}`);
2464
+ }
2465
+ const urlPayload = await urlRes.json();
2466
+ const presignedUrl = urlPayload.data?.url;
2467
+ const uploadKey = urlPayload.data?.key ?? "";
2468
+ if (!presignedUrl)
2469
+ throw new Error("upload-url response missing `data.url`");
2470
+ const putRes = await fetchImpl(presignedUrl, {
2471
+ method: "PUT",
2472
+ headers: { "content-type": "application/json" },
2473
+ body
2474
+ });
2475
+ if (!putRes.ok) {
2476
+ const text = await putRes.text().catch(() => "");
2477
+ throw new Error(`S3 sidecar PUT failed: HTTP ${putRes.status.toString()} ${text}`);
2478
+ }
2479
+ return {
2480
+ uploaded: true,
2481
+ resolutionCount: req.sidecar.resolutions.length,
2482
+ uploadKey
2483
+ };
2484
+ }
2485
+ async function uploadSidecarLegacy(req, body, fetchImpl) {
2486
+ const url = `${req.apiUrl.replace(/\/$/, "")}/v1/repos/${encodeURIComponent(req.repoId)}/typed-resolution`;
2442
2487
  const res = await fetchImpl(url, {
2443
2488
  method: "POST",
2444
2489
  headers: {
@@ -2551,7 +2596,7 @@ var init_sidecar_client = __esm({
2551
2596
  "../listener/dist/typed-resolution/sidecar-client.js"() {
2552
2597
  "use strict";
2553
2598
  init_src();
2554
- UPLOAD_BODY_BYTE_LIMIT = 8 * 1024 * 1024;
2599
+ UPLOAD_BODY_BYTE_LIMIT = 32 * 1024 * 1024;
2555
2600
  }
2556
2601
  });
2557
2602
 
@@ -3067,17 +3112,53 @@ Treat RepoWise as your default starting point for every task that touches this c
3067
3112
 
3068
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.
3069
3114
 
3070
- 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:
3071
3116
 
3072
3117
  _Powered by RepoWise._
3073
3118
 
3074
- 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.
3075
3120
 
3076
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.
3077
3122
  ${END_MARKER}`;
3078
- 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") {
3079
3160
  const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
3080
- const canonicalBlock = AI_AGENT_USAGE_INSTRUCTIONS.replace(/\n/g, lineEnding);
3161
+ const canonicalBlock = agentInstructionsFor(variant).replace(/\n/g, lineEnding);
3081
3162
  const startIdx = content.indexOf(START_MARKER);
3082
3163
  const endIdx = content.indexOf(END_MARKER);
3083
3164
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -3121,14 +3202,20 @@ function claudeToolPrefix(repoName) {
3121
3202
  }
3122
3203
  var GRAPH_TOOLS = "find_symbol, get_symbol, get_impact, find_callers, find_references, batch_query";
3123
3204
  var LSP_TOOLS = "lsp_definition, lsp_references, lsp_hover";
3124
- function buildRepoWiseFirstDirective(repoName, contextFolder, style = "generic") {
3205
+ function buildRepoWiseFirstDirective(repoName, contextFolder, style = "generic", variant = "full") {
3125
3206
  const repo = sanitizeForShellText(repoName);
3126
3207
  const folder = sanitizeForShellText(contextFolder);
3127
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
+ }
3128
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.`;
3129
3213
  }
3130
- function buildSubagentDirective(contextFolder) {
3214
+ function buildSubagentDirective(contextFolder, variant = "full") {
3131
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
+ }
3132
3219
  return `IMPORTANT: Read ${folder}/project-overview.md before performing any work. This file maps every context file to its domain.`;
3133
3220
  }
3134
3221
  var GATE_SCRIPT_REL = ".claude/repowise-gate.cjs";
@@ -3137,9 +3224,10 @@ var GATE_MAX_DENIES = 2;
3137
3224
  function buildGateHookCommand() {
3138
3225
  return `node "$CLAUDE_PROJECT_DIR/${GATE_SCRIPT_REL}" ${REPOWISE_MANAGED_COMMENT}`;
3139
3226
  }
3140
- function buildGateScript(contextFolder) {
3227
+ function buildGateScript(contextFolder, variant = "full") {
3141
3228
  const folder = contextFolder.replace(/[^A-Za-z0-9_./-]/g, "");
3142
- 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);
3143
3231
  }
3144
3232
  var GATE_SCRIPT_TEMPLATE = [
3145
3233
  "#!/usr/bin/env node",
@@ -3178,7 +3266,7 @@ var GATE_SCRIPT_TEMPLATE = [
3178
3266
  "if (state.consulted) { logMiss(); process.exit(0); }",
3179
3267
  "if ((state.denies || 0) >= MAX_DENIES) { process.exit(0); }",
3180
3268
  "state.denies = (state.denies || 0) + 1; writeState(state);",
3181
- '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__";',
3182
3270
  'process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }));',
3183
3271
  "process.exit(0);",
3184
3272
  ""
@@ -3287,7 +3375,7 @@ function mergeClaudeHookSettings(existingContent, params) {
3287
3375
  hooks: [
3288
3376
  {
3289
3377
  type: "command",
3290
- command: buildHookEchoCommand("SubagentStart", buildSubagentDirective(params.contextFolder))
3378
+ command: buildHookEchoCommand("SubagentStart", buildSubagentDirective(params.contextFolder, params.variant))
3291
3379
  }
3292
3380
  ]
3293
3381
  },
@@ -3295,7 +3383,7 @@ function mergeClaudeHookSettings(existingContent, params) {
3295
3383
  hooks: [
3296
3384
  {
3297
3385
  type: "command",
3298
- command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "claude"))
3386
+ command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "claude", params.variant))
3299
3387
  },
3300
3388
  // Re-arm the per-question gate (silent; the echo above carries the reminder).
3301
3389
  { type: "command", command: buildGateHookCommand() }
@@ -3320,7 +3408,7 @@ function mergeGeminiHookSettings(existingContent, params) {
3320
3408
  name: "repowise-first",
3321
3409
  command: buildPlainJsonEchoCommand({
3322
3410
  hookSpecificOutput: {
3323
- additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3411
+ additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3324
3412
  }
3325
3413
  })
3326
3414
  }
@@ -3337,7 +3425,7 @@ function mergeCodexHookSettings(existingContent, params) {
3337
3425
  hooks: [
3338
3426
  {
3339
3427
  type: "command",
3340
- command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder))
3428
+ command: buildHookEchoCommand("UserPromptSubmit", buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant))
3341
3429
  }
3342
3430
  ]
3343
3431
  }
@@ -3348,7 +3436,7 @@ function removeCodexHookSettings(existingContent) {
3348
3436
  }
3349
3437
  function buildCopilotHooksFile(params) {
3350
3438
  const command = buildPlainJsonEchoCommand({
3351
- additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3439
+ additionalContext: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3352
3440
  });
3353
3441
  return JSON.stringify({
3354
3442
  version: 1,
@@ -3359,7 +3447,7 @@ function buildCopilotHooksFile(params) {
3359
3447
  }
3360
3448
  function buildClineHookScript(params) {
3361
3449
  const command = buildPlainJsonEchoCommand({
3362
- contextModification: buildRepoWiseFirstDirective(params.repoName, params.contextFolder)
3450
+ contextModification: buildRepoWiseFirstDirective(params.repoName, params.contextFolder, "generic", params.variant)
3363
3451
  });
3364
3452
  return [
3365
3453
  "#!/bin/sh",
@@ -3444,7 +3532,7 @@ async function applyGitignoreChanges(repoRoot, changes) {
3444
3532
  await writeFile(path, next, "utf-8");
3445
3533
  }
3446
3534
  }
3447
- async function writeClaudeHooksToRepo(repoRoot, contextFolder) {
3535
+ async function writeClaudeHooksToRepo(repoRoot, contextFolder, variant = "full") {
3448
3536
  const repoName = basename(repoRoot);
3449
3537
  const projectTracked = await isGitTracked(repoRoot, CLAUDE_PROJECT_SETTINGS);
3450
3538
  if (projectTracked) {
@@ -3468,14 +3556,14 @@ async function writeClaudeHooksToRepo(repoRoot, contextFolder) {
3468
3556
  existing = await readFile(targetPath, "utf-8");
3469
3557
  } catch {
3470
3558
  }
3471
- const merged = mergeClaudeHookSettings(existing, { repoName, contextFolder });
3559
+ const merged = mergeClaudeHookSettings(existing, { repoName, contextFolder, variant });
3472
3560
  let status2 = "unchanged";
3473
3561
  if (merged !== existing) {
3474
3562
  await mkdir(join2(repoRoot, ".claude"), { recursive: true });
3475
3563
  await writeFile(targetPath, merged, "utf-8");
3476
3564
  status2 = "written";
3477
3565
  }
3478
- const gateScript = buildGateScript(contextFolder);
3566
+ const gateScript = buildGateScript(contextFolder, variant);
3479
3567
  const gatePath = join2(repoRoot, GATE_SCRIPT_REL);
3480
3568
  let gateExisting = null;
3481
3569
  try {
@@ -3521,8 +3609,8 @@ async function removeClaudeHooksFromRepo(repoRoot) {
3521
3609
  }
3522
3610
 
3523
3611
  // ../../packages/shared/dist/lib/ai-tools.js
3524
- var AI_TOOLS_REFERENCE_REV = 3;
3525
- 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);
3526
3614
  var AI_TOOL_CONFIG = {
3527
3615
  cursor: {
3528
3616
  label: "Cursor",
@@ -3664,7 +3752,7 @@ function sanitizeRepoName(name) {
3664
3752
  function fileDescriptionFromName(fileName) {
3665
3753
  return fileName.replace(/\.md$/, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3666
3754
  }
3667
- function generateReference(tool, repoName, contextFolder, contextFiles) {
3755
+ function generateReference(tool, repoName, contextFolder, contextFiles, variant = "full") {
3668
3756
  const config2 = AI_TOOL_CONFIG[tool];
3669
3757
  const safeName = sanitizeRepoName(repoName);
3670
3758
  const fileLines = contextFiles.map((f) => {
@@ -3676,19 +3764,21 @@ function generateReference(tool, repoName, contextFolder, contextFiles) {
3676
3764
  const hasFiles = fileLines.length > 0;
3677
3765
  const mcpName = repoName.replace(/`/g, "");
3678
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.`;
3679
3769
  const contentLines = [
3680
3770
  `## Project Context \u2014 ${safeName}`,
3681
3771
  "",
3682
- `This repository has AI-optimized context files generated by RepoWise.`,
3683
- "",
3684
- AI_AGENT_USAGE_INSTRUCTIONS,
3772
+ introLine,
3685
3773
  "",
3686
- `**Start here:** \`${contextFolder}/project-overview.md\` \u2014 the routing document that maps every context file to its domain.`,
3687
- "",
3688
- mcpNamingNote,
3774
+ agentInstructionsFor(variant),
3689
3775
  ""
3690
3776
  ];
3691
- 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) {
3692
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.`);
3693
3783
  }
3694
3784
  if (config2.owned) {
@@ -3701,14 +3791,14 @@ function generateReference(tool, repoName, contextFolder, contextFiles) {
3701
3791
  }
3702
3792
  return [config2.markerStart, "", ...contentLines, "", config2.markerEnd].join("\n");
3703
3793
  }
3704
- async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
3794
+ async function updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant = "full") {
3705
3795
  const config2 = AI_TOOL_CONFIG[tool];
3706
3796
  const fullPath = join3(repoRoot, config2.filePath);
3707
3797
  const dir = dirname(fullPath);
3708
3798
  if (dir !== repoRoot) {
3709
3799
  await mkdir2(dir, { recursive: true });
3710
3800
  }
3711
- const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles);
3801
+ const referenceBlock = generateReference(tool, repoName, contextFolder, contextFiles, variant);
3712
3802
  if (config2.owned) {
3713
3803
  let created2 = true;
3714
3804
  try {
@@ -3771,7 +3861,7 @@ async function removeToolConfig(repoRoot, tool) {
3771
3861
  const content = (before + after).replace(/\n{3,}/g, "\n\n").trim() + "\n";
3772
3862
  await writeFile2(fullPath, content, "utf-8");
3773
3863
  }
3774
- async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles) {
3864
+ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant = "full") {
3775
3865
  const config2 = AI_TOOL_CONFIG[tool];
3776
3866
  if (!config2.legacyFilePath)
3777
3867
  return { migrated: false, legacyRemoved: false };
@@ -3781,11 +3871,11 @@ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contex
3781
3871
  const s = await stat(legacyPath);
3782
3872
  legacyIsFile = s.isFile();
3783
3873
  } catch {
3784
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3874
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3785
3875
  return { migrated: false, legacyRemoved: false };
3786
3876
  }
3787
3877
  if (!legacyIsFile) {
3788
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3878
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3789
3879
  return { migrated: false, legacyRemoved: false };
3790
3880
  }
3791
3881
  const legacyContent = await readFile2(legacyPath, "utf-8");
@@ -3815,7 +3905,7 @@ async function migrateToolConfig(repoRoot, tool, repoName, contextFolder, contex
3815
3905
  } else {
3816
3906
  await unlink2(legacyPath);
3817
3907
  }
3818
- await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
3908
+ await updateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles, variant);
3819
3909
  return { migrated: true, legacyRemoved: cleaned.length === 0 };
3820
3910
  }
3821
3911
  async function fileExists(path) {
@@ -3891,6 +3981,9 @@ async function scanLocalContextFiles(repoRoot, contextFolder) {
3891
3981
  }
3892
3982
  }
3893
3983
 
3984
+ // ../listener/dist/main.js
3985
+ init_src();
3986
+
3894
3987
  // ../listener/dist/lib/config.js
3895
3988
  init_config_dir();
3896
3989
  import { readFile as readFile3, writeFile as writeFile3, rename, unlink as unlink3, mkdir as mkdir3, chmod, open } from "fs/promises";
@@ -3926,7 +4019,8 @@ async function getListenerConfig() {
3926
4019
  noAutoUpdate: raw.noAutoUpdate ?? false,
3927
4020
  lspEnabled: raw.lspEnabled ?? true,
3928
4021
  lspAutoWarm: raw.lspAutoWarm ?? true,
3929
- ...lspOverrides ? { lspOverrides } : {}
4022
+ ...lspOverrides ? { lspOverrides } : {},
4023
+ ...typeof raw.graphOnly === "boolean" ? { graphOnly: raw.graphOnly } : {}
3930
4024
  };
3931
4025
  } catch {
3932
4026
  return {
@@ -3966,7 +4060,10 @@ async function saveListenerConfig(config2) {
3966
4060
  // the listener round-trips them here unchanged.
3967
4061
  lspEnabled: config2.lspEnabled,
3968
4062
  lspAutoWarm: config2.lspAutoWarm,
3969
- ...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 } : {}
3970
4067
  };
3971
4068
  const tmpPath = configPath + ".tmp";
3972
4069
  try {
@@ -7977,7 +8074,7 @@ var claudeCodeHooksWriter = {
7977
8074
  return await fileExists3(join24(repoRoot, "CLAUDE.md")) || await fileExists3(join24(home, ".claude.json"));
7978
8075
  },
7979
8076
  async write(ctx) {
7980
- const result = await writeClaudeHooksToRepo(ctx.repoRoot, ctx.contextFolder ?? "repowise-context");
8077
+ const result = await writeClaudeHooksToRepo(ctx.repoRoot, ctx.contextFolder ?? "repowise-context", ctx.variant);
7981
8078
  return { status: result.status, path: join24(ctx.repoRoot, result.relPath) };
7982
8079
  },
7983
8080
  async remove(ctx) {
@@ -8047,7 +8144,8 @@ var clineHooksWriter = {
8047
8144
  const path = join26(ctx.repoRoot, SCRIPT_REL);
8048
8145
  const next = buildClineHookScript({
8049
8146
  repoName: ctx.repoName,
8050
- contextFolder: ctx.contextFolder ?? "repowise-context"
8147
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8148
+ variant: ctx.variant
8051
8149
  });
8052
8150
  const existing = await readOrNull(path);
8053
8151
  if (existing !== null && !existing.includes(OWNERSHIP_MARKER)) {
@@ -8159,7 +8257,8 @@ var codexHooksWriter = {
8159
8257
  const existing = await readOrNull2(path);
8160
8258
  const merged = mergeCodexHookSettings(existing, {
8161
8259
  repoName: ctx.repoName,
8162
- contextFolder: ctx.contextFolder ?? "repowise-context"
8260
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8261
+ variant: ctx.variant
8163
8262
  });
8164
8263
  if (merged === existing) {
8165
8264
  return { status: "unchanged", path };
@@ -8245,7 +8344,8 @@ var copilotHooksWriter = {
8245
8344
  const path = join30(ctx.repoRoot, HOOKS_REL2);
8246
8345
  const next = buildCopilotHooksFile({
8247
8346
  repoName: ctx.repoName,
8248
- contextFolder: ctx.contextFolder ?? "repowise-context"
8347
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8348
+ variant: ctx.variant
8249
8349
  });
8250
8350
  const existing = await readOrNull3(path);
8251
8351
  if (await isGitTracked(ctx.repoRoot, HOOKS_REL2)) {
@@ -8384,7 +8484,8 @@ var geminiHooksWriter = {
8384
8484
  const existing = await readOrNull4(path);
8385
8485
  const merged = mergeGeminiHookSettings(existing, {
8386
8486
  repoName: ctx.repoName,
8387
- contextFolder: ctx.contextFolder ?? "repowise-context"
8487
+ contextFolder: ctx.contextFolder ?? "repowise-context",
8488
+ variant: ctx.variant
8388
8489
  });
8389
8490
  if (merged === existing) {
8390
8491
  return { status: "unchanged", path };
@@ -8549,7 +8650,8 @@ async function runAutoConfig(opts) {
8549
8650
  shimCmd: opts.shimCmd,
8550
8651
  home,
8551
8652
  contextFolder: opts.contextFolder,
8552
- selectedTools: opts.selectedTools
8653
+ selectedTools: opts.selectedTools,
8654
+ variant: opts.variant
8553
8655
  });
8554
8656
  results.push({ tool: writer.tool, detected: true, outcome });
8555
8657
  } catch (err) {
@@ -10195,7 +10297,8 @@ async function readRawToolConfig() {
10195
10297
  const raw = JSON.parse(data);
10196
10298
  return {
10197
10299
  aiTools: Array.isArray(raw["aiTools"]) ? raw["aiTools"] : void 0,
10198
- 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
10199
10302
  };
10200
10303
  } catch {
10201
10304
  return {};
@@ -10216,12 +10319,14 @@ async function updateToolConfigsForRepo(localPath, config2, state, repoId) {
10216
10319
  }
10217
10320
  await migrateRooToKilo(localPath).catch(() => {
10218
10321
  });
10322
+ const variant = config2.graphOnly ? "graph" : "full";
10219
10323
  const contextFiles = await scanLocalContextFiles(localPath, contextFolder);
10220
- if (contextFiles.length === 0)
10324
+ if (contextFiles.length === 0 && variant === "full")
10221
10325
  return;
10222
10326
  const hash = JSON.stringify({
10223
10327
  rev: AI_TOOLS_REFERENCE_REV,
10224
10328
  block: AGENT_INSTRUCTIONS_HASH,
10329
+ variant,
10225
10330
  tools,
10226
10331
  files: contextFiles.map((f) => f.fileName)
10227
10332
  });
@@ -10236,13 +10341,13 @@ async function updateToolConfigsForRepo(localPath, config2, state, repoId) {
10236
10341
  continue;
10237
10342
  written.add(toolConfig.filePath);
10238
10343
  if (toolConfig.legacyFilePath) {
10239
- await migrateToolConfig(localPath, tool, repoName, contextFolder, contextFiles);
10344
+ await migrateToolConfig(localPath, tool, repoName, contextFolder, contextFiles, variant);
10240
10345
  } else {
10241
- await updateToolConfig(localPath, tool, repoName, contextFolder, contextFiles);
10346
+ await updateToolConfig(localPath, tool, repoName, contextFolder, contextFiles, variant);
10242
10347
  }
10243
10348
  }
10244
10349
  if (!written.has("AGENTS.md")) {
10245
- await updateToolConfig(localPath, "codex", repoName, contextFolder, contextFiles);
10350
+ await updateToolConfig(localPath, "codex", repoName, contextFolder, contextFiles, variant);
10246
10351
  }
10247
10352
  if (!state.repos[repoId]) {
10248
10353
  state.repos[repoId] = { lastSyncTimestamp: "", lastSyncCommitSha: null };
@@ -10561,7 +10666,8 @@ async function checkStaleContext(repos, state, groups) {
10561
10666
  }
10562
10667
  async function reconcileMcpConfigs(repos, packageName) {
10563
10668
  const shimCmd = packageName;
10564
- const { contextFolder, aiTools } = await readRawToolConfig();
10669
+ const { contextFolder, aiTools, graphOnly } = await readRawToolConfig();
10670
+ const variant = graphOnly ? "graph" : "full";
10565
10671
  for (const repo of repos) {
10566
10672
  if (!repo.localPath)
10567
10673
  continue;
@@ -10578,7 +10684,8 @@ async function reconcileMcpConfigs(repos, packageName) {
10578
10684
  repoId: repo.repoId,
10579
10685
  shimCmd,
10580
10686
  contextFolder,
10581
- selectedTools: aiTools
10687
+ selectedTools: aiTools,
10688
+ variant
10582
10689
  });
10583
10690
  const written = results.filter((r) => r.detected && r.outcome?.status === "written");
10584
10691
  if (written.length > 0) {
@@ -10590,6 +10697,8 @@ async function reconcileMcpConfigs(repos, packageName) {
10590
10697
  }
10591
10698
  }
10592
10699
  async function reconcileAgentInstructions(repos) {
10700
+ const { graphOnly } = await readRawToolConfig();
10701
+ const variant = graphOnly ? "graph" : "full";
10593
10702
  for (const repo of repos) {
10594
10703
  const path = join40(repo.localPath, "repowise-context", "project-overview.md");
10595
10704
  let content;
@@ -10603,7 +10712,7 @@ async function reconcileAgentInstructions(repos) {
10603
10712
  console.warn(`[reconcile] read failed for ${repo.repoId}:`, err instanceof Error ? err.message : String(err));
10604
10713
  continue;
10605
10714
  }
10606
- const injected = injectAgentInstructions(content);
10715
+ const injected = injectAgentInstructions(content, variant);
10607
10716
  if (injected === content)
10608
10717
  continue;
10609
10718
  try {
@@ -11037,6 +11146,22 @@ async function startListener() {
11037
11146
  if (response.latestCliVersion && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/.test(response.latestCliVersion)) {
11038
11147
  latestCliVersion = response.latestCliVersion;
11039
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
+ }
11040
11165
  if (shouldReconcile) {
11041
11166
  try {
11042
11167
  const freshConfig = await getListenerConfig();
@@ -13079,7 +13204,7 @@ async function create() {
13079
13204
  let graphOnly = false;
13080
13205
  try {
13081
13206
  const usage = await apiRequest("/v1/account/usage");
13082
- graphOnly = usage.plan === "free";
13207
+ graphOnly = agentInstructionsVariantForPlan(usage.plan ?? "free") === "graph";
13083
13208
  } catch {
13084
13209
  }
13085
13210
  let pollAttempts = 0;
@@ -13331,19 +13456,28 @@ Files are stored on our servers (not in git). Retry when online.`
13331
13456
  spinner.start("Configuring AI tools...");
13332
13457
  const results = [];
13333
13458
  const written = /* @__PURE__ */ new Set();
13459
+ const toolVariant = graphOnly ? "graph" : "full";
13334
13460
  for (const tool of tools) {
13335
13461
  const config2 = AI_TOOL_CONFIG[tool];
13336
13462
  if (written.has(config2.filePath)) continue;
13337
13463
  written.add(config2.filePath);
13338
13464
  if (config2.legacyFilePath) {
13339
- await migrateToolConfig(repoRoot, tool, repoName, contextFolder, contextFiles);
13465
+ await migrateToolConfig(
13466
+ repoRoot,
13467
+ tool,
13468
+ repoName,
13469
+ contextFolder,
13470
+ contextFiles,
13471
+ toolVariant
13472
+ );
13340
13473
  }
13341
13474
  const { created: wasCreated } = await updateToolConfig(
13342
13475
  repoRoot,
13343
13476
  tool,
13344
13477
  repoName,
13345
13478
  contextFolder,
13346
- contextFiles
13479
+ contextFiles,
13480
+ toolVariant
13347
13481
  );
13348
13482
  const action = wasCreated ? "Created" : "Updated";
13349
13483
  results.push(` ${action} ${config2.filePath}`);
@@ -13354,12 +13488,13 @@ Files are stored on our servers (not in git). Retry when online.`
13354
13488
  "codex",
13355
13489
  repoName,
13356
13490
  contextFolder,
13357
- contextFiles
13491
+ contextFiles,
13492
+ toolVariant
13358
13493
  );
13359
13494
  results.push(` ${wasCreated ? "Created" : "Updated"} AGENTS.md`);
13360
13495
  }
13361
13496
  if (tools.includes("claude-code")) {
13362
- const { relPath } = await writeClaudeHooksToRepo(repoRoot, contextFolder);
13497
+ const { relPath } = await writeClaudeHooksToRepo(repoRoot, contextFolder, toolVariant);
13363
13498
  results.push(` Configured ${relPath} (RepoWise-first + SubagentStart hooks, local-only)`);
13364
13499
  }
13365
13500
  spinner.succeed("AI tools configured");
@@ -13378,7 +13513,7 @@ Files are stored on our servers (not in git). Retry when online.`
13378
13513
  })
13379
13514
  );
13380
13515
  }
13381
- const configUpdate = { aiTools: tools, contextFolder };
13516
+ const configUpdate = { aiTools: tools, contextFolder, graphOnly };
13382
13517
  if (updatedRepos.length > 0) configUpdate.repos = updatedRepos;
13383
13518
  await mergeAndSaveConfig(configUpdate);
13384
13519
  let listenerRunning = false;
@@ -14192,7 +14327,8 @@ async function sync() {
14192
14327
  const aiTools = (existingConfig.aiTools ?? []).map((t) => t === "roo-code" ? "kilo" : t).filter((t, i, arr) => arr.indexOf(t) === i);
14193
14328
  if (aiTools.length > 0) {
14194
14329
  const contextFiles = await scanLocalContextFiles(repoRoot, DEFAULT_CONTEXT_FOLDER3);
14195
- if (contextFiles.length > 0) {
14330
+ const toolVariant = existingConfig.graphOnly ? "graph" : "full";
14331
+ if (toolVariant === "graph" || contextFiles.length > 0) {
14196
14332
  const written = /* @__PURE__ */ new Set();
14197
14333
  for (const tool of aiTools) {
14198
14334
  const config2 = AI_TOOL_CONFIG[tool];
@@ -14203,7 +14339,8 @@ async function sync() {
14203
14339
  tool,
14204
14340
  repoName,
14205
14341
  DEFAULT_CONTEXT_FOLDER3,
14206
- contextFiles
14342
+ contextFiles,
14343
+ toolVariant
14207
14344
  );
14208
14345
  }
14209
14346
  if (!written.has("AGENTS.md")) {
@@ -14212,7 +14349,8 @@ async function sync() {
14212
14349
  "codex",
14213
14350
  repoName,
14214
14351
  DEFAULT_CONTEXT_FOLDER3,
14215
- contextFiles
14352
+ contextFiles,
14353
+ toolVariant
14216
14354
  );
14217
14355
  }
14218
14356
  }