glassbox 0.20.0-beta.1 → 0.20.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11937,21 +11937,21 @@ var init_to_json_schema = __esm({
11937
11937
  // node_modules/zod/v4/core/json-schema-processors.js
11938
11938
  function toJSONSchema(input, params) {
11939
11939
  if ("_idmap" in input) {
11940
- const registry2 = input;
11940
+ const registry3 = input;
11941
11941
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
11942
11942
  const defs = {};
11943
- for (const entry of registry2._idmap.entries()) {
11943
+ for (const entry of registry3._idmap.entries()) {
11944
11944
  const [_, schema] = entry;
11945
11945
  process2(schema, ctx2);
11946
11946
  }
11947
11947
  const schemas = {};
11948
11948
  const external = {
11949
- registry: registry2,
11949
+ registry: registry3,
11950
11950
  uri: params?.uri,
11951
11951
  defs
11952
11952
  };
11953
11953
  ctx2.external = external;
11954
- for (const entry of registry2._idmap.entries()) {
11954
+ for (const entry of registry3._idmap.entries()) {
11955
11955
  const [key, schema] = entry;
11956
11956
  extractDefs(ctx2, schema);
11957
11957
  schemas[key] = finalize(ctx2, schema);
@@ -17254,6 +17254,178 @@ var init_debug = __esm({
17254
17254
  }
17255
17255
  });
17256
17256
 
17257
+ // src/global-config.ts
17258
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
17259
+ import { homedir } from "os";
17260
+ import { join as join4 } from "path";
17261
+ function resolveGlobalConfigDir() {
17262
+ const override = process.env.GLASSBOX_CONFIG_DIR;
17263
+ if (override !== void 0 && override.trim() !== "") return override;
17264
+ return join4(homedir(), ".glassbox");
17265
+ }
17266
+ function readGlobalConfig() {
17267
+ try {
17268
+ if (existsSync4(GLOBAL_CONFIG_PATH)) {
17269
+ const raw2 = JSON.parse(readFileSync3(GLOBAL_CONFIG_PATH, "utf-8"));
17270
+ const parsed = GlobalConfigSchema.safeParse(raw2);
17271
+ if (parsed.success) return parsed.data;
17272
+ }
17273
+ } catch {
17274
+ }
17275
+ return {};
17276
+ }
17277
+ function writeGlobalConfig(config2) {
17278
+ mkdirSync5(GLOBAL_CONFIG_DIR, { recursive: true });
17279
+ writeFileSync3(GLOBAL_CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
17280
+ try {
17281
+ chmodSync(GLOBAL_CONFIG_PATH, 384);
17282
+ } catch {
17283
+ }
17284
+ }
17285
+ function updateGlobalConfig(mutator) {
17286
+ const cfg = readGlobalConfig();
17287
+ const result = mutator(cfg);
17288
+ writeGlobalConfig(result === void 0 ? cfg : result);
17289
+ }
17290
+ var GlobalConfigSchema, GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH;
17291
+ var init_global_config = __esm({
17292
+ "src/global-config.ts"() {
17293
+ "use strict";
17294
+ init_zod();
17295
+ GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
17296
+ GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
17297
+ GLOBAL_CONFIG_PATH = join4(GLOBAL_CONFIG_DIR, "config.json");
17298
+ }
17299
+ });
17300
+
17301
+ // src/ai/keychain.ts
17302
+ import { spawnSync as spawnSync5 } from "child_process";
17303
+ function winTargetForAccount(account) {
17304
+ return `glassbox-${account}`;
17305
+ }
17306
+ function winCredTarget(platform) {
17307
+ return winTargetForAccount(`${platform}-api-key`);
17308
+ }
17309
+ function getSecretFromKeychain(account) {
17310
+ const os = process.platform;
17311
+ try {
17312
+ if (os === "darwin") {
17313
+ const r = spawnSync5("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
17314
+ const result = r.stdout.trim();
17315
+ return r.status === 0 && result !== "" ? result : null;
17316
+ }
17317
+ if (os === "linux") {
17318
+ const r = spawnSync5("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
17319
+ const result = r.stdout.trim();
17320
+ return r.status === 0 && result !== "" ? result : null;
17321
+ }
17322
+ if (os === "win32") {
17323
+ const target = winTargetForAccount(account);
17324
+ const list = spawnSync5("cmdkey", ["/list:" + target], { encoding: "utf-8" });
17325
+ if (list.status !== 0 || (list.stdout || "").includes("* NONE *")) return null;
17326
+ const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
17327
+ const r = spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
17328
+ const result = r.stdout.trim();
17329
+ return r.status === 0 && result !== "" ? result : null;
17330
+ }
17331
+ } catch {
17332
+ return null;
17333
+ }
17334
+ return null;
17335
+ }
17336
+ function getKeyFromKeychain(platform) {
17337
+ return getSecretFromKeychain(`${platform}-api-key`);
17338
+ }
17339
+ function assertSpawnOk(label, r) {
17340
+ if (r.error) throw new Error(`${label} failed: ${r.error.message}`);
17341
+ if (r.status !== 0) {
17342
+ const detail = (String(r.stderr) || String(r.stdout)).trim();
17343
+ throw new Error(`${label} failed (exit ${String(r.status)})${detail ? `: ${detail}` : ""}`);
17344
+ }
17345
+ }
17346
+ function saveSecretToKeychain(account, value, label = "Glassbox") {
17347
+ const os = process.platform;
17348
+ if (os === "darwin") {
17349
+ spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
17350
+ assertSpawnOk("Keychain write", spawnSync5("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", value], { encoding: "utf-8" }));
17351
+ return;
17352
+ }
17353
+ if (os === "linux") {
17354
+ assertSpawnOk("System keyring write", spawnSync5("secret-tool", ["store", `--label=${label}`, "service", "glassbox", "account", account], { input: value, encoding: "utf-8" }));
17355
+ return;
17356
+ }
17357
+ if (os === "win32") {
17358
+ const target = winTargetForAccount(account);
17359
+ const escapedValue = value.replace(/'/g, "''");
17360
+ const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedValue}'`;
17361
+ assertSpawnOk("Credential Manager write", spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" }));
17362
+ }
17363
+ }
17364
+ function deleteSecretFromKeychain(account) {
17365
+ const os = process.platform;
17366
+ try {
17367
+ if (os === "darwin") {
17368
+ spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
17369
+ } else if (os === "linux") {
17370
+ spawnSync5("secret-tool", ["clear", "service", "glassbox", "account", account], { stdio: "pipe" });
17371
+ } else if (os === "win32") {
17372
+ spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: `cmdkey /delete:'${winTargetForAccount(account)}'`, encoding: "utf-8" });
17373
+ }
17374
+ } catch {
17375
+ }
17376
+ }
17377
+ function saveKeyToKeychain(platform, key) {
17378
+ saveSecretToKeychain(`${platform}-api-key`, key, "Glassbox API Key");
17379
+ }
17380
+ function isKeychainAvailable() {
17381
+ const os = process.platform;
17382
+ if (os === "darwin" || os === "win32") return true;
17383
+ if (os === "linux") {
17384
+ return spawnSync5("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
17385
+ }
17386
+ return false;
17387
+ }
17388
+ function getKeychainLabel() {
17389
+ const os = process.platform;
17390
+ if (os === "darwin") return "Keychain";
17391
+ if (os === "linux") return "System Keyring";
17392
+ if (os === "win32") return "Credential Manager";
17393
+ return "System Keychain";
17394
+ }
17395
+ var WIN_CRED_READ_PS;
17396
+ var init_keychain = __esm({
17397
+ "src/ai/keychain.ts"() {
17398
+ "use strict";
17399
+ WIN_CRED_READ_PS = `
17400
+ Add-Type -TypeDefinition @'
17401
+ using System;
17402
+ using System.Runtime.InteropServices;
17403
+ public class CredHelper {
17404
+ [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
17405
+ static extern bool CredRead(string t, int type, int f, out IntPtr p);
17406
+ [DllImport("advapi32")]
17407
+ static extern void CredFree(IntPtr p);
17408
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
17409
+ struct CRED {
17410
+ public int Flags; public int Type; public string TargetName; public string Comment;
17411
+ public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob;
17412
+ public int Persist; public int AttributeCount; public IntPtr Attributes;
17413
+ public string TargetAlias; public string UserName;
17414
+ }
17415
+ public static string Read(string target) {
17416
+ IntPtr ptr;
17417
+ if (!CredRead(target, 1, 0, out ptr)) return "";
17418
+ CRED c = (CRED)Marshal.PtrToStructure(ptr, typeof(CRED));
17419
+ string r = Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
17420
+ CredFree(ptr);
17421
+ return r;
17422
+ }
17423
+ }
17424
+ '@
17425
+ `;
17426
+ }
17427
+ });
17428
+
17257
17429
  // src/git/image-blobs.ts
17258
17430
  var image_blobs_exports = {};
17259
17431
  __export(image_blobs_exports, {
@@ -17266,33 +17438,1004 @@ import { join as join5 } from "path";
17266
17438
  function blobDir(dataDir) {
17267
17439
  return join5(dataDir, "image-blobs");
17268
17440
  }
17269
- function blobName(fileId, side) {
17270
- return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
17441
+ function blobName(fileId, side) {
17442
+ return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
17443
+ }
17444
+ function writeImageBlob(dataDir, fileId, side, bytes) {
17445
+ if (bytes.length === 0) return;
17446
+ const dir = blobDir(dataDir);
17447
+ mkdirSync6(dir, { recursive: true });
17448
+ writeFileSync4(join5(dir, blobName(fileId, side)), bytes);
17449
+ }
17450
+ function readImageBlob(dataDir, fileId, side) {
17451
+ const path = join5(blobDir(dataDir), blobName(fileId, side));
17452
+ if (!existsSync5(path)) return null;
17453
+ try {
17454
+ return readFileSync4(path);
17455
+ } catch {
17456
+ return null;
17457
+ }
17458
+ }
17459
+ function clearImageBlobs(dataDir) {
17460
+ try {
17461
+ rmSync3(blobDir(dataDir), { recursive: true, force: true });
17462
+ } catch {
17463
+ }
17464
+ }
17465
+ var init_image_blobs = __esm({
17466
+ "src/git/image-blobs.ts"() {
17467
+ "use strict";
17468
+ }
17469
+ });
17470
+
17471
+ // src/feature-flags.ts
17472
+ var PLUGINS_ENABLED;
17473
+ var init_feature_flags = __esm({
17474
+ "src/feature-flags.ts"() {
17475
+ "use strict";
17476
+ PLUGINS_ENABLED = process.env.GLASSBOX_PLUGINS_DISABLED === "1" ? false : true;
17477
+ }
17478
+ });
17479
+
17480
+ // src/api/_runner.ts
17481
+ function currentReviewId() {
17482
+ if (typeof document === "undefined") return "";
17483
+ return document.body.dataset.reviewId ?? "";
17484
+ }
17485
+ async function apiCall(responseSchema, path, opts = {}) {
17486
+ const separator = path.includes("?") ? "&" : "?";
17487
+ const url2 = "/api" + path + separator + "reviewId=" + encodeURIComponent(currentReviewId());
17488
+ const res = await fetch(url2, {
17489
+ headers: { "Content-Type": "application/json" },
17490
+ method: opts.method,
17491
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
17492
+ });
17493
+ const json2 = await res.json();
17494
+ const result = responseSchema.safeParse(json2);
17495
+ if (!result.success) {
17496
+ const summary = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
17497
+ throw new Error(`API response from ${path} failed validation: ${summary}`);
17498
+ }
17499
+ return result.data;
17500
+ }
17501
+ function qs(params) {
17502
+ const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
17503
+ if (entries.length === 0) return "";
17504
+ const usp = new URLSearchParams();
17505
+ for (const [k, v] of entries) usp.set(k, String(v));
17506
+ return "?" + usp.toString();
17507
+ }
17508
+ var OkResponseSchema;
17509
+ var init_runner = __esm({
17510
+ "src/api/_runner.ts"() {
17511
+ "use strict";
17512
+ init_zod();
17513
+ OkResponseSchema = external_exports.object({ ok: external_exports.literal(true) });
17514
+ }
17515
+ });
17516
+
17517
+ // src/api/project-settings.ts
17518
+ var project_settings_exports = {};
17519
+ __export(project_settings_exports, {
17520
+ GetProjectSettingsRespSchema: () => GetProjectSettingsRespSchema,
17521
+ ProjectSettingsSchema: () => ProjectSettingsSchema,
17522
+ UpdateProjectSettingsReqSchema: () => UpdateProjectSettingsReqSchema,
17523
+ UpdateProjectSettingsRespSchema: () => UpdateProjectSettingsRespSchema,
17524
+ getProjectSettings: () => getProjectSettings,
17525
+ updateProjectSettings: () => updateProjectSettings
17526
+ });
17527
+ async function getProjectSettings() {
17528
+ return apiCall(GetProjectSettingsRespSchema, "/project-settings");
17529
+ }
17530
+ async function updateProjectSettings(req) {
17531
+ return apiCall(UpdateProjectSettingsRespSchema, "/project-settings", { method: "PATCH", body: req });
17532
+ }
17533
+ var ProjectSettingsSchema, GetProjectSettingsRespSchema, UpdateProjectSettingsReqSchema, UpdateProjectSettingsRespSchema;
17534
+ var init_project_settings = __esm({
17535
+ "src/api/project-settings.ts"() {
17536
+ "use strict";
17537
+ init_zod();
17538
+ init_runner();
17539
+ ProjectSettingsSchema = external_exports.object({
17540
+ appName: external_exports.string().optional(),
17541
+ /** Content-plugin ids disabled for THIS project (doc 29 FR-29.16). A plugin is
17542
+ * enabled unless it appears here or in the global disabled list. */
17543
+ disabledPlugins: external_exports.array(external_exports.string()).optional(),
17544
+ /** Per-project plugin preference values (doc 29 FR-29.12): pluginId → key → value. */
17545
+ pluginSettings: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.string())).optional()
17546
+ });
17547
+ GetProjectSettingsRespSchema = ProjectSettingsSchema;
17548
+ UpdateProjectSettingsReqSchema = ProjectSettingsSchema.partial();
17549
+ UpdateProjectSettingsRespSchema = ProjectSettingsSchema;
17550
+ }
17551
+ });
17552
+
17553
+ // src/project-settings-store.ts
17554
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
17555
+ import { join as join10 } from "path";
17556
+ function settingsPath(repoRoot) {
17557
+ return join10(repoRoot, ".glassbox", "settings.json");
17558
+ }
17559
+ function readProjectSettings(repoRoot) {
17560
+ try {
17561
+ const path = settingsPath(repoRoot);
17562
+ if (existsSync10(path)) {
17563
+ const raw2 = JSON.parse(readFileSync9(path, "utf-8"));
17564
+ const parsed = ProjectSettingsSchema.safeParse(raw2);
17565
+ if (parsed.success) return parsed.data;
17566
+ }
17567
+ } catch {
17568
+ }
17569
+ return {};
17570
+ }
17571
+ function updateProjectSettings2(repoRoot, mutator) {
17572
+ const current = readProjectSettings(repoRoot);
17573
+ const result = mutator(current);
17574
+ const next = result === void 0 ? current : result;
17575
+ const dir = join10(repoRoot, ".glassbox");
17576
+ mkdirSync8(dir, { recursive: true });
17577
+ writeFileSync9(join10(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
17578
+ }
17579
+ var init_project_settings_store = __esm({
17580
+ "src/project-settings-store.ts"() {
17581
+ "use strict";
17582
+ init_project_settings();
17583
+ }
17584
+ });
17585
+
17586
+ // src/plugins/enablement.ts
17587
+ function toStringArray(v) {
17588
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
17589
+ }
17590
+ function readGlobalDisabled() {
17591
+ return toStringArray(readGlobalConfig().disabledPlugins);
17592
+ }
17593
+ function setGlobalDisabled(id, disabled) {
17594
+ updateGlobalConfig((cfg) => {
17595
+ const cur = toStringArray(cfg.disabledPlugins);
17596
+ const next = disabled ? [.../* @__PURE__ */ new Set([...cur, id])] : cur.filter((x) => x !== id);
17597
+ return { ...cfg, disabledPlugins: next };
17598
+ });
17599
+ }
17600
+ function readProjectDisabled(repoRoot) {
17601
+ return toStringArray(readProjectSettings(repoRoot).disabledPlugins);
17602
+ }
17603
+ function setProjectDisabled(repoRoot, id, disabled) {
17604
+ updateProjectSettings2(repoRoot, (s) => {
17605
+ const cur = toStringArray(s.disabledPlugins);
17606
+ s.disabledPlugins = disabled ? [.../* @__PURE__ */ new Set([...cur, id])] : cur.filter((x) => x !== id);
17607
+ });
17608
+ }
17609
+ function disabledScope(id, lists) {
17610
+ if (lists.globalDisabled.includes(id)) return "global";
17611
+ if (lists.projectDisabled.includes(id)) return "project";
17612
+ return null;
17613
+ }
17614
+ function readEnablementLists(repoRoot) {
17615
+ return { globalDisabled: readGlobalDisabled(), projectDisabled: readProjectDisabled(repoRoot) };
17616
+ }
17617
+ var init_enablement = __esm({
17618
+ "src/plugins/enablement.ts"() {
17619
+ "use strict";
17620
+ init_global_config();
17621
+ init_project_settings_store();
17622
+ }
17623
+ });
17624
+
17625
+ // src/plugins/manifest.ts
17626
+ function parseManifest(raw2) {
17627
+ const parsed = PluginManifestSchema.safeParse(raw2);
17628
+ return parsed.success ? parsed.data : null;
17629
+ }
17630
+ var ContentTypeSchema, PluginPreferenceSchema, ConfigLabelColorSchema, ConfigLayoutItemSchema, PluginRequirementSchema, PluginProvisionStepSchema, PluginInstallSchema, PluginManifestSchema;
17631
+ var init_manifest2 = __esm({
17632
+ "src/plugins/manifest.ts"() {
17633
+ "use strict";
17634
+ init_zod();
17635
+ ContentTypeSchema = external_exports.object({
17636
+ extensions: external_exports.array(external_exports.string()).optional(),
17637
+ mimeTypes: external_exports.array(external_exports.string()).optional()
17638
+ }).loose();
17639
+ PluginPreferenceSchema = external_exports.object({
17640
+ key: external_exports.string().min(1),
17641
+ label: external_exports.string().min(1),
17642
+ type: external_exports.enum(["string", "number", "boolean", "select"]),
17643
+ /** Stored as a string; the plugin coerces. */
17644
+ default: external_exports.string().optional(),
17645
+ description: external_exports.string().optional(),
17646
+ /** `select` options (value === label unless an object form is used later). */
17647
+ options: external_exports.array(external_exports.string()).optional(),
17648
+ /** Per-project vs global storage (default global). Ignored for secrets
17649
+ * (always keychain). */
17650
+ scope: external_exports.enum(["global", "project"]).optional(),
17651
+ /** Secret prefs are stored in the OS keychain, never in config (GB-1054). */
17652
+ secret: external_exports.boolean().optional()
17653
+ }).loose();
17654
+ ConfigLabelColorSchema = external_exports.enum(["default", "success", "error", "warning", "transient"]);
17655
+ ConfigLayoutItemSchema = external_exports.lazy(
17656
+ () => external_exports.object({
17657
+ type: external_exports.enum(["preference", "divider", "spacer", "label", "button", "group"]),
17658
+ key: external_exports.string().optional(),
17659
+ id: external_exports.string().optional(),
17660
+ text: external_exports.string().optional(),
17661
+ color: ConfigLabelColorSchema.optional(),
17662
+ label: external_exports.string().optional(),
17663
+ action: external_exports.string().optional(),
17664
+ style: external_exports.string().optional(),
17665
+ title: external_exports.string().optional(),
17666
+ collapsed: external_exports.boolean().optional(),
17667
+ items: external_exports.array(ConfigLayoutItemSchema).optional()
17668
+ }).loose()
17669
+ );
17670
+ PluginRequirementSchema = external_exports.object({
17671
+ id: external_exports.string().min(1),
17672
+ label: external_exports.string().min(1),
17673
+ /** Executable whose presence proves the requirement (run with `checkArgs`). */
17674
+ command: external_exports.string().min(1),
17675
+ /** Args for the presence check (default `['--version']`). */
17676
+ checkArgs: external_exports.array(external_exports.string()).optional(),
17677
+ /** Human remediation shown when the requirement is missing. */
17678
+ hint: external_exports.string().min(1),
17679
+ docUrl: external_exports.string().optional()
17680
+ }).loose();
17681
+ PluginProvisionStepSchema = external_exports.union([
17682
+ external_exports.object({
17683
+ kind: external_exports.literal("fetch"),
17684
+ url: external_exports.string().min(1),
17685
+ /** Destination filename inside the install dir. */
17686
+ dest: external_exports.string().min(1),
17687
+ /** Optional expected sha-256 (hex) of the downloaded bytes. */
17688
+ sha256: external_exports.string().optional()
17689
+ }).loose(),
17690
+ external_exports.object({
17691
+ kind: external_exports.literal("npm-install"),
17692
+ packages: external_exports.array(external_exports.string().min(1)).min(1),
17693
+ /** Requirement id whose command must be present to run this (default `npm`). */
17694
+ requires: external_exports.string().optional(),
17695
+ note: external_exports.string().optional()
17696
+ }).loose()
17697
+ ]);
17698
+ PluginInstallSchema = external_exports.object({
17699
+ requirements: external_exports.array(PluginRequirementSchema).optional(),
17700
+ provision: external_exports.array(PluginProvisionStepSchema).optional(),
17701
+ /** Fallback command to run by hand when auto-provisioning can't complete. */
17702
+ cliHint: external_exports.string().optional()
17703
+ }).loose();
17704
+ PluginManifestSchema = external_exports.object({
17705
+ id: external_exports.string().min(1),
17706
+ name: external_exports.string().min(1),
17707
+ version: external_exports.string().min(1),
17708
+ /** Entry module relative to the plugin dir; defaults to `index.js`. */
17709
+ entry: external_exports.string().min(1).optional(),
17710
+ description: external_exports.string().optional(),
17711
+ author: external_exports.string().optional(),
17712
+ /** The content types this plugin handles (informational for the UI; the
17713
+ * authoritative match lives in the registered renderer/differ). */
17714
+ contentTypes: external_exports.array(ContentTypeSchema).optional(),
17715
+ /** User-configurable preferences (doc 29 FR-29.12); rendered in Settings. */
17716
+ preferences: external_exports.array(PluginPreferenceSchema).optional(),
17717
+ /** Optional arrangement of the preferences into groups/dividers/labels/
17718
+ * buttons (doc 29 FR-29.18). Omitted → flat preference list. */
17719
+ configLayout: external_exports.array(ConfigLayoutItemSchema).optional(),
17720
+ /** `false` marks a **separately-installable** plugin (e.g. one with a system
17721
+ * requirement like Java): it's built + may ship in the bundle, but
17722
+ * `installBundledPlugins` does NOT auto-install it — the user opts in
17723
+ * (GB-1046). Defaults to auto-install (true) when omitted. */
17724
+ autoInstall: external_exports.boolean().optional(),
17725
+ /** How to install an opt-in plugin from the UI (doc 29 §29.2, GB-1069):
17726
+ * system requirements + auto-run provisioning steps. Absent → self-contained. */
17727
+ install: PluginInstallSchema.optional()
17728
+ }).loose();
17729
+ }
17730
+ });
17731
+
17732
+ // src/plugins/registry.ts
17733
+ import { extname } from "path";
17734
+ function matchSpecificity(match, input) {
17735
+ if (match.sniff && input.bytes.length > 0) {
17736
+ try {
17737
+ if (match.sniff(input.bytes)) return 3;
17738
+ } catch {
17739
+ }
17740
+ }
17741
+ if (match.mimeTypes && input.mime !== void 0 && match.mimeTypes.includes(input.mime)) return 2;
17742
+ const ext = extname(input.path).toLowerCase();
17743
+ if (ext !== "" && match.extensions?.some((e) => e.toLowerCase() === ext) === true) return 1;
17744
+ return 0;
17745
+ }
17746
+ function pathMatches(match, path, mime) {
17747
+ const ext = extname(path).toLowerCase();
17748
+ if (ext !== "" && match.extensions?.some((e) => e.toLowerCase() === ext) === true) return true;
17749
+ if (mime !== void 0 && match.mimeTypes?.includes(mime) === true) return true;
17750
+ return false;
17751
+ }
17752
+ function pickBest(handlers, input) {
17753
+ let best;
17754
+ let bestScore = 0;
17755
+ for (const h of handlers) {
17756
+ const spec = matchSpecificity(h.match, input);
17757
+ if (spec === 0) continue;
17758
+ const score = (h.priority ?? 0) * 10 + spec;
17759
+ if (score > bestScore) {
17760
+ bestScore = score;
17761
+ best = h;
17762
+ }
17763
+ }
17764
+ return best;
17765
+ }
17766
+ var ContentPluginRegistry;
17767
+ var init_registry = __esm({
17768
+ "src/plugins/registry.ts"() {
17769
+ "use strict";
17770
+ ContentPluginRegistry = class {
17771
+ renderers = [];
17772
+ differs = [];
17773
+ imageDecoders = [];
17774
+ addRenderers(rs) {
17775
+ if (rs) this.renderers.push(...rs);
17776
+ }
17777
+ addDiffers(ds) {
17778
+ if (ds) this.differs.push(...ds);
17779
+ }
17780
+ addImageDecoders(ds) {
17781
+ if (ds) this.imageDecoders.push(...ds);
17782
+ }
17783
+ /** The best-matching renderer for a single content blob, or undefined. */
17784
+ findRenderer(input) {
17785
+ return pickBest(this.renderers, input);
17786
+ }
17787
+ /** The best-matching differ for a pair; matching keys off the new side. */
17788
+ findDiffer(input) {
17789
+ return pickBest(this.differs, input.new);
17790
+ }
17791
+ /** The best-matching image decoder (bytes → RGBA) for a blob, or undefined. */
17792
+ findImageDecoder(input) {
17793
+ return pickBest(this.imageDecoders, input);
17794
+ }
17795
+ /** Cheap path pre-check: could any renderer/differ handle this path (by ext /
17796
+ * MIME)? Skips sniff so no bytes are read. */
17797
+ mightHandleByPath(path, mime) {
17798
+ for (const h of this.renderers) if (pathMatches(h.match, path, mime)) return true;
17799
+ for (const h of this.differs) if (pathMatches(h.match, path, mime)) return true;
17800
+ return false;
17801
+ }
17802
+ get rendererCount() {
17803
+ return this.renderers.length;
17804
+ }
17805
+ get differCount() {
17806
+ return this.differs.length;
17807
+ }
17808
+ get imageDecoderCount() {
17809
+ return this.imageDecoders.length;
17810
+ }
17811
+ };
17812
+ }
17813
+ });
17814
+
17815
+ // src/plugins/settings.ts
17816
+ function secretAccount(pluginId, key) {
17817
+ return `plugin-${pluginId}-${key}`;
17818
+ }
17819
+ function coerceSettingsMap(raw2) {
17820
+ const out = {};
17821
+ if (raw2 === null || typeof raw2 !== "object") return out;
17822
+ for (const [pluginId, byKey] of Object.entries(raw2)) {
17823
+ if (byKey === null || typeof byKey !== "object") continue;
17824
+ const entry = {};
17825
+ for (const [k, v] of Object.entries(byKey)) {
17826
+ if (typeof v === "string") entry[k] = v;
17827
+ }
17828
+ out[pluginId] = entry;
17829
+ }
17830
+ return out;
17831
+ }
17832
+ function prefFor(manifest, key) {
17833
+ return manifest.preferences?.find((p) => p.key === key);
17834
+ }
17835
+ function scopeOf(pref) {
17836
+ return pref?.scope === "project" ? "project" : "global";
17837
+ }
17838
+ function readGlobalSettings() {
17839
+ return coerceSettingsMap(readGlobalConfig().pluginSettings);
17840
+ }
17841
+ function readProjectSettingsMap(repoRoot) {
17842
+ return coerceSettingsMap(readProjectSettings(repoRoot).pluginSettings);
17843
+ }
17844
+ function lookup(map2, pluginId, key) {
17845
+ return map2[pluginId]?.[key];
17846
+ }
17847
+ function readPluginSetting(manifest, repoRoot, key) {
17848
+ const pref = prefFor(manifest, key);
17849
+ if (pref?.secret === true) {
17850
+ return getSecretFromKeychain(secretAccount(manifest.id, key)) ?? pref.default ?? null;
17851
+ }
17852
+ const map2 = scopeOf(pref) === "project" ? readProjectSettingsMap(repoRoot) : readGlobalSettings();
17853
+ const stored = lookup(map2, manifest.id, key);
17854
+ if (stored !== void 0) return stored;
17855
+ return pref?.default ?? null;
17856
+ }
17857
+ function writePluginSetting(manifest, repoRoot, key, value) {
17858
+ const pref = prefFor(manifest, key);
17859
+ if (pref?.secret === true) {
17860
+ if (value === "") deleteSecretFromKeychain(secretAccount(manifest.id, key));
17861
+ else saveSecretToKeychain(secretAccount(manifest.id, key), value, `Glassbox plugin: ${manifest.id}`);
17862
+ return;
17863
+ }
17864
+ if (scopeOf(pref) === "project") {
17865
+ updateProjectSettings2(repoRoot, (s) => {
17866
+ const map2 = coerceSettingsMap(s.pluginSettings);
17867
+ map2[manifest.id] = { ...map2[manifest.id], [key]: value };
17868
+ s.pluginSettings = map2;
17869
+ });
17870
+ } else {
17871
+ updateGlobalConfig((cfg) => {
17872
+ const map2 = coerceSettingsMap(cfg.pluginSettings);
17873
+ map2[manifest.id] = { ...map2[manifest.id], [key]: value };
17874
+ return { ...cfg, pluginSettings: map2 };
17875
+ });
17876
+ }
17877
+ }
17878
+ function readPluginPreferenceDisplay(manifest, repoRoot) {
17879
+ const values = {};
17880
+ const secretConfigured = [];
17881
+ for (const p of manifest.preferences ?? []) {
17882
+ if (p.secret === true) {
17883
+ const stored = getSecretFromKeychain(secretAccount(manifest.id, p.key));
17884
+ if (stored !== null && stored !== "") secretConfigured.push(p.key);
17885
+ values[p.key] = "";
17886
+ } else {
17887
+ values[p.key] = readPluginSetting(manifest, repoRoot, p.key) ?? "";
17888
+ }
17889
+ }
17890
+ return { values, secretConfigured };
17891
+ }
17892
+ var init_settings = __esm({
17893
+ "src/plugins/settings.ts"() {
17894
+ "use strict";
17895
+ init_keychain();
17896
+ init_global_config();
17897
+ init_project_settings_store();
17898
+ }
17899
+ });
17900
+
17901
+ // src/plugins/loader.ts
17902
+ import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
17903
+ import { join as join11 } from "path";
17904
+ import { pathToFileURL } from "url";
17905
+ function getConfigLabelOverride(pluginId, labelId) {
17906
+ return configLabelOverrides.get(`${pluginId}:${labelId}`);
17907
+ }
17908
+ function clearConfigLabelOverrides(pluginId) {
17909
+ const prefix = `${pluginId}:`;
17910
+ for (const key of configLabelOverrides.keys()) {
17911
+ if (key.startsWith(prefix)) configLabelOverrides.delete(key);
17912
+ }
17913
+ }
17914
+ function getPluginUIElements() {
17915
+ return Array.from(pluginUIElements.entries()).map(([pluginId, elements]) => ({ pluginId, elements }));
17916
+ }
17917
+ function clearPluginUIElements(pluginId) {
17918
+ pluginUIElements.delete(pluginId);
17919
+ }
17920
+ function clearAllPluginUIElements() {
17921
+ pluginUIElements.clear();
17922
+ }
17923
+ function pluginsDir() {
17924
+ return join11(GLOBAL_CONFIG_DIR, "plugins");
17925
+ }
17926
+ function discoverPluginDirs(root = pluginsDir()) {
17927
+ if (!existsSync11(root)) return [];
17928
+ let entries;
17929
+ try {
17930
+ entries = readdirSync2(root);
17931
+ } catch {
17932
+ return [];
17933
+ }
17934
+ const out = [];
17935
+ for (const name of entries) {
17936
+ if (name.startsWith(".")) continue;
17937
+ const dir = join11(root, name);
17938
+ try {
17939
+ if (statSync3(dir).isDirectory()) out.push(dir);
17940
+ } catch {
17941
+ }
17942
+ }
17943
+ return out.sort();
17944
+ }
17945
+ function readManifest(dir) {
17946
+ const manifestPath = join11(dir, "manifest.json");
17947
+ if (existsSync11(manifestPath)) {
17948
+ try {
17949
+ return parseManifest(JSON.parse(readFileSync10(manifestPath, "utf-8")));
17950
+ } catch {
17951
+ return null;
17952
+ }
17953
+ }
17954
+ const pkgPath = join11(dir, "package.json");
17955
+ if (existsSync11(pkgPath)) {
17956
+ try {
17957
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
17958
+ const gb = pkg.glassbox;
17959
+ if (gb !== null && typeof gb === "object") {
17960
+ return parseManifest({ id: pkg.name, version: pkg.version, entry: pkg.main, ...gb });
17961
+ }
17962
+ } catch {
17963
+ return null;
17964
+ }
17965
+ }
17966
+ return null;
17967
+ }
17968
+ function makeContext(manifest, repoRoot) {
17969
+ const id = manifest.id;
17970
+ return {
17971
+ log: (level, message) => {
17972
+ const line = `[plugin:${id}] ${message}`;
17973
+ if (level === "error") console.error(line);
17974
+ else if (level === "warn") console.warn(line);
17975
+ else console.log(line);
17976
+ },
17977
+ getSetting: (key) => Promise.resolve(readPluginSetting(manifest, repoRoot, key)),
17978
+ setSetting: (key, value) => {
17979
+ writePluginSetting(manifest, repoRoot, key, value);
17980
+ return Promise.resolve();
17981
+ },
17982
+ updateConfigLabel: (labelId, text, color) => {
17983
+ configLabelOverrides.set(`${id}:${labelId}`, { text, color });
17984
+ },
17985
+ registerUI: (elements) => {
17986
+ pluginUIElements.set(id, elements);
17987
+ }
17988
+ };
17989
+ }
17990
+ async function loadPluginDir(dir, registry3, isEnabled = ALWAYS_ENABLED, repoRoot = "") {
17991
+ const manifest = readManifest(dir);
17992
+ if (manifest === null) {
17993
+ return { id: dir, dir, manifest: null, status: "error", error: "invalid or missing manifest" };
17994
+ }
17995
+ const enablement = isEnabled(manifest.id);
17996
+ if (enablement.disabled) {
17997
+ return { id: manifest.id, dir, manifest, status: "disabled", disabledScope: enablement.scope };
17998
+ }
17999
+ try {
18000
+ const entry = join11(dir, manifest.entry ?? "index.js");
18001
+ if (!existsSync11(entry)) throw new Error(`entry not found: ${manifest.entry ?? "index.js"}`);
18002
+ const mod = await import(pathToFileURL(entry).href);
18003
+ const plugin = mod.default ?? (typeof mod.activate === "function" ? { activate: mod.activate, onAction: mod.onAction } : void 0);
18004
+ if (plugin === void 0 || typeof plugin.activate !== "function") {
18005
+ throw new Error("plugin exports no activate()");
18006
+ }
18007
+ const context = makeContext(manifest, repoRoot);
18008
+ const registration = await plugin.activate(context) ?? void 0;
18009
+ if (registration !== void 0) {
18010
+ registry3.addRenderers(registration.renderers);
18011
+ registry3.addDiffers(registration.differs);
18012
+ registry3.addImageDecoders(registration.imageDecoders);
18013
+ }
18014
+ return { id: manifest.id, dir, manifest, status: "loaded", registration, instance: plugin, context };
18015
+ } catch (e) {
18016
+ return { id: manifest.id, dir, manifest, status: "error", error: e instanceof Error ? e.message : String(e) };
18017
+ }
18018
+ }
18019
+ async function loadAllPlugins(root = pluginsDir(), isEnabled = ALWAYS_ENABLED, repoRoot = "") {
18020
+ const registry3 = new ContentPluginRegistry();
18021
+ if (!PLUGINS_ENABLED) return { registry: registry3, loaded: [] };
18022
+ const loaded = [];
18023
+ for (const dir of discoverPluginDirs(root)) {
18024
+ loaded.push(await loadPluginDir(dir, registry3, isEnabled, repoRoot));
18025
+ }
18026
+ return { registry: registry3, loaded };
18027
+ }
18028
+ var configLabelOverrides, pluginUIElements, ALWAYS_ENABLED;
18029
+ var init_loader = __esm({
18030
+ "src/plugins/loader.ts"() {
18031
+ "use strict";
18032
+ init_feature_flags();
18033
+ init_global_config();
18034
+ init_manifest2();
18035
+ init_registry();
18036
+ init_settings();
18037
+ configLabelOverrides = /* @__PURE__ */ new Map();
18038
+ pluginUIElements = /* @__PURE__ */ new Map();
18039
+ ALWAYS_ENABLED = () => ({ disabled: false });
18040
+ }
18041
+ });
18042
+
18043
+ // src/plugins/install.ts
18044
+ import { createHash as createHash3 } from "crypto";
18045
+ import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync11, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
18046
+ import { dirname as dirname7, join as join12 } from "path";
18047
+ import { fileURLToPath as fileURLToPath2 } from "url";
18048
+ function dismissedPathFor(userDir) {
18049
+ return join12(dirname7(userDir), DISMISSED_FILE);
18050
+ }
18051
+ function bundledPluginsDir() {
18052
+ const override = process.env.GLASSBOX_BUNDLED_PLUGINS_DIR;
18053
+ if (override !== void 0 && override.trim() !== "") return override;
18054
+ const sibling = join12(dirname7(fileURLToPath2(import.meta.url)), "plugins");
18055
+ if (existsSync12(sibling)) return sibling;
18056
+ return join12(process.cwd(), "dist", "plugins");
18057
+ }
18058
+ function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18059
+ if (!existsSync12(bundledDir)) return [];
18060
+ let entries;
18061
+ try {
18062
+ entries = readdirSync3(bundledDir);
18063
+ } catch {
18064
+ return [];
18065
+ }
18066
+ const out = [];
18067
+ for (const name of entries.sort()) {
18068
+ const dir = join12(bundledDir, name);
18069
+ try {
18070
+ if (!statSync4(dir).isDirectory()) continue;
18071
+ const manifest = readManifest(dir);
18072
+ if (manifest !== null) out.push({ dir, manifest });
18073
+ } catch {
18074
+ }
18075
+ }
18076
+ return out;
18077
+ }
18078
+ function readDismissed(userDir = pluginsDir()) {
18079
+ try {
18080
+ const raw2 = JSON.parse(readFileSync11(dismissedPathFor(userDir), "utf-8"));
18081
+ return Array.isArray(raw2) ? raw2.filter((x) => typeof x === "string") : [];
18082
+ } catch {
18083
+ return [];
18084
+ }
17271
18085
  }
17272
- function writeImageBlob(dataDir, fileId, side, bytes) {
17273
- if (bytes.length === 0) return;
17274
- const dir = blobDir(dataDir);
17275
- mkdirSync6(dir, { recursive: true });
17276
- writeFileSync4(join5(dir, blobName(fileId, side)), bytes);
18086
+ function writeDismissed(ids, userDir) {
18087
+ const path = dismissedPathFor(userDir);
18088
+ mkdirSync9(dirname7(path), { recursive: true });
18089
+ writeFileSync10(path, JSON.stringify([...new Set(ids)], null, 2), "utf-8");
17277
18090
  }
17278
- function readImageBlob(dataDir, fileId, side) {
17279
- const path = join5(blobDir(dataDir), blobName(fileId, side));
17280
- if (!existsSync5(path)) return null;
18091
+ function compareVersions(a, b) {
18092
+ const pa = a.split(".").map((p) => parseInt(p, 10) || 0);
18093
+ const pb = b.split(".").map((p) => parseInt(p, 10) || 0);
18094
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
18095
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
18096
+ if (d !== 0) return d > 0 ? 1 : -1;
18097
+ }
18098
+ return 0;
18099
+ }
18100
+ function hashPluginDir(dir) {
18101
+ const h = createHash3("sha1");
18102
+ const walk = (d, rel) => {
18103
+ for (const name of readdirSync3(d).sort()) {
18104
+ const abs = join12(d, name);
18105
+ const r = rel === "" ? name : `${rel}/${name}`;
18106
+ if (statSync4(abs).isDirectory()) walk(abs, r);
18107
+ else {
18108
+ h.update(r);
18109
+ h.update(readFileSync11(abs));
18110
+ }
18111
+ }
18112
+ };
18113
+ walk(dir, "");
18114
+ return h.digest("hex");
18115
+ }
18116
+ function shouldInstall(src, dest, bundledVersion) {
18117
+ if (!existsSync12(dest)) return true;
18118
+ const destManifest = readManifest(dest);
18119
+ const destVersion = destManifest?.version ?? "0";
18120
+ const cmp = compareVersions(bundledVersion, destVersion);
18121
+ if (cmp > 0) return true;
18122
+ if (cmp < 0) return false;
17281
18123
  try {
17282
- return readFileSync4(path);
18124
+ return hashPluginDir(src) !== hashPluginDir(dest);
18125
+ } catch {
18126
+ return false;
18127
+ }
18128
+ }
18129
+ function installBundledPlugins(opts) {
18130
+ const bundledDir = opts?.bundledDir ?? bundledPluginsDir();
18131
+ const userDir = opts?.userDir ?? pluginsDir();
18132
+ if (!existsSync12(bundledDir)) return;
18133
+ const dismissed = new Set(readDismissed(userDir));
18134
+ let entries;
18135
+ try {
18136
+ entries = readdirSync3(bundledDir);
18137
+ } catch {
18138
+ return;
18139
+ }
18140
+ for (const name of entries) {
18141
+ const src = join12(bundledDir, name);
18142
+ try {
18143
+ if (!statSync4(src).isDirectory()) continue;
18144
+ const manifest = readManifest(src);
18145
+ if (manifest === null || dismissed.has(manifest.id)) continue;
18146
+ if (manifest.autoInstall === false) continue;
18147
+ const dest = join12(userDir, manifest.id);
18148
+ if (!shouldInstall(src, dest, manifest.version)) continue;
18149
+ mkdirSync9(userDir, { recursive: true });
18150
+ rmSync6(dest, { recursive: true, force: true });
18151
+ cpSync(src, dest, { recursive: true });
18152
+ } catch {
18153
+ }
18154
+ }
18155
+ }
18156
+ function installPluginFromDisk(sourceDir, opts) {
18157
+ const manifest = readManifest(sourceDir);
18158
+ if (manifest === null) throw new Error("The selected folder is not a Glassbox plugin (no manifest.json).");
18159
+ const userDir = opts?.userDir ?? pluginsDir();
18160
+ mkdirSync9(userDir, { recursive: true });
18161
+ const dest = join12(userDir, manifest.id);
18162
+ rmSync6(dest, { recursive: true, force: true });
18163
+ try {
18164
+ symlinkSync(sourceDir, dest, "dir");
18165
+ } catch {
18166
+ cpSync(sourceDir, dest, { recursive: true });
18167
+ }
18168
+ writeDismissed(readDismissed(userDir).filter((x) => x !== manifest.id), userDir);
18169
+ return { id: manifest.id };
18170
+ }
18171
+ function undismissPlugin(id, userDir = pluginsDir()) {
18172
+ writeDismissed(readDismissed(userDir).filter((x) => x !== id), userDir);
18173
+ }
18174
+ function uninstallPlugin(id, opts) {
18175
+ const userDir = opts?.userDir ?? pluginsDir();
18176
+ rmSync6(join12(userDir, id), { recursive: true, force: true });
18177
+ writeDismissed([...readDismissed(userDir), id], userDir);
18178
+ }
18179
+ var DISMISSED_FILE;
18180
+ var init_install = __esm({
18181
+ "src/plugins/install.ts"() {
18182
+ "use strict";
18183
+ init_loader();
18184
+ DISMISSED_FILE = "dismissed-plugins.json";
18185
+ }
18186
+ });
18187
+
18188
+ // src/plugins/index.ts
18189
+ var plugins_exports = {};
18190
+ __export(plugins_exports, {
18191
+ __resetContentPluginsForTest: () => __resetContentPluginsForTest,
18192
+ __setContentRegistryForTest: () => __setContentRegistryForTest,
18193
+ decodeImageWithPlugin: () => decodeImageWithPlugin,
18194
+ describeInstalledPlugins: () => describeInstalledPlugins,
18195
+ diffContent: () => diffContent,
18196
+ getLoadedPlugins: () => getLoadedPlugins,
18197
+ getPluginManifest: () => getPluginManifest,
18198
+ initContentPlugins: () => initContentPlugins,
18199
+ listPluginUIElements: () => listPluginUIElements,
18200
+ mightHandleFile: () => mightHandleFile,
18201
+ notifyReviewCompleted: () => notifyReviewCompleted,
18202
+ notifyReviewCreated: () => notifyReviewCreated,
18203
+ persistPluginUIState: () => persistPluginUIState,
18204
+ pluginsEnabled: () => pluginsEnabled,
18205
+ reloadContentPlugins: () => reloadContentPlugins,
18206
+ renderContent: () => renderContent,
18207
+ runPluginAction: () => runPluginAction
18208
+ });
18209
+ function pluginsEnabled() {
18210
+ return PLUGINS_ENABLED;
18211
+ }
18212
+ function enablementCheckFor(repoRoot) {
18213
+ const lists = readEnablementLists(repoRoot);
18214
+ return (id) => {
18215
+ const scope = disabledScope(id, lists);
18216
+ return scope === null ? { disabled: false } : { disabled: true, scope };
18217
+ };
18218
+ }
18219
+ async function loadFor(repoRoot) {
18220
+ installBundledPlugins();
18221
+ clearAllPluginUIElements();
18222
+ const res = await loadAllPlugins(void 0, enablementCheckFor(repoRoot), repoRoot);
18223
+ registry2 = res.registry;
18224
+ loadedPlugins = res.loaded;
18225
+ currentRepoRoot = repoRoot;
18226
+ const failed = loadedPlugins.filter((p) => p.status === "error").length;
18227
+ const disabled = loadedPlugins.filter((p) => p.status === "disabled").length;
18228
+ if (loadedPlugins.length > 0) {
18229
+ const parts = [`${loadedPlugins.length - failed - disabled} loaded`];
18230
+ if (disabled > 0) parts.push(`${disabled} disabled`);
18231
+ if (failed > 0) parts.push(`${failed} failed`);
18232
+ console.log(` Content plugins: ${parts.join(", ")}.`);
18233
+ }
18234
+ }
18235
+ async function initContentPlugins(repoRoot = "") {
18236
+ if (initialized) return;
18237
+ initialized = true;
18238
+ if (!PLUGINS_ENABLED) return;
18239
+ try {
18240
+ await loadFor(repoRoot);
18241
+ } catch (e) {
18242
+ console.warn(` Content plugins failed to load: ${e instanceof Error ? e.message : String(e)}`);
18243
+ }
18244
+ }
18245
+ async function reloadContentPlugins(repoRoot = currentRepoRoot) {
18246
+ if (!PLUGINS_ENABLED) return;
18247
+ initialized = true;
18248
+ try {
18249
+ await loadFor(repoRoot);
18250
+ } catch (e) {
18251
+ console.warn(` Content plugins failed to reload: ${e instanceof Error ? e.message : String(e)}`);
18252
+ }
18253
+ }
18254
+ function getLoadedPlugins() {
18255
+ return loadedPlugins;
18256
+ }
18257
+ function resolveConfigLabels(manifest) {
18258
+ const out = {};
18259
+ const walk = (items) => {
18260
+ for (const item of items ?? []) {
18261
+ if (item.type === "label" && item.id !== void 0 && item.id !== "") {
18262
+ const override = getConfigLabelOverride(manifest.id, item.id);
18263
+ out[item.id] = override ?? { text: item.text ?? "", color: item.color };
18264
+ }
18265
+ if (item.type === "group") walk(item.items);
18266
+ }
18267
+ };
18268
+ walk(manifest.configLayout);
18269
+ return out;
18270
+ }
18271
+ function describeInstalledPlugins(repoRoot) {
18272
+ const globalDisabled = new Set(readGlobalDisabled());
18273
+ const projectDisabled = new Set(readProjectDisabled(repoRoot));
18274
+ return loadedPlugins.map((p) => {
18275
+ const extensions = (p.manifest?.contentTypes ?? []).flatMap((ct) => ct.extensions ?? []);
18276
+ const display = p.manifest ? readPluginPreferenceDisplay(p.manifest, repoRoot) : { values: {}, secretConfigured: [] };
18277
+ return {
18278
+ id: p.id,
18279
+ name: p.manifest?.name ?? p.id,
18280
+ version: p.manifest?.version ?? "0",
18281
+ extensions,
18282
+ status: p.status,
18283
+ error: p.error,
18284
+ enabled: p.status === "loaded",
18285
+ disabledScope: p.disabledScope,
18286
+ globalDisabled: globalDisabled.has(p.id),
18287
+ projectDisabled: projectDisabled.has(p.id),
18288
+ preferences: p.manifest?.preferences ?? [],
18289
+ preferenceValues: display.values,
18290
+ secretConfigured: display.secretConfigured,
18291
+ configLayout: p.manifest?.configLayout,
18292
+ configLabels: p.manifest ? resolveConfigLabels(p.manifest) : {}
18293
+ };
18294
+ });
18295
+ }
18296
+ function getPluginManifest(id) {
18297
+ return loadedPlugins.find((p) => p.id === id)?.manifest ?? void 0;
18298
+ }
18299
+ async function runPluginAction(id, actionId, value) {
18300
+ if (!PLUGINS_ENABLED) throw new Error("Plugins are disabled");
18301
+ const loaded = loadedPlugins.find((p) => p.id === id);
18302
+ if (loaded === void 0 || loaded.status !== "loaded" || loaded.instance === void 0 || loaded.context === void 0) {
18303
+ throw new Error("Plugin not active");
18304
+ }
18305
+ if (typeof loaded.instance.onAction !== "function") {
18306
+ throw new Error("Plugin does not handle actions");
18307
+ }
18308
+ return await loaded.instance.onAction(actionId, loaded.context, value);
18309
+ }
18310
+ function statefulKey(e) {
18311
+ return e.type === "toggle" || e.type === "switch" || e.type === "segmented-control" ? e.stateKey : void 0;
18312
+ }
18313
+ function elementAction(e) {
18314
+ return e.type === "link" ? void 0 : e.action;
18315
+ }
18316
+ function persistPluginUIState(id, actionId, value, repoRoot) {
18317
+ const manifest = loadedPlugins.find((p) => p.id === id && p.status === "loaded")?.manifest;
18318
+ if (manifest === null || manifest === void 0) return;
18319
+ const group = getPluginUIElements().find((g) => g.pluginId === id);
18320
+ const el = group?.elements.find((e) => elementAction(e) === actionId && statefulKey(e) !== void 0 && statefulKey(e) !== "");
18321
+ const key = el ? statefulKey(el) : void 0;
18322
+ if (key === void 0 || key === "") return;
18323
+ try {
18324
+ writePluginSetting(manifest, repoRoot, key, value);
18325
+ } catch {
18326
+ }
18327
+ }
18328
+ function toReviewHookInfo(review) {
18329
+ return { id: review.id, repoPath: review.repo_path, repoName: review.repo_name, mode: review.mode, status: review.status };
18330
+ }
18331
+ function toAnnotationHookInfo(a) {
18332
+ return { id: a.id, filePath: a.file_path, lineNumber: a.line_number, side: a.side, category: a.category, content: a.content };
18333
+ }
18334
+ async function notifyReviewCreated(review) {
18335
+ if (!PLUGINS_ENABLED) return;
18336
+ const info = toReviewHookInfo(review);
18337
+ for (const p of loadedPlugins) {
18338
+ const hook = p.registration?.reviewHooks?.onReviewCreated;
18339
+ if (p.status !== "loaded" || hook === void 0 || p.context === void 0) continue;
18340
+ try {
18341
+ await hook(info, p.context);
18342
+ } catch (e) {
18343
+ console.warn(` [plugin:${p.id}] onReviewCreated hook failed: ${e instanceof Error ? e.message : String(e)}`);
18344
+ }
18345
+ }
18346
+ }
18347
+ async function notifyReviewCompleted(review, annotations, exportPath) {
18348
+ if (!PLUGINS_ENABLED) return;
18349
+ const info = toReviewHookInfo(review);
18350
+ const anns = annotations.map(toAnnotationHookInfo);
18351
+ for (const p of loadedPlugins) {
18352
+ const hook = p.registration?.reviewHooks?.onReviewCompleted;
18353
+ if (p.status !== "loaded" || hook === void 0 || p.context === void 0) continue;
18354
+ try {
18355
+ await hook(info, anns, exportPath, p.context);
18356
+ } catch (e) {
18357
+ console.warn(` [plugin:${p.id}] onReviewCompleted hook failed: ${e instanceof Error ? e.message : String(e)}`);
18358
+ }
18359
+ }
18360
+ }
18361
+ function listPluginUIElements(repoRoot = "") {
18362
+ if (!PLUGINS_ENABLED) return [];
18363
+ const loaded = new Map(loadedPlugins.filter((p) => p.status === "loaded").map((p) => [p.id, p]));
18364
+ const out = [];
18365
+ for (const { pluginId, elements } of getPluginUIElements()) {
18366
+ const plugin = loaded.get(pluginId);
18367
+ if (plugin === void 0) continue;
18368
+ for (const el of elements) {
18369
+ const key = statefulKey(el);
18370
+ const value = key !== void 0 && key !== "" && plugin.manifest !== null ? readPluginSetting(plugin.manifest, repoRoot, key) ?? void 0 : void 0;
18371
+ out.push({ ...el, pluginId, value });
18372
+ }
18373
+ }
18374
+ return out;
18375
+ }
18376
+ function mightHandleFile(path, mime) {
18377
+ return PLUGINS_ENABLED && registry2.mightHandleByPath(path, mime);
18378
+ }
18379
+ function usableView(view) {
18380
+ if (view === void 0) return null;
18381
+ const svg = typeof view.svg === "string" && view.svg !== "";
18382
+ const html = typeof view.html === "string" && view.html !== "";
18383
+ return svg || html ? view : null;
18384
+ }
18385
+ async function renderContent(input) {
18386
+ if (!PLUGINS_ENABLED) return null;
18387
+ const renderer = registry2.findRenderer(input);
18388
+ if (renderer === void 0) return null;
18389
+ try {
18390
+ return usableView(await renderer.render(input));
17283
18391
  } catch {
17284
18392
  return null;
17285
18393
  }
17286
18394
  }
17287
- function clearImageBlobs(dataDir) {
18395
+ async function decodeImageWithPlugin(bytes, path, mime) {
18396
+ if (!PLUGINS_ENABLED) return null;
18397
+ const decoder = registry2.findImageDecoder({ bytes, path, mime });
18398
+ if (decoder === void 0) return null;
17288
18399
  try {
17289
- rmSync3(blobDir(dataDir), { recursive: true, force: true });
18400
+ return await decoder.decode({ bytes, path }) ?? null;
17290
18401
  } catch {
18402
+ return null;
17291
18403
  }
17292
18404
  }
17293
- var init_image_blobs = __esm({
17294
- "src/git/image-blobs.ts"() {
18405
+ async function diffContent(input) {
18406
+ if (!PLUGINS_ENABLED) return null;
18407
+ const differ = registry2.findDiffer(input);
18408
+ if (differ === void 0) return null;
18409
+ try {
18410
+ return usableView(await differ.diff(input));
18411
+ } catch {
18412
+ return null;
18413
+ }
18414
+ }
18415
+ function __setContentRegistryForTest(r, loaded = []) {
18416
+ registry2 = r;
18417
+ loadedPlugins = loaded;
18418
+ initialized = true;
18419
+ }
18420
+ function __resetContentPluginsForTest() {
18421
+ registry2 = new ContentPluginRegistry();
18422
+ loadedPlugins = [];
18423
+ initialized = false;
18424
+ }
18425
+ var registry2, loadedPlugins, initialized, currentRepoRoot;
18426
+ var init_plugins = __esm({
18427
+ "src/plugins/index.ts"() {
17295
18428
  "use strict";
18429
+ init_feature_flags();
18430
+ init_enablement();
18431
+ init_install();
18432
+ init_loader();
18433
+ init_registry();
18434
+ init_settings();
18435
+ registry2 = new ContentPluginRegistry();
18436
+ loadedPlugins = [];
18437
+ initialized = false;
18438
+ currentRepoRoot = "";
17296
18439
  }
17297
18440
  });
17298
18441
 
@@ -17449,17 +18592,17 @@ __export(difftool_discovery_exports, {
17449
18592
  tryAcquireStartingLock: () => tryAcquireStartingLock,
17450
18593
  writeDiscovery: () => writeDiscovery
17451
18594
  });
17452
- import { existsSync as existsSync16, mkdirSync as mkdirSync14, readFileSync as readFileSync20, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync14 } from "fs";
18595
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync22, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "fs";
17453
18596
  import { homedir as homedir3 } from "os";
17454
- import { join as join19 } from "path";
18597
+ import { join as join23 } from "path";
17455
18598
  function difftoolHome() {
17456
- return join19(homedir3(), ".glassbox");
18599
+ return join23(homedir3(), ".glassbox");
17457
18600
  }
17458
18601
  function discoveryPath(home = difftoolHome()) {
17459
- return join19(home, "difftool.lock");
18602
+ return join23(home, "difftool.lock");
17460
18603
  }
17461
18604
  function startingLockPath(home = difftoolHome()) {
17462
- return join19(home, "difftool-starting.lock");
18605
+ return join23(home, "difftool-starting.lock");
17463
18606
  }
17464
18607
  function parseDiscovery(raw2) {
17465
18608
  let parsed;
@@ -17473,35 +18616,35 @@ function parseDiscovery(raw2) {
17473
18616
  }
17474
18617
  function readDiscovery(home = difftoolHome()) {
17475
18618
  const path = discoveryPath(home);
17476
- if (!existsSync16(path)) return null;
18619
+ if (!existsSync19(path)) return null;
17477
18620
  try {
17478
- return parseDiscovery(readFileSync20(path, "utf-8"));
18621
+ return parseDiscovery(readFileSync22(path, "utf-8"));
17479
18622
  } catch {
17480
18623
  return null;
17481
18624
  }
17482
18625
  }
17483
18626
  function writeDiscovery(port, home = difftoolHome()) {
17484
- mkdirSync14(home, { recursive: true });
17485
- writeFileSync14(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
18627
+ mkdirSync16(home, { recursive: true });
18628
+ writeFileSync16(discoveryPath(home), JSON.stringify({ port, pid: process.pid }));
17486
18629
  }
17487
18630
  function clearDiscovery(home = difftoolHome()) {
17488
18631
  try {
17489
- rmSync6(discoveryPath(home), { force: true });
18632
+ rmSync7(discoveryPath(home), { force: true });
17490
18633
  } catch {
17491
18634
  }
17492
18635
  }
17493
18636
  function tryAcquireStartingLock(home = difftoolHome()) {
17494
- mkdirSync14(home, { recursive: true });
18637
+ mkdirSync16(home, { recursive: true });
17495
18638
  const path = startingLockPath(home);
17496
18639
  try {
17497
- writeFileSync14(path, String(process.pid), { flag: "wx" });
18640
+ writeFileSync16(path, String(process.pid), { flag: "wx" });
17498
18641
  return true;
17499
18642
  } catch {
17500
18643
  try {
17501
- const ageMs = Date.now() - statSync5(path).mtimeMs;
18644
+ const ageMs = Date.now() - statSync7(path).mtimeMs;
17502
18645
  if (ageMs > STARTING_LOCK_STALE_MS) {
17503
- rmSync6(path, { force: true });
17504
- writeFileSync14(path, String(process.pid), { flag: "wx" });
18646
+ rmSync7(path, { force: true });
18647
+ writeFileSync16(path, String(process.pid), { flag: "wx" });
17505
18648
  return true;
17506
18649
  }
17507
18650
  } catch {
@@ -17511,7 +18654,7 @@ function tryAcquireStartingLock(home = difftoolHome()) {
17511
18654
  }
17512
18655
  function releaseStartingLock(home = difftoolHome()) {
17513
18656
  try {
17514
- rmSync6(startingLockPath(home), { force: true });
18657
+ rmSync7(startingLockPath(home), { force: true });
17515
18658
  } catch {
17516
18659
  }
17517
18660
  }
@@ -21966,16 +23109,16 @@ __export(perceptual_diff_exports, {
21966
23109
  decodeImage: () => decodeImage,
21967
23110
  isIdentical: () => isIdentical
21968
23111
  });
21969
- import { readFileSync as readFileSync21 } from "fs";
21970
- import { extname as extname2 } from "path";
23112
+ import { readFileSync as readFileSync23 } from "fs";
23113
+ import { extname as extname3 } from "path";
21971
23114
  function decodeImage(path) {
21972
23115
  let buf;
21973
23116
  try {
21974
- buf = readFileSync21(path);
23117
+ buf = readFileSync23(path);
21975
23118
  } catch {
21976
23119
  return null;
21977
23120
  }
21978
- const ext = extname2(path).toLowerCase();
23121
+ const ext = extname3(path).toLowerCase();
21979
23122
  try {
21980
23123
  if (ext === ".png") {
21981
23124
  const png = import_pngjs.PNG.sync.read(buf);
@@ -21990,9 +23133,24 @@ function decodeImage(path) {
21990
23133
  }
21991
23134
  return null;
21992
23135
  }
21993
- function comparePerceptual(actualPath, expectedPath) {
21994
- const actual = decodeImage(actualPath);
21995
- const expected = decodeImage(expectedPath);
23136
+ async function decodeWithFallback(path, pluginDecode) {
23137
+ const core = decodeImage(path);
23138
+ if (core !== null) return core;
23139
+ let buf;
23140
+ try {
23141
+ buf = readFileSync23(path);
23142
+ } catch {
23143
+ return null;
23144
+ }
23145
+ try {
23146
+ return await pluginDecode(new Uint8Array(buf), path);
23147
+ } catch {
23148
+ return null;
23149
+ }
23150
+ }
23151
+ async function comparePerceptual(actualPath, expectedPath, pluginDecode = decodeImageWithPlugin) {
23152
+ const actual = await decodeWithFallback(actualPath, pluginDecode);
23153
+ const expected = await decodeWithFallback(expectedPath, pluginDecode);
21996
23154
  if (actual === null || expected === null) {
21997
23155
  return { scorable: false, score: null, reason: "undecodable" };
21998
23156
  }
@@ -22023,14 +23181,15 @@ var init_perceptual_diff = __esm({
22023
23181
  import_jpeg_js = __toESM(require_jpeg_js(), 1);
22024
23182
  init_pixelmatch();
22025
23183
  import_pngjs = __toESM(require_png(), 1);
23184
+ init_plugins();
22026
23185
  PERCEPTUAL_THRESHOLD = 0.1;
22027
23186
  }
22028
23187
  });
22029
23188
 
22030
23189
  // src/cli.ts
22031
- import { existsSync as existsSync17, mkdirSync as mkdirSync15, realpathSync, statSync as statSync6 } from "fs";
23190
+ import { existsSync as existsSync20, mkdirSync as mkdirSync17, realpathSync, statSync as statSync8 } from "fs";
22032
23191
  import { tmpdir as tmpdir2 } from "os";
22033
- import { basename as basename5, join as join20, resolve as resolve12 } from "path";
23192
+ import { basename as basename5, join as join24, resolve as resolve12 } from "path";
22034
23193
 
22035
23194
  // src/cli-subcommands.ts
22036
23195
  import { resolve as resolve3 } from "path";
@@ -22093,162 +23252,25 @@ async function handleDifftoolRegistration(action, local, force) {
22093
23252
  if (res.removed) {
22094
23253
  console.log(`Glassbox unregistered as git difftool at --${scope} scope.`);
22095
23254
  } else {
22096
- console.log(`Nothing to unregister at --${scope} scope (current tool: ${status.tool ?? "none"}).`);
22097
- }
22098
- process.exit(0);
22099
- }
22100
-
22101
- // src/cli.ts
22102
- init_connection();
22103
- init_queries();
22104
- init_debug();
22105
-
22106
- // src/ai/config.ts
22107
- init_zod();
22108
-
22109
- // src/global-config.ts
22110
- init_zod();
22111
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
22112
- import { homedir } from "os";
22113
- import { join as join4 } from "path";
22114
- var GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
22115
- function resolveGlobalConfigDir() {
22116
- const override = process.env.GLASSBOX_CONFIG_DIR;
22117
- if (override !== void 0 && override.trim() !== "") return override;
22118
- return join4(homedir(), ".glassbox");
22119
- }
22120
- var GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
22121
- var GLOBAL_CONFIG_PATH = join4(GLOBAL_CONFIG_DIR, "config.json");
22122
- function readGlobalConfig() {
22123
- try {
22124
- if (existsSync4(GLOBAL_CONFIG_PATH)) {
22125
- const raw2 = JSON.parse(readFileSync3(GLOBAL_CONFIG_PATH, "utf-8"));
22126
- const parsed = GlobalConfigSchema.safeParse(raw2);
22127
- if (parsed.success) return parsed.data;
22128
- }
22129
- } catch {
22130
- }
22131
- return {};
22132
- }
22133
- function writeGlobalConfig(config2) {
22134
- mkdirSync5(GLOBAL_CONFIG_DIR, { recursive: true });
22135
- writeFileSync3(GLOBAL_CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
22136
- try {
22137
- chmodSync(GLOBAL_CONFIG_PATH, 384);
22138
- } catch {
22139
- }
22140
- }
22141
- function updateGlobalConfig(mutator) {
22142
- const cfg = readGlobalConfig();
22143
- const result = mutator(cfg);
22144
- writeGlobalConfig(result === void 0 ? cfg : result);
22145
- }
22146
-
22147
- // src/ai/api-keys.ts
22148
- import { spawnSync as spawnSync6 } from "child_process";
22149
-
22150
- // src/ai/keychain.ts
22151
- import { spawnSync as spawnSync5 } from "child_process";
22152
- var WIN_CRED_READ_PS = `
22153
- Add-Type -TypeDefinition @'
22154
- using System;
22155
- using System.Runtime.InteropServices;
22156
- public class CredHelper {
22157
- [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
22158
- static extern bool CredRead(string t, int type, int f, out IntPtr p);
22159
- [DllImport("advapi32")]
22160
- static extern void CredFree(IntPtr p);
22161
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
22162
- struct CRED {
22163
- public int Flags; public int Type; public string TargetName; public string Comment;
22164
- public long LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob;
22165
- public int Persist; public int AttributeCount; public IntPtr Attributes;
22166
- public string TargetAlias; public string UserName;
22167
- }
22168
- public static string Read(string target) {
22169
- IntPtr ptr;
22170
- if (!CredRead(target, 1, 0, out ptr)) return "";
22171
- CRED c = (CRED)Marshal.PtrToStructure(ptr, typeof(CRED));
22172
- string r = Marshal.PtrToStringUni(c.CredentialBlob, c.CredentialBlobSize / 2);
22173
- CredFree(ptr);
22174
- return r;
22175
- }
22176
- }
22177
- '@
22178
- `;
22179
- function winCredTarget(platform) {
22180
- return `glassbox-${platform}-api-key`;
22181
- }
22182
- function getKeyFromKeychain(platform) {
22183
- const os = process.platform;
22184
- const account = `${platform}-api-key`;
22185
- try {
22186
- if (os === "darwin") {
22187
- const r = spawnSync5("security", ["find-generic-password", "-s", "glassbox", "-a", account, "-w"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
22188
- const result = r.stdout.trim();
22189
- return r.status === 0 && result !== "" ? result : null;
22190
- }
22191
- if (os === "linux") {
22192
- const r = spawnSync5("secret-tool", ["lookup", "service", "glassbox", "account", account], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
22193
- const result = r.stdout.trim();
22194
- return r.status === 0 && result !== "" ? result : null;
22195
- }
22196
- if (os === "win32") {
22197
- const target = winCredTarget(platform);
22198
- const list = spawnSync5("cmdkey", ["/list:" + target], { encoding: "utf-8" });
22199
- if (list.status !== 0 || (list.stdout || "").includes("* NONE *")) return null;
22200
- const script = WIN_CRED_READ_PS + `Write-Output ([CredHelper]::Read('${target}'))`;
22201
- const r = spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" });
22202
- const result = r.stdout.trim();
22203
- return r.status === 0 && result !== "" ? result : null;
22204
- }
22205
- } catch {
22206
- return null;
22207
- }
22208
- return null;
22209
- }
22210
- function assertSpawnOk(label, r) {
22211
- if (r.error) throw new Error(`${label} failed: ${r.error.message}`);
22212
- if (r.status !== 0) {
22213
- const detail = (String(r.stderr) || String(r.stdout)).trim();
22214
- throw new Error(`${label} failed (exit ${String(r.status)})${detail ? `: ${detail}` : ""}`);
22215
- }
22216
- }
22217
- function saveKeyToKeychain(platform, key) {
22218
- const os = process.platform;
22219
- const account = `${platform}-api-key`;
22220
- if (os === "darwin") {
22221
- spawnSync5("security", ["delete-generic-password", "-s", "glassbox", "-a", account], { stdio: "pipe" });
22222
- assertSpawnOk("Keychain write", spawnSync5("security", ["add-generic-password", "-s", "glassbox", "-a", account, "-w", key], { encoding: "utf-8" }));
22223
- return;
22224
- }
22225
- if (os === "linux") {
22226
- assertSpawnOk("System keyring write", spawnSync5("secret-tool", ["store", "--label=Glassbox API Key", "service", "glassbox", "account", account], { input: key, encoding: "utf-8" }));
22227
- return;
22228
- }
22229
- if (os === "win32") {
22230
- const target = winCredTarget(platform);
22231
- const escapedKey = key.replace(/'/g, "''");
22232
- const script = `cmdkey /generic:'${target}' /user:'glassbox' /pass:'${escapedKey}'`;
22233
- assertSpawnOk("Credential Manager write", spawnSync5("powershell", ["-NoProfile", "-Command", "-"], { input: script, encoding: "utf-8" }));
22234
- }
22235
- }
22236
- function isKeychainAvailable() {
22237
- const os = process.platform;
22238
- if (os === "darwin" || os === "win32") return true;
22239
- if (os === "linux") {
22240
- return spawnSync5("which", ["secret-tool"], { stdio: "pipe" }).status === 0;
23255
+ console.log(`Nothing to unregister at --${scope} scope (current tool: ${status.tool ?? "none"}).`);
22241
23256
  }
22242
- return false;
22243
- }
22244
- function getKeychainLabel() {
22245
- const os = process.platform;
22246
- if (os === "darwin") return "Keychain";
22247
- if (os === "linux") return "System Keyring";
22248
- if (os === "win32") return "Credential Manager";
22249
- return "System Keychain";
23257
+ process.exit(0);
22250
23258
  }
22251
23259
 
23260
+ // src/cli.ts
23261
+ init_connection();
23262
+ init_queries();
23263
+ init_debug();
23264
+
23265
+ // src/ai/config.ts
23266
+ init_zod();
23267
+ init_global_config();
23268
+
23269
+ // src/ai/api-keys.ts
23270
+ init_global_config();
23271
+ import { spawnSync as spawnSync6 } from "child_process";
23272
+ init_keychain();
23273
+
22252
23274
  // src/ai/models.ts
22253
23275
  init_zod();
22254
23276
  var AIModelSchema = external_exports.object({
@@ -22340,6 +23362,7 @@ function resolveModelId(platform, modelId) {
22340
23362
  }
22341
23363
 
22342
23364
  // src/ai/api-keys.ts
23365
+ init_keychain();
22343
23366
  function getKeyFromEnv(platform) {
22344
23367
  const envName = ENV_KEY_NAMES[platform];
22345
23368
  return process.env[envName] ?? null;
@@ -23952,10 +24975,10 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
23952
24975
 
23953
24976
  // src/server.ts
23954
24977
  import { serve } from "@hono/node-server";
23955
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
23956
- import { Hono as Hono20 } from "hono";
23957
- import { dirname as dirname7, join as join16 } from "path";
23958
- import { fileURLToPath as fileURLToPath2 } from "url";
24978
+ import { existsSync as existsSync16, readFileSync as readFileSync19 } from "fs";
24979
+ import { Hono as Hono21 } from "hono";
24980
+ import { dirname as dirname8, join as join20 } from "path";
24981
+ import { fileURLToPath as fileURLToPath3 } from "url";
23959
24982
 
23960
24983
  // src/channel-config.ts
23961
24984
  init_zod();
@@ -24055,6 +25078,11 @@ async function triggerChannel(dataDir, message) {
24055
25078
  }
24056
25079
  }
24057
25080
 
25081
+ // src/server.ts
25082
+ init_queries();
25083
+ init_global_config();
25084
+ init_plugins();
25085
+
24058
25086
  // src/routes/ai-api.ts
24059
25087
  import { Hono as Hono3 } from "hono";
24060
25088
 
@@ -25296,39 +26324,7 @@ __export(ai_exports, {
25296
26324
  startAnalysis: () => startAnalysis
25297
26325
  });
25298
26326
  init_zod();
25299
-
25300
- // src/api/_runner.ts
25301
- init_zod();
25302
- var OkResponseSchema = external_exports.object({ ok: external_exports.literal(true) });
25303
- function currentReviewId() {
25304
- if (typeof document === "undefined") return "";
25305
- return document.body.dataset.reviewId ?? "";
25306
- }
25307
- async function apiCall(responseSchema, path, opts = {}) {
25308
- const separator = path.includes("?") ? "&" : "?";
25309
- const url2 = "/api" + path + separator + "reviewId=" + encodeURIComponent(currentReviewId());
25310
- const res = await fetch(url2, {
25311
- headers: { "Content-Type": "application/json" },
25312
- method: opts.method,
25313
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
25314
- });
25315
- const json2 = await res.json();
25316
- const result = responseSchema.safeParse(json2);
25317
- if (!result.success) {
25318
- const summary = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
25319
- throw new Error(`API response from ${path} failed validation: ${summary}`);
25320
- }
25321
- return result.data;
25322
- }
25323
- function qs(params) {
25324
- const entries = Object.entries(params).filter(([, v]) => v !== void 0 && v !== null);
25325
- if (entries.length === 0) return "";
25326
- const usp = new URLSearchParams();
25327
- for (const [k, v] of entries) usp.set(k, String(v));
25328
- return "?" + usp.toString();
25329
- }
25330
-
25331
- // src/api/ai.ts
26327
+ init_runner();
25332
26328
  var KeySourceSchema = external_exports.enum(["env", "keychain", "config"]).nullable();
25333
26329
  var KeyStorageSchema = external_exports.enum(["keychain", "config"]);
25334
26330
  var GuidedReviewConfigShapeSchema = external_exports.object({
@@ -25541,6 +26537,7 @@ __export(annotations_exports, {
25541
26537
  });
25542
26538
  init_zod();
25543
26539
  init_schemas3();
26540
+ init_runner();
25544
26541
  var AnnotationCategorySchema = external_exports.enum([
25545
26542
  "bug",
25546
26543
  "fix",
@@ -25646,6 +26643,7 @@ __export(channel_exports, {
25646
26643
  triggerChannel: () => triggerChannel2
25647
26644
  });
25648
26645
  init_zod();
26646
+ init_runner();
25649
26647
  var GetChannelStatusRespSchema = external_exports.object({
25650
26648
  enabled: external_exports.boolean(),
25651
26649
  connected: external_exports.boolean()
@@ -25689,6 +26687,7 @@ __export(context_exports, {
25689
26687
  getContextLines: () => getContextLines
25690
26688
  });
25691
26689
  init_zod();
26690
+ init_runner();
25692
26691
  var ContextLineSchema = external_exports.object({
25693
26692
  num: external_exports.number().int(),
25694
26693
  content: external_exports.string()
@@ -25738,6 +26737,7 @@ __export(files_exports, {
25738
26737
  });
25739
26738
  init_zod();
25740
26739
  init_schemas3();
26740
+ init_runner();
25741
26741
  var FileStatusSchema = external_exports.enum(["pending", "reviewed"]);
25742
26742
  var GroundTruthMetaSchema = external_exports.object({
25743
26743
  label: external_exports.string().optional(),
@@ -25752,6 +26752,10 @@ var GroundTruthMetaSchema = external_exports.object({
25752
26752
  });
25753
26753
  var ListFilesRespSchema = external_exports.object({
25754
26754
  files: external_exports.array(ReviewFileSchema),
26755
+ /** Ids of files a content plugin renders (doc 29, GB-1052) — the client shows
26756
+ * the Code/Rendered toggle and routes the Rendered view through the image
26757
+ * viewer for these, like an SVG file. */
26758
+ pluginRendered: external_exports.array(external_exports.string()).optional(),
25755
26759
  annotationCounts: external_exports.record(external_exports.string(), external_exports.number()),
25756
26760
  staleCounts: external_exports.record(external_exports.string(), external_exports.number()),
25757
26761
  /** Keyed by review-file id; omitted/empty for non-ground-truth reviews. */
@@ -25809,6 +26813,7 @@ __export(image_exports, {
25809
26813
  imageUrl: () => imageUrl
25810
26814
  });
25811
26815
  init_zod();
26816
+ init_runner();
25812
26817
  var ImageSideSchema = external_exports.enum(["old", "new"]);
25813
26818
  var GetImageMetadataReqSchema = external_exports.object({ fileId: external_exports.string() });
25814
26819
  var GetImageMetadataRespSchema = external_exports.object({
@@ -25835,6 +26840,7 @@ __export(outline_exports, {
25835
26840
  getOutline: () => getOutline
25836
26841
  });
25837
26842
  init_zod();
26843
+ init_runner();
25838
26844
  var baseOutlineSymbol = external_exports.object({
25839
26845
  name: external_exports.string(),
25840
26846
  kind: external_exports.enum(["class", "function"]),
@@ -25870,29 +26876,8 @@ async function findSymbolDefinition(req) {
25870
26876
  );
25871
26877
  }
25872
26878
 
25873
- // src/api/project-settings.ts
25874
- var project_settings_exports = {};
25875
- __export(project_settings_exports, {
25876
- GetProjectSettingsRespSchema: () => GetProjectSettingsRespSchema,
25877
- ProjectSettingsSchema: () => ProjectSettingsSchema,
25878
- UpdateProjectSettingsReqSchema: () => UpdateProjectSettingsReqSchema,
25879
- UpdateProjectSettingsRespSchema: () => UpdateProjectSettingsRespSchema,
25880
- getProjectSettings: () => getProjectSettings,
25881
- updateProjectSettings: () => updateProjectSettings
25882
- });
25883
- init_zod();
25884
- var ProjectSettingsSchema = external_exports.object({
25885
- appName: external_exports.string().optional()
25886
- });
25887
- var GetProjectSettingsRespSchema = ProjectSettingsSchema;
25888
- var UpdateProjectSettingsReqSchema = ProjectSettingsSchema.partial();
25889
- var UpdateProjectSettingsRespSchema = ProjectSettingsSchema;
25890
- async function getProjectSettings() {
25891
- return apiCall(GetProjectSettingsRespSchema, "/project-settings");
25892
- }
25893
- async function updateProjectSettings(req) {
25894
- return apiCall(UpdateProjectSettingsRespSchema, "/project-settings", { method: "PATCH", body: req });
25895
- }
26879
+ // src/api/index.ts
26880
+ init_project_settings();
25896
26881
 
25897
26882
  // src/api/reviews.ts
25898
26883
  var reviews_exports = {};
@@ -25918,6 +26903,7 @@ __export(reviews_exports, {
25918
26903
  });
25919
26904
  init_zod();
25920
26905
  init_schemas3();
26906
+ init_runner();
25921
26907
  var ListReviewsRespSchema = external_exports.array(ReviewSchema);
25922
26908
  var GetCurrentReviewRespSchema = ReviewSchema.nullable();
25923
26909
  var OnCompleteHookResultSchema = external_exports.object({
@@ -25981,6 +26967,7 @@ __export(share_prompt_exports, {
25981
26967
  tickSharePrompt: () => tickSharePrompt
25982
26968
  });
25983
26969
  init_zod();
26970
+ init_runner();
25984
26971
  var GetSharePromptStateRespSchema = external_exports.object({
25985
26972
  dismissedAt: external_exports.number().nullable(),
25986
26973
  totalOpenMs: external_exports.number()
@@ -26008,6 +26995,7 @@ __export(system_exports, {
26008
26995
  openExternal: () => openExternal
26009
26996
  });
26010
26997
  init_zod();
26998
+ init_runner();
26011
26999
  var OpenExternalReqSchema = external_exports.object({
26012
27000
  // Restricted to http(s): this hands the value to the OS "open" handler, so
26013
27001
  // we don't want to let it launch arbitrary schemes (file:, custom apps).
@@ -26521,6 +27509,7 @@ function themeToInlineStyle(colors) {
26521
27509
  }
26522
27510
 
26523
27511
  // src/api/themes.ts
27512
+ init_runner();
26524
27513
  function buildThemeColorsSchema() {
26525
27514
  const shape = {};
26526
27515
  for (const key of THEME_VARIABLES) shape[key] = external_exports.string();
@@ -26613,11 +27602,13 @@ async function deleteTheme(req) {
26613
27602
  // src/api/attachments.ts
26614
27603
  init_zod();
26615
27604
  init_schemas3();
27605
+ init_runner();
26616
27606
  var ListAttachmentsRespSchema = external_exports.array(AttachmentSchema);
26617
27607
 
26618
27608
  // src/api/difftool.ts
26619
27609
  init_zod();
26620
27610
  init_schemas3();
27611
+ init_runner();
26621
27612
  var DifftoolStatusRespSchema = external_exports.object({
26622
27613
  tool: external_exports.string().nullable(),
26623
27614
  cmd: external_exports.string().nullable(),
@@ -26652,8 +27643,163 @@ var DifftoolPollRespSchema = external_exports.object({
26652
27643
  });
26653
27644
  var DifftoolEndRespSchema = external_exports.object({ ok: external_exports.literal(true) });
26654
27645
 
27646
+ // src/api/plugins.ts
27647
+ init_zod();
27648
+ init_runner();
27649
+ var PluginStatusSchema = external_exports.enum(["loaded", "disabled", "error"]);
27650
+ var PluginPreferenceInfoSchema = external_exports.object({
27651
+ key: external_exports.string(),
27652
+ label: external_exports.string(),
27653
+ type: external_exports.enum(["string", "number", "boolean", "select"]),
27654
+ default: external_exports.string().optional(),
27655
+ description: external_exports.string().optional(),
27656
+ options: external_exports.array(external_exports.string()).optional(),
27657
+ scope: external_exports.enum(["global", "project"]).optional(),
27658
+ secret: external_exports.boolean().optional()
27659
+ });
27660
+ var ConfigLabelColorSchema2 = external_exports.enum(["default", "success", "error", "warning", "transient"]);
27661
+ var ConfigLayoutItemSchema2 = external_exports.lazy(
27662
+ () => external_exports.object({
27663
+ type: external_exports.enum(["preference", "divider", "spacer", "label", "button", "group"]),
27664
+ key: external_exports.string().optional(),
27665
+ id: external_exports.string().optional(),
27666
+ text: external_exports.string().optional(),
27667
+ color: ConfigLabelColorSchema2.optional(),
27668
+ label: external_exports.string().optional(),
27669
+ action: external_exports.string().optional(),
27670
+ style: external_exports.string().optional(),
27671
+ title: external_exports.string().optional(),
27672
+ collapsed: external_exports.boolean().optional(),
27673
+ items: external_exports.array(ConfigLayoutItemSchema2).optional()
27674
+ })
27675
+ );
27676
+ var PluginInfoSchema = external_exports.object({
27677
+ id: external_exports.string(),
27678
+ name: external_exports.string(),
27679
+ version: external_exports.string(),
27680
+ /** Extensions the plugin's content types declare (informational). */
27681
+ extensions: external_exports.array(external_exports.string()),
27682
+ status: PluginStatusSchema,
27683
+ error: external_exports.string().optional(),
27684
+ /** True when active (loaded). False when disabled by either scope. */
27685
+ enabled: external_exports.boolean(),
27686
+ /** Which scope disables it (global wins), when disabled. */
27687
+ disabledScope: external_exports.enum(["global", "project"]).optional(),
27688
+ /** The two independent disable flags, so the UI can render both toggles. */
27689
+ globalDisabled: external_exports.boolean(),
27690
+ projectDisabled: external_exports.boolean(),
27691
+ /** Manifest-declared preferences + their current values (doc 29 FR-29.12).
27692
+ * Secret values are never sent; `secretConfigured` names the secret keys that
27693
+ * have a stored value (GB-1054). */
27694
+ preferences: external_exports.array(PluginPreferenceInfoSchema),
27695
+ preferenceValues: external_exports.record(external_exports.string(), external_exports.string()),
27696
+ secretConfigured: external_exports.array(external_exports.string()),
27697
+ /** Optional manifest arrangement of the preferences (doc 29 FR-29.18). */
27698
+ configLayout: external_exports.array(ConfigLayoutItemSchema2).optional(),
27699
+ /** Effective `label`-item text/color, keyed by label id (doc 29 FR-29.18). */
27700
+ configLabels: external_exports.record(external_exports.string(), external_exports.object({ text: external_exports.string(), color: ConfigLabelColorSchema2.optional() }))
27701
+ });
27702
+ var ListPluginsRespSchema = external_exports.object({
27703
+ plugins: external_exports.array(PluginInfoSchema),
27704
+ /** Set when a mutation (e.g. install-from-disk of a non-plugin folder) failed;
27705
+ * the list is still returned so the UI stays in sync + can show the message. */
27706
+ error: external_exports.string().optional()
27707
+ });
27708
+ var SetPluginDisabledReqSchema = external_exports.object({
27709
+ scope: external_exports.enum(["global", "project"]),
27710
+ disabled: external_exports.boolean()
27711
+ });
27712
+ var InstallPluginReqSchema = external_exports.object({ path: external_exports.string().min(1) });
27713
+ var RequirementStatusSchema = external_exports.object({
27714
+ id: external_exports.string(),
27715
+ label: external_exports.string(),
27716
+ met: external_exports.boolean(),
27717
+ hint: external_exports.string(),
27718
+ docUrl: external_exports.string().optional()
27719
+ });
27720
+ var AvailablePluginSchema = external_exports.object({
27721
+ id: external_exports.string(),
27722
+ name: external_exports.string(),
27723
+ version: external_exports.string(),
27724
+ description: external_exports.string().optional(),
27725
+ extensions: external_exports.array(external_exports.string()),
27726
+ requirements: external_exports.array(RequirementStatusSchema),
27727
+ provisionNotes: external_exports.array(external_exports.string()),
27728
+ selfContained: external_exports.boolean(),
27729
+ cliHint: external_exports.string().optional()
27730
+ });
27731
+ var ListAvailablePluginsRespSchema = external_exports.object({ available: external_exports.array(AvailablePluginSchema) });
27732
+ var ProvisionOutcomeSchema = external_exports.object({
27733
+ step: external_exports.string(),
27734
+ ok: external_exports.boolean(),
27735
+ skipped: external_exports.boolean(),
27736
+ detail: external_exports.string()
27737
+ });
27738
+ var InstallResultSchema = external_exports.object({
27739
+ id: external_exports.string(),
27740
+ installed: external_exports.boolean(),
27741
+ status: external_exports.enum(["ready", "needs-setup", "error"]),
27742
+ requirements: external_exports.array(RequirementStatusSchema),
27743
+ provisioned: external_exports.array(ProvisionOutcomeSchema),
27744
+ instructions: external_exports.array(external_exports.string()),
27745
+ error: external_exports.string().optional()
27746
+ });
27747
+ var InstallBundledPluginRespSchema = external_exports.object({
27748
+ result: InstallResultSchema,
27749
+ plugins: external_exports.array(PluginInfoSchema),
27750
+ available: external_exports.array(AvailablePluginSchema)
27751
+ });
27752
+ var UninstallPluginRespSchema = ListPluginsRespSchema.extend({
27753
+ available: external_exports.array(AvailablePluginSchema).optional()
27754
+ });
27755
+ var SetPluginPreferenceReqSchema = external_exports.object({ key: external_exports.string().min(1), value: external_exports.string() });
27756
+ var RunPluginActionReqSchema = external_exports.object({ actionId: external_exports.string().min(1), value: external_exports.string().optional() });
27757
+ var UIElementFaceSchema = external_exports.object({
27758
+ label: external_exports.string().optional(),
27759
+ icon: external_exports.string().optional(),
27760
+ title: external_exports.string().optional(),
27761
+ style: external_exports.string().optional()
27762
+ });
27763
+ var UISegmentSchema = external_exports.object({
27764
+ id: external_exports.string(),
27765
+ label: external_exports.string().optional(),
27766
+ icon: external_exports.string().optional(),
27767
+ title: external_exports.string().optional()
27768
+ });
27769
+ var PluginUIElementSchema = external_exports.object({
27770
+ pluginId: external_exports.string(),
27771
+ id: external_exports.string(),
27772
+ type: external_exports.string(),
27773
+ location: external_exports.string(),
27774
+ label: external_exports.string().optional(),
27775
+ icon: external_exports.string().optional(),
27776
+ title: external_exports.string().optional(),
27777
+ style: external_exports.string().optional(),
27778
+ action: external_exports.string().optional(),
27779
+ url: external_exports.string().optional(),
27780
+ // Stateful controls (doc 30 FR-30.3): toggle/switch (`on`/`off`), segmented
27781
+ // (`segments`/`selectionMode`); `stateKey` persists, `value` is the resolved
27782
+ // current state the host attaches when listing.
27783
+ on: UIElementFaceSchema.optional(),
27784
+ off: UIElementFaceSchema.optional(),
27785
+ onLabel: external_exports.string().optional(),
27786
+ offLabel: external_exports.string().optional(),
27787
+ segments: external_exports.array(UISegmentSchema).optional(),
27788
+ selectionMode: external_exports.string().optional(),
27789
+ stateKey: external_exports.string().optional(),
27790
+ value: external_exports.string().optional()
27791
+ });
27792
+ var ListPluginUiRespSchema = external_exports.object({ elements: external_exports.array(PluginUIElementSchema) });
27793
+ var RunPluginActionRespSchema = ListPluginsRespSchema.extend({
27794
+ result: external_exports.object({ message: external_exports.string().optional() }).optional()
27795
+ });
27796
+
27797
+ // src/api/index.ts
27798
+ init_project_settings();
27799
+
26655
27800
  // src/api/review-notes.ts
26656
27801
  init_zod();
27802
+ init_runner();
26657
27803
  var DiscardReviewNoteReqSchema = external_exports.object({
26658
27804
  guid: external_exports.string().min(1),
26659
27805
  /** Repo-relative source file the note is on (scopes the shard search). */
@@ -27100,7 +28246,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
27100
28246
  aiApiRoutes.route("/", aiAnalysisRoutes);
27101
28247
 
27102
28248
  // src/routes/api.ts
27103
- import { Hono as Hono15 } from "hono";
28249
+ import { Hono as Hono16 } from "hono";
27104
28250
 
27105
28251
  // src/routes/api/annotations.ts
27106
28252
  import { Hono as Hono4 } from "hono";
@@ -27110,8 +28256,8 @@ init_queries();
27110
28256
  init_attachment_queries();
27111
28257
  init_queries();
27112
28258
  init_schemas3();
27113
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
27114
- import { join as join10 } from "path";
28259
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
28260
+ import { join as join13 } from "path";
27115
28261
  init_image_metadata();
27116
28262
 
27117
28263
  // src/utils/formatReviewMode.ts
@@ -27330,10 +28476,10 @@ function buildRegion(a, gt, dimsFor) {
27330
28476
 
27331
28477
  // src/export/generate.ts
27332
28478
  function deleteReviewExport(reviewId, repoRoot) {
27333
- const exportDir = join10(repoRoot, ".glassbox");
28479
+ const exportDir = join13(repoRoot, ".glassbox");
27334
28480
  for (const ext of ["md", "json"]) {
27335
- const archivePath = join10(exportDir, `review-${reviewId}.${ext}`);
27336
- if (existsSync10(archivePath)) unlinkSync2(archivePath);
28481
+ const archivePath = join13(exportDir, `review-${reviewId}.${ext}`);
28482
+ if (existsSync13(archivePath)) unlinkSync2(archivePath);
27337
28483
  }
27338
28484
  }
27339
28485
  function annotationAnchorLabel(a) {
@@ -27360,8 +28506,8 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
27360
28506
  if (!review) throw new Error("Review not found");
27361
28507
  const files = await getReviewFiles(reviewId);
27362
28508
  const annotations = await getAnnotationsForReview(reviewId);
27363
- const exportDir = join10(repoRoot, ".glassbox");
27364
- mkdirSync8(exportDir, { recursive: true });
28509
+ const exportDir = join13(repoRoot, ".glassbox");
28510
+ mkdirSync10(exportDir, { recursive: true });
27365
28511
  const byFile = {};
27366
28512
  for (const a of annotations) {
27367
28513
  if (!(a.file_path in byFile)) byFile[a.file_path] = [];
@@ -27454,20 +28600,20 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
27454
28600
  resolveDims: resolveImageDims
27455
28601
  });
27456
28602
  const json2 = JSON.stringify(exportData, null, 2);
27457
- const archivePath = join10(exportDir, `review-${review.id}.md`);
27458
- writeFileSync9(archivePath, content, "utf-8");
27459
- writeFileSync9(join10(exportDir, `review-${review.id}.json`), json2, "utf-8");
28603
+ const archivePath = join13(exportDir, `review-${review.id}.md`);
28604
+ writeFileSync11(archivePath, content, "utf-8");
28605
+ writeFileSync11(join13(exportDir, `review-${review.id}.json`), json2, "utf-8");
27460
28606
  if (isCurrent) {
27461
- const latestPath = join10(exportDir, "latest-review.md");
27462
- writeFileSync9(latestPath, content, "utf-8");
27463
- writeFileSync9(join10(exportDir, "latest-review.json"), json2, "utf-8");
28607
+ const latestPath = join13(exportDir, "latest-review.md");
28608
+ writeFileSync11(latestPath, content, "utf-8");
28609
+ writeFileSync11(join13(exportDir, "latest-review.json"), json2, "utf-8");
27464
28610
  return latestPath;
27465
28611
  }
27466
28612
  return archivePath;
27467
28613
  }
27468
28614
  function resolveImageDims(absPath) {
27469
28615
  try {
27470
- const meta3 = extractMetadata(readFileSync9(absPath), absPath);
28616
+ const meta3 = extractMetadata(readFileSync12(absPath), absPath);
27471
28617
  if (meta3.width !== null && meta3.height !== null) {
27472
28618
  return { width: meta3.width, height: meta3.height };
27473
28619
  }
@@ -27572,7 +28718,7 @@ annotationsRoutes.get("/annotations/all", async (c) => {
27572
28718
  init_store2();
27573
28719
  init_attachment_queries();
27574
28720
  init_queries();
27575
- import { readFileSync as readFileSync10, statSync as statSync3 } from "fs";
28721
+ import { readFileSync as readFileSync13, statSync as statSync5 } from "fs";
27576
28722
  import { Hono as Hono5 } from "hono";
27577
28723
 
27578
28724
  // src/utils/mime.ts
@@ -27686,9 +28832,9 @@ attachmentsRoutes.get("/attachments/:id/raw", async (c) => {
27686
28832
  const attachment = await getAttachment(idParam.data);
27687
28833
  if (attachment === void 0) return c.text("Not found", 404);
27688
28834
  try {
27689
- const stat = statSync3(attachment.stored_path);
28835
+ const stat = statSync5(attachment.stored_path);
27690
28836
  if (!stat.isFile()) return c.text("Not found", 404);
27691
- const bytes = readFileSync10(attachment.stored_path);
28837
+ const bytes = readFileSync13(attachment.stored_path);
27692
28838
  return new Response(new Uint8Array(bytes), {
27693
28839
  headers: {
27694
28840
  "Content-Type": attachment.mime_type,
@@ -27706,7 +28852,7 @@ attachmentsRoutes.post("/attachments/:id/quicklook", async (c) => {
27706
28852
  const attachment = await getAttachment(idParam.data);
27707
28853
  if (attachment === void 0) return errorResponse(c, "Attachment not found", 404);
27708
28854
  try {
27709
- statSync3(attachment.stored_path);
28855
+ statSync5(attachment.stored_path);
27710
28856
  } catch {
27711
28857
  return errorResponse(c, "Attachment file missing", 404);
27712
28858
  }
@@ -27808,6 +28954,19 @@ function groundTruthMetaByFileId(mode, files) {
27808
28954
  return out;
27809
28955
  }
27810
28956
 
28957
+ // src/plugins/fileView.ts
28958
+ init_plugins();
28959
+ function pluginRendersFile(filePath) {
28960
+ return pluginsEnabled() && mightHandleFile(filePath);
28961
+ }
28962
+ async function renderPluginSvgSide(mode, filePath, oldPath, side, cwd) {
28963
+ if (mode === null || !pluginRendersFile(filePath)) return null;
28964
+ const source = getModeFileContent(mode, side === "old" ? oldPath : filePath, side, cwd);
28965
+ if (source.trim() === "") return null;
28966
+ const view = await renderContent({ bytes: new TextEncoder().encode(source), text: source, path: filePath, side });
28967
+ return view?.svg !== void 0 && view.svg !== "" ? view.svg : null;
28968
+ }
28969
+
27811
28970
  // src/routes/api/files.ts
27812
28971
  init_openOS();
27813
28972
  var filesRoutes = new Hono7();
@@ -27820,7 +28979,14 @@ filesRoutes.get("/files", async (c) => {
27820
28979
  getReview(reviewId)
27821
28980
  ]);
27822
28981
  const groundTruth = review ? groundTruthMetaByFileId(parseModeString(review.mode), files) : void 0;
27823
- return c.json({ files, annotationCounts, staleCounts, ...groundTruth ? { groundTruth } : {} });
28982
+ const pluginRendered = files.filter((f) => pluginRendersFile(f.file_path)).map((f) => f.id);
28983
+ return c.json({
28984
+ files,
28985
+ annotationCounts,
28986
+ staleCounts,
28987
+ ...groundTruth ? { groundTruth } : {},
28988
+ ...pluginRendered.length > 0 ? { pluginRendered } : {}
28989
+ });
27824
28990
  });
27825
28991
  filesRoutes.get("/files/:fileId", async (c) => {
27826
28992
  const fileId = requirePathParam(c, "fileId");
@@ -27882,8 +29048,8 @@ import { Hono as Hono8 } from "hono";
27882
29048
 
27883
29049
  // src/git/image.ts
27884
29050
  import { spawnSync as spawnSync8 } from "child_process";
27885
- import { readFileSync as readFileSync11 } from "fs";
27886
- import { join as join11, resolve as resolve8 } from "path";
29051
+ import { readFileSync as readFileSync14 } from "fs";
29052
+ import { join as join14, resolve as resolve8 } from "path";
27887
29053
  init_image_metadata();
27888
29054
  function getOldRef(mode) {
27889
29055
  switch (mode.type) {
@@ -27947,14 +29113,14 @@ function gitShowFile(ref, filePath, repoRoot) {
27947
29113
  }
27948
29114
  function readWorkingFile(filePath, repoRoot) {
27949
29115
  try {
27950
- return readFileSync11(resolve8(repoRoot, filePath));
29116
+ return readFileSync14(resolve8(repoRoot, filePath));
27951
29117
  } catch {
27952
29118
  return null;
27953
29119
  }
27954
29120
  }
27955
29121
  function readDiskImage(absPath) {
27956
29122
  try {
27957
- const data = readFileSync11(absPath);
29123
+ const data = readFileSync14(absPath);
27958
29124
  return { data, size: data.length };
27959
29125
  } catch {
27960
29126
  return null;
@@ -27965,7 +29131,7 @@ function groundTruthEntry(mode, filePath) {
27965
29131
  }
27966
29132
  function getOldImage(mode, filePath, oldPath, repoRoot) {
27967
29133
  if (mode.type === "diff") {
27968
- return readDiskImage(join11(directComparisonRoots(mode).rootA, oldPath ?? filePath));
29134
+ return readDiskImage(join14(directComparisonRoots(mode).rootA, oldPath ?? filePath));
27969
29135
  }
27970
29136
  if (mode.type === "ground-truth") {
27971
29137
  const entry = groundTruthEntry(mode, filePath);
@@ -27985,7 +29151,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
27985
29151
  }
27986
29152
  function getNewImage(mode, filePath, repoRoot) {
27987
29153
  if (mode.type === "diff") {
27988
- return readDiskImage(join11(directComparisonRoots(mode).rootB, filePath));
29154
+ return readDiskImage(join14(directComparisonRoots(mode).rootB, filePath));
27989
29155
  }
27990
29156
  if (mode.type === "ground-truth") {
27991
29157
  const entry = groundTruthEntry(mode, filePath);
@@ -28059,6 +29225,12 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
28059
29225
  const diff = parseDiffData(file2.diff_data);
28060
29226
  const oldPath = diff?.oldPath ?? null;
28061
29227
  const status = diff?.status ?? "modified";
29228
+ if (!isImageFile(file2.file_path) && !(side === "old" && status === "added") && !(side === "new" && status === "deleted")) {
29229
+ const svg = await renderPluginSvgSide(mode, file2.file_path, oldPath ?? file2.file_path, side, repoRoot);
29230
+ if (svg !== null) {
29231
+ return new Response(svg, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" } });
29232
+ }
29233
+ }
28062
29234
  const image = resolveImageSide(
28063
29235
  fileIdParam.data,
28064
29236
  side,
@@ -28081,7 +29253,7 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
28081
29253
  init_queries();
28082
29254
  init_debug();
28083
29255
  import { spawnSync as spawnSync9 } from "child_process";
28084
- import { readFileSync as readFileSync12 } from "fs";
29256
+ import { readFileSync as readFileSync15 } from "fs";
28085
29257
  import { Hono as Hono9 } from "hono";
28086
29258
  import { resolve as resolve9 } from "path";
28087
29259
 
@@ -28459,7 +29631,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
28459
29631
  }
28460
29632
  let content = "";
28461
29633
  try {
28462
- content = readFileSync12(resolve9(repoRoot, filePath), "utf-8");
29634
+ content = readFileSync15(resolve9(repoRoot, filePath), "utf-8");
28463
29635
  } catch {
28464
29636
  continue;
28465
29637
  }
@@ -28494,48 +29666,268 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
28494
29666
  }
28495
29667
  }
28496
29668
 
28497
- // src/routes/api/project-settings.ts
28498
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
29669
+ // src/routes/api/plugins.ts
28499
29670
  import { Hono as Hono10 } from "hono";
28500
- import { join as join12 } from "path";
28501
- var projectSettingsRoutes = new Hono10();
28502
- function readProjectSettings(repoRoot) {
28503
- const settingsPath = join12(repoRoot, ".glassbox", "settings.json");
29671
+
29672
+ // src/plugins/available.ts
29673
+ init_install();
29674
+ init_loader();
29675
+ import { existsSync as existsSync14 } from "fs";
29676
+ import { join as join15 } from "path";
29677
+
29678
+ // src/plugins/readiness.ts
29679
+ import { spawnSync as spawnSync10 } from "child_process";
29680
+ var defaultProbe = (command, args) => {
28504
29681
  try {
28505
- if (existsSync11(settingsPath)) {
28506
- const raw2 = JSON.parse(readFileSync13(settingsPath, "utf-8"));
28507
- const parsed = ProjectSettingsSchema.safeParse(raw2);
28508
- if (parsed.success) return parsed.data;
28509
- }
29682
+ const r = spawnSync10(command, args, { stdio: "ignore", timeout: 1e4 });
29683
+ return r.error === void 0 && r.status === 0;
28510
29684
  } catch {
29685
+ return false;
28511
29686
  }
28512
- return {};
29687
+ };
29688
+ function checkRequirement(req, runProbe = defaultProbe) {
29689
+ const met = runProbe(req.command, req.checkArgs ?? ["--version"]);
29690
+ return { id: req.id, label: req.label, met, hint: req.hint, docUrl: req.docUrl };
28513
29691
  }
28514
- function writeProjectSettings(repoRoot, settings) {
28515
- const dir = join12(repoRoot, ".glassbox");
28516
- mkdirSync9(dir, { recursive: true });
28517
- writeFileSync10(join12(dir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8");
29692
+ function checkRequirements(reqs, runProbe = defaultProbe) {
29693
+ return (reqs ?? []).map((r) => checkRequirement(r, runProbe));
28518
29694
  }
28519
- projectSettingsRoutes.get("/project-settings", (c) => {
29695
+ function requirementMet(statuses, id) {
29696
+ return statuses.find((s) => s.id === id)?.met ?? false;
29697
+ }
29698
+
29699
+ // src/plugins/available.ts
29700
+ function manifestExtensions(manifest) {
29701
+ return (manifest.contentTypes ?? []).flatMap((ct) => ct.extensions ?? []);
29702
+ }
29703
+ function provisionNotes(install) {
29704
+ const notes = [];
29705
+ for (const step of install?.provision ?? []) {
29706
+ if (step.kind === "fetch") notes.push(`Downloads ${step.dest}.`);
29707
+ else notes.push(step.note ?? `Installs ${step.packages.join(", ")}.`);
29708
+ }
29709
+ return notes;
29710
+ }
29711
+ function describeAvailablePlugin(manifest, runProbe) {
29712
+ const requirements = checkRequirements(manifest.install?.requirements, runProbe);
29713
+ const notes = provisionNotes(manifest.install);
29714
+ return {
29715
+ id: manifest.id,
29716
+ name: manifest.name,
29717
+ version: manifest.version,
29718
+ description: manifest.description,
29719
+ extensions: manifestExtensions(manifest),
29720
+ requirements,
29721
+ provisionNotes: notes,
29722
+ selfContained: requirements.length === 0 && notes.length === 0,
29723
+ cliHint: manifest.install?.cliHint
29724
+ };
29725
+ }
29726
+ function listAvailablePlugins(opts) {
29727
+ const bundledDir = opts?.bundledDir ?? bundledPluginsDir();
29728
+ const userDir = opts?.userDir ?? pluginsDir();
29729
+ const out = [];
29730
+ for (const { manifest } of discoverBundledPlugins(bundledDir)) {
29731
+ if (manifest.autoInstall !== false) continue;
29732
+ if (existsSync14(join15(userDir, manifest.id))) continue;
29733
+ out.push(describeAvailablePlugin(manifest, opts?.runProbe));
29734
+ }
29735
+ return out.sort((a, b) => a.name.localeCompare(b.name));
29736
+ }
29737
+
29738
+ // src/routes/api/plugins.ts
29739
+ init_enablement();
29740
+ init_plugins();
29741
+ init_install();
29742
+
29743
+ // src/plugins/install-action.ts
29744
+ init_install();
29745
+ init_loader();
29746
+ import { spawnSync as spawnSync11 } from "child_process";
29747
+ import { createHash as createHash4 } from "crypto";
29748
+ import { cpSync as cpSync2, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
29749
+ import { join as join16 } from "path";
29750
+ var defaultFetchBytes = async (url2) => {
29751
+ const res = await fetch(url2);
29752
+ if (!res.ok) throw new Error(`HTTP ${String(res.status)}`);
29753
+ return new Uint8Array(await res.arrayBuffer());
29754
+ };
29755
+ var defaultRunNpmInstall = (dir, packages) => {
29756
+ const r = spawnSync11("npm", ["install", "--no-save", "--prefix", dir, ...packages], { stdio: "ignore", timeout: 6e5 });
29757
+ if (r.error !== void 0) return { ok: false, detail: r.error.message };
29758
+ if (r.status !== 0) return { ok: false, detail: `npm exited ${String(r.status)}` };
29759
+ return { ok: true, detail: "installed" };
29760
+ };
29761
+ async function runProvisionStep(step, installDir, requirements, deps) {
29762
+ if (step.kind === "fetch") {
29763
+ const label2 = `Download ${step.dest}`;
29764
+ try {
29765
+ const bytes = await deps.fetchBytes(step.url);
29766
+ if (step.sha256 !== void 0) {
29767
+ const got = createHash4("sha256").update(bytes).digest("hex");
29768
+ if (got.toLowerCase() !== step.sha256.toLowerCase()) {
29769
+ return { step: label2, ok: false, skipped: false, detail: `checksum mismatch (expected ${step.sha256})` };
29770
+ }
29771
+ }
29772
+ writeFileSync12(join16(installDir, step.dest), bytes);
29773
+ return { step: label2, ok: true, skipped: false, detail: `saved ${step.dest}` };
29774
+ } catch (e) {
29775
+ return { step: label2, ok: false, skipped: false, detail: e instanceof Error ? e.message : "download failed" };
29776
+ }
29777
+ }
29778
+ const label = `Install ${step.packages.join(", ")}`;
29779
+ const reqId = step.requires ?? "npm";
29780
+ if (!requirementMet(requirements, reqId)) {
29781
+ return { step: label, ok: false, skipped: true, detail: `${reqId} is not available` };
29782
+ }
29783
+ const r = deps.runNpmInstall(installDir, step.packages);
29784
+ return { step: label, ok: r.ok, skipped: false, detail: r.detail };
29785
+ }
29786
+ function buildInstructions(manifest, requirements, provisioned) {
29787
+ const out = [];
29788
+ for (const r of requirements) if (!r.met) out.push(`${r.label}: ${r.hint}`);
29789
+ for (const p of provisioned) {
29790
+ if (p.ok) continue;
29791
+ out.push(p.skipped ? `${p.step} was skipped \u2014 ${p.detail}.` : `${p.step} failed \u2014 ${p.detail}.`);
29792
+ }
29793
+ const cliHint = manifest.install?.cliHint;
29794
+ if (out.length > 0 && cliHint !== void 0 && cliHint !== "") out.push(`Or finish from a terminal: ${cliHint}`);
29795
+ return out;
29796
+ }
29797
+ async function installAvailablePlugin(id, deps = {}) {
29798
+ const bundledDir = deps.bundledDir ?? bundledPluginsDir();
29799
+ const userDir = deps.userDir ?? pluginsDir();
29800
+ const fetchBytes = deps.fetchBytes ?? defaultFetchBytes;
29801
+ const runNpmInstall = deps.runNpmInstall ?? defaultRunNpmInstall;
29802
+ const bundled = discoverBundledPlugins(bundledDir).find((b) => b.manifest.id === id);
29803
+ if (bundled === void 0) {
29804
+ return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: "Not a bundled plugin available to install." };
29805
+ }
29806
+ const manifest = bundled.manifest;
29807
+ const dest = join16(userDir, id);
29808
+ try {
29809
+ mkdirSync11(userDir, { recursive: true });
29810
+ cpSync2(bundled.dir, dest, { recursive: true });
29811
+ undismissPlugin(id, userDir);
29812
+ } catch (e) {
29813
+ return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: e instanceof Error ? e.message : "Copy failed." };
29814
+ }
29815
+ const requirements = checkRequirements(manifest.install?.requirements, deps.runProbe);
29816
+ const provisioned = [];
29817
+ for (const step of manifest.install?.provision ?? []) {
29818
+ provisioned.push(await runProvisionStep(step, dest, requirements, { fetchBytes, runNpmInstall }));
29819
+ }
29820
+ const instructions = buildInstructions(manifest, requirements, provisioned);
29821
+ const status = instructions.length === 0 ? "ready" : "needs-setup";
29822
+ return { id, installed: true, status, requirements, provisioned, instructions };
29823
+ }
29824
+
29825
+ // src/routes/api/plugins.ts
29826
+ init_loader();
29827
+ init_settings();
29828
+ var pluginsRoutes = new Hono10();
29829
+ pluginsRoutes.get("/plugins", (c) => {
29830
+ return c.json({ plugins: describeInstalledPlugins(c.get("repoRoot")) });
29831
+ });
29832
+ pluginsRoutes.get("/plugins/ui", (c) => {
29833
+ return c.json({ elements: listPluginUIElements(c.get("repoRoot")) });
29834
+ });
29835
+ pluginsRoutes.get("/plugins/available", (c) => {
29836
+ return c.json({ available: listAvailablePlugins() });
29837
+ });
29838
+ pluginsRoutes.post("/plugins/:id/install-bundled", async (c) => {
29839
+ const id = requirePathParam(c, "id");
29840
+ if (!id.ok) return id.response;
28520
29841
  const repoRoot = c.get("repoRoot");
28521
- return c.json(readProjectSettings(repoRoot));
29842
+ const result = await installAvailablePlugin(id.data);
29843
+ if (result.installed) await reloadContentPlugins(repoRoot);
29844
+ return c.json({ result, plugins: describeInstalledPlugins(repoRoot), available: listAvailablePlugins() });
29845
+ });
29846
+ pluginsRoutes.post("/plugins/:id/disabled", async (c) => {
29847
+ const id = requirePathParam(c, "id");
29848
+ if (!id.ok) return id.response;
29849
+ const parsed = await parseBody(c, SetPluginDisabledReqSchema);
29850
+ if (!parsed.ok) return parsed.response;
29851
+ const repoRoot = c.get("repoRoot");
29852
+ if (parsed.data.scope === "global") setGlobalDisabled(id.data, parsed.data.disabled);
29853
+ else setProjectDisabled(repoRoot, id.data, parsed.data.disabled);
29854
+ await reloadContentPlugins(repoRoot);
29855
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29856
+ });
29857
+ pluginsRoutes.post("/plugins/install", async (c) => {
29858
+ const parsed = await parseBody(c, InstallPluginReqSchema);
29859
+ if (!parsed.ok) return parsed.response;
29860
+ const repoRoot = c.get("repoRoot");
29861
+ try {
29862
+ installPluginFromDisk(parsed.data.path);
29863
+ } catch (e) {
29864
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), error: e instanceof Error ? e.message : "Install failed" }, 400);
29865
+ }
29866
+ await reloadContentPlugins(repoRoot);
29867
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29868
+ });
29869
+ pluginsRoutes.post("/plugins/:id/preferences", async (c) => {
29870
+ const id = requirePathParam(c, "id");
29871
+ if (!id.ok) return id.response;
29872
+ const parsed = await parseBody(c, SetPluginPreferenceReqSchema);
29873
+ if (!parsed.ok) return parsed.response;
29874
+ const manifest = getPluginManifest(id.data);
29875
+ if (manifest === void 0) return c.json({ error: "Plugin not found" }, 404);
29876
+ const repoRoot = c.get("repoRoot");
29877
+ writePluginSetting(manifest, repoRoot, parsed.data.key, parsed.data.value);
29878
+ await reloadContentPlugins(repoRoot);
29879
+ return c.json({ plugins: describeInstalledPlugins(repoRoot) });
29880
+ });
29881
+ pluginsRoutes.post("/plugins/:id/action", async (c) => {
29882
+ const id = requirePathParam(c, "id");
29883
+ if (!id.ok) return id.response;
29884
+ const parsed = await parseBody(c, RunPluginActionReqSchema);
29885
+ if (!parsed.ok) return parsed.response;
29886
+ const repoRoot = c.get("repoRoot");
29887
+ if (parsed.data.value !== void 0) persistPluginUIState(id.data, parsed.data.actionId, parsed.data.value, repoRoot);
29888
+ let result;
29889
+ try {
29890
+ result = await runPluginAction(id.data, parsed.data.actionId, parsed.data.value);
29891
+ } catch (e) {
29892
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), error: e instanceof Error ? e.message : "Action failed" }, 400);
29893
+ }
29894
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), result: result ?? void 0 });
29895
+ });
29896
+ pluginsRoutes.delete("/plugins/:id", async (c) => {
29897
+ const id = requirePathParam(c, "id");
29898
+ if (!id.ok) return id.response;
29899
+ const repoRoot = c.get("repoRoot");
29900
+ uninstallPlugin(id.data);
29901
+ clearConfigLabelOverrides(id.data);
29902
+ clearPluginUIElements(id.data);
29903
+ await reloadContentPlugins(repoRoot);
29904
+ return c.json({ plugins: describeInstalledPlugins(repoRoot), available: listAvailablePlugins() });
29905
+ });
29906
+
29907
+ // src/routes/api/project-settings.ts
29908
+ init_project_settings();
29909
+ init_project_settings_store();
29910
+ import { Hono as Hono11 } from "hono";
29911
+ var projectSettingsRoutes = new Hono11();
29912
+ projectSettingsRoutes.get("/project-settings", (c) => {
29913
+ return c.json(readProjectSettings(c.get("repoRoot")));
28522
29914
  });
28523
29915
  projectSettingsRoutes.patch("/project-settings", async (c) => {
28524
29916
  const repoRoot = c.get("repoRoot");
28525
29917
  const parsed = await parseBody(c, UpdateProjectSettingsReqSchema);
28526
29918
  if (!parsed.ok) return parsed.response;
28527
- const current = readProjectSettings(repoRoot);
28528
- if (parsed.data.appName !== void 0) current.appName = parsed.data.appName || void 0;
28529
- writeProjectSettings(repoRoot, current);
28530
- return c.json(current);
29919
+ updateProjectSettings2(repoRoot, (s) => {
29920
+ if (parsed.data.appName !== void 0) s.appName = parsed.data.appName || void 0;
29921
+ });
29922
+ return c.json(readProjectSettings(repoRoot));
28531
29923
  });
28532
29924
 
28533
29925
  // src/routes/api/review-notes.ts
28534
29926
  init_store();
28535
- import { readFileSync as readFileSync14, statSync as statSync4 } from "fs";
28536
- import { Hono as Hono11 } from "hono";
28537
- import { extname, relative as relative2, resolve as resolve10 } from "path";
28538
- var reviewNotesRoutes = new Hono11();
29927
+ import { readFileSync as readFileSync16, statSync as statSync6 } from "fs";
29928
+ import { Hono as Hono12 } from "hono";
29929
+ import { extname as extname2, relative as relative2, resolve as resolve10 } from "path";
29930
+ var reviewNotesRoutes = new Hono12();
28539
29931
  var ARTIFACT_SERVE_MAX_BYTES = 1e7;
28540
29932
  var IMAGE_CONTENT_TYPES = {
28541
29933
  ".png": "image/png",
@@ -28553,13 +29945,13 @@ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
28553
29945
  const abs = resolve10(repoRoot, file2);
28554
29946
  const rel = relative2(repoRoot, abs);
28555
29947
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
28556
- const ext = extname(abs).toLowerCase();
29948
+ const ext = extname2(abs).toLowerCase();
28557
29949
  if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
28558
29950
  const contentType = IMAGE_CONTENT_TYPES[ext];
28559
29951
  try {
28560
- const stat = statSync4(abs);
29952
+ const stat = statSync6(abs);
28561
29953
  if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
28562
- const body = readFileSync14(abs);
29954
+ const body = readFileSync16(abs);
28563
29955
  return c.body(body, 200, { "Content-Type": contentType });
28564
29956
  } catch {
28565
29957
  return c.text("Not found", 404);
@@ -28576,17 +29968,17 @@ reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
28576
29968
 
28577
29969
  // src/routes/api/reviews.ts
28578
29970
  init_queries();
28579
- import { Hono as Hono12 } from "hono";
29971
+ import { Hono as Hono13 } from "hono";
28580
29972
 
28581
29973
  // src/export/on-complete-hook.ts
28582
29974
  import { spawn as spawn2 } from "child_process";
28583
29975
  import { appendFileSync } from "fs";
28584
- import { join as join13 } from "path";
29976
+ import { join as join17 } from "path";
28585
29977
  function runOnCompleteHook(command, ctx) {
28586
29978
  if (command === null || command.trim() === "") {
28587
29979
  return Promise.resolve({ ran: false, ok: true, exitCode: 0 });
28588
29980
  }
28589
- const logPath = join13(ctx.repoRoot, ".glassbox", "on-complete.log");
29981
+ const logPath = join17(ctx.repoRoot, ".glassbox", "on-complete.log");
28590
29982
  const log = (s) => {
28591
29983
  try {
28592
29984
  appendFileSync(logPath, s);
@@ -28638,7 +30030,8 @@ $ ${command}
28638
30030
  }
28639
30031
 
28640
30032
  // src/routes/api/reviews.ts
28641
- var reviewsRoutes = new Hono12();
30033
+ init_plugins();
30034
+ var reviewsRoutes = new Hono13();
28642
30035
  reviewsRoutes.get("/reviews", async (c) => {
28643
30036
  const repoRoot = c.get("repoRoot");
28644
30037
  const reviews = await listReviews(repoRoot);
@@ -28662,6 +30055,13 @@ reviewsRoutes.post("/review/complete", async (c) => {
28662
30055
  jsonPath: exportPath.replace(/\.md$/, ".json"),
28663
30056
  markdownPath: exportPath
28664
30057
  });
30058
+ try {
30059
+ const completed = await getReview(reviewId);
30060
+ if (completed !== void 0) {
30061
+ await notifyReviewCompleted(completed, await getAnnotationsForReview(reviewId), exportPath);
30062
+ }
30063
+ } catch {
30064
+ }
28665
30065
  return c.json({ status: "completed", exportPath, isCurrent, reviewId, hook });
28666
30066
  });
28667
30067
  reviewsRoutes.post("/review/reopen", async (c) => {
@@ -28723,8 +30123,9 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
28723
30123
 
28724
30124
  // src/routes/api/share-prompt.ts
28725
30125
  init_zod();
28726
- import { Hono as Hono13 } from "hono";
28727
- var sharePromptRoutes = new Hono13();
30126
+ import { Hono as Hono14 } from "hono";
30127
+ init_global_config();
30128
+ var sharePromptRoutes = new Hono14();
28728
30129
  var SharePromptShapeSchema = external_exports.object({
28729
30130
  dismissedAt: external_exports.number().nullable().optional(),
28730
30131
  totalOpenMs: external_exports.number().optional()
@@ -28763,9 +30164,9 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
28763
30164
  });
28764
30165
 
28765
30166
  // src/routes/api/system.ts
28766
- import { Hono as Hono14 } from "hono";
30167
+ import { Hono as Hono15 } from "hono";
28767
30168
  init_openOS();
28768
- var systemRoutes = new Hono14();
30169
+ var systemRoutes = new Hono15();
28769
30170
  systemRoutes.post("/open-external", async (c) => {
28770
30171
  const parsed = await parseBody(c, OpenExternalReqSchema);
28771
30172
  if (!parsed.ok) return parsed.response;
@@ -28777,7 +30178,7 @@ systemRoutes.post("/open-external", async (c) => {
28777
30178
  });
28778
30179
 
28779
30180
  // src/routes/api.ts
28780
- var apiRoutes = new Hono15();
30181
+ var apiRoutes = new Hono16();
28781
30182
  apiRoutes.route("/", reviewsRoutes);
28782
30183
  apiRoutes.route("/", filesRoutes);
28783
30184
  apiRoutes.route("/", annotationsRoutes);
@@ -28785,22 +30186,24 @@ apiRoutes.route("/", attachmentsRoutes);
28785
30186
  apiRoutes.route("/", outlineRoutes);
28786
30187
  apiRoutes.route("/", contextRoutes);
28787
30188
  apiRoutes.route("/", projectSettingsRoutes);
30189
+ apiRoutes.route("/", pluginsRoutes);
28788
30190
  apiRoutes.route("/", imageRoutes);
28789
30191
  apiRoutes.route("/", reviewNotesRoutes);
28790
30192
  apiRoutes.route("/", sharePromptRoutes);
28791
30193
  apiRoutes.route("/", systemRoutes);
28792
30194
 
28793
30195
  // src/routes/channel-api.ts
28794
- import { spawnSync as spawnSync10 } from "child_process";
28795
- import { mkdirSync as mkdirSync10 } from "fs";
28796
- import { Hono as Hono16 } from "hono";
28797
- import { join as join14 } from "path";
28798
- var channelApiRoutes = new Hono16();
30196
+ import { spawnSync as spawnSync12 } from "child_process";
30197
+ import { mkdirSync as mkdirSync12 } from "fs";
30198
+ import { Hono as Hono17 } from "hono";
30199
+ import { join as join18 } from "path";
30200
+ init_global_config();
30201
+ var channelApiRoutes = new Hono17();
28799
30202
  channelApiRoutes.get("/status", async (c) => {
28800
30203
  const config2 = readGlobalConfig();
28801
30204
  const enabled = config2.channelEnabled === true;
28802
30205
  const repoRoot = c.get("repoRoot");
28803
- const dataDir = join14(repoRoot, ".glassbox");
30206
+ const dataDir = join18(repoRoot, ".glassbox");
28804
30207
  const connected = enabled ? await isChannelAlive(dataDir) : false;
28805
30208
  return c.json({ enabled, connected });
28806
30209
  });
@@ -28809,8 +30212,8 @@ channelApiRoutes.post("/enable", (c) => {
28809
30212
  config2.channelEnabled = true;
28810
30213
  });
28811
30214
  const repoRoot = c.get("repoRoot");
28812
- const dataDir = join14(repoRoot, ".glassbox");
28813
- mkdirSync10(dataDir, { recursive: true });
30215
+ const dataDir = join18(repoRoot, ".glassbox");
30216
+ mkdirSync12(dataDir, { recursive: true });
28814
30217
  registerChannel(dataDir);
28815
30218
  return c.json({ ok: true });
28816
30219
  });
@@ -28819,7 +30222,7 @@ channelApiRoutes.post("/disable", (c) => {
28819
30222
  config2.channelEnabled = false;
28820
30223
  });
28821
30224
  const repoRoot = c.get("repoRoot");
28822
- const dataDir = join14(repoRoot, ".glassbox");
30225
+ const dataDir = join18(repoRoot, ".glassbox");
28823
30226
  unregisterChannel(dataDir);
28824
30227
  return c.json({ ok: true });
28825
30228
  });
@@ -28827,7 +30230,7 @@ channelApiRoutes.post("/trigger", async (c) => {
28827
30230
  const parsed = await parseBody(c, TriggerChannelReqSchema);
28828
30231
  if (!parsed.ok) return parsed.response;
28829
30232
  const repoRoot = c.get("repoRoot");
28830
- const dataDir = join14(repoRoot, ".glassbox");
30233
+ const dataDir = join18(repoRoot, ".glassbox");
28831
30234
  const sent = await triggerChannel(dataDir, parsed.data.message);
28832
30235
  if (!sent) {
28833
30236
  return c.json({ error: "Channel not connected" }, 503);
@@ -28836,7 +30239,7 @@ channelApiRoutes.post("/trigger", async (c) => {
28836
30239
  });
28837
30240
  channelApiRoutes.get("/claude-check", (c) => {
28838
30241
  try {
28839
- const result = spawnSync10("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
30242
+ const result = spawnSync12("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
28840
30243
  if (result.status !== 0) {
28841
30244
  return c.json({ installed: false, version: null, meetsMinimum: false });
28842
30245
  }
@@ -28855,13 +30258,13 @@ channelApiRoutes.get("/claude-check", (c) => {
28855
30258
  });
28856
30259
 
28857
30260
  // src/routes/difftool-api.ts
28858
- import { Hono as Hono17 } from "hono";
30261
+ import { Hono as Hono18 } from "hono";
28859
30262
  init_connection();
28860
30263
  init_queries();
28861
30264
  init_session();
28862
30265
  init_difftool();
28863
30266
  init_image_blobs();
28864
- var difftoolApiRoutes = new Hono17();
30267
+ var difftoolApiRoutes = new Hono18();
28865
30268
  difftoolApiRoutes.get("/status", (c) => {
28866
30269
  return c.json(getDifftoolStatus("global"));
28867
30270
  });
@@ -28938,8 +30341,8 @@ difftoolApiRoutes.post("/end", (c) => {
28938
30341
  });
28939
30342
 
28940
30343
  // src/routes/pages.tsx
28941
- import { readFileSync as readFileSync16 } from "fs";
28942
- import { Hono as Hono18 } from "hono";
30344
+ import { readFileSync as readFileSync18 } from "fs";
30345
+ import { Hono as Hono19 } from "hono";
28943
30346
  import { resolve as resolve11 } from "path";
28944
30347
 
28945
30348
  // src/components/diffView.tsx
@@ -29730,7 +31133,28 @@ function ReviewNoteRows({ notes, repliesByNote }) {
29730
31133
  /* @__PURE__ */ jsx4("button", { className: "ai-note-discard-btn", title: "Remove this note from .pr-notes/", children: "Discard" })
29731
31134
  ] }) : null
29732
31135
  ] }),
29733
- n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.content !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", children: [
31136
+ n.artifacts !== void 0 && n.artifacts.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "ai-note-artifacts", children: n.artifacts.map((a) => a.renderedSvg !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", open: true, children: [
31137
+ /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
31138
+ /* @__PURE__ */ jsx4(IconPaperclip, {}),
31139
+ /* @__PURE__ */ jsx4("span", { children: a.uri })
31140
+ ] }),
31141
+ /* @__PURE__ */ jsx4("div", { className: "ai-note-artifact-imgwrap", children: /* @__PURE__ */ jsx4(
31142
+ "img",
31143
+ {
31144
+ className: "ai-note-artifact-img",
31145
+ loading: "lazy",
31146
+ alt: a.uri,
31147
+ draggable: false,
31148
+ src: `data:image/svg+xml;utf8,${encodeURIComponent(a.renderedSvg)}`
31149
+ }
31150
+ ) })
31151
+ ] }) : a.renderedHtml !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", open: true, children: [
31152
+ /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
31153
+ /* @__PURE__ */ jsx4(IconPaperclip, {}),
31154
+ /* @__PURE__ */ jsx4("span", { children: a.uri })
31155
+ ] }),
31156
+ /* @__PURE__ */ jsx4("div", { className: "ai-note-artifact-rendered", children: raw(a.renderedHtml) })
31157
+ ] }) : a.content !== void 0 ? /* @__PURE__ */ jsxs4("details", { className: "ai-note-artifact", children: [
29734
31158
  /* @__PURE__ */ jsxs4("summary", { className: "ai-note-artifact-label", children: [
29735
31159
  /* @__PURE__ */ jsx4(IconPaperclip, {}),
29736
31160
  /* @__PURE__ */ jsx4("span", { children: a.uri })
@@ -29801,9 +31225,10 @@ function AnnotationRows({ annotations }) {
29801
31225
  }
29802
31226
 
29803
31227
  // src/themes/config.ts
29804
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, readdirSync as readdirSync2, readFileSync as readFileSync15, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "fs";
29805
- import { join as join15 } from "path";
29806
- var THEMES_DIR = join15(GLOBAL_CONFIG_DIR, "themes");
31228
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
31229
+ import { join as join19 } from "path";
31230
+ init_global_config();
31231
+ var THEMES_DIR = join19(GLOBAL_CONFIG_DIR, "themes");
29807
31232
  function getActiveThemeId() {
29808
31233
  const config2 = readGlobalConfig();
29809
31234
  const theme = config2.theme;
@@ -29817,13 +31242,13 @@ function setActiveThemeId(id) {
29817
31242
  });
29818
31243
  }
29819
31244
  function loadCustomThemes() {
29820
- if (!existsSync12(THEMES_DIR)) return [];
31245
+ if (!existsSync15(THEMES_DIR)) return [];
29821
31246
  const themes = [];
29822
31247
  try {
29823
- const files = readdirSync2(THEMES_DIR).filter((f) => f.endsWith(".json"));
31248
+ const files = readdirSync4(THEMES_DIR).filter((f) => f.endsWith(".json"));
29824
31249
  for (const file2 of files) {
29825
31250
  try {
29826
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync15(join15(THEMES_DIR, file2), "utf-8")));
31251
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync17(join19(THEMES_DIR, file2), "utf-8")));
29827
31252
  if (!parsed.success) continue;
29828
31253
  const d = parsed.data;
29829
31254
  themes.push({ id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" });
@@ -29835,21 +31260,21 @@ function loadCustomThemes() {
29835
31260
  return themes;
29836
31261
  }
29837
31262
  function saveCustomTheme(theme) {
29838
- mkdirSync11(THEMES_DIR, { recursive: true });
29839
- const filePath = join15(THEMES_DIR, `${theme.id}.json`);
29840
- writeFileSync11(filePath, JSON.stringify(theme, null, 2), "utf-8");
31263
+ mkdirSync13(THEMES_DIR, { recursive: true });
31264
+ const filePath = join19(THEMES_DIR, `${theme.id}.json`);
31265
+ writeFileSync13(filePath, JSON.stringify(theme, null, 2), "utf-8");
29841
31266
  }
29842
31267
  function deleteCustomTheme(id) {
29843
- const filePath = join15(THEMES_DIR, `${id}.json`);
29844
- if (existsSync12(filePath)) {
31268
+ const filePath = join19(THEMES_DIR, `${id}.json`);
31269
+ if (existsSync15(filePath)) {
29845
31270
  unlinkSync3(filePath);
29846
31271
  }
29847
31272
  }
29848
31273
  function getCustomTheme(id) {
29849
- const filePath = join15(THEMES_DIR, `${id}.json`);
29850
- if (!existsSync12(filePath)) return void 0;
31274
+ const filePath = join19(THEMES_DIR, `${id}.json`);
31275
+ if (!existsSync15(filePath)) return void 0;
29851
31276
  try {
29852
- const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync15(filePath, "utf-8")));
31277
+ const parsed = StoredCustomThemeSchema.safeParse(JSON.parse(readFileSync17(filePath, "utf-8")));
29853
31278
  if (!parsed.success) return void 0;
29854
31279
  const d = parsed.data;
29855
31280
  return { id: d.id, name: d.name, colors: d.colors, builtIn: false, baseTheme: d.baseTheme ?? "" };
@@ -30049,7 +31474,10 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30049
31474
  /* @__PURE__ */ jsx8("div", { className: "file-filter", children: /* @__PURE__ */ jsx8("input", { type: "text", className: "file-filter-input", id: "file-filter", placeholder: "Filter files..." }) }),
30050
31475
  /* @__PURE__ */ jsx8(FileList, { files, annotationCounts, staleCounts }),
30051
31476
  /* @__PURE__ */ jsx8("div", { className: "sidebar-share", id: "sidebar-share" }),
30052
- /* @__PURE__ */ jsx8("div", { className: "sidebar-footer", children: footer })
31477
+ /* @__PURE__ */ jsxs8("div", { className: "sidebar-footer", children: [
31478
+ footer,
31479
+ /* @__PURE__ */ jsx8("div", { className: "plugin-ui-slot", id: "plugin-ui-sidebar-footer" })
31480
+ ] })
30053
31481
  ] }),
30054
31482
  /* @__PURE__ */ jsx8("div", { className: "sidebar-resize", id: "sidebar-resize" }),
30055
31483
  /* @__PURE__ */ jsxs8("main", { className: "main-content", children: [
@@ -30068,7 +31496,8 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30068
31496
  ] }) }),
30069
31497
  /* @__PURE__ */ jsx8("button", { className: "nav-btn disabled", id: "nav-back-btn", disabled: true, title: "Back", children: /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx8("path", { d: "m15 18-6-6 6-6" }) }) }),
30070
31498
  /* @__PURE__ */ jsx8("button", { className: "nav-btn disabled", id: "nav-forward-btn", disabled: true, title: "Forward", children: /* @__PURE__ */ jsx8("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx8("path", { d: "m9 18 6-6-6-6" }) }) }),
30071
- /* @__PURE__ */ jsx8("span", { className: "nav-file-path", id: "nav-file-path" })
31499
+ /* @__PURE__ */ jsx8("span", { className: "nav-file-path", id: "nav-file-path" }),
31500
+ /* @__PURE__ */ jsx8("span", { className: "plugin-ui-slot", id: "plugin-ui-header" })
30072
31501
  ] }),
30073
31502
  /* @__PURE__ */ jsx8("div", { className: "diff-container", id: "diff-container", style: "display:none" }),
30074
31503
  /* @__PURE__ */ jsxs8("div", { className: "diff-toolbar", id: "diff-toolbar", style: "display:none", children: [
@@ -30109,7 +31538,8 @@ function ReviewShell({ reviewId, review, files, annotationCounts, staleCounts, f
30109
31538
  /* @__PURE__ */ jsx8("button", { className: "image-zoom-btn", "data-zoom-action": "actual", title: "Actual size (1:1)", children: /* @__PURE__ */ jsx8(IconActualSize, {}) }),
30110
31539
  /* @__PURE__ */ jsx8("button", { className: "image-zoom-btn", "data-zoom-action": "in", title: "Zoom in", children: /* @__PURE__ */ jsx8(IconZoomIn, {}) })
30111
31540
  ] })
30112
- ] })
31541
+ ] }),
31542
+ /* @__PURE__ */ jsx8("div", { className: "plugin-ui-slot", id: "plugin-ui-diff-toolbar" })
30113
31543
  ] })
30114
31544
  ] })
30115
31545
  ] })
@@ -30143,6 +31573,25 @@ function svgUsesExternalFonts(svgData) {
30143
31573
  return /<text[\s>]/i.test(svg) || /font-family/i.test(svg) || /@font-face/i.test(svg);
30144
31574
  }
30145
31575
 
31576
+ // src/plugins/artifacts.ts
31577
+ init_plugins();
31578
+ async function renderNoteArtifacts(views) {
31579
+ for (const view of views) {
31580
+ for (const artifact of view.artifacts ?? []) {
31581
+ if (artifact.isImage === true || artifact.content === void 0) continue;
31582
+ const rendered = await renderContent({
31583
+ bytes: new TextEncoder().encode(artifact.content),
31584
+ text: artifact.content,
31585
+ path: artifact.uri
31586
+ });
31587
+ if (rendered === null) continue;
31588
+ if (rendered.svg !== void 0 && rendered.svg !== "") artifact.renderedSvg = rendered.svg;
31589
+ else if (rendered.html !== void 0 && rendered.html !== "") artifact.renderedHtml = rendered.html;
31590
+ }
31591
+ }
31592
+ return views;
31593
+ }
31594
+
30146
31595
  // src/review-notes/reanchor.ts
30147
31596
  var MATCH_RADIUS = 50;
30148
31597
  function reanchorReviewNotes(notes, diff) {
@@ -30176,7 +31625,7 @@ function reanchorReviewNotes(notes, diff) {
30176
31625
  // src/routes/pages.tsx
30177
31626
  init_store();
30178
31627
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "kerfjs/jsx-runtime";
30179
- var pageRoutes = new Hono18();
31628
+ var pageRoutes = new Hono19();
30180
31629
  pageRoutes.get("/", async (c) => {
30181
31630
  const reviewId = c.get("reviewId");
30182
31631
  const review = await getReview(reviewId);
@@ -30205,12 +31654,14 @@ pageRoutes.get("/file/:fileId", async (c) => {
30205
31654
  const reviewMode = review ? parseModeString(review.mode) : null;
30206
31655
  const imageSideLabels = reviewMode ? groundTruthSideLabels(reviewMode) : void 0;
30207
31656
  const stepNav = reviewMode?.type === "ground-truth" ? groundTruthStepNav(reviewMode, file2.id, await getReviewFiles(file2.review_id)) : void 0;
30208
- if (view === "rendered" && isSvgFile(file2.file_path)) {
31657
+ const isSvg = isSvgFile(file2.file_path);
31658
+ const isPluginSvg = !isSvg && !diff.isBinary && pluginRendersFile(file2.file_path);
31659
+ if (view === "rendered" && (isSvg || isPluginSvg)) {
30209
31660
  const repoRoot = c.get("repoRoot");
30210
31661
  let fontWarning = false;
30211
31662
  let svgBaseWidth = 300;
30212
31663
  let svgBaseHeight = 150;
30213
- if (reviewMode) {
31664
+ if (isSvg && reviewMode) {
30214
31665
  const oldImg = diff.status !== "added" ? getOldImage(reviewMode, file2.file_path, diff.oldPath ?? null, repoRoot) : null;
30215
31666
  const newImg = diff.status !== "deleted" ? getNewImage(reviewMode, file2.file_path, repoRoot) : null;
30216
31667
  const svgData = newImg ?? oldImg;
@@ -30222,6 +31673,14 @@ pageRoutes.get("/file/:fileId", async (c) => {
30222
31673
  if (oldImg && svgUsesExternalFonts(oldImg.data) || newImg && svgUsesExternalFonts(newImg.data)) {
30223
31674
  fontWarning = true;
30224
31675
  }
31676
+ } else if (isPluginSvg) {
31677
+ const svg = await renderPluginSvgSide(reviewMode, file2.file_path, diff.oldPath ?? file2.file_path, diff.status !== "deleted" ? "new" : "old", repoRoot);
31678
+ if (svg !== null) {
31679
+ const dims = parseSvgDimensions(svg);
31680
+ svgBaseWidth = dims.width;
31681
+ svgBaseHeight = dims.height;
31682
+ if (svgUsesExternalFonts(Buffer.from(svg))) fontWarning = true;
31683
+ }
30225
31684
  }
30226
31685
  const html2 = /* @__PURE__ */ jsxs9("div", { className: "diff-view", "data-file-id": file2.id, "data-file-path": file2.file_path, "data-is-svg": "true", children: [
30227
31686
  /* @__PURE__ */ jsxs9("div", { className: "diff-header", children: [
@@ -30286,6 +31745,7 @@ pageRoutes.get("/file/:fileId", async (c) => {
30286
31745
  }
30287
31746
  const rawNotes = getDemoMode() !== null ? demoReviewNotes(file2.file_path) : loadReviewNotesForFile(c.get("repoRoot"), file2.file_path);
30288
31747
  const reviewNotes = reanchorReviewNotes(rawNotes, finalDiff);
31748
+ await renderNoteArtifacts(reviewNotes);
30289
31749
  const html = /* @__PURE__ */ jsx9(DiffView, { file: file2, diff: finalDiff, annotations, mode, reviewNotes, imageSideLabels, stepNav });
30290
31750
  return c.html(html.toString());
30291
31751
  });
@@ -30295,7 +31755,7 @@ pageRoutes.get("/file-raw", (c) => {
30295
31755
  const repoRoot = c.get("repoRoot");
30296
31756
  let content;
30297
31757
  try {
30298
- content = readFileSync16(resolve11(repoRoot, filePath), "utf-8");
31758
+ content = readFileSync18(resolve11(repoRoot, filePath), "utf-8");
30299
31759
  } catch {
30300
31760
  return c.text("File not found", 404);
30301
31761
  }
@@ -30351,8 +31811,8 @@ pageRoutes.get("/history", async (c) => {
30351
31811
  });
30352
31812
 
30353
31813
  // src/routes/theme-api.ts
30354
- import { Hono as Hono19 } from "hono";
30355
- var themeApiRoutes = new Hono19();
31814
+ import { Hono as Hono20 } from "hono";
31815
+ var themeApiRoutes = new Hono20();
30356
31816
  themeApiRoutes.get("/", (c) => {
30357
31817
  const themes = getAllThemes();
30358
31818
  const activeId = getActiveThemeId();
@@ -30479,7 +31939,7 @@ function tryServe(appFetch, port) {
30479
31939
  });
30480
31940
  }
30481
31941
  async function startServer(port, reviewId, repoRoot, options) {
30482
- const app = new Hono20();
31942
+ const app = new Hono21();
30483
31943
  const onCompleteCommand = options?.onComplete ?? null;
30484
31944
  app.use("*", async (c, next) => {
30485
31945
  c.set("reviewId", reviewId);
@@ -30488,22 +31948,22 @@ async function startServer(port, reviewId, repoRoot, options) {
30488
31948
  c.set("onCompleteCommand", onCompleteCommand);
30489
31949
  await next();
30490
31950
  });
30491
- const selfDir = dirname7(fileURLToPath2(import.meta.url));
30492
- const distDir = existsSync13(join16(selfDir, "client", "styles.css")) ? join16(selfDir, "client") : join16(selfDir, "..", "dist", "client");
31951
+ const selfDir = dirname8(fileURLToPath3(import.meta.url));
31952
+ const distDir = existsSync16(join20(selfDir, "client", "styles.css")) ? join20(selfDir, "client") : join20(selfDir, "..", "dist", "client");
30493
31953
  app.get("/static/styles.css", (c) => {
30494
- const css = readFileSync17(join16(distDir, "styles.css"), "utf-8");
31954
+ const css = readFileSync19(join20(distDir, "styles.css"), "utf-8");
30495
31955
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
30496
31956
  });
30497
31957
  app.get("/static/app.js", (c) => {
30498
- const js = readFileSync17(join16(distDir, "app.global.js"), "utf-8");
31958
+ const js = readFileSync19(join20(distDir, "app.global.js"), "utf-8");
30499
31959
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
30500
31960
  });
30501
31961
  app.get("/static/history.js", (c) => {
30502
- const js = readFileSync17(join16(distDir, "history.global.js"), "utf-8");
31962
+ const js = readFileSync19(join20(distDir, "history.global.js"), "utf-8");
30503
31963
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
30504
31964
  });
30505
31965
  app.get("/favicon.svg", (c) => {
30506
- const svg = readFileSync17(join16(distDir, "favicon.svg"), "utf-8");
31966
+ const svg = readFileSync19(join20(distDir, "favicon.svg"), "utf-8");
30507
31967
  return c.body(svg, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" });
30508
31968
  });
30509
31969
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -30538,10 +31998,16 @@ async function startServer(port, reviewId, repoRoot, options) {
30538
31998
  console.log(`
30539
31999
  Glassbox running at ${url2}
30540
32000
  `);
32001
+ await initContentPlugins(repoRoot);
32002
+ try {
32003
+ const review = await getReview(reviewId);
32004
+ if (review !== void 0) await notifyReviewCreated(review);
32005
+ } catch {
32006
+ }
30541
32007
  try {
30542
32008
  const globalConfig2 = readGlobalConfig();
30543
32009
  if (globalConfig2.channelEnabled === true) {
30544
- const dataDir = join16(repoRoot, ".glassbox");
32010
+ const dataDir = join20(repoRoot, ".glassbox");
30545
32011
  registerChannel(dataDir);
30546
32012
  }
30547
32013
  } catch {
@@ -30556,8 +32022,8 @@ async function startServer(port, reviewId, repoRoot, options) {
30556
32022
  }
30557
32023
 
30558
32024
  // src/skills.ts
30559
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
30560
- import { join as join17 } from "path";
32025
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
32026
+ import { join as join21 } from "path";
30561
32027
  var SKILL_VERSION = 1;
30562
32028
  function versionHeader() {
30563
32029
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -30568,14 +32034,14 @@ function parseVersionHeader(content) {
30568
32034
  return parseInt(match[1], 10);
30569
32035
  }
30570
32036
  function updateFile(path, content) {
30571
- if (existsSync14(path)) {
30572
- const existing = readFileSync18(path, "utf-8");
32037
+ if (existsSync17(path)) {
32038
+ const existing = readFileSync20(path, "utf-8");
30573
32039
  const version2 = parseVersionHeader(existing);
30574
32040
  if (version2 !== null && version2 >= SKILL_VERSION) {
30575
32041
  return false;
30576
32042
  }
30577
32043
  }
30578
- writeFileSync12(path, content, "utf-8");
32044
+ writeFileSync14(path, content, "utf-8");
30579
32045
  return true;
30580
32046
  }
30581
32047
  function skillBody() {
@@ -30595,8 +32061,8 @@ function skillBody() {
30595
32061
  ].join("\n");
30596
32062
  }
30597
32063
  function ensureClaudeSkills(cwd) {
30598
- const dir = join17(cwd, ".claude", "skills", "glassbox");
30599
- mkdirSync12(dir, { recursive: true });
32064
+ const dir = join21(cwd, ".claude", "skills", "glassbox");
32065
+ mkdirSync14(dir, { recursive: true });
30600
32066
  const content = [
30601
32067
  "---",
30602
32068
  "name: glassbox",
@@ -30608,11 +32074,11 @@ function ensureClaudeSkills(cwd) {
30608
32074
  skillBody(),
30609
32075
  ""
30610
32076
  ].join("\n");
30611
- return updateFile(join17(dir, "SKILL.md"), content);
32077
+ return updateFile(join21(dir, "SKILL.md"), content);
30612
32078
  }
30613
32079
  function ensureCursorRules(cwd) {
30614
- const rulesDir = join17(cwd, ".cursor", "rules");
30615
- mkdirSync12(rulesDir, { recursive: true });
32080
+ const rulesDir = join21(cwd, ".cursor", "rules");
32081
+ mkdirSync14(rulesDir, { recursive: true });
30616
32082
  const content = [
30617
32083
  "---",
30618
32084
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -30623,11 +32089,11 @@ function ensureCursorRules(cwd) {
30623
32089
  skillBody(),
30624
32090
  ""
30625
32091
  ].join("\n");
30626
- return updateFile(join17(rulesDir, "glassbox.mdc"), content);
32092
+ return updateFile(join21(rulesDir, "glassbox.mdc"), content);
30627
32093
  }
30628
32094
  function ensureCopilotPrompts(cwd) {
30629
- const promptsDir = join17(cwd, ".github", "prompts");
30630
- mkdirSync12(promptsDir, { recursive: true });
32095
+ const promptsDir = join21(cwd, ".github", "prompts");
32096
+ mkdirSync14(promptsDir, { recursive: true });
30631
32097
  const content = [
30632
32098
  "---",
30633
32099
  "description: Read the latest Glassbox code review and apply all feedback annotations",
@@ -30637,11 +32103,11 @@ function ensureCopilotPrompts(cwd) {
30637
32103
  skillBody(),
30638
32104
  ""
30639
32105
  ].join("\n");
30640
- return updateFile(join17(promptsDir, "glassbox.prompt.md"), content);
32106
+ return updateFile(join21(promptsDir, "glassbox.prompt.md"), content);
30641
32107
  }
30642
32108
  function ensureWindsurfRules(cwd) {
30643
- const rulesDir = join17(cwd, ".windsurf", "rules");
30644
- mkdirSync12(rulesDir, { recursive: true });
32109
+ const rulesDir = join21(cwd, ".windsurf", "rules");
32110
+ mkdirSync14(rulesDir, { recursive: true });
30645
32111
  const content = [
30646
32112
  "---",
30647
32113
  "trigger: manual",
@@ -30652,21 +32118,21 @@ function ensureWindsurfRules(cwd) {
30652
32118
  skillBody(),
30653
32119
  ""
30654
32120
  ].join("\n");
30655
- return updateFile(join17(rulesDir, "glassbox.md"), content);
32121
+ return updateFile(join21(rulesDir, "glassbox.md"), content);
30656
32122
  }
30657
32123
  function ensureSkills() {
30658
32124
  const cwd = process.cwd();
30659
32125
  const platforms = [];
30660
- if (existsSync14(join17(cwd, ".claude"))) {
32126
+ if (existsSync17(join21(cwd, ".claude"))) {
30661
32127
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
30662
32128
  }
30663
- if (existsSync14(join17(cwd, ".cursor"))) {
32129
+ if (existsSync17(join21(cwd, ".cursor"))) {
30664
32130
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
30665
32131
  }
30666
- if (existsSync14(join17(cwd, ".github", "prompts")) || existsSync14(join17(cwd, ".github", "copilot-instructions.md"))) {
32132
+ if (existsSync17(join21(cwd, ".github", "prompts")) || existsSync17(join21(cwd, ".github", "copilot-instructions.md"))) {
30667
32133
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
30668
32134
  }
30669
- if (existsSync14(join17(cwd, ".windsurf"))) {
32135
+ if (existsSync17(join21(cwd, ".windsurf"))) {
30670
32136
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
30671
32137
  }
30672
32138
  return platforms;
@@ -30674,19 +32140,19 @@ function ensureSkills() {
30674
32140
 
30675
32141
  // src/update-check.ts
30676
32142
  init_zod();
30677
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
32143
+ import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
30678
32144
  import { get } from "https";
30679
32145
  import { homedir as homedir2 } from "os";
30680
- import { dirname as dirname8, join as join18 } from "path";
30681
- import { fileURLToPath as fileURLToPath3 } from "url";
30682
- var DATA_DIR = join18(homedir2(), ".glassbox");
30683
- var CHECK_FILE = join18(DATA_DIR, "last-update-check");
32146
+ import { dirname as dirname9, join as join22 } from "path";
32147
+ import { fileURLToPath as fileURLToPath4 } from "url";
32148
+ var DATA_DIR = join22(homedir2(), ".glassbox");
32149
+ var CHECK_FILE = join22(DATA_DIR, "last-update-check");
30684
32150
  var PACKAGE_NAME = "glassbox";
30685
32151
  var VersionPayloadSchema = external_exports.object({ version: external_exports.string() });
30686
32152
  function getCurrentVersion() {
30687
32153
  try {
30688
- const dir = dirname8(fileURLToPath3(import.meta.url));
30689
- const raw2 = JSON.parse(readFileSync19(join18(dir, "..", "package.json"), "utf-8"));
32154
+ const dir = dirname9(fileURLToPath4(import.meta.url));
32155
+ const raw2 = JSON.parse(readFileSync21(join22(dir, "..", "package.json"), "utf-8"));
30690
32156
  return VersionPayloadSchema.parse(raw2).version;
30691
32157
  } catch {
30692
32158
  return "0.0.0";
@@ -30694,16 +32160,16 @@ function getCurrentVersion() {
30694
32160
  }
30695
32161
  function getLastCheckDate() {
30696
32162
  try {
30697
- if (existsSync15(CHECK_FILE)) {
30698
- return readFileSync19(CHECK_FILE, "utf-8").trim();
32163
+ if (existsSync18(CHECK_FILE)) {
32164
+ return readFileSync21(CHECK_FILE, "utf-8").trim();
30699
32165
  }
30700
32166
  } catch {
30701
32167
  }
30702
32168
  return null;
30703
32169
  }
30704
32170
  function saveCheckDate() {
30705
- mkdirSync13(DATA_DIR, { recursive: true });
30706
- writeFileSync13(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
32171
+ mkdirSync15(DATA_DIR, { recursive: true });
32172
+ writeFileSync15(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
30707
32173
  }
30708
32174
  function isFirstUseToday() {
30709
32175
  const last = getLastCheckDate();
@@ -30753,7 +32219,7 @@ function detectUpgradeCommand() {
30753
32219
  }
30754
32220
  return `npm update -g ${PACKAGE_NAME}`;
30755
32221
  }
30756
- function compareVersions(current, latest) {
32222
+ function compareVersions2(current, latest) {
30757
32223
  const a = current.split(".").map(Number);
30758
32224
  const b = latest.split(".").map(Number);
30759
32225
  for (let i = 0; i < 3; i++) {
@@ -30767,7 +32233,7 @@ async function checkForUpdates(force) {
30767
32233
  const current = getCurrentVersion();
30768
32234
  const latest = await fetchLatestVersion();
30769
32235
  saveCheckDate();
30770
- if (latest === null || compareVersions(current, latest) >= 0) return;
32236
+ if (latest === null || compareVersions2(current, latest) >= 0) return;
30771
32237
  const cmd = detectUpgradeCommand();
30772
32238
  const updateLine = `Update available: ${current} \u2192 ${latest}`;
30773
32239
  const cmdLine = `Run: ${cmd}`;
@@ -31008,23 +32474,23 @@ async function main() {
31008
32474
  console.log("AI service test mode enabled \u2014 using mock AI responses");
31009
32475
  }
31010
32476
  if (debug) {
31011
- console.log(`[debug] Build timestamp: ${"2026-07-02T09:57:40.917Z"}`);
32477
+ console.log(`[debug] Build timestamp: ${"2026-07-22T04:50:30.153Z"}`);
31012
32478
  }
31013
32479
  if (projectDir !== null) {
31014
- if (!existsSync17(projectDir) || !statSync6(projectDir).isDirectory()) {
32480
+ if (!existsSync20(projectDir) || !statSync8(projectDir).isDirectory()) {
31015
32481
  console.error(`--project-dir is not a directory: ${projectDir}`);
31016
32482
  process.exit(1);
31017
32483
  }
31018
32484
  process.chdir(projectDir);
31019
32485
  }
31020
32486
  if (dataDir === null) {
31021
- dataDir = join20(process.cwd(), ".glassbox");
32487
+ dataDir = join24(process.cwd(), ".glassbox");
31022
32488
  }
31023
32489
  if (difftoolServe) {
31024
32490
  const { initDifftoolSession: initDifftoolSession2 } = await Promise.resolve().then(() => (init_session(), session_exports));
31025
32491
  const { writeDiscovery: writeDiscovery2, clearDiscovery: clearDiscovery2, releaseStartingLock: releaseStartingLock2 } = await Promise.resolve().then(() => (init_difftool_discovery(), difftool_discovery_exports));
31026
32492
  const { clearImageBlobs: clearImageBlobs2 } = await Promise.resolve().then(() => (init_image_blobs(), image_blobs_exports));
31027
- mkdirSync15(dataDir, { recursive: true });
32493
+ mkdirSync17(dataDir, { recursive: true });
31028
32494
  setDataDir(dataDir);
31029
32495
  ensureGlassboxGitignored(dataDir);
31030
32496
  const sessionDataDir = dataDir;
@@ -31060,13 +32526,13 @@ async function main() {
31060
32526
  }
31061
32527
  process.exit(1);
31062
32528
  }
31063
- dataDir = join20(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
32529
+ dataDir = join24(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
31064
32530
  setDemoMode(demo);
31065
32531
  console.log(`
31066
32532
  DEMO MODE: ${scenario.label}
31067
32533
  `);
31068
32534
  }
31069
- mkdirSync15(dataDir, { recursive: true });
32535
+ mkdirSync17(dataDir, { recursive: true });
31070
32536
  if (demo === null) {
31071
32537
  acquireLock(dataDir);
31072
32538
  }
@@ -31091,12 +32557,12 @@ async function main() {
31091
32557
  if (mode.type === "diff") {
31092
32558
  const { pathA, pathB } = mode;
31093
32559
  for (const p of [pathA, pathB]) {
31094
- if (!existsSync17(p)) {
32560
+ if (!existsSync20(p)) {
31095
32561
  console.error(`Error: path does not exist: ${p}`);
31096
32562
  process.exit(1);
31097
32563
  }
31098
32564
  }
31099
- if (statSync6(pathA).isDirectory() !== statSync6(pathB).isDirectory()) {
32565
+ if (statSync8(pathA).isDirectory() !== statSync8(pathB).isDirectory()) {
31100
32566
  console.error("Error: --diff requires two files or two folders, not a mix of both.");
31101
32567
  process.exit(1);
31102
32568
  }
@@ -31113,17 +32579,19 @@ async function main() {
31113
32579
  }
31114
32580
  for (const entry of comparisons) {
31115
32581
  for (const [role, p] of [["actual", entry.actualPath], ["expected", entry.expectedPath]]) {
31116
- if (!existsSync17(p)) {
32582
+ if (!existsSync20(p)) {
31117
32583
  console.error(`Error: ${role} image does not exist: ${p}`);
31118
32584
  process.exit(1);
31119
32585
  }
31120
32586
  }
31121
32587
  }
32588
+ const { initContentPlugins: initContentPlugins2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
32589
+ await initContentPlugins2(cwd);
31122
32590
  const { comparePerceptual: comparePerceptual2 } = await Promise.resolve().then(() => (init_perceptual_diff(), perceptual_diff_exports));
31123
32591
  let identical = 0;
31124
32592
  let undecodable = 0;
31125
32593
  for (const entry of comparisons) {
31126
- const result = comparePerceptual2(entry.actualPath, entry.expectedPath);
32594
+ const result = await comparePerceptual2(entry.actualPath, entry.expectedPath);
31127
32595
  groundTruthScores.set(entry.key, result.score);
31128
32596
  if (result.reason === "undecodable") undecodable++;
31129
32597
  else if (result.score === 0) identical++;