opensocrates 1.1.4 → 1.2.0

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.
@@ -2,7 +2,14 @@
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { createHash, randomInt, randomUUID } from "node:crypto";
5
- import { constants as fsConstants, createReadStream, createWriteStream, realpathSync } from "node:fs";
5
+ import {
6
+ constants as fsConstants,
7
+ createReadStream,
8
+ createWriteStream,
9
+ existsSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ } from "node:fs";
6
13
  import {
7
14
  access,
8
15
  chmod,
@@ -24,13 +31,20 @@ import { Readable } from "node:stream";
24
31
  import { pipeline } from "node:stream/promises";
25
32
  import { fileURLToPath } from "node:url";
26
33
 
27
- export const PRODUCT_VERSION = "1.1.4";
34
+ export const PRODUCT_VERSION = "1.2.0";
28
35
  export const REPOSITORY = "ParkerHwang/OpenSocrates";
29
36
  export const MARKETPLACE_NAME = "opensocrates";
30
37
  export const PLUGIN_NAME = "opensocrates";
31
38
  export const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
32
39
  export const DEFAULT_HOST = "codex";
33
- export const SUPPORTED_HOSTS = Object.freeze(["claude", "codex"]);
40
+ export const SUPPORTED_HOSTS = Object.freeze([
41
+ "antigravity",
42
+ "claude",
43
+ "codex",
44
+ "cursor",
45
+ "grok",
46
+ "opencode",
47
+ ]);
34
48
  export const ALL_HOST = "all";
35
49
  export const HOST_CHOICES = Object.freeze([ALL_HOST, ...SUPPORTED_HOSTS]);
36
50
  export function assetNameFor(host = DEFAULT_HOST) {
@@ -54,16 +68,75 @@ const CLAUDE_MARKER = Object.freeze({
54
68
  pluginName: PLUGIN_NAME,
55
69
  host: "claude",
56
70
  });
71
+ const CURSOR_MARKER = Object.freeze({
72
+ schemaVersion: 1,
73
+ marketplaceName: MARKETPLACE_NAME,
74
+ pluginName: PLUGIN_NAME,
75
+ host: "cursor",
76
+ registrationKind: "file-drop",
77
+ });
78
+ const ANTIGRAVITY_MARKER = Object.freeze({
79
+ schemaVersion: 1,
80
+ marketplaceName: MARKETPLACE_NAME,
81
+ pluginName: PLUGIN_NAME,
82
+ host: "antigravity",
83
+ registrationKind: "file-drop",
84
+ });
85
+ const GROK_MARKER = Object.freeze({
86
+ schemaVersion: 1,
87
+ marketplaceName: MARKETPLACE_NAME,
88
+ pluginName: PLUGIN_NAME,
89
+ host: "grok",
90
+ registrationKind: "file-drop",
91
+ });
92
+ const OPENCODE_MARKER = Object.freeze({
93
+ schemaVersion: 1,
94
+ marketplaceName: MARKETPLACE_NAME,
95
+ pluginName: PLUGIN_NAME,
96
+ host: "opencode",
97
+ registrationKind: "automatic-file-discovery",
98
+ });
99
+ const OPENCODE_INSTALL_MANIFEST = ".opensocrates-installation.json";
100
+ const OPENCODE_BRIDGE_MARKER = ".opensocrates-managed.json";
101
+ // OpenCode is discovered automatically from its config directory, so it is
102
+ // deliberately not a file-drop host.
103
+ const FILE_DROP_HOSTS = Object.freeze(["antigravity", "cursor", "grok"]);
57
104
  const HOST_LAYOUTS = Object.freeze({
105
+ antigravity: {
106
+ marketplaceRelative: null,
107
+ pluginRelative: ".",
108
+ manifestRelative: "plugin.json",
109
+ requiresRuntime: false,
110
+ },
58
111
  claude: {
59
112
  marketplaceRelative: join(".claude-plugin", "marketplace.json"),
60
113
  pluginRelative: join("plugins", PLUGIN_NAME),
61
114
  manifestRelative: join(".claude-plugin", "plugin.json"),
115
+ requiresRuntime: true,
62
116
  },
63
117
  codex: {
64
118
  marketplaceRelative: join(".agents", "plugins", "marketplace.json"),
65
119
  pluginRelative: join("build", "generated", "plugins", "codex"),
66
120
  manifestRelative: join(".codex-plugin", "plugin.json"),
121
+ requiresRuntime: true,
122
+ },
123
+ cursor: {
124
+ marketplaceRelative: null,
125
+ pluginRelative: ".",
126
+ manifestRelative: "plugin.json",
127
+ requiresRuntime: false,
128
+ },
129
+ grok: {
130
+ marketplaceRelative: null,
131
+ pluginRelative: ".",
132
+ manifestRelative: "plugin.json",
133
+ requiresRuntime: false,
134
+ },
135
+ opencode: {
136
+ marketplaceRelative: null,
137
+ pluginRelative: ".",
138
+ manifestRelative: "opencode-plugin.json",
139
+ requiresRuntime: false,
67
140
  },
68
141
  });
69
142
  const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
@@ -97,9 +170,7 @@ async function exists(target) {
97
170
  }
98
171
 
99
172
  export function statePaths() {
100
- const stateDirectory = resolve(
101
- process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"),
102
- );
173
+ const stateDirectory = resolve(process.env.OPENSOCRATES_STATE_DIR || join(homedir(), ".opensocrates"));
103
174
  const launchAgentsDirectory = resolve(
104
175
  process.env.OPENSOCRATES_LAUNCH_AGENTS_DIR || join(homedir(), "Library", "LaunchAgents"),
105
176
  );
@@ -135,12 +206,7 @@ function defaultDesiredState() {
135
206
  }
136
207
 
137
208
  function normalizeDesiredState(value) {
138
- if (
139
- value === null ||
140
- typeof value !== "object" ||
141
- Array.isArray(value) ||
142
- value.schema !== DESIRED_STATE_SCHEMA
143
- ) {
209
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.schema !== DESIRED_STATE_SCHEMA) {
144
210
  fail("the OpenSocrates desired-state manifest has an unsupported schema");
145
211
  }
146
212
  const installedHosts = value.installedHosts;
@@ -166,16 +232,10 @@ function normalizeDesiredState(value) {
166
232
  fail("the OpenSocrates desired-state manifest has an invalid update policy");
167
233
  }
168
234
  const autoUpdateHosts =
169
- value.autoUpdate.hosts === undefined
170
- ? value.autoUpdate.enabled
171
- ? installedHosts
172
- : []
173
- : value.autoUpdate.hosts;
235
+ value.autoUpdate.hosts === undefined ? (value.autoUpdate.enabled ? installedHosts : []) : value.autoUpdate.hosts;
174
236
  if (
175
237
  !Array.isArray(autoUpdateHosts) ||
176
- autoUpdateHosts.some(
177
- (host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host),
178
- ) ||
238
+ autoUpdateHosts.some((host) => !SUPPORTED_HOSTS.includes(host) || !installedHosts.includes(host)) ||
179
239
  new Set(autoUpdateHosts).size !== autoUpdateHosts.length ||
180
240
  (value.autoUpdate.enabled && autoUpdateHosts.length === 0) ||
181
241
  (!value.autoUpdate.enabled && autoUpdateHosts.length > 0)
@@ -234,7 +294,11 @@ async function atomicWritePrivateFile(target, contents) {
234
294
  await ensurePrivateDirectory(parent);
235
295
  const temporary = join(parent, `.${basename(target)}.${randomUUID()}.tmp`);
236
296
  try {
237
- await writeFile(temporary, contents, { encoding: "utf8", mode: 0o600, flag: "wx" });
297
+ await writeFile(temporary, contents, {
298
+ encoding: "utf8",
299
+ mode: 0o600,
300
+ flag: "wx",
301
+ });
238
302
  await chmod(temporary, 0o600);
239
303
  await rename(temporary, target);
240
304
  } finally {
@@ -246,10 +310,7 @@ async function atomicWritePrivateFile(target, contents) {
246
310
 
247
311
  export async function writeDesiredState(value) {
248
312
  const normalized = normalizeDesiredState(value);
249
- await atomicWritePrivateFile(
250
- statePaths().desiredState,
251
- `${JSON.stringify(normalized, null, 2)}\n`,
252
- );
313
+ await atomicWritePrivateFile(statePaths().desiredState, `${JSON.stringify(normalized, null, 2)}\n`);
253
314
  return normalized;
254
315
  }
255
316
 
@@ -260,14 +321,14 @@ async function writeAutoUpdateReceipt({ version, checkedAt, hosts, result, error
260
321
  checkedAt,
261
322
  hosts: [...hosts]
262
323
  .sort((left, right) => left.host.localeCompare(right.host))
263
- .map(({ host, result: hostResult }) => ({ host, result: hostResult })),
324
+ .map(({ host, result: hostResult }) => ({
325
+ host,
326
+ result: hostResult,
327
+ })),
264
328
  result,
265
329
  errorCategory: errorCategory ?? null,
266
330
  };
267
- await atomicWritePrivateFile(
268
- statePaths().receipt,
269
- `${JSON.stringify(receipt, null, 2)}\n`,
270
- );
331
+ await atomicWritePrivateFile(statePaths().receipt, `${JSON.stringify(receipt, null, 2)}\n`);
271
332
  }
272
333
 
273
334
  function nowIso() {
@@ -303,10 +364,7 @@ async function acquireOperationLock() {
303
364
  try {
304
365
  const handle = await open(paths.lock, "wx", 0o600);
305
366
  try {
306
- await handle.writeFile(
307
- `${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`,
308
- "utf8",
309
- );
367
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, startedAt: nowIso() })}\n`, "utf8");
310
368
  } catch (writeError) {
311
369
  await handle.close().catch(() => undefined);
312
370
  await rm(paths.lock, { force: true }).catch(() => undefined);
@@ -338,9 +396,7 @@ async function acquireOperationLock() {
338
396
  }
339
397
  stale =
340
398
  ownerAlive === false ||
341
- (ownerAlive === null &&
342
- Number.isFinite(started) &&
343
- Date.now() - started > LOCK_STALE_MILLISECONDS);
399
+ (ownerAlive === null && Number.isFinite(started) && Date.now() - started > LOCK_STALE_MILLISECONDS);
344
400
  } catch {
345
401
  const info = await lstat(paths.lock);
346
402
  stale = Date.now() - info.mtimeMs > LOCK_STALE_MILLISECONDS;
@@ -373,16 +429,15 @@ function jsonEqual(left, right) {
373
429
  }
374
430
 
375
431
  function markerFor(host) {
432
+ if (host === "cursor") return CURSOR_MARKER;
433
+ if (host === "antigravity") return ANTIGRAVITY_MARKER;
434
+ if (host === "grok") return GROK_MARKER;
435
+ if (host === "opencode") return OPENCODE_MARKER;
376
436
  return host === "claude" ? CLAUDE_MARKER : CODEX_MARKER;
377
437
  }
378
438
 
379
439
  export function markerMatches(value, host = DEFAULT_HOST) {
380
- return (
381
- value !== null &&
382
- typeof value === "object" &&
383
- !Array.isArray(value) &&
384
- jsonEqual(value, markerFor(host))
385
- );
440
+ return value !== null && typeof value === "object" && !Array.isArray(value) && jsonEqual(value, markerFor(host));
386
441
  }
387
442
 
388
443
  export function isSafeArchivePath(value) {
@@ -422,15 +477,7 @@ export function parseChecksumText(text, expectedName = ASSET_NAME) {
422
477
  }
423
478
 
424
479
  export function parseCli(argv) {
425
- const actions = new Set([
426
- "install",
427
- "status",
428
- "update",
429
- "remove",
430
- "verify",
431
- "auto-update",
432
- "help",
433
- ]);
480
+ const actions = new Set(["install", "status", "update", "remove", "verify", "auto-update", "help"]);
434
481
  const args = [...argv];
435
482
  let action = "install";
436
483
  if (args[0] && !args[0].startsWith("-")) {
@@ -457,10 +504,7 @@ export function parseCli(argv) {
457
504
  host: action === "auto-update" ? ALL_HOST : DEFAULT_HOST,
458
505
  asset: null,
459
506
  checksum: null,
460
- hostAssets: {
461
- claude: { asset: null, checksum: null },
462
- codex: { asset: null, checksum: null },
463
- },
507
+ hostAssets: Object.fromEntries(SUPPORTED_HOSTS.map((host) => [host, { asset: null, checksum: null }])),
464
508
  channel: "stable",
465
509
  intervalHours: AUTO_UPDATE_DEFAULT_INTERVAL_HOURS,
466
510
  allowMajor: false,
@@ -518,7 +562,7 @@ export function parseCli(argv) {
518
562
  options.intervalHours = value;
519
563
  continue;
520
564
  }
521
- const qualified = flag?.match(/^--(asset|checksum)-(claude|codex)$/u);
565
+ const qualified = flag?.match(new RegExp(`^--(asset|checksum)-(${SUPPORTED_HOSTS.join("|")})$`, "u"));
522
566
  if (qualified) {
523
567
  seenOptions.add(qualified[1]);
524
568
  const value = args.shift();
@@ -550,17 +594,7 @@ export function parseCli(argv) {
550
594
  if (options.host === ALL_HOST && options.asset !== null) {
551
595
  fail("--host all requires host-qualified --asset-<host> and --checksum-<host> options");
552
596
  }
553
- if (
554
- options.host === ALL_HOST &&
555
- SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null) &&
556
- SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset === null)
557
- ) {
558
- fail("local --host all verification requires asset and checksum pairs for both hosts");
559
- }
560
- if (
561
- options.host !== ALL_HOST &&
562
- SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)
563
- ) {
597
+ if (options.host !== ALL_HOST && SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null)) {
564
598
  fail("host-qualified asset options are only valid with --host all");
565
599
  }
566
600
  if (["status", "remove", "help"].includes(options.action) && options.asset !== null) {
@@ -575,13 +609,11 @@ export function parseCli(argv) {
575
609
  if (
576
610
  options.action === "auto-update" &&
577
611
  options.autoUpdateAction !== "run" &&
578
- (options.asset !== null ||
579
- SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
612
+ (options.asset !== null || SUPPORTED_HOSTS.some((host) => options.hostAssets[host].asset !== null))
580
613
  ) {
581
614
  fail(`auto-update ${options.autoUpdateAction} does not accept local asset options`);
582
615
  }
583
- const autoUpdateEnable =
584
- options.action === "auto-update" && options.autoUpdateAction === "enable";
616
+ const autoUpdateEnable = options.action === "auto-update" && options.autoUpdateAction === "enable";
585
617
  const autoUpdateRun = options.action === "auto-update" && options.autoUpdateAction === "run";
586
618
  for (const policyOption of ["allow-major", "channel", "interval-hours"]) {
587
619
  if (seenOptions.has(policyOption) && !autoUpdateEnable) {
@@ -608,12 +640,12 @@ function showHelp() {
608
640
  console.log(`OpenSocrates ${PRODUCT_VERSION}
609
641
 
610
642
  Usage:
611
- opensocrates install [--host all|codex|claude] [--asset ZIP --checksum SHA256]
612
- opensocrates status [--host all|codex|claude]
613
- opensocrates update [--host all|codex|claude] [--asset ZIP --checksum SHA256]
614
- opensocrates remove [--host all|codex|claude]
615
- opensocrates verify [--host all|codex|claude] [--asset ZIP --checksum SHA256]
616
- opensocrates auto-update enable [--host all|codex|claude]
643
+ opensocrates install [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
644
+ opensocrates status [--host all|antigravity|claude|codex|cursor|grok|opencode]
645
+ opensocrates update [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
646
+ opensocrates remove [--host all|antigravity|claude|codex|cursor|grok|opencode]
647
+ opensocrates verify [--host all|antigravity|claude|codex|cursor|grok|opencode] [--asset ZIP --checksum SHA256]
648
+ opensocrates auto-update enable [--host all|antigravity|claude|codex|cursor|grok|opencode]
617
649
  [--channel stable|next] [--interval-hours ${AUTO_UPDATE_DEFAULT_INTERVAL_HOURS}]
618
650
  [--allow-major]
619
651
  opensocrates auto-update status
@@ -621,17 +653,36 @@ Usage:
621
653
 
622
654
  Without --asset, install, update, and verify download the v${PRODUCT_VERSION}
623
655
  package and checksum from GitHub Releases. The default lifecycle host is codex.
624
- For offline --host all verification, use --asset-claude/--checksum-claude
625
- and --asset-codex/--checksum-codex together. Automatic updates are opt-in.
656
+ With --host all, supplied host-qualified asset/checksum pairs define the exact
657
+ transaction set and are never mixed with downloads for other hosts. Without
658
+ qualified assets, every ready host participates. Automatic updates are opt-in.
626
659
  `);
627
660
  }
628
661
 
629
662
  function managedPaths(host) {
630
663
  const configured =
631
- host === "claude" ? process.env.CLAUDE_CONFIG_DIR : process.env.CODEX_HOME;
632
- const configuredHome = resolve(
633
- configured ? configured : join(homedir(), host === "claude" ? ".claude" : ".codex"),
634
- );
664
+ host === "antigravity"
665
+ ? process.env.ANTIGRAVITY_CONFIG_DIR
666
+ : host === "cursor"
667
+ ? process.env.CURSOR_CONFIG_DIR
668
+ : host === "grok"
669
+ ? process.env.GROK_HOME
670
+ : host === "opencode"
671
+ ? process.env.OPENCODE_CONFIG_DIR
672
+ : host === "claude"
673
+ ? process.env.CLAUDE_CONFIG_DIR
674
+ : process.env.CODEX_HOME;
675
+ const defaultHome =
676
+ host === "antigravity"
677
+ ? join(homedir(), ".gemini", "config")
678
+ : host === "cursor"
679
+ ? join(homedir(), ".cursor")
680
+ : host === "grok"
681
+ ? join(homedir(), ".grok")
682
+ : host === "opencode"
683
+ ? join(homedir(), ".config", "opencode")
684
+ : join(homedir(), host === "claude" ? ".claude" : ".codex");
685
+ const configuredHome = resolve(configured ? configured : defaultHome);
635
686
  let hostHome = configuredHome;
636
687
  try {
637
688
  hostHome = realpathSync(configuredHome);
@@ -640,18 +691,46 @@ function managedPaths(host) {
640
691
  // explicit, non-recursive target. Lexical resolution is the only
641
692
  // available normalization until then.
642
693
  }
643
- const root = join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
694
+ const root =
695
+ host === "antigravity"
696
+ ? join(hostHome, "plugins", PLUGIN_NAME)
697
+ : host === "cursor"
698
+ ? join(hostHome, "plugins", "local", PLUGIN_NAME)
699
+ : host === "grok"
700
+ ? join(hostHome, "plugins", PLUGIN_NAME)
701
+ : host === "opencode"
702
+ ? join(hostHome, "skills", PLUGIN_NAME)
703
+ : join(hostHome, "managed-marketplaces", MARKETPLACE_NAME);
644
704
  const layout = HOST_LAYOUTS[host];
645
705
  return {
646
706
  hostHome,
647
707
  root,
648
708
  parent: dirname(root),
649
709
  marker: join(root, MARKER_NAME),
650
- marketplace: join(root, layout.marketplaceRelative),
710
+ marketplace: typeof layout.marketplaceRelative === "string" ? join(root, layout.marketplaceRelative) : null,
651
711
  plugin: join(root, layout.pluginRelative),
712
+ bridge: host === "opencode" ? join(hostHome, "plugins", "opensocrates.js") : null,
713
+ bridgeMarker: host === "opencode" ? join(hostHome, "plugins", OPENCODE_BRIDGE_MARKER) : null,
714
+ bridgeParent: host === "opencode" ? join(hostHome, "plugins") : null,
652
715
  };
653
716
  }
654
717
 
718
+ // Directory that holds staging trees and rollback backups for one host.
719
+ //
720
+ // Grok scans every directory below ~/.grok/plugins, including dot-prefixed
721
+ // ones, so a staging tree or rollback backup left there would be discovered as
722
+ // a second OpenSocrates plugin. An interrupted install must not be able to
723
+ // leave a shadow copy inside Grok's plugin discovery root, so both transient
724
+ // directories live in the Grok home instead, next to the scanned directory.
725
+ function transientParent(host, paths) {
726
+ return host === "grok" ? paths.hostHome : paths.parent;
727
+ }
728
+
729
+ export function transientPathsFor(host) {
730
+ const paths = managedPaths(host);
731
+ return { root: paths.root, parent: paths.parent, transient: transientParent(host, paths) };
732
+ }
733
+
655
734
  function codexBinary() {
656
735
  return process.env.CODEX_BIN || "codex";
657
736
  }
@@ -660,6 +739,26 @@ function claudeBinary() {
660
739
  return process.env.CLAUDE_BIN || "claude";
661
740
  }
662
741
 
742
+ function cursorBinary() {
743
+ return process.env.CURSOR_BIN || "cursor";
744
+ }
745
+
746
+ function cursorAppPaths() {
747
+ return ["/Applications/Cursor.app", join(homedir(), "Applications", "Cursor.app")];
748
+ }
749
+
750
+ function antigravityBinary() {
751
+ return process.env.AGY_BIN || "agy";
752
+ }
753
+
754
+ function grokBinary() {
755
+ return process.env.GROK_BIN || "grok";
756
+ }
757
+
758
+ function opencodeBinary() {
759
+ return process.env.OPENCODE_BIN || "opencode";
760
+ }
761
+
663
762
  function run(command, args, { allowFailure = false } = {}) {
664
763
  const result = spawnSync(command, args, {
665
764
  encoding: "utf8",
@@ -707,6 +806,10 @@ function runClaudeJson(args) {
707
806
  return runJson(claudeBinary(), args, "Claude Code");
708
807
  }
709
808
 
809
+ function runGrokJson(args) {
810
+ return runJson(grokBinary(), args, "Grok Build");
811
+ }
812
+
710
813
  function claudeListEntries(payload, wrapper, label) {
711
814
  if (Array.isArray(payload)) {
712
815
  return payload;
@@ -752,11 +855,7 @@ function claudeMarketplaceEntries(payload) {
752
855
  }
753
856
 
754
857
  function claudePluginEntries(payload) {
755
- return requireUniqueClaudeEntries(
756
- claudeListEntries(payload, "plugins", "plugin list"),
757
- "id",
758
- "plugin list",
759
- );
858
+ return requireUniqueClaudeEntries(claudeListEntries(payload, "plugins", "plugin list"), "id", "plugin list");
760
859
  }
761
860
 
762
861
  function versionAtLeast(value, minimum) {
@@ -773,12 +872,63 @@ function versionAtLeast(value, minimum) {
773
872
  }
774
873
 
775
874
  function requireHostCli(host, { authenticated = false } = {}) {
875
+ if (host === "grok") {
876
+ const result = run(grokBinary(), ["--version"]);
877
+ if (!versionAtLeast(result.stdout, [1, 0, 3])) {
878
+ fail(`Grok Build 1.0.3 or later is required; got ${result.stdout.trim()}`);
879
+ }
880
+ return;
881
+ }
882
+ if (host === "cursor") {
883
+ if (process.env.CURSOR_CONFIG_DIR) return;
884
+ if (process.env.CURSOR_BIN) {
885
+ const result = run(cursorBinary(), ["--version"]);
886
+ if (!versionAtLeast(result.stdout, [2, 5, 0])) {
887
+ fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
888
+ }
889
+ return;
890
+ }
891
+ const app = cursorAppPaths().find((candidate) => existsSync(candidate));
892
+ if (app) {
893
+ const result = run(
894
+ "/usr/bin/plutil",
895
+ ["-extract", "CFBundleShortVersionString", "raw", join(app, "Contents", "Info.plist")],
896
+ { allowFailure: true },
897
+ );
898
+ if (result.status !== 0 || !versionAtLeast(result.stdout, [2, 5, 0])) {
899
+ fail("Cursor 2.5.0 or later is required for Agent Plugin support");
900
+ }
901
+ return;
902
+ }
903
+ const result = run(cursorBinary(), ["--version"]);
904
+ if (!versionAtLeast(result.stdout, [2, 5, 0])) {
905
+ fail(`Cursor 2.5.0 or later is required; got ${result.stdout.trim()}`);
906
+ }
907
+ return;
908
+ }
909
+ if (host === "antigravity") {
910
+ if (process.env.ANTIGRAVITY_CONFIG_DIR) return;
911
+ run(antigravityBinary(), ["--version"]);
912
+ return;
913
+ }
914
+ if (host === "opencode") {
915
+ // OPENCODE_CONFIG_DIR only redirects where the managed files are written;
916
+ // it does not waive the supported-host gate. Unlike the content-only
917
+ // file-drop hosts above, OpenCode receives an executing bridge bound to the
918
+ // stable chat.message contract, and the shipped package declares
919
+ // minimum_opencode_version 1.18.18 with a fail-closed <2.0.0 ceiling.
920
+ // Installing that bridge without confirming the host version would ship
921
+ // executable code against an unverified plugin API.
922
+ const result = run(opencodeBinary(), ["--version"]);
923
+ if (!versionAtLeast(result.stdout, [1, 18, 18]) || versionAtLeast(result.stdout, [2, 0, 0])) {
924
+ fail(`OpenCode >=1.18.18 and <2.0.0 is required; got ${result.stdout.trim()}`);
925
+ }
926
+ return;
927
+ }
776
928
  if (host === "claude") {
777
929
  const result = run(claudeBinary(), ["--version"]);
778
930
  if (!versionAtLeast(result.stdout, [2, 1, 205])) {
779
- fail(
780
- `Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`,
781
- );
931
+ fail(`Claude Code 2.1.205 or later is required for structured selector output; got ${result.stdout.trim()}`);
782
932
  }
783
933
  if (authenticated) {
784
934
  run(claudeBinary(), ["auth", "status"]);
@@ -792,6 +942,12 @@ function requireHostCli(host, { authenticated = false } = {}) {
792
942
  }
793
943
 
794
944
  function marketplaceEntries(host) {
945
+ if (FILE_DROP_HOSTS.includes(host) || host === "opencode") {
946
+ const paths = managedPaths(host);
947
+ const present =
948
+ existsSync(paths.root) || (host === "opencode" && (existsSync(paths.bridge) || existsSync(paths.bridgeMarker)));
949
+ return present ? [{ name: MARKETPLACE_NAME, root: paths.root }] : [];
950
+ }
795
951
  const payload =
796
952
  host === "claude"
797
953
  ? runClaudeJson(["plugin", "marketplace", "list", "--json"])
@@ -812,7 +968,124 @@ function marketplaceEntry(host) {
812
968
  return matches[0] ?? null;
813
969
  }
814
970
 
815
- function pluginState(host) {
971
+ // Read Grok Build's machine-readable plugin state.
972
+ //
973
+ // The managed Grok files are host-independent content, so inspecting or
974
+ // removing what the installer owns must not depend on a runnable `grok`
975
+ // command: a user who uninstalls Grok Build still has to be able to clean up.
976
+ // Transactional callers pass requireHostState so that install and update keep
977
+ // confirming activation against the host itself.
978
+ function grokInspection(requireHostState) {
979
+ if (requireHostState) return runGrokJson(["inspect", "--json"]);
980
+ const result = run(grokBinary(), ["inspect", "--json"], { allowFailure: true });
981
+ if (result.error || result.status !== 0) {
982
+ const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
983
+ console.warn(
984
+ `warning: could not read Grok Build plugin state (${detail}); ` +
985
+ "reporting the managed files only",
986
+ );
987
+ return null;
988
+ }
989
+ try {
990
+ const payload = JSON.parse(result.stdout);
991
+ if (payload === null || typeof payload !== "object") {
992
+ fail("Grok Build returned a non-container JSON value");
993
+ }
994
+ return payload;
995
+ } catch (error) {
996
+ if (error instanceof InstallerError) throw error;
997
+ fail(`Grok Build returned invalid JSON for inspect --json: ${error.message}`);
998
+ }
999
+ }
1000
+
1001
+ function pluginState(host, { requireHostState = false } = {}) {
1002
+ if (FILE_DROP_HOSTS.includes(host)) {
1003
+ const paths = managedPaths(host);
1004
+ if (!existsSync(paths.root)) return { kind: "missing", version: null };
1005
+ let manifest;
1006
+ try {
1007
+ manifest = JSON.parse(readFileSync(join(paths.root, "plugin.json"), "utf8"));
1008
+ } catch (error) {
1009
+ fail(`${host} plugin manifest is unreadable: ${error.message}`);
1010
+ }
1011
+ if (
1012
+ manifest === null ||
1013
+ typeof manifest !== "object" ||
1014
+ Array.isArray(manifest) ||
1015
+ (host === "cursor" &&
1016
+ manifest.$schema !== "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json") ||
1017
+ (host === "grok" && manifest.skills !== "./skills") ||
1018
+ manifest.name !== PLUGIN_NAME ||
1019
+ typeof manifest.version !== "string" ||
1020
+ manifest.version.trim().length === 0
1021
+ ) {
1022
+ fail(`${host} reported an invalid OpenSocrates plugin manifest`);
1023
+ }
1024
+ if (host !== "grok") return { kind: "installed", version: manifest.version };
1025
+ const payload = grokInspection(requireHostState);
1026
+ if (payload === null) return { kind: "installed", version: manifest.version };
1027
+ if (!Array.isArray(payload.plugins)) {
1028
+ fail("Grok Build inspect returned an unexpected plugin schema");
1029
+ }
1030
+ const matches = payload.plugins.filter((entry) => entry?.name === PLUGIN_NAME);
1031
+ if (matches.length !== 1) {
1032
+ fail(
1033
+ matches.length === 0
1034
+ ? "Grok Build did not discover the managed OpenSocrates plugin"
1035
+ : "Grok Build reported duplicate OpenSocrates plugins",
1036
+ );
1037
+ }
1038
+ const entry = matches[0];
1039
+ if (
1040
+ typeof entry.enabled !== "boolean" ||
1041
+ typeof entry.path !== "string" ||
1042
+ entry.provides?.skills !== 1 ||
1043
+ entry.provides?.agents !== 0 ||
1044
+ entry.provides?.hooks !== false ||
1045
+ entry.provides?.mcpServers !== 0
1046
+ ) {
1047
+ fail("Grok Build reported an invalid OpenSocrates plugin state");
1048
+ }
1049
+ let observedPath = resolve(entry.path);
1050
+ try {
1051
+ observedPath = realpathSync(observedPath);
1052
+ } catch {
1053
+ // The managed root exists here, so a missing reported path is invalid below.
1054
+ }
1055
+ let expectedPath = resolve(paths.root);
1056
+ try {
1057
+ expectedPath = realpathSync(expectedPath);
1058
+ } catch {
1059
+ // Ownership checks report a more specific error for a missing root.
1060
+ }
1061
+ if (observedPath !== expectedPath) {
1062
+ fail(`Grok Build resolved OpenSocrates from an unmanaged location: ${observedPath}`);
1063
+ }
1064
+ return { kind: entry.enabled ? "installed" : "disabled", version: manifest.version };
1065
+ }
1066
+ if (host === "opencode") {
1067
+ const paths = managedPaths(host);
1068
+ if (!existsSync(paths.root) && !existsSync(paths.bridge) && !existsSync(paths.bridgeMarker)) {
1069
+ return { kind: "missing", version: null };
1070
+ }
1071
+ let manifest;
1072
+ try {
1073
+ manifest = JSON.parse(readFileSync(join(paths.root, ".opensocrates-package", "opencode-plugin.json"), "utf8"));
1074
+ } catch (error) {
1075
+ fail(`opencode installed manifest is unreadable: ${error.message}`);
1076
+ }
1077
+ if (
1078
+ manifest === null ||
1079
+ typeof manifest !== "object" ||
1080
+ Array.isArray(manifest) ||
1081
+ manifest.name !== PLUGIN_NAME ||
1082
+ typeof manifest.version !== "string" ||
1083
+ manifest.version.trim().length === 0
1084
+ ) {
1085
+ fail("opencode reported an invalid OpenSocrates installation manifest");
1086
+ }
1087
+ return { kind: "installed", version: manifest.version };
1088
+ }
816
1089
  if (host === "claude") {
817
1090
  const installed = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
818
1091
  const matches = installed.filter((entry) => entry?.id === PLUGIN_ID);
@@ -831,9 +1104,7 @@ function pluginState(host) {
831
1104
  version: entry.version ?? null,
832
1105
  };
833
1106
  }
834
- const payload = runCodexJson([
835
- "plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json",
836
- ]);
1107
+ const payload = runCodexJson(["plugin", "list", "--marketplace", MARKETPLACE_NAME, "--available", "--json"]);
837
1108
  const installed = Array.isArray(payload.installed) ? payload.installed : null;
838
1109
  const available = Array.isArray(payload.available) ? payload.available : null;
839
1110
  if (installed === null || available === null) {
@@ -845,10 +1116,16 @@ function pluginState(host) {
845
1116
  fail(`Codex reported duplicate entries for ${PLUGIN_ID}`);
846
1117
  }
847
1118
  if (installedMatches.length === 1) {
848
- return { kind: "installed", version: installedMatches[0].version ?? null };
1119
+ return {
1120
+ kind: "installed",
1121
+ version: installedMatches[0].version ?? null,
1122
+ };
849
1123
  }
850
1124
  if (availableMatches.length === 1) {
851
- return { kind: "available", version: availableMatches[0].version ?? null };
1125
+ return {
1126
+ kind: "available",
1127
+ version: availableMatches[0].version ?? null,
1128
+ };
852
1129
  }
853
1130
  return { kind: "missing", version: null };
854
1131
  }
@@ -861,13 +1138,12 @@ function detectLegacyClaudeInstallation() {
861
1138
  entry.name.toLowerCase() === MARKETPLACE_NAME,
862
1139
  );
863
1140
  const plugins = claudePluginEntries(runClaudeJson(["plugin", "list", "--json"]));
864
- const legacyPlugins = plugins.filter(
865
- (entry) =>
866
- entry.id !== PLUGIN_ID &&
867
- entry.id.toLowerCase() === PLUGIN_ID,
868
- );
1141
+ const legacyPlugins = plugins.filter((entry) => entry.id !== PLUGIN_ID && entry.id.toLowerCase() === PLUGIN_ID);
869
1142
  const names = marketplaces.map((entry) => entry.name).join(", ") || "OpenSocrates";
870
- return { found: marketplaces.length > 0 || legacyPlugins.length > 0, names };
1143
+ return {
1144
+ found: marketplaces.length > 0 || legacyPlugins.length > 0,
1145
+ names,
1146
+ };
871
1147
  }
872
1148
 
873
1149
  // Install and update write into the managed root, so a case-variant pre-1.0
@@ -932,6 +1208,85 @@ async function sha256File(target) {
932
1208
  return hash.digest("hex");
933
1209
  }
934
1210
 
1211
+ function openCodeSidecar(bridgeSha256) {
1212
+ return {
1213
+ ...OPENCODE_MARKER,
1214
+ version: PRODUCT_VERSION,
1215
+ bridgeSha256,
1216
+ };
1217
+ }
1218
+
1219
+ async function ensureSafeDirectory(target, label) {
1220
+ if (await exists(target)) {
1221
+ const info = await lstat(target);
1222
+ if (!info.isDirectory() || info.isSymbolicLink()) {
1223
+ fail(`refusing to use an unsafe ${label} directory: ${target}`);
1224
+ }
1225
+ return;
1226
+ }
1227
+ await mkdir(target, { recursive: true, mode: 0o700 });
1228
+ const info = await lstat(target);
1229
+ if (!info.isDirectory() || info.isSymbolicLink()) {
1230
+ fail(`refusing to use an unsafe ${label} directory: ${target}`);
1231
+ }
1232
+ }
1233
+
1234
+ async function ensureOpenCodeDirectories(paths) {
1235
+ await ensureSafeDirectory(paths.hostHome, "OpenCode configuration");
1236
+ await ensureSafeDirectory(paths.parent, "OpenCode skills");
1237
+ await ensureSafeDirectory(paths.bridgeParent, "OpenCode plugins");
1238
+ }
1239
+
1240
+ async function verifyOpenCodeInstallation(paths) {
1241
+ await requireOwnedRoot(paths.root, "opencode");
1242
+ for (const [target, label] of [
1243
+ [paths.bridge, "plugin bridge"],
1244
+ [paths.bridgeMarker, "plugin ownership sidecar"],
1245
+ ]) {
1246
+ if (!(await exists(target))) fail(`OpenCode ${label} is missing: ${target}`);
1247
+ const info = await lstat(target);
1248
+ if (!info.isFile() || info.isSymbolicLink()) {
1249
+ fail(`OpenCode ${label} is not an owned regular file: ${target}`);
1250
+ }
1251
+ }
1252
+ const sidecar = await readJsonObject(paths.bridgeMarker);
1253
+ const sidecarMarker = Object.fromEntries(Object.keys(OPENCODE_MARKER).map((key) => [key, sidecar[key]]));
1254
+ if (
1255
+ !markerMatches(sidecarMarker, "opencode") ||
1256
+ sidecar.version !== PRODUCT_VERSION ||
1257
+ typeof sidecar.bridgeSha256 !== "string" ||
1258
+ sidecar.bridgeSha256 !== (await sha256File(paths.bridge))
1259
+ ) {
1260
+ fail("OpenCode plugin bridge has an invalid ownership sidecar or checksum");
1261
+ }
1262
+ const manifest = await readJsonObject(join(paths.root, OPENCODE_INSTALL_MANIFEST));
1263
+ if (
1264
+ manifest.schema !== "opensocrates.opencode-installation/1.0.0" ||
1265
+ manifest.version !== PRODUCT_VERSION ||
1266
+ manifest.bridgeSha256 !== sidecar.bridgeSha256 ||
1267
+ manifest.files === null ||
1268
+ typeof manifest.files !== "object" ||
1269
+ Array.isArray(manifest.files)
1270
+ ) {
1271
+ fail("OpenCode installation inventory is invalid");
1272
+ }
1273
+ const actualFiles = new Set(await walkFiles(paths.root));
1274
+ actualFiles.delete(OPENCODE_INSTALL_MANIFEST);
1275
+ const declaredFiles = Object.keys(manifest.files);
1276
+ if (
1277
+ actualFiles.size !== declaredFiles.length ||
1278
+ declaredFiles.some((item) => !isSafeArchivePath(item) || !actualFiles.has(item))
1279
+ ) {
1280
+ fail("OpenCode installation inventory does not cover the complete managed skill");
1281
+ }
1282
+ for (const item of declaredFiles) {
1283
+ if (manifest.files[item] !== (await sha256File(join(paths.root, ...item.split("/"))))) {
1284
+ fail(`OpenCode installed file checksum mismatch for ${item}`);
1285
+ }
1286
+ }
1287
+ return manifest;
1288
+ }
1289
+
935
1290
  async function downloadFile(url, destination) {
936
1291
  const response = await fetch(url, {
937
1292
  headers: {
@@ -962,6 +1317,11 @@ function localAssetInputs(options, host) {
962
1317
  return { asset: options.asset, checksum: options.checksum };
963
1318
  }
964
1319
 
1320
+ function qualifiedAssetHosts(options) {
1321
+ if (options.host !== ALL_HOST) return [];
1322
+ return SUPPORTED_HOSTS.filter((host) => options.hostAssets[host].asset !== null);
1323
+ }
1324
+
965
1325
  async function resolveAssetInputs(options, scratch, host) {
966
1326
  const local = localAssetInputs(options, host);
967
1327
  if (local.asset !== null) {
@@ -1063,10 +1423,7 @@ async function verifyPackageChecksums(pluginRoot) {
1063
1423
  }
1064
1424
  }
1065
1425
  const actualFiles = new Set((await walkFiles(pluginRoot)).filter((item) => item !== "checksums.sha256"));
1066
- if (
1067
- actualFiles.size !== declared.size ||
1068
- [...actualFiles].some((item) => !declared.has(item))
1069
- ) {
1426
+ if (actualFiles.size !== declared.size || [...actualFiles].some((item) => !declared.has(item))) {
1070
1427
  fail("package checksum manifest does not cover the complete archive");
1071
1428
  }
1072
1429
  return declared.size;
@@ -1088,16 +1445,36 @@ async function verifyExtractedPackage(pluginRoot, host) {
1088
1445
  ) {
1089
1446
  fail("package release manifest does not match this installer");
1090
1447
  }
1091
- const runtime = join(
1092
- pluginRoot,
1093
- "runtime",
1094
- "darwin-arm64",
1095
- "opensocrates-runtime",
1096
- "opensocrates-runtime",
1097
- );
1098
- const runtimeInfo = await stat(runtime);
1099
- if (!runtimeInfo.isFile() || (runtimeInfo.mode & 0o111) === 0) {
1100
- fail("package is missing the executable darwin-arm64 runtime");
1448
+ if (HOST_LAYOUTS[host].requiresRuntime) {
1449
+ const runtime = join(pluginRoot, "runtime", "darwin-arm64", "opensocrates-runtime", "opensocrates-runtime");
1450
+ const runtimeInfo = await stat(runtime);
1451
+ if (!runtimeInfo.isFile() || (runtimeInfo.mode & 0o111) === 0) {
1452
+ fail("package is missing the executable darwin-arm64 runtime");
1453
+ }
1454
+ } else if (
1455
+ release.launchers?.length !== 0 ||
1456
+ release.runtime_targets?.length !== 0 ||
1457
+ (await exists(join(pluginRoot, "runtime"))) ||
1458
+ (await exists(join(pluginRoot, "hooks"))) ||
1459
+ (await exists(join(pluginRoot, "bin"))) ||
1460
+ ((host === "cursor" || host === "grok") && (await exists(join(pluginRoot, "mcp.json")))) ||
1461
+ (host === "grok" &&
1462
+ ((await exists(join(pluginRoot, "commands"))) ||
1463
+ (await exists(join(pluginRoot, "agents"))) ||
1464
+ (await exists(join(pluginRoot, ".mcp.json")))))
1465
+ ) {
1466
+ fail(`${host} package contains an unexpected native runtime or launcher surface`);
1467
+ }
1468
+ if (host === "opencode") {
1469
+ if (
1470
+ manifest.minimum_opencode_version !== "1.18.18" ||
1471
+ manifest.stable_plugin_hook !== "chat.message" ||
1472
+ manifest.beta_v2_api !== false ||
1473
+ !(await exists(join(pluginRoot, "plugins", "opensocrates.js"))) ||
1474
+ !(await exists(join(pluginRoot, "skills", "opensocrates", "SKILL.md")))
1475
+ ) {
1476
+ fail("OpenCode package does not match the verified stable bridge contract");
1477
+ }
1101
1478
  }
1102
1479
  return verifyPackageChecksums(pluginRoot);
1103
1480
  }
@@ -1121,17 +1498,11 @@ async function prepareVerifiedPackage(options, host = options.host) {
1121
1498
  }
1122
1499
 
1123
1500
  async function prepareVerifiedPackages(options, hosts) {
1124
- const settled = await Promise.allSettled(
1125
- hosts.map((host) => prepareVerifiedPackage(options, host)),
1126
- );
1127
- const prepared = settled
1128
- .filter((result) => result.status === "fulfilled")
1129
- .map((result) => result.value);
1501
+ const settled = await Promise.allSettled(hosts.map((host) => prepareVerifiedPackage(options, host)));
1502
+ const prepared = settled.filter((result) => result.status === "fulfilled").map((result) => result.value);
1130
1503
  const failure = settled.find((result) => result.status === "rejected");
1131
1504
  if (failure) {
1132
- await Promise.all(
1133
- prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
1134
- );
1505
+ await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
1135
1506
  throw failure.reason;
1136
1507
  }
1137
1508
  return prepared;
@@ -1150,8 +1521,7 @@ function expectedMarketplace(host) {
1150
1521
  {
1151
1522
  name: PLUGIN_NAME,
1152
1523
  source: `./plugins/${PLUGIN_NAME}`,
1153
- description:
1154
- "Local reasoning-system selection for Claude Code and Cowork, plus one /opensocrates entry.",
1524
+ description: "Local reasoning-system selection for Claude Code and Cowork, plus one /opensocrates entry.",
1155
1525
  category: "workflow",
1156
1526
  },
1157
1527
  ],
@@ -1181,11 +1551,23 @@ async function buildStagingTree(parent, pluginSource, host) {
1181
1551
  const layout = HOST_LAYOUTS[host];
1182
1552
  const staging = await mkdtemp(join(parent, ".opensocrates.staging-"));
1183
1553
  try {
1554
+ if (FILE_DROP_HOSTS.includes(host)) {
1555
+ await cp(pluginSource, staging, { recursive: true, preserveTimestamps: true });
1556
+ await verifyExtractedPackage(staging, host);
1557
+ await writeFile(join(staging, MARKER_NAME), `${JSON.stringify(markerFor(host), null, 2)}\n`, {
1558
+ encoding: "utf8",
1559
+ mode: 0o600,
1560
+ });
1561
+ return staging;
1562
+ }
1184
1563
  const marketplace = join(staging, layout.marketplaceRelative);
1185
1564
  const plugin = join(staging, layout.pluginRelative);
1186
1565
  await mkdir(dirname(marketplace), { recursive: true, mode: 0o700 });
1187
1566
  await mkdir(dirname(plugin), { recursive: true, mode: 0o700 });
1188
- await cp(pluginSource, plugin, { recursive: true, preserveTimestamps: true });
1567
+ await cp(pluginSource, plugin, {
1568
+ recursive: true,
1569
+ preserveTimestamps: true,
1570
+ });
1189
1571
  await writeFile(marketplace, `${JSON.stringify(expectedMarketplace(host), null, 2)}\n`, {
1190
1572
  encoding: "utf8",
1191
1573
  mode: 0o600,
@@ -1202,20 +1584,72 @@ async function buildStagingTree(parent, pluginSource, host) {
1202
1584
  }
1203
1585
  }
1204
1586
 
1587
+ async function buildOpenCodeStaging(paths, pluginSource) {
1588
+ await ensureOpenCodeDirectories(paths);
1589
+ const root = await mkdtemp(join(paths.parent, ".opensocrates.staging-"));
1590
+ const bridge = join(paths.bridgeParent, `.opensocrates.js.staging-${randomUUID()}`);
1591
+ const bridgeMarker = join(paths.bridgeParent, `.opensocrates-managed.json.staging-${randomUUID()}`);
1592
+ try {
1593
+ await cp(join(pluginSource, "skills", "opensocrates"), root, {
1594
+ recursive: true,
1595
+ preserveTimestamps: true,
1596
+ });
1597
+ const metadata = join(root, ".opensocrates-package");
1598
+ await mkdir(metadata, { mode: 0o700 });
1599
+ for (const name of ["opencode-plugin.json", "release-manifest.json", "checksums.sha256"]) {
1600
+ await cp(join(pluginSource, name), join(metadata, name), {
1601
+ preserveTimestamps: true,
1602
+ });
1603
+ }
1604
+ await writeFile(join(root, MARKER_NAME), `${JSON.stringify(markerFor("opencode"), null, 2)}\n`, {
1605
+ encoding: "utf8",
1606
+ mode: 0o600,
1607
+ });
1608
+ await cp(join(pluginSource, "plugins", "opensocrates.js"), bridge, {
1609
+ preserveTimestamps: true,
1610
+ });
1611
+ const bridgeSha256 = await sha256File(bridge);
1612
+ const files = {};
1613
+ for (const item of (await walkFiles(root)).sort()) {
1614
+ files[item] = await sha256File(join(root, ...item.split("/")));
1615
+ }
1616
+ await writeFile(
1617
+ join(root, OPENCODE_INSTALL_MANIFEST),
1618
+ `${JSON.stringify(
1619
+ {
1620
+ schema: "opensocrates.opencode-installation/1.0.0",
1621
+ version: PRODUCT_VERSION,
1622
+ bridgeSha256,
1623
+ files,
1624
+ },
1625
+ null,
1626
+ 2,
1627
+ )}\n`,
1628
+ { encoding: "utf8", mode: 0o600 },
1629
+ );
1630
+ await writeFile(bridgeMarker, `${JSON.stringify(openCodeSidecar(bridgeSha256), null, 2)}\n`, {
1631
+ encoding: "utf8",
1632
+ mode: 0o600,
1633
+ });
1634
+ return { root, bridge, bridgeMarker };
1635
+ } catch (error) {
1636
+ await rm(root, { recursive: true, force: true });
1637
+ await rm(bridge, { force: true });
1638
+ await rm(bridgeMarker, { force: true });
1639
+ throw error;
1640
+ }
1641
+ }
1642
+
1205
1643
  function entryRoot(entry, host) {
1206
1644
  if (entry === null) {
1207
1645
  return null;
1208
1646
  }
1209
1647
  let value;
1210
1648
  if (host === "claude") {
1211
- const fields = ["path", "installLocation"].filter(
1212
- (field) => entry[field] !== undefined && entry[field] !== null,
1213
- );
1649
+ const fields = ["path", "installLocation"].filter((field) => entry[field] !== undefined && entry[field] !== null);
1214
1650
  if (
1215
1651
  fields.length === 0 ||
1216
- fields.some(
1217
- (field) => typeof entry[field] !== "string" || entry[field].trim().length === 0,
1218
- )
1652
+ fields.some((field) => typeof entry[field] !== "string" || entry[field].trim().length === 0)
1219
1653
  ) {
1220
1654
  fail(`${host} marketplace ${MARKETPLACE_NAME} has no usable root`);
1221
1655
  }
@@ -1250,14 +1684,13 @@ function entryRoot(entry, host) {
1250
1684
  }
1251
1685
 
1252
1686
  function removeRegistration(host, entry, state) {
1687
+ if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
1253
1688
  if (host === "claude") {
1254
1689
  if (["installed", "disabled"].includes(state.kind)) {
1255
1690
  run(claudeBinary(), ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"]);
1256
1691
  }
1257
1692
  if (entry !== null) {
1258
- run(claudeBinary(), [
1259
- "plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user",
1260
- ]);
1693
+ run(claudeBinary(), ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--scope", "user"]);
1261
1694
  }
1262
1695
  return;
1263
1696
  }
@@ -1270,6 +1703,7 @@ function removeRegistration(host, entry, state) {
1270
1703
  }
1271
1704
 
1272
1705
  function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
1706
+ if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return null;
1273
1707
  if (host === "claude") {
1274
1708
  run(claudeBinary(), ["plugin", "marketplace", "add", root, "--scope", "user"]);
1275
1709
  if (installPlugin) {
@@ -1288,6 +1722,7 @@ function addRegistration(host, root, installPlugin, { enabled = true } = {}) {
1288
1722
  }
1289
1723
 
1290
1724
  function removeRegistrationBestEffort(host) {
1725
+ if (FILE_DROP_HOSTS.includes(host) || host === "opencode") return;
1291
1726
  const entry = (() => {
1292
1727
  try {
1293
1728
  return marketplaceEntry(host);
@@ -1324,15 +1759,19 @@ async function recoveryStep(label, action) {
1324
1759
  await action();
1325
1760
  return true;
1326
1761
  } catch (recoveryError) {
1327
- const detail =
1328
- recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
1762
+ const detail = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
1329
1763
  console.error(`warning: rollback step failed (${label}): ${detail}`);
1330
1764
  return false;
1331
1765
  }
1332
1766
  }
1333
1767
 
1334
1768
  async function preflightHost(host, action) {
1335
- requireHostCli(host, { authenticated: ["install", "update"].includes(action) });
1769
+ if (
1770
+ (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") ||
1771
+ ["install", "update"].includes(action)
1772
+ ) {
1773
+ requireHostCli(host, { authenticated: ["install", "update"].includes(action) });
1774
+ }
1336
1775
  if (host === "claude") {
1337
1776
  if (["install", "update"].includes(action)) {
1338
1777
  requireNoLegacyClaudeInstallation();
@@ -1341,6 +1780,22 @@ async function preflightHost(host, action) {
1341
1780
  }
1342
1781
  }
1343
1782
  const paths = managedPaths(host);
1783
+ if (host === "opencode") {
1784
+ const presence = await Promise.all([paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)));
1785
+ if (presence.some(Boolean) && !presence.every(Boolean)) {
1786
+ fail("OpenCode has a partial or unowned OpenSocrates installation; refusing to replace it");
1787
+ }
1788
+ if (presence.every(Boolean)) await verifyOpenCodeInstallation(paths);
1789
+ const previousEntry = presence.every(Boolean) ? { name: MARKETPLACE_NAME, root: paths.root } : null;
1790
+ const previousState = previousEntry === null ? { kind: "missing", version: null } : pluginState(host);
1791
+ return {
1792
+ host,
1793
+ paths,
1794
+ previousEntry,
1795
+ previousState,
1796
+ rootExists: presence[0],
1797
+ };
1798
+ }
1344
1799
  const previousEntry = marketplaceEntry(host);
1345
1800
  if (previousEntry !== null && entryRoot(previousEntry, host) !== paths.root) {
1346
1801
  fail(
@@ -1356,7 +1811,9 @@ async function preflightHost(host, action) {
1356
1811
  fail(`${host} has a managed registration whose root is missing: ${paths.root}`);
1357
1812
  }
1358
1813
  const previousState =
1359
- previousEntry === null ? { kind: "missing", version: null } : pluginState(host);
1814
+ previousEntry === null
1815
+ ? { kind: "missing", version: null }
1816
+ : pluginState(host, { requireHostState: ["install", "update"].includes(action) });
1360
1817
  return { host, paths, previousEntry, previousState, rootExists };
1361
1818
  }
1362
1819
 
@@ -1364,32 +1821,39 @@ async function preflightSelectedHosts(options, action, desiredState) {
1364
1821
  if (options.host !== ALL_HOST) {
1365
1822
  return [await preflightHost(options.host, action)];
1366
1823
  }
1824
+ // A pre-release or offline caller may supply only the exact host archives it
1825
+ // intends to transact. Never mix those candidate packages with downloads
1826
+ // for other locally available hosts: the qualified asset set is the exact
1827
+ // transaction boundary. With no local assets, --host all retains discovery
1828
+ // of every ready host.
1829
+ const assetHosts = qualifiedAssetHosts(options);
1830
+ const candidates = assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS;
1367
1831
  const desiredHosts = new Set(desiredState.installedHosts);
1368
1832
  const rootPresence = Object.fromEntries(
1369
- await Promise.all(
1370
- SUPPORTED_HOSTS.map(async (host) => [host, await exists(managedPaths(host).root)]),
1371
- ),
1372
- );
1373
- const settled = await Promise.allSettled(
1374
- SUPPORTED_HOSTS.map((host) => preflightHost(host, action)),
1833
+ await Promise.all(candidates.map(async (host) => [host, await exists(managedPaths(host).root)])),
1375
1834
  );
1835
+ const settled = await Promise.allSettled(candidates.map((host) => preflightHost(host, action)));
1376
1836
  const successes = new Map();
1377
1837
  const failures = new Map();
1378
1838
  settled.forEach((result, index) => {
1379
- const host = SUPPORTED_HOSTS[index];
1839
+ const host = candidates[index];
1380
1840
  if (result.status === "fulfilled") successes.set(host, result.value);
1381
1841
  else failures.set(host, result.reason);
1382
1842
  });
1383
1843
 
1384
1844
  const required = new Set();
1385
- if (action === "install") {
1386
- for (const host of SUPPORTED_HOSTS) {
1845
+ if (assetHosts.length > 0) {
1846
+ for (const host of assetHosts) required.add(host);
1847
+ } else if (action === "install") {
1848
+ for (const host of candidates) {
1387
1849
  if (desiredHosts.has(host) || rootPresence[host]) required.add(host);
1388
1850
  }
1389
1851
  } else if (desiredHosts.size > 0) {
1390
- for (const host of desiredHosts) required.add(host);
1852
+ for (const host of desiredHosts) {
1853
+ if (candidates.includes(host)) required.add(host);
1854
+ }
1391
1855
  } else if (action === "remove") {
1392
- for (const host of SUPPORTED_HOSTS) {
1856
+ for (const host of candidates) {
1393
1857
  if (rootPresence[host]) required.add(host);
1394
1858
  }
1395
1859
  }
@@ -1401,7 +1865,9 @@ async function preflightSelectedHosts(options, action, desiredState) {
1401
1865
  }
1402
1866
 
1403
1867
  let selected;
1404
- if (action === "update" && desiredHosts.size > 0) {
1868
+ if (assetHosts.length > 0) {
1869
+ selected = assetHosts.map((host) => successes.get(host)).filter(Boolean);
1870
+ } else if (action === "update" && desiredHosts.size > 0) {
1405
1871
  selected = [...desiredHosts].map((host) => successes.get(host)).filter(Boolean);
1406
1872
  } else if (action === "remove") {
1407
1873
  selected = [...successes.values()].filter(
@@ -1427,14 +1893,26 @@ async function preflightSelectedHosts(options, action, desiredState) {
1427
1893
  async function stageInstallation(preflight, pluginSource) {
1428
1894
  const { host, paths } = preflight;
1429
1895
  await mkdir(paths.parent, { recursive: true, mode: 0o700 });
1430
- const staging = await buildStagingTree(paths.parent, pluginSource, host);
1896
+ const transient = transientParent(host, paths);
1897
+ await mkdir(transient, { recursive: true, mode: 0o700 });
1898
+ const staging =
1899
+ host === "opencode"
1900
+ ? await buildOpenCodeStaging(paths, pluginSource)
1901
+ : await buildStagingTree(transient, pluginSource, host);
1431
1902
  return {
1432
1903
  ...preflight,
1433
1904
  staging,
1434
- backup: join(paths.parent, `.opensocrates.backup-${randomUUID()}`),
1905
+ backup: join(transient, `.opensocrates.backup-${randomUUID()}`),
1906
+ bridgeBackup: host === "opencode" ? join(paths.bridgeParent, `.opensocrates.js.backup-${randomUUID()}`) : null,
1907
+ bridgeMarkerBackup:
1908
+ host === "opencode" ? join(paths.bridgeParent, `.opensocrates-managed.json.backup-${randomUUID()}`) : null,
1435
1909
  registrationRemoved: false,
1436
1910
  backupCreated: false,
1437
1911
  newRootActive: false,
1912
+ newBridgeActive: false,
1913
+ newBridgeMarkerActive: false,
1914
+ bridgeBackupCreated: false,
1915
+ bridgeMarkerBackupCreated: false,
1438
1916
  activationStarted: false,
1439
1917
  };
1440
1918
  }
@@ -1450,13 +1928,27 @@ async function activateInstallation(transaction) {
1450
1928
  await rename(paths.root, transaction.backup);
1451
1929
  transaction.backupCreated = true;
1452
1930
  }
1453
- await rename(transaction.staging, paths.root);
1931
+ if (host === "opencode" && (await exists(paths.bridge))) {
1932
+ await rename(paths.bridge, transaction.bridgeBackup);
1933
+ transaction.bridgeBackupCreated = true;
1934
+ }
1935
+ if (host === "opencode" && (await exists(paths.bridgeMarker))) {
1936
+ await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
1937
+ transaction.bridgeMarkerBackupCreated = true;
1938
+ }
1939
+ await rename(host === "opencode" ? transaction.staging.root : transaction.staging, paths.root);
1454
1940
  transaction.newRootActive = true;
1941
+ if (host === "opencode") {
1942
+ await rename(transaction.staging.bridge, paths.bridge);
1943
+ transaction.newBridgeActive = true;
1944
+ await rename(transaction.staging.bridgeMarker, paths.bridgeMarker);
1945
+ transaction.newBridgeMarkerActive = true;
1946
+ await verifyOpenCodeInstallation(paths);
1947
+ }
1455
1948
  const result = addRegistration(host, paths.root, true);
1456
- const state = pluginState(host);
1949
+ const state = pluginState(host, { requireHostState: true });
1457
1950
  if (
1458
- (host === "codex" &&
1459
- (result?.pluginId !== PLUGIN_ID || result?.version !== PRODUCT_VERSION)) ||
1951
+ (host === "codex" && (result?.pluginId !== PLUGIN_ID || result?.version !== PRODUCT_VERSION)) ||
1460
1952
  state.kind !== "installed" ||
1461
1953
  state.version !== PRODUCT_VERSION
1462
1954
  ) {
@@ -1479,41 +1971,56 @@ async function rollbackInstallation(transaction) {
1479
1971
  await rm(paths.root, { recursive: true, force: true });
1480
1972
  });
1481
1973
  }
1974
+ if (host === "opencode") {
1975
+ if (transaction.newBridgeActive && (await exists(paths.bridge))) {
1976
+ await recoveryStep("remove the failed OpenCode bridge", async () => {
1977
+ await rm(paths.bridge, { force: true });
1978
+ });
1979
+ }
1980
+ if (transaction.newBridgeMarkerActive && (await exists(paths.bridgeMarker))) {
1981
+ await recoveryStep("remove the failed OpenCode bridge sidecar", async () => {
1982
+ await rm(paths.bridgeMarker, { force: true });
1983
+ });
1984
+ }
1985
+ if (transaction.bridgeBackupCreated && (await exists(transaction.bridgeBackup))) {
1986
+ await recoveryStep("restore the previous OpenCode bridge", async () => {
1987
+ await rename(transaction.bridgeBackup, paths.bridge);
1988
+ });
1989
+ }
1990
+ if (transaction.bridgeMarkerBackupCreated && (await exists(transaction.bridgeMarkerBackup))) {
1991
+ await recoveryStep("restore the previous OpenCode bridge sidecar", async () => {
1992
+ await rename(transaction.bridgeMarkerBackup, paths.bridgeMarker);
1993
+ });
1994
+ }
1995
+ }
1482
1996
  if (transaction.backupCreated && (await exists(transaction.backup))) {
1483
1997
  restored = await recoveryStep(`restore the previous ${host} installation`, async () => {
1484
1998
  await requireOwnedRoot(transaction.backup, host);
1485
1999
  await rename(transaction.backup, paths.root);
1486
2000
  });
1487
2001
  }
1488
- if (
1489
- transaction.registrationRemoved &&
1490
- previousEntry !== null &&
1491
- (restored || !transaction.backupCreated)
1492
- ) {
2002
+ if (transaction.registrationRemoved && previousEntry !== null && (restored || !transaction.backupCreated)) {
1493
2003
  await recoveryStep(`re-register the previous ${host} installation`, async () => {
1494
- addRegistration(
1495
- host,
1496
- paths.root,
1497
- ["installed", "disabled"].includes(previousState.kind),
1498
- { enabled: previousState.kind !== "disabled" },
1499
- );
2004
+ addRegistration(host, paths.root, ["installed", "disabled"].includes(previousState.kind), {
2005
+ enabled: previousState.kind !== "disabled",
2006
+ });
1500
2007
  });
1501
2008
  }
1502
2009
  if (transaction.backupCreated && !restored) {
1503
- console.error(
1504
- `error: the previous ${host} OpenSocrates installation could not be restored automatically.`,
1505
- );
2010
+ console.error(`error: the previous ${host} OpenSocrates installation could not be restored automatically.`);
1506
2011
  console.error(`error: your previous files are preserved at: ${transaction.backup}`);
1507
2012
  console.error(`error: recovery command: /bin/rm -rf -- ${shellQuote(paths.root)}`);
1508
- console.error(
1509
- `error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`,
1510
- );
2013
+ console.error(`error: recovery command: /bin/mv -- ${shellQuote(transaction.backup)} ${shellQuote(paths.root)}`);
1511
2014
  console.error(`error: recovery command: opensocrates install --host ${host}`);
1512
2015
  }
1513
2016
  }
1514
2017
 
1515
2018
  async function cleanupInstallationTransaction(transaction) {
1516
- if (await exists(transaction.staging)) {
2019
+ if (transaction.host === "opencode") {
2020
+ await rm(transaction.staging.root, { recursive: true, force: true });
2021
+ await rm(transaction.staging.bridge, { force: true });
2022
+ await rm(transaction.staging.bridgeMarker, { force: true });
2023
+ } else if (await exists(transaction.staging)) {
1517
2024
  await rm(transaction.staging, { recursive: true, force: true });
1518
2025
  }
1519
2026
  }
@@ -1524,6 +2031,14 @@ async function commitInstallation(transaction) {
1524
2031
  await rm(transaction.backup, { recursive: true });
1525
2032
  transaction.backupCreated = false;
1526
2033
  }
2034
+ if (transaction.host === "opencode") {
2035
+ for (const [created, backup] of [
2036
+ [transaction.bridgeBackupCreated, transaction.bridgeBackup],
2037
+ [transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup],
2038
+ ]) {
2039
+ if (created && (await exists(backup))) await rm(backup);
2040
+ }
2041
+ }
1527
2042
  }
1528
2043
 
1529
2044
  function installedDesiredState(previous, hosts) {
@@ -1546,8 +2061,7 @@ async function runInstallOrUpdate(options, action) {
1546
2061
  for (const preflight of preflights) {
1547
2062
  const item = packages.get(preflight.host);
1548
2063
  console.log(
1549
- `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` +
1550
- `${item.checkedFiles} package files`,
2064
+ `${preflight.host}: verified OpenSocrates ${PRODUCT_VERSION} and ` + `${item.checkedFiles} package files`,
1551
2065
  );
1552
2066
  transactions.push(await stageInstallation(preflight, item.pluginRoot));
1553
2067
  }
@@ -1563,9 +2077,7 @@ async function runInstallOrUpdate(options, action) {
1563
2077
  throw error;
1564
2078
  } finally {
1565
2079
  await Promise.all(transactions.map(cleanupInstallationTransaction));
1566
- await Promise.all(
1567
- prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
1568
- );
2080
+ await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
1569
2081
  }
1570
2082
  for (const transaction of transactions) {
1571
2083
  const cleaned = await recoveryStep(`remove the committed ${transaction.host} backup`, async () => {
@@ -1576,17 +2088,47 @@ async function runInstallOrUpdate(options, action) {
1576
2088
  }
1577
2089
  }
1578
2090
  const verb = action === "update" ? "updated" : "installed";
1579
- console.log(
1580
- `OpenSocrates ${PRODUCT_VERSION} ${verb} successfully for ${hosts.join(", ")}.`,
1581
- );
1582
- console.log("Start new host tasks to load the updated skills and hooks.");
2091
+ console.log(`OpenSocrates ${PRODUCT_VERSION} ${verb} successfully for ${hosts.join(", ")}.`);
2092
+ if (hosts.includes("codex")) {
2093
+ console.log(
2094
+ "Codex approval required: open one interactive Codex session and approve the " +
2095
+ "OpenSocrates hooks before relying on automatic selection. Non-interactive " +
2096
+ "codex exec silently skips hooks that have not been trusted.",
2097
+ );
2098
+ }
2099
+ if (hosts.includes("cursor")) {
2100
+ console.log(
2101
+ "Cursor: run Developer: Reload Window, then invoke /opensocrates from Agent chat. " +
2102
+ "This experimental package adds no automatic OpenSocrates hook selector.",
2103
+ );
2104
+ }
2105
+ if (hosts.includes("grok")) {
2106
+ console.log(
2107
+ "Grok Build: start a new task for automatic native-skill selection, or invoke /opensocrates explicitly.",
2108
+ );
2109
+ }
2110
+ if (hosts.includes("opencode")) {
2111
+ console.log(
2112
+ "OpenCode: automatic same-turn selection uses the stable chat.message bridge; " +
2113
+ "the native opensocrates skill remains available as an explicit fallback.",
2114
+ );
2115
+ }
2116
+ console.log("Start new host tasks to load the updated OpenSocrates integration.");
1583
2117
  return hosts;
1584
2118
  }
1585
2119
 
1586
2120
  async function inspectHostStatus(host) {
1587
- requireHostCli(host);
2121
+ if (!FILE_DROP_HOSTS.includes(host) && host !== "opencode") requireHostCli(host);
1588
2122
  if (host === "claude") warnLegacyClaudeInstallation();
1589
2123
  const paths = managedPaths(host);
2124
+ if (host === "opencode") {
2125
+ const presence = await Promise.all([paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)));
2126
+ if (!presence.some(Boolean)) return { host, kind: "missing", version: null, paths };
2127
+ if (!presence.every(Boolean)) return { host, kind: "files-only", version: null, paths };
2128
+ await verifyOpenCodeInstallation(paths);
2129
+ const state = pluginState(host);
2130
+ return { host, kind: state.kind, version: state.version, paths };
2131
+ }
1590
2132
  const entry = marketplaceEntry(host);
1591
2133
  if (entry === null) {
1592
2134
  if (await exists(paths.root)) {
@@ -1617,14 +2159,19 @@ async function showStatus(host) {
1617
2159
  const status = await inspectHostStatus(candidate);
1618
2160
  const expected = desired.installedHosts.includes(candidate);
1619
2161
  const hostDrift = expected
1620
- ? status.kind !== "installed" ||
1621
- desired.activeVersion === null ||
1622
- status.version !== desired.activeVersion
2162
+ ? status.kind !== "installed" || desired.activeVersion === null || status.version !== desired.activeVersion
1623
2163
  : status.kind !== "missing";
1624
2164
  drift ||= hostDrift;
1625
2165
  if (status.kind === "installed") {
1626
2166
  console.log(
1627
2167
  `${candidate}: installed ${status.version ?? "unknown"}` +
2168
+ (["antigravity", "cursor"].includes(candidate)
2169
+ ? " (experimental explicit-skill tier)"
2170
+ : candidate === "grok"
2171
+ ? " (native skill; automatic and explicit invocation)"
2172
+ : candidate === "opencode"
2173
+ ? " (stable same-turn bridge plus native skill fallback)"
2174
+ : "") +
1628
2175
  (expected
1629
2176
  ? hostDrift
1630
2177
  ? ` (drift from ${desired.activeVersion ?? "desired state"})`
@@ -1644,9 +2191,7 @@ async function showStatus(host) {
1644
2191
  } else if (status.kind === "files-only") {
1645
2192
  console.log(`${candidate}: managed files present but not registered`);
1646
2193
  } else {
1647
- console.log(
1648
- `${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""),
1649
- );
2194
+ console.log(`${candidate}: not installed` + (expected ? " (drift: desired host is missing)" : ""));
1650
2195
  }
1651
2196
  } catch (error) {
1652
2197
  if (desired.installedHosts.includes(candidate)) drift = true;
@@ -1661,11 +2206,24 @@ async function showStatus(host) {
1661
2206
  }
1662
2207
  const status = await inspectHostStatus(host);
1663
2208
  if (status.kind === "installed") {
1664
- console.log(`OpenSocrates ${status.version ?? "unknown"} is installed.`);
2209
+ console.log(
2210
+ `OpenSocrates ${status.version ?? "unknown"} is installed.` +
2211
+ (host === "antigravity"
2212
+ ? " Antigravity support is experimental and explicit-skill only."
2213
+ : host === "cursor"
2214
+ ? " Cursor support is experimental and explicit-skill first."
2215
+ : host === "grok"
2216
+ ? " Grok Build uses automatic native-skill selection and explicit /opensocrates invocation."
2217
+ : host === "opencode"
2218
+ ? " OpenCode uses the stable same-turn bridge and native skill fallback."
2219
+ : ""),
2220
+ );
1665
2221
  } else if (status.kind === "disabled") {
1666
2222
  console.log(
1667
2223
  `OpenSocrates ${status.version ?? "unknown"} is installed but disabled. ` +
1668
- "Run install or update to re-enable it.",
2224
+ (host === "grok"
2225
+ ? "Remove the OpenSocrates disabled entry from Grok Build configuration, then run status again."
2226
+ : "Run install or update to re-enable it."),
1669
2227
  );
1670
2228
  } else if (status.kind === "available") {
1671
2229
  console.log(`OpenSocrates ${status.version ?? "unknown"} is available but not installed.`);
@@ -1679,9 +2237,22 @@ async function showStatus(host) {
1679
2237
  function removalTransaction(preflight) {
1680
2238
  return {
1681
2239
  ...preflight,
1682
- backup: join(preflight.paths.parent, `.opensocrates.removed-${randomUUID()}`),
2240
+ backup: join(
2241
+ transientParent(preflight.host, preflight.paths),
2242
+ `.opensocrates.removed-${randomUUID()}`,
2243
+ ),
2244
+ bridgeBackup:
2245
+ preflight.host === "opencode"
2246
+ ? join(preflight.paths.bridgeParent, `.opensocrates.js.removed-${randomUUID()}`)
2247
+ : null,
2248
+ bridgeMarkerBackup:
2249
+ preflight.host === "opencode"
2250
+ ? join(preflight.paths.bridgeParent, `.opensocrates-managed.json.removed-${randomUUID()}`)
2251
+ : null,
1683
2252
  registrationRemoved: false,
1684
2253
  backupCreated: false,
2254
+ bridgeBackupCreated: false,
2255
+ bridgeMarkerBackupCreated: false,
1685
2256
  };
1686
2257
  }
1687
2258
 
@@ -1695,6 +2266,14 @@ async function activateRemoval(transaction) {
1695
2266
  await rename(paths.root, transaction.backup);
1696
2267
  transaction.backupCreated = true;
1697
2268
  }
2269
+ if (host === "opencode" && (await exists(paths.bridge))) {
2270
+ await rename(paths.bridge, transaction.bridgeBackup);
2271
+ transaction.bridgeBackupCreated = true;
2272
+ }
2273
+ if (host === "opencode" && (await exists(paths.bridgeMarker))) {
2274
+ await rename(paths.bridgeMarker, transaction.bridgeMarkerBackup);
2275
+ transaction.bridgeMarkerBackupCreated = true;
2276
+ }
1698
2277
  }
1699
2278
 
1700
2279
  async function rollbackRemoval(transaction) {
@@ -1715,6 +2294,18 @@ async function rollbackRemoval(transaction) {
1715
2294
  );
1716
2295
  });
1717
2296
  }
2297
+ if (transaction.host === "opencode") {
2298
+ if (transaction.bridgeBackupCreated && (await exists(transaction.bridgeBackup))) {
2299
+ await recoveryStep("restore the removed OpenCode bridge", async () => {
2300
+ await rename(transaction.bridgeBackup, transaction.paths.bridge);
2301
+ });
2302
+ }
2303
+ if (transaction.bridgeMarkerBackupCreated && (await exists(transaction.bridgeMarkerBackup))) {
2304
+ await recoveryStep("restore the removed OpenCode bridge sidecar", async () => {
2305
+ await rename(transaction.bridgeMarkerBackup, transaction.paths.bridgeMarker);
2306
+ });
2307
+ }
2308
+ }
1718
2309
  }
1719
2310
 
1720
2311
  async function commitRemoval(transaction) {
@@ -1722,6 +2313,19 @@ async function commitRemoval(transaction) {
1722
2313
  await requireOwnedRoot(transaction.backup, transaction.host);
1723
2314
  await rm(transaction.backup, { recursive: true });
1724
2315
  }
2316
+ if (transaction.host === "opencode") {
2317
+ for (const [created, backup, label] of [
2318
+ [transaction.bridgeBackupCreated, transaction.bridgeBackup, "bridge"],
2319
+ [transaction.bridgeMarkerBackupCreated, transaction.bridgeMarkerBackup, "bridge sidecar"],
2320
+ ]) {
2321
+ if (!created || !(await exists(backup))) continue;
2322
+ const info = await lstat(backup);
2323
+ if (!info.isFile() || info.isSymbolicLink()) {
2324
+ fail(`refusing to remove an unsafe OpenCode ${label} backup: ${backup}`);
2325
+ }
2326
+ await rm(backup);
2327
+ }
2328
+ }
1725
2329
  }
1726
2330
 
1727
2331
  function xmlEscape(value) {
@@ -1762,29 +2366,15 @@ function launchctlDomain() {
1762
2366
 
1763
2367
  function launchAgentDocument(npx, channel, environment) {
1764
2368
  const packageTag = channel === "next" ? "next" : "latest";
1765
- const arguments_ = [
1766
- npx,
1767
- "--yes",
1768
- `opensocrates@${packageTag}`,
1769
- "auto-update",
1770
- "run",
1771
- ];
1772
- const argumentsXml = arguments_
1773
- .map((argument) => ` <string>${xmlEscape(argument)}</string>`)
1774
- .join("\n");
1775
- const environmentEntries = Object.entries(environment).sort(([left], [right]) =>
1776
- left.localeCompare(right),
1777
- );
2369
+ const arguments_ = [npx, "--yes", `opensocrates@${packageTag}`, "auto-update", "run"];
2370
+ const argumentsXml = arguments_.map((argument) => ` <string>${xmlEscape(argument)}</string>`).join("\n");
2371
+ const environmentEntries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
1778
2372
  const environmentXml =
1779
2373
  environmentEntries.length === 0
1780
2374
  ? ""
1781
2375
  : `\n <key>EnvironmentVariables</key>\n <dict>\n` +
1782
2376
  environmentEntries
1783
- .map(
1784
- ([key, value]) =>
1785
- ` <key>${xmlEscape(key)}</key>\n` +
1786
- ` <string>${xmlEscape(value)}</string>`,
1787
- )
2377
+ .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n` + ` <string>${xmlEscape(value)}</string>`)
1788
2378
  .join("\n") +
1789
2379
  "\n </dict>";
1790
2380
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -1817,7 +2407,11 @@ function launchAgentTarget() {
1817
2407
  }
1818
2408
 
1819
2409
  function launchAgentLoaded() {
1820
- return run(launchctlBinary(), ["print", launchAgentTarget()], { allowFailure: true }).status === 0;
2410
+ return (
2411
+ run(launchctlBinary(), ["print", launchAgentTarget()], {
2412
+ allowFailure: true,
2413
+ }).status === 0
2414
+ );
1821
2415
  }
1822
2416
 
1823
2417
  function stopLoadedLaunchAgent() {
@@ -1834,17 +2428,69 @@ function stopLoadedLaunchAgent() {
1834
2428
  async function updaterEnvironment(hosts, npx) {
1835
2429
  const node = await executablePath("node", "OPENSOCRATES_NODE_BIN");
1836
2430
  const environment = {
1837
- PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(
1838
- ":",
1839
- ),
2431
+ PATH: [...new Set([dirname(npx), dirname(node), "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":"),
1840
2432
  };
1841
2433
  for (const host of hosts) {
2434
+ if (host === "cursor") {
2435
+ if (process.env.CURSOR_CONFIG_DIR) {
2436
+ environment.CURSOR_CONFIG_DIR = resolve(process.env.CURSOR_CONFIG_DIR);
2437
+ continue;
2438
+ }
2439
+ const app = cursorAppPaths().find((candidate) => existsSync(candidate));
2440
+ if (app) continue;
2441
+ const executable = await executablePath("cursor", "CURSOR_BIN");
2442
+ environment.CURSOR_BIN = executable;
2443
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
2444
+ continue;
2445
+ }
2446
+ if (host === "antigravity") {
2447
+ if (process.env.ANTIGRAVITY_CONFIG_DIR) {
2448
+ environment.ANTIGRAVITY_CONFIG_DIR = resolve(process.env.ANTIGRAVITY_CONFIG_DIR);
2449
+ continue;
2450
+ }
2451
+ const executable = await executablePath("agy", "AGY_BIN");
2452
+ environment.AGY_BIN = executable;
2453
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
2454
+ continue;
2455
+ }
2456
+ if (host === "opencode") {
2457
+ // OPENCODE_CONFIG_DIR redirects configuration only. Unlike the
2458
+ // content-only hosts above, a scheduled OpenCode update re-runs the same
2459
+ // requireHostCli version gate as an interactive install, so recording
2460
+ // the config directory and stopping would leave the LaunchAgent without
2461
+ // a resolvable opencode: enable would succeed in a shell that has it on
2462
+ // PATH, then every scheduled run would fail the gate whenever opencode
2463
+ // lives outside the launchd default PATH.
2464
+ if (process.env.OPENCODE_CONFIG_DIR) {
2465
+ environment.OPENCODE_CONFIG_DIR = resolve(process.env.OPENCODE_CONFIG_DIR);
2466
+ }
2467
+ const executable = await executablePath("opencode", "OPENCODE_BIN");
2468
+ environment.OPENCODE_BIN = executable;
2469
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
2470
+ continue;
2471
+ }
2472
+ if (host === "grok") {
2473
+ const executable = await executablePath("grok", "GROK_BIN");
2474
+ environment.GROK_BIN = executable;
2475
+ environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(
2476
+ ":",
2477
+ );
2478
+ continue;
2479
+ }
1842
2480
  const key = host === "claude" ? "CLAUDE_BIN" : "CODEX_BIN";
1843
2481
  const executable = await executablePath(host, key);
1844
2482
  environment[key] = executable;
1845
2483
  environment.PATH = [...new Set([dirname(executable), ...environment.PATH.split(":")])].join(":");
1846
2484
  }
1847
- for (const key of ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "OPENSOCRATES_STATE_DIR"]) {
2485
+ for (const key of [
2486
+ "ANTIGRAVITY_CONFIG_DIR",
2487
+ "CLAUDE_CONFIG_DIR",
2488
+ "CODEX_HOME",
2489
+ "CURSOR_CONFIG_DIR",
2490
+ "GROK_HOME",
2491
+ "OPENCODE_CONFIG_DIR",
2492
+ "OPENSOCRATES_STATE_DIR",
2493
+ ]) {
1848
2494
  if (process.env[key]) environment[key] = resolve(process.env[key]);
1849
2495
  }
1850
2496
  return environment;
@@ -1852,10 +2498,7 @@ async function updaterEnvironment(hosts, npx) {
1852
2498
 
1853
2499
  async function installLaunchAgent(channel, hosts) {
1854
2500
  if (process.platform !== "darwin" || process.arch !== "arm64") {
1855
- fail(
1856
- `automatic updates currently support darwin-arm64 only; detected ` +
1857
- `${process.platform}-${process.arch}`,
1858
- );
2501
+ fail(`automatic updates currently support darwin-arm64 only; detected ` + `${process.platform}-${process.arch}`);
1859
2502
  }
1860
2503
  const paths = statePaths();
1861
2504
  const npx = await executablePath("npx", "OPENSOCRATES_NPX_BIN");
@@ -1876,22 +2519,16 @@ async function installLaunchAgent(channel, hosts) {
1876
2519
  const wasLoaded = launchAgentLoaded();
1877
2520
  if (wasLoaded) stopLoadedLaunchAgent();
1878
2521
  await atomicWritePrivateFile(paths.launchAgent, document);
1879
- const launched = run(
1880
- launchctlBinary(),
1881
- ["bootstrap", launchctlDomain(), paths.launchAgent],
1882
- { allowFailure: true },
1883
- );
2522
+ const launched = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], { allowFailure: true });
1884
2523
  if (launched.status !== 0) {
1885
2524
  if (previousDocument === null) {
1886
2525
  await rm(paths.launchAgent, { force: true });
1887
2526
  } else {
1888
2527
  await atomicWritePrivateFile(paths.launchAgent, previousDocument);
1889
2528
  if (wasLoaded) {
1890
- const restored = run(
1891
- launchctlBinary(),
1892
- ["bootstrap", launchctlDomain(), paths.launchAgent],
1893
- { allowFailure: true },
1894
- );
2529
+ const restored = run(launchctlBinary(), ["bootstrap", launchctlDomain(), paths.launchAgent], {
2530
+ allowFailure: true,
2531
+ });
1895
2532
  if (restored.status !== 0) {
1896
2533
  console.error("warning: the previous OpenSocrates LaunchAgent could not be reloaded");
1897
2534
  }
@@ -1930,9 +2567,7 @@ async function runRemove(options) {
1930
2567
  const preflights = await preflightSelectedHosts(options, "remove", desired);
1931
2568
  const removedHosts = preflights.map((item) => item.host);
1932
2569
  const remainingHosts = desired.installedHosts.filter((host) => !removedHosts.includes(host));
1933
- const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter(
1934
- (host) => !removedHosts.includes(host),
1935
- );
2570
+ const remainingAutoUpdateHosts = desired.autoUpdate.hosts.filter((host) => !removedHosts.includes(host));
1936
2571
  const keepAutoUpdate = desired.autoUpdate.enabled && remainingAutoUpdateHosts.length > 0;
1937
2572
  const nextDesired = {
1938
2573
  ...desired,
@@ -1964,21 +2599,22 @@ async function runRemove(options) {
1964
2599
  } catch (error) {
1965
2600
  for (const transaction of [...transactions].reverse()) await rollbackRemoval(transaction);
1966
2601
  if (schedulerTouched) {
1967
- const schedulerRestored = await recoveryStep(
1968
- "restore the automatic updater after removal failure",
1969
- async () => {
1970
- if (desired.autoUpdate.enabled) {
1971
- await installLaunchAgent(desired.channel, desired.autoUpdate.hosts);
1972
- } else {
1973
- await disableLaunchAgent();
1974
- }
1975
- },
1976
- );
2602
+ const schedulerRestored = await recoveryStep("restore the automatic updater after removal failure", async () => {
2603
+ if (desired.autoUpdate.enabled) {
2604
+ await installLaunchAgent(desired.channel, desired.autoUpdate.hosts);
2605
+ } else {
2606
+ await disableLaunchAgent();
2607
+ }
2608
+ });
1977
2609
  if (!schedulerRestored && desired.autoUpdate.enabled) {
1978
2610
  await recoveryStep("record the disabled updater after removal failure", async () => {
1979
2611
  await writeDesiredState({
1980
2612
  ...desired,
1981
- autoUpdate: { enabled: false, hosts: [], nextCheckAt: null },
2613
+ autoUpdate: {
2614
+ enabled: false,
2615
+ hosts: [],
2616
+ nextCheckAt: null,
2617
+ },
1982
2618
  });
1983
2619
  });
1984
2620
  }
@@ -1986,7 +2622,11 @@ async function runRemove(options) {
1986
2622
  await recoveryStep("record the disabled updater after removal failure", async () => {
1987
2623
  await writeDesiredState({
1988
2624
  ...desired,
1989
- autoUpdate: { enabled: false, hosts: [], nextCheckAt: null },
2625
+ autoUpdate: {
2626
+ enabled: false,
2627
+ hosts: [],
2628
+ nextCheckAt: null,
2629
+ },
1990
2630
  });
1991
2631
  });
1992
2632
  }
@@ -1994,7 +2634,19 @@ async function runRemove(options) {
1994
2634
  }
1995
2635
  for (const transaction of transactions) await commitRemoval(transaction);
1996
2636
  for (const host of removedHosts) {
1997
- console.log(`OpenSocrates was removed from ${host === "claude" ? "Claude" : "Codex"}.`);
2637
+ const label =
2638
+ host === "antigravity"
2639
+ ? "Antigravity"
2640
+ : host === "claude"
2641
+ ? "Claude"
2642
+ : host === "cursor"
2643
+ ? "Cursor"
2644
+ : host === "grok"
2645
+ ? "Grok Build"
2646
+ : host === "opencode"
2647
+ ? "OpenCode"
2648
+ : "Codex";
2649
+ console.log(`OpenSocrates was removed from ${label}.`);
1998
2650
  }
1999
2651
  if (removedHosts.length === 0) console.log("OpenSocrates is not installed on any managed host.");
2000
2652
  }
@@ -2035,19 +2687,14 @@ async function enableAutoUpdate(options) {
2035
2687
  const observedVersions = new Set();
2036
2688
  for (const host of installedHosts) {
2037
2689
  const preflight = preflights.get(host) ?? (await preflightHost(host, "update"));
2038
- if (
2039
- preflight.previousState.kind !== "installed" ||
2040
- typeof preflight.previousState.version !== "string"
2041
- ) {
2690
+ if (preflight.previousState.kind !== "installed" || typeof preflight.previousState.version !== "string") {
2042
2691
  fail(`cannot determine the installed OpenSocrates version on ${host}`);
2043
2692
  }
2044
2693
  preflights.set(host, preflight);
2045
2694
  observedVersions.add(preflight.previousState.version);
2046
2695
  }
2047
2696
  if (observedVersions.size !== 1) {
2048
- fail(
2049
- "installed hosts do not share one known version; run update --host all before enabling automatic updates",
2050
- );
2697
+ fail("installed hosts do not share one known version; run update --host all before enabling automatic updates");
2051
2698
  }
2052
2699
  [activeVersion] = observedVersions;
2053
2700
  }
@@ -2151,18 +2798,20 @@ async function runScheduledUpdate(options) {
2151
2798
  for (const host of hosts) ready.push(await preflightHost(host, "update"));
2152
2799
  const alreadyCurrent =
2153
2800
  desired.activeVersion === PRODUCT_VERSION &&
2154
- ready.every(
2155
- (item) =>
2156
- item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION,
2157
- );
2801
+ ready.every((item) => item.previousState.kind === "installed" && item.previousState.version === PRODUCT_VERSION);
2158
2802
  if (!alreadyCurrent) {
2159
- if (hosts.length === SUPPORTED_HOSTS.length) {
2803
+ if (hosts.length > 1) {
2160
2804
  await runInstallOrUpdate({ ...options, host: ALL_HOST }, "update");
2161
2805
  } else {
2162
2806
  const [host] = hosts;
2163
2807
  const local = localAssetInputs(options, host);
2164
2808
  await runInstallOrUpdate(
2165
- { ...options, host, asset: local.asset, checksum: local.checksum },
2809
+ {
2810
+ ...options,
2811
+ host,
2812
+ asset: local.asset,
2813
+ checksum: local.checksum,
2814
+ },
2166
2815
  "update",
2167
2816
  );
2168
2817
  }
@@ -2172,15 +2821,16 @@ async function runScheduledUpdate(options) {
2172
2821
  ...refreshed,
2173
2822
  availableVersion: PRODUCT_VERSION,
2174
2823
  lastCheckAt: checkedAt,
2175
- lastSuccessfulUpdateAt: alreadyCurrent
2176
- ? refreshed.lastSuccessfulUpdateAt
2177
- : checkedAt,
2824
+ lastSuccessfulUpdateAt: alreadyCurrent ? refreshed.lastSuccessfulUpdateAt : checkedAt,
2178
2825
  autoUpdate: { ...refreshed.autoUpdate, nextCheckAt: next },
2179
2826
  });
2180
2827
  await writeAutoUpdateReceipt({
2181
2828
  version: PRODUCT_VERSION,
2182
2829
  checkedAt,
2183
- hosts: hosts.map((host) => ({ host, result: alreadyCurrent ? "current" : "updated" })),
2830
+ hosts: hosts.map((host) => ({
2831
+ host,
2832
+ result: alreadyCurrent ? "current" : "updated",
2833
+ })),
2184
2834
  result: alreadyCurrent ? "no-update" : "updated",
2185
2835
  errorCategory: null,
2186
2836
  });
@@ -2219,19 +2869,31 @@ function requireSupportedPlatform() {
2219
2869
  }
2220
2870
 
2221
2871
  async function verifyPackages(options) {
2222
- const hosts = options.host === ALL_HOST ? SUPPORTED_HOSTS : [options.host];
2872
+ const assetHosts = qualifiedAssetHosts(options);
2873
+ const hosts =
2874
+ options.host === ALL_HOST ? (assetHosts.length > 0 ? assetHosts : SUPPORTED_HOSTS) : [options.host];
2223
2875
  const prepared = await prepareVerifiedPackages(options, hosts);
2224
2876
  try {
2225
2877
  for (const item of prepared) {
2226
2878
  console.log(
2227
- `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` +
2228
- `${item.checkedFiles} package files.`,
2879
+ `${item.host}: verified OpenSocrates ${PRODUCT_VERSION} release and ` + `${item.checkedFiles} package files.`,
2229
2880
  );
2881
+ if (item.host === "opencode") {
2882
+ const paths = managedPaths("opencode");
2883
+ const presence = await Promise.all(
2884
+ [paths.root, paths.bridge, paths.bridgeMarker].map((target) => exists(target)),
2885
+ );
2886
+ if (presence.some(Boolean)) {
2887
+ if (!presence.every(Boolean)) {
2888
+ fail("OpenCode installed state is partial and failed verification");
2889
+ }
2890
+ await verifyOpenCodeInstallation(paths);
2891
+ console.log("opencode: verified installed bridge, skill inventory, and ownership.");
2892
+ }
2893
+ }
2230
2894
  }
2231
2895
  } finally {
2232
- await Promise.all(
2233
- prepared.map((item) => rm(item.scratch, { recursive: true, force: true })),
2234
- );
2896
+ await Promise.all(prepared.map((item) => rm(item.scratch, { recursive: true, force: true })));
2235
2897
  }
2236
2898
  }
2237
2899