ostacky 0.7.4 → 0.8.1

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
@@ -293,6 +293,36 @@ var init_security = __esm(() => {
293
293
  });
294
294
 
295
295
  // src/fs.ts
296
+ var exports_fs = {};
297
+ __export(exports_fs, {
298
+ USER_AGENT: () => USER_AGENT,
299
+ checkBunAvailability: () => checkBunAvailability,
300
+ computeTreeHash: () => computeTreeHash,
301
+ copyDirRecursive: () => copyDirRecursive,
302
+ createOpenCodeDir: () => createOpenCodeDir,
303
+ detectPlatformTarget: () => detectPlatformTarget,
304
+ downloadAndExtract: () => downloadAndExtract,
305
+ downloadAndExtractWithRetry: () => downloadAndExtractWithRetry,
306
+ downloadToFile: () => downloadToFile,
307
+ downloadWithRetry: () => downloadWithRetry,
308
+ ensureOpenCodePaths: () => ensureOpenCodePaths,
309
+ ensureToolDirs: () => ensureToolDirs,
310
+ findBinaryInDir: () => findBinaryInDir,
311
+ findExecutablePath: () => findExecutablePath,
312
+ findOpenCodeDir: () => findOpenCodeDir,
313
+ findProjectRoot: () => findProjectRoot,
314
+ getBunInstallCommand: () => getBunInstallCommand,
315
+ getCommandInvocation: () => getCommandInvocation,
316
+ getEngramReleaseTarget: () => getEngramReleaseTarget,
317
+ getExecutableName: () => getExecutableName,
318
+ getExecutableNames: () => getExecutableNames,
319
+ getGlobalOpenCodeDir: () => getGlobalOpenCodeDir,
320
+ getOpenCodeDirForScope: () => getOpenCodeDirForScope,
321
+ isCommandAvailable: () => isCommandAvailable,
322
+ parseScopeArg: () => parseScopeArg,
323
+ promoteStagedDirectory: () => promoteStagedDirectory,
324
+ shouldRetryDownload: () => shouldRetryDownload
325
+ });
296
326
  import {
297
327
  existsSync,
298
328
  mkdirSync,
@@ -366,6 +396,22 @@ function getOpenCodeDirForScope(scope, cwd = process.cwd()) {
366
396
  }
367
397
  return getGlobalOpenCodeDir();
368
398
  }
399
+ function parseScopeArg(argv = process.argv) {
400
+ for (let i = 0;i < argv.length; i++) {
401
+ const arg = argv[i];
402
+ if (arg === "--scope" && i + 1 < argv.length) {
403
+ const v3 = argv[i + 1];
404
+ if (v3 === "local" || v3 === "global" || v3 === "auto")
405
+ return v3;
406
+ }
407
+ if (arg.startsWith("--scope=")) {
408
+ const v3 = arg.split("=")[1];
409
+ if (v3 === "local" || v3 === "global" || v3 === "auto")
410
+ return v3;
411
+ }
412
+ }
413
+ return null;
414
+ }
369
415
  function ensureOpenCodePaths(opencodeDir) {
370
416
  const paths = {
371
417
  root: opencodeDir,
@@ -389,6 +435,9 @@ function ensureToolDirs(toolsDir, toolNames) {
389
435
  mkdirSync(dir, { recursive: true });
390
436
  }
391
437
  }
438
+ function createOpenCodeDir(baseDir) {
439
+ return ensureOpenCodePaths(join(baseDir, ".opencode"));
440
+ }
392
441
  function copyDirRecursive(src, dest, skipGenerated = false) {
393
442
  if (!existsSync(dest))
394
443
  mkdirSync(dest, { recursive: true });
@@ -449,6 +498,44 @@ function findExecutablePath(cmd) {
449
498
  return null;
450
499
  }
451
500
  }
501
+ function getBunInstallCommand() {
502
+ const platform = process.platform;
503
+ if (platform === "darwin" || platform === "linux") {
504
+ return {
505
+ command: "curl -fsSL https://bun.com/install | bash",
506
+ note: platform === "linux" ? "Requires 'unzip' package (sudo apt install unzip)" : undefined
507
+ };
508
+ }
509
+ if (platform === "win32") {
510
+ return {
511
+ command: 'powershell -c "irm bun.sh/install.ps1|iex"',
512
+ note: "Requires Windows 10 v1809 or later"
513
+ };
514
+ }
515
+ return {
516
+ command: "npm install -g bun",
517
+ note: "Cross-platform fallback"
518
+ };
519
+ }
520
+ function checkBunAvailability() {
521
+ const bunPath = findExecutablePath("bun");
522
+ if (bunPath) {
523
+ let version;
524
+ try {
525
+ version = execFileSync(bunPath, ["--version"], {
526
+ encoding: "utf-8",
527
+ stdio: ["pipe", "pipe", "pipe"]
528
+ }).trim();
529
+ } catch {}
530
+ return { available: true, path: bunPath, version };
531
+ }
532
+ const { command, note } = getBunInstallCommand();
533
+ return {
534
+ available: false,
535
+ installCommand: command,
536
+ installNote: note
537
+ };
538
+ }
452
539
  function detectPlatformTarget(platform = process.platform, arch = process.arch) {
453
540
  let os;
454
541
  let cpu;
@@ -570,6 +657,23 @@ function downloadToFile(url, dest, timeoutMs = 180000) {
570
657
  });
571
658
  });
572
659
  }
660
+ async function downloadWithRetry(url, dest, maxRetries = 2, timeoutMs = 180000) {
661
+ let lastError = null;
662
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
663
+ try {
664
+ await downloadToFile(url, dest, timeoutMs);
665
+ return;
666
+ } catch (err) {
667
+ lastError = err;
668
+ if (attempt < maxRetries && shouldRetryDownload(lastError)) {
669
+ const delay = 1000 * Math.pow(2, attempt);
670
+ console.error(`[download] Intento ${attempt + 1} falló: ${lastError.message}. Reintentando en ${delay}ms...`);
671
+ await new Promise((r2) => setTimeout(r2, delay));
672
+ }
673
+ }
674
+ }
675
+ throw lastError;
676
+ }
573
677
  async function downloadAndExtract(url, destDir, stripComponents = 1, timeoutMs = 180000) {
574
678
  const tmp = join(dirname(destDir), `.${basename(destDir)}.download-${Date.now()}-${process.pid}`);
575
679
  if (!existsSync(tmp))
@@ -640,31 +744,31 @@ var init_fs = __esm(() => {
640
744
  var manifest_default;
641
745
  var init_manifest = __esm(() => {
642
746
  manifest_default = {
643
- version: "0.7.4",
747
+ version: "0.8.1",
644
748
  repo: "JaimeHoracio/Ostacky",
645
- tag: "v0.7.4",
749
+ tag: "v0.8.1",
646
750
  agents: [
647
751
  {
648
752
  name: "ostacky",
649
753
  file: "assets/agents/ostacky.md",
650
- description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.7.4: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
651
- version: "0.7.4",
652
- sha256: "ffda231d1a9df1693f32460bdb6811077f7f53ba7902b2616d82231a3d6d43bb"
754
+ description: "Orquestador con recuperación automática (nunca se congela), ruteo por nivel de impacto, controller MCP con SDK oficial, edición segura con fallback inline, y delegación en OpenSpec + Superpowers. v0.8.1: prune de skills obsoletas, lastHandoff, getAvailableTransitions, consecutiveFailures real, mem_session_summary automático al cierre.",
755
+ version: "0.8.1",
756
+ sha256: "1dea56ffe3866e830c5138b44ff3e7531324ddd2784b1d34a8956614a53c5ec9"
653
757
  }
654
758
  ],
655
759
  commands: [
656
760
  {
657
761
  name: "install-stack",
658
762
  file: "assets/commands/install-stack.md",
659
- description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP con SDK oficial). v0.7.4: Paso 1.5 controller expandido con verificación post-instalación y troubleshooting.",
660
- version: "0.7.4",
661
- sha256: "6887499074a82c3b4538e38ca501290cacd3afad676bd127662fed77e268f299"
763
+ description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, controller plugin). v0.8.1: controller plugin como alma hard-gate, Context7 removido del stack.",
764
+ version: "0.8.1",
765
+ sha256: "43ad96a63ceab8730cf0459838e7fad2953333307cfc664a849ca39f7ba7d532"
662
766
  },
663
767
  {
664
768
  name: "opsx-sync",
665
769
  file: "assets/commands/opsx-sync.md",
666
770
  description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
667
- version: "0.7.4",
771
+ version: "0.8.1",
668
772
  sha256: "fe0158478f2ca63b315037a85fc1632b77868532e319af6c7384285441767d64"
669
773
  }
670
774
  ],
@@ -672,15 +776,15 @@ var init_manifest = __esm(() => {
672
776
  {
673
777
  name: "ostacky-controller",
674
778
  file: "assets/mcp/ostacky-controller/",
675
- description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.7.4: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
676
- version: "0.7.4",
677
- sha256: "4f86b6829293024caeb3d09c83b61aee158c383a7bb053a49e5a33c7c55862d1"
779
+ description: "Máquina de estados persistida con @modelcontextprotocol/server SDK. v0.8.1: 22 tools (incluye set_handoff, get_handoff, clear_handoff, get_available_transitions funcional). Bugfixes: consecutiveFailures real, lastHandoff state, defaultChoice persistido. Robustez: degraded mode automático tras 3 fallos, persistence condicional para Nivel 0, prune de skills obsoletas.",
780
+ version: "0.8.1",
781
+ sha256: "ec554bc97f9d9b1e64c0b42813547dedc923e86e326c43ae87afab88ba249b90"
678
782
  },
679
783
  {
680
784
  name: "openspec",
681
785
  file: "assets/mcp/openspec/",
682
786
  description: "MCP server local para OpenSpec - proposal, apply, archive, sync de cambios",
683
- version: "0.7.4",
787
+ version: "0.8.1",
684
788
  sha256: "fa4be28bfc1e75db8f1a037e2580f04a60066c80e8e5934e90e1041020d04609"
685
789
  }
686
790
  ],
@@ -689,105 +793,105 @@ var init_manifest = __esm(() => {
689
793
  name: "brainstorming",
690
794
  file: "assets/skills/brainstorming/SKILL.md",
691
795
  description: "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a implementación directa o openspec-propose) y open-exploration (exploración libre)",
692
- version: "0.7.4",
693
- sha256: "7b0020f31f346d8f2070219930cfb9341135d40941195f0ea50c3bd7f4a42990"
796
+ version: "0.8.1",
797
+ sha256: "87f8d6ed28205f9272792d3865d8cd3232ab5e511a604d6d63d41e3a02a5f422"
694
798
  },
695
799
  {
696
800
  name: "execution-mode-evaluation",
697
801
  file: "assets/skills/execution-mode-evaluation/SKILL.md",
698
802
  description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
699
- version: "0.7.4",
700
- sha256: "e17f1e0309572841dc1fa537b1f32c7468eb70b3acf453b9ff5cc4fa8d94fd39"
803
+ version: "0.8.1",
804
+ sha256: "7b89dda29927c88f1eb13ffcea541a12423384c172f66c4f3d9b61efe0c7a163"
701
805
  },
702
806
  {
703
807
  name: "tdd",
704
808
  file: "assets/skills/tdd/SKILL.md",
705
809
  description: "Skill de test-driven development (Superpowers)",
706
- version: "0.7.4",
810
+ version: "0.8.1",
707
811
  sha256: "aa412298980b7826165c211145c1b8e9135f68f36247bb3cb96d0a0eae274486"
708
812
  },
709
813
  {
710
814
  name: "subagent-driven-development",
711
815
  file: "assets/skills/subagent-driven-development/SKILL.md",
712
816
  description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
713
- version: "0.7.4",
817
+ version: "0.8.1",
714
818
  sha256: "1a42d714a9a13faf05f0bf7b580e8d837c2e77a5633839542ccce603023419e8"
715
819
  },
716
820
  {
717
821
  name: "dispatching-parallel-agents",
718
822
  file: "assets/skills/dispatching-parallel-agents/SKILL.md",
719
823
  description: "Skill de dispatch paralelo de agentes (Superpowers)",
720
- version: "0.7.4",
824
+ version: "0.8.1",
721
825
  sha256: "3c8a66d51ae2e719e877d02c3dac32bd804722c3bb9227c258ecae78252a20ab"
722
826
  },
723
827
  {
724
828
  name: "review",
725
829
  file: "assets/skills/review/SKILL.md",
726
830
  description: "Skill de revisión de código (Superpowers)",
727
- version: "0.7.4",
831
+ version: "0.8.1",
728
832
  sha256: "b19650ed4d1d4d9857a4dd5b7e08e91328a1a1fc9c45bcc2fb600fa9d0213279"
729
833
  },
730
834
  {
731
835
  name: "receiving-code-review",
732
836
  file: "assets/skills/receiving-code-review/SKILL.md",
733
837
  description: "Skill de recibir y procesar feedback de code review",
734
- version: "0.7.4",
838
+ version: "0.8.1",
735
839
  sha256: "d761e884e71d8d3476ac734d287cae403a4024506a7d12e7361a448217c0a831"
736
840
  },
737
841
  {
738
842
  name: "openspec-propose",
739
843
  file: "assets/skills/openspec-propose/SKILL.md",
740
844
  description: "Skill de generación de proposal (OpenSpec)",
741
- version: "0.7.4",
845
+ version: "0.8.1",
742
846
  sha256: "bb59100b9fd3c9f9a1ec6509fa975ec55bf7fc737df4fe73bb3e9bbc217b83f7"
743
847
  },
744
848
  {
745
849
  name: "openspec-apply-change",
746
850
  file: "assets/skills/openspec-apply-change/SKILL.md",
747
851
  description: "Skill de aplicación de change (OpenSpec)",
748
- version: "0.7.4",
852
+ version: "0.8.1",
749
853
  sha256: "dfc823bf89fc7505e91ab6dee9c1f004be38a410bd3d88fb67b211b1b6cbb1d0"
750
854
  },
751
855
  {
752
856
  name: "openspec-archive-change",
753
857
  file: "assets/skills/openspec-archive-change/SKILL.md",
754
858
  description: "Skill de archivo de change (OpenSpec)",
755
- version: "0.7.4",
859
+ version: "0.8.1",
756
860
  sha256: "16e4b561de7747283663fed602e506abf502452c49a8d4b86d7a5539c40f0195"
757
861
  },
758
862
  {
759
863
  name: "openspec-explore",
760
864
  file: "assets/skills/openspec-explore/SKILL.md",
761
865
  description: "Modo explore para OpenSpec — thinking partner para explorar ideas, investigar problemas y clarificar requisitos antes/durante un cambio",
762
- version: "0.7.4",
866
+ version: "0.8.1",
763
867
  sha256: "37ae4aaf17ea71a395bab6dc4d9d61b9a49d7910f6692535ae8553244c46880b"
764
868
  },
765
869
  {
766
870
  name: "using-git-worktrees",
767
871
  file: "assets/skills/using-git-worktrees/SKILL.md",
768
872
  description: "Skill de uso de git worktrees para aislamiento de trabajo",
769
- version: "0.7.4",
873
+ version: "0.8.1",
770
874
  sha256: "93341bc1b7c053618a8b6dc07e3615b77990d7b549a0201f5835d95fef67ce13"
771
875
  },
772
876
  {
773
877
  name: "using-superpowers",
774
878
  file: "assets/skills/using-superpowers/SKILL.md",
775
879
  description: "Skill de orquestación de Superpowers skills",
776
- version: "0.7.4",
880
+ version: "0.8.1",
777
881
  sha256: "7e54536f96d2a379185a10bfc1e970850caa2561b6d8aca7080f0defea0381a4"
778
882
  },
779
883
  {
780
884
  name: "writing-skills",
781
885
  file: "assets/skills/writing-skills/SKILL.md",
782
886
  description: "Skill de creación y edición de skills",
783
- version: "0.7.4",
887
+ version: "0.8.1",
784
888
  sha256: "3d76b906cee518a2b809febb70db95697a35b9f368986bb504b4120c3bfb437a"
785
889
  },
786
890
  {
787
891
  name: "graceful-degradation",
788
892
  file: "assets/skills/graceful-degradation/SKILL.md",
789
893
  description: "Skill de degradación graceful cuando múltiples tools están indisponibles",
790
- version: "0.7.4",
894
+ version: "0.8.1",
791
895
  sha256: "40f929e8032b70d51b3548bddcc383b931d6044fd9d4acce8a5b9f7acd014f59"
792
896
  }
793
897
  ]
@@ -829,6 +933,19 @@ var init_cache = __esm(() => {
829
933
  });
830
934
 
831
935
  // src/github.ts
936
+ var exports_github = {};
937
+ __export(exports_github, {
938
+ BUNDLED_MCP_DIR: () => BUNDLED_MCP_DIR,
939
+ BUNDLED_SKILLS_DIR: () => BUNDLED_SKILLS_DIR,
940
+ PACKAGE_ROOT: () => PACKAGE_ROOT,
941
+ downloadFile: () => downloadFile,
942
+ fetchLatestManifest: () => fetchLatestManifest,
943
+ fetchLatestReleaseTag: () => fetchLatestReleaseTag,
944
+ fetchManifest: () => fetchManifest,
945
+ getBundledMcpPath: () => getBundledMcpPath,
946
+ getBundledSkillPath: () => getBundledSkillPath,
947
+ getRawUrl: () => getRawUrl
948
+ });
832
949
  import { fileURLToPath } from "url";
833
950
  import { dirname as dirname3, join as join3 } from "path";
834
951
  function getBundledSkillPath(name) {
@@ -1143,6 +1260,12 @@ function patchOpenCodeConfig(projectRoot = findProjectRoot()) {
1143
1260
  delete config.plugin;
1144
1261
  changed = true;
1145
1262
  }
1263
+ if (config.mcp && typeof config.mcp === "object" && "context7" in config.mcp) {
1264
+ delete config.mcp.context7;
1265
+ if (Object.keys(config.mcp).length === 0)
1266
+ delete config.mcp;
1267
+ changed = true;
1268
+ }
1146
1269
  if (changed) {
1147
1270
  writeOpenCodeConfig(configPath, config);
1148
1271
  return { success: true, message: "Config actualizada (plugin legacy eliminado)" };
@@ -1162,7 +1285,6 @@ __export(exports_stack, {
1162
1285
  installCodeGraph: () => installCodeGraph,
1163
1286
  installEngram: () => installEngram,
1164
1287
  installStack: () => installStack,
1165
- setupContext7: () => setupContext7,
1166
1288
  setupOpenSpec: () => setupOpenSpec,
1167
1289
  uninstallEngramConfig: () => uninstallEngramConfig,
1168
1290
  uninstallStackConfig: () => uninstallStackConfig,
@@ -1216,6 +1338,15 @@ function copyEngramPlugin(projectRoot) {
1216
1338
  mkdirSync4(pluginsDir, { recursive: true });
1217
1339
  copyFileSync3(pluginSource, join7(pluginsDir, "engram.ts"));
1218
1340
  }
1341
+ function copyOstackyControllerPlugin(projectRoot) {
1342
+ const pluginSource = join7(PACKAGE_ROOT, "assets", "plugins", "ostacky-plugin.ts");
1343
+ const pluginsDir = join7(projectRoot, ".opencode", "plugins");
1344
+ if (!existsSync6(pluginSource)) {
1345
+ throw new Error(`Plugin bundleado de OstackyController no encontrado: ${pluginSource}`);
1346
+ }
1347
+ mkdirSync4(pluginsDir, { recursive: true });
1348
+ copyFileSync3(pluginSource, join7(pluginsDir, "ostacky-plugin.ts"));
1349
+ }
1219
1350
  function buildEngramDownloadUrl(tag, platform = process.platform, arch = process.arch) {
1220
1351
  const target = getEngramReleaseTarget(platform, arch);
1221
1352
  if (!target)
@@ -1439,6 +1570,7 @@ async function installEngram(toolsDir) {
1439
1570
  try {
1440
1571
  runTool(localBin, ["--version"], projectRoot, 1e4);
1441
1572
  copyEngramPlugin(projectRoot);
1573
+ copyOstackyControllerPlugin(projectRoot);
1442
1574
  configureLocalTool(projectRoot, "engram", buildLocalMcpCommand(localBin, ["mcp"]));
1443
1575
  } catch (error) {
1444
1576
  return failAfterExtraction(`Engram fue instalado pero no se pudo verificar o configurar: ${error.message}`);
@@ -1446,44 +1578,15 @@ async function installEngram(toolsDir) {
1446
1578
  archivePromotion?.commit();
1447
1579
  return { success: true, message: "Engram instalado localmente y configurado para OpenCode (MCP + plugin)" };
1448
1580
  }
1449
- function setupContext7(toolsDir) {
1450
- const location = resolveToolInstallLocation(toolsDir);
1451
- const { projectRoot } = location;
1452
- const ctx7ToolDir = join7(location.toolsDir, "context7");
1453
- if (!existsSync6(ctx7ToolDir))
1454
- mkdirSync4(ctx7ToolDir, { recursive: true });
1455
- try {
1456
- ensureMcpEntryAtProjectRoot(projectRoot, "context7", {
1457
- type: "remote",
1458
- url: "https://mcp.context7.com/mcp",
1459
- enabled: true
1460
- });
1461
- } catch (error) {
1462
- return { success: false, message: `No se pudo registrar Context7: ${error.message}` };
1463
- }
1464
- const useBun = isCommandAvailable("bun");
1465
- try {
1466
- const invocation = getCommandInvocation(useBun ? "bunx" : "npx", useBun ? ["ctx7", "setup", "--opencode"] : ["--yes", "ctx7", "setup", "--opencode"]);
1467
- execFileSync3(invocation.command, invocation.args, {
1468
- stdio: "pipe",
1469
- timeout: 60000,
1470
- cwd: projectRoot
1471
- });
1472
- return { success: true, message: "Context7 configurado (MCP + skill instalado)" };
1473
- } catch {
1474
- return {
1475
- success: true,
1476
- message: "Context7 MCP registrado (skill opcional no instalada — corrí `npx ctx7 setup --opencode` manualmente si la querés)"
1477
- };
1478
- }
1479
- }
1480
1581
  async function installStack(toolsDir) {
1481
1582
  const { projectRoot } = resolveToolInstallLocation(toolsDir);
1583
+ try {
1584
+ copyOstackyControllerPlugin(projectRoot);
1585
+ } catch {}
1482
1586
  return {
1483
1587
  codegraph: await installCodeGraph(toolsDir),
1484
1588
  openspec: setupOpenSpec(projectRoot),
1485
1589
  engram: await installEngram(toolsDir),
1486
- context7: setupContext7(toolsDir),
1487
1590
  config: patchOpenCodeConfig(projectRoot)
1488
1591
  };
1489
1592
  }
@@ -1518,7 +1621,7 @@ function uninstallStackConfig(paths) {
1518
1621
  const mcp = config.mcp;
1519
1622
  let changed = false;
1520
1623
  if (mcp) {
1521
- for (const name of ["codegraph", "context7", "engram"]) {
1624
+ for (const name of ["codegraph", "engram", "context7"]) {
1522
1625
  if (name in mcp) {
1523
1626
  delete mcp[name];
1524
1627
  removed.push(`mcp.${name}`);
@@ -1588,7 +1691,7 @@ var init_stack = __esm(() => {
1588
1691
  // package.json
1589
1692
  var package_default = {
1590
1693
  name: "ostacky",
1591
- version: "0.7.4",
1694
+ version: "0.8.1",
1592
1695
  description: "Instalador interactivo de agentes y comandos para OpenCode",
1593
1696
  type: "module",
1594
1697
  bin: {
@@ -2763,7 +2866,7 @@ async function probeMcpServer(nodeExecutable, serverPath, cwd, statePath, option
2763
2866
  params: {
2764
2867
  protocolVersion: "2025-03-26",
2765
2868
  capabilities: {},
2766
- clientInfo: { name: "ostacky-installer", version: "0.7.4" }
2869
+ clientInfo: { name: "ostacky-installer", version: "0.8.1" }
2767
2870
  }
2768
2871
  });
2769
2872
  });
@@ -3072,12 +3175,13 @@ function uninstallAll(paths) {
3072
3175
  for (const name of Object.keys(lockfile.mcpServers ?? {})) {
3073
3176
  uninstallMcpServer(name, paths);
3074
3177
  }
3075
- if (existsSync5(paths.mcp)) {
3076
- for (const entry of readdirSync2(paths.mcp)) {
3077
- const dirPath = join6(paths.mcp, entry);
3078
- if (statSync2(dirPath).isDirectory()) {
3079
- uninstallMcpServer(entry, paths);
3080
- }
3178
+ const ostackyPlugins = ["ostacky-plugin.ts", "engram.ts", "ostacky-guard.ts", "ostacky-controller.ts"];
3179
+ for (const file of ostackyPlugins) {
3180
+ const fp = join6(paths.plugins, file);
3181
+ if (existsSync5(fp)) {
3182
+ try {
3183
+ unlinkSync2(fp);
3184
+ } catch {}
3081
3185
  }
3082
3186
  }
3083
3187
  clearLockfile(paths.root);
@@ -3277,11 +3381,6 @@ async function doInstallStack(toolsDir, projectRoot) {
3277
3381
  spin.stop(eng.success ? `✓ ${eng.message}` : `✗ ${eng.message}`);
3278
3382
  if (!eng.success)
3279
3383
  allOk = false;
3280
- spin.start("Configurando Context7...");
3281
- const ctx = setupContext7(toolsDir);
3282
- spin.stop(ctx.success ? `✓ ${ctx.message}` : `✗ ${ctx.message}`);
3283
- if (!ctx.success)
3284
- allOk = false;
3285
3384
  spin.start("Verificando configuración...");
3286
3385
  const { patchOpenCodeConfig: patchOpenCodeConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
3287
3386
  const cfg = patchOpenCodeConfig2(resolvedProjectRoot);
@@ -3298,9 +3397,9 @@ async function doInstallAll(manifest, paths) {
3298
3397
  let errors = 0;
3299
3398
  const isGlobal = isGlobalScope(paths);
3300
3399
  if (!isGlobal) {
3301
- ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
3400
+ ensureToolDirs(paths.tools, ["codegraph", "engram"]);
3302
3401
  } else {
3303
- v2.info("Scope global detectado: el stack (CodeGraph/Engram) permanece siempre en <proyecto>/.opencode/tools — se omite instalación de stack global.");
3402
+ v2.info("Scope global detectado: el stack (CodeGraph/Engram) y plugins (ostacky-controller, engram) permanecen siempre en <proyecto>/.opencode — se omite instalación de stack global.");
3304
3403
  v2.info("Para instalar el stack, ejecutá 'npx ostacky install-stack --scope local' dentro de cada proyecto.");
3305
3404
  }
3306
3405
  for (const agent of manifest.agents) {
@@ -3347,6 +3446,49 @@ async function doInstallAll(manifest, paths) {
3347
3446
  errors++;
3348
3447
  }
3349
3448
  }
3449
+ try {
3450
+ const { copyFileSync: copyFileSync4, mkdirSync: mkdirSync5, existsSync: existsSync8, rmSync: rmSync5 } = await import("fs");
3451
+ const { join: join9 } = await import("path");
3452
+ const { PACKAGE_ROOT: PACKAGE_ROOT2 } = await Promise.resolve().then(() => (init_github(), exports_github));
3453
+ const { findProjectRoot: findProjectRoot3 } = await Promise.resolve().then(() => (init_fs(), exports_fs));
3454
+ const src = join9(PACKAGE_ROOT2, "assets", "plugins", "ostacky-plugin.ts");
3455
+ const dest = join9(paths.plugins, "ostacky-plugin.ts");
3456
+ if (existsSync8(src)) {
3457
+ mkdirSync5(paths.plugins, { recursive: true });
3458
+ copyFileSync4(src, dest);
3459
+ }
3460
+ for (const legacy of ["ostacky-guard.ts", "ostacky-controller.ts"]) {
3461
+ const lp = join9(paths.plugins, legacy);
3462
+ if (existsSync8(lp))
3463
+ try {
3464
+ rmSync5(lp, { force: true });
3465
+ } catch {}
3466
+ }
3467
+ const srcEng = join9(PACKAGE_ROOT2, "assets", "plugins", "engram.ts");
3468
+ const destEng = join9(paths.plugins, "engram.ts");
3469
+ if (existsSync8(srcEng)) {
3470
+ mkdirSync5(paths.plugins, { recursive: true });
3471
+ copyFileSync4(srcEng, destEng);
3472
+ }
3473
+ if (isGlobal) {
3474
+ try {
3475
+ const projRoot = findProjectRoot3();
3476
+ const localPlugins = join9(projRoot, ".opencode", "plugins");
3477
+ mkdirSync5(localPlugins, { recursive: true });
3478
+ if (existsSync8(src))
3479
+ copyFileSync4(src, join9(localPlugins, "ostacky-plugin.ts"));
3480
+ if (existsSync8(srcEng))
3481
+ copyFileSync4(srcEng, join9(localPlugins, "engram.ts"));
3482
+ for (const legacy of ["ostacky-guard.ts", "ostacky-controller.ts"]) {
3483
+ const lp = join9(localPlugins, legacy);
3484
+ if (existsSync8(lp))
3485
+ try {
3486
+ rmSync5(lp, { force: true });
3487
+ } catch {}
3488
+ }
3489
+ } catch {}
3490
+ }
3491
+ } catch {}
3350
3492
  let stackOk = true;
3351
3493
  let missingTools = [];
3352
3494
  if (isGlobal) {
@@ -3584,10 +3726,10 @@ async function doUninstallTotal(paths) {
3584
3726
  for (const name of Object.keys(lockfile.mcpServers ?? {})) {
3585
3727
  pathsToDelete.push(join9(paths.mcp, name));
3586
3728
  }
3587
- ye(pathsToDelete.join(`
3588
- `), `Se eliminarán ${pathsToDelete.length} archivo(s) / directorio(s)`);
3729
+ ye([`Scope: ${paths.root}`, ...pathsToDelete].join(`
3730
+ `), `Se eliminarán ${pathsToDelete.length} item(s) trackeados en lockfile (safe-delete)`);
3589
3731
  const confirm = await me({
3590
- message: `¿Confirmar desinstalación de ${pathsToDelete.length} item(s)?`
3732
+ message: `¿Confirmar desinstalación de ${pathsToDelete.length} item(s) en ${paths.root}?`
3591
3733
  });
3592
3734
  onCancel(confirm);
3593
3735
  if (!confirm) {
@@ -3595,7 +3737,7 @@ async function doUninstallTotal(paths) {
3595
3737
  return;
3596
3738
  }
3597
3739
  uninstallAll(paths);
3598
- v2.success(`${pathsToDelete.length} item(s) eliminado(s).`);
3740
+ v2.success(`${pathsToDelete.length} item(s) eliminado(s) + plugins Ostacky-owned limpiados.`);
3599
3741
  const cleanEngram = await me({
3600
3742
  message: "¿Deseas remover la configuración de Engram del proyecto (mcp.engram)?"
3601
3743
  });
@@ -3876,9 +4018,122 @@ async function doUninstallMcpByName(name, paths) {
3876
4018
  }
3877
4019
  }
3878
4020
 
4021
+ // src/opencode.ts
4022
+ init_fs();
4023
+ import { execSync, execFileSync as execFileSync4 } from "node:child_process";
4024
+ function isOpencodeInstalled() {
4025
+ if (isCommandAvailable("opencode"))
4026
+ return true;
4027
+ try {
4028
+ execFileSync4("opencode", ["--version"], { stdio: "ignore" });
4029
+ return true;
4030
+ } catch {
4031
+ return false;
4032
+ }
4033
+ }
4034
+ function getOpencodeInstallCommand(platform = process.platform, _arch = process.arch) {
4035
+ if (platform === "win32") {
4036
+ return {
4037
+ display: "npm install -g opencode-ai",
4038
+ command: "npm",
4039
+ args: ["install", "-g", "opencode-ai"],
4040
+ note: "Alternativas Windows: choco install opencode | scoop install opencode | mise use -g github:anomalyco/opencode (ver https://opencode.ai/download). WSL recomendado."
4041
+ };
4042
+ }
4043
+ if (platform === "darwin") {
4044
+ return {
4045
+ display: "curl -fsSL https://opencode.ai/install | bash",
4046
+ command: "bash",
4047
+ args: ["-c", "curl -fsSL https://opencode.ai/install | bash"],
4048
+ note: "Alternativa macOS: brew install anomalyco/tap/opencode (tap oficial, más actualizado que brew install opencode)"
4049
+ };
4050
+ }
4051
+ return {
4052
+ display: "curl -fsSL https://opencode.ai/install | bash",
4053
+ command: "bash",
4054
+ args: ["-c", "curl -fsSL https://opencode.ai/install | bash"],
4055
+ note: "Alternativas Linux: npm install -g opencode-ai | bun add -g opencode-ai | brew install anomalyco/tap/opencode | paru -S opencode (Arch)"
4056
+ };
4057
+ }
4058
+ async function ensureOpencodeInstalled() {
4059
+ if (isOpencodeInstalled())
4060
+ return;
4061
+ const platform = process.platform;
4062
+ const info = getOpencodeInstallCommand(platform);
4063
+ v2.warn("OpenCode no detectado en este sistema.");
4064
+ ye(`${info.display}
4065
+ Docs: https://opencode.ai/download${info.note ? `
4066
+ ${info.note}` : ""}`, `Instalación requerida (${platform})`);
4067
+ const shouldInstall = await me({
4068
+ message: "Ostacky no funciona sin OpenCode. ¿Querés instalar OpenCode ahora?",
4069
+ initialValue: true
4070
+ });
4071
+ if (BD(shouldInstall) || !shouldInstall) {
4072
+ fe("Instalación cancelada. Instalá OpenCode manualmente y volvé a ejecutar: https://opencode.ai/download");
4073
+ process.exit(1);
4074
+ }
4075
+ const spin = L2();
4076
+ spin.start(`Instalando OpenCode (${info.display})...`);
4077
+ try {
4078
+ if (platform === "win32") {
4079
+ execSync("npm install -g opencode-ai", { stdio: "inherit", shell: "cmd.exe" });
4080
+ } else {
4081
+ if (!isCommandAvailable("curl")) {
4082
+ spin.stop("curl no disponible");
4083
+ v2.warn("curl no está instalado — se intentará con npm como fallback.");
4084
+ execSync("npm install -g opencode-ai", { stdio: "inherit", shell: "/bin/bash" });
4085
+ } else {
4086
+ execSync("curl -fsSL https://opencode.ai/install | bash", { stdio: "inherit", shell: "/bin/bash" });
4087
+ }
4088
+ }
4089
+ spin.stop("Instalación ejecutada, verificando...");
4090
+ } catch (e2) {
4091
+ spin.stop("Fallo la instalación automática.");
4092
+ const msg = e2.message ?? String(e2);
4093
+ v2.error(`No se pudo instalar OpenCode automáticamente: ${msg}`);
4094
+ if (platform === "win32") {
4095
+ ye([
4096
+ "Intentá manualmente una de estas opciones:",
4097
+ " npm install -g opencode-ai",
4098
+ " choco install opencode",
4099
+ " scoop install opencode",
4100
+ " (WSL) curl -fsSL https://opencode.ai/install | bash",
4101
+ "Luego verificá: opencode --version",
4102
+ "Docs: https://opencode.ai/download"
4103
+ ].join(`
4104
+ `), "Acción manual requerida");
4105
+ } else {
4106
+ ye([
4107
+ "Intentá manualmente:",
4108
+ ` ${info.display}`,
4109
+ " npm install -g opencode-ai (si no tenés curl)",
4110
+ " brew install anomalyco/tap/opencode (macOS/Linux con brew)",
4111
+ "Luego verificá: opencode --version",
4112
+ "Docs: https://opencode.ai/download"
4113
+ ].join(`
4114
+ `), "Acción manual requerida");
4115
+ }
4116
+ process.exit(1);
4117
+ }
4118
+ if (!isOpencodeInstalled()) {
4119
+ v2.error("OpenCode aún no está disponible en el PATH después de la instalación.");
4120
+ ye([
4121
+ "Probá cerrar y reabrir la terminal y ejecutar:",
4122
+ " opencode --version",
4123
+ "Si persiste, instalá manualmente:",
4124
+ ` ${info.display}`,
4125
+ "Docs: https://opencode.ai/download"
4126
+ ].join(`
4127
+ `), "Verificación falló");
4128
+ process.exit(1);
4129
+ }
4130
+ v2.success("OpenCode instalado correctamente ✓");
4131
+ }
4132
+
3879
4133
  // src/prompts/index.ts
3880
4134
  async function runInteractiveMenu(scope) {
3881
4135
  we(" OpenCode Installer ");
4136
+ await ensureOpencodeInstalled();
3882
4137
  const manifest = await loadManifest();
3883
4138
  const paths = await resolveOpenCodePaths(scope ?? null);
3884
4139
  if (!paths) {
@@ -3893,7 +4148,7 @@ async function runInteractiveMenu(scope) {
3893
4148
  { value: "command", label: "Instalar command" },
3894
4149
  { value: "skill", label: "Instalar skill" },
3895
4150
  { value: "mcp", label: "Instalar MCP server" },
3896
- { value: "stack", label: "Instalar stack de herramientas (CodeGraph, Engram, Context7)" },
4151
+ { value: "stack", label: "Instalar stack de herramientas (CodeGraph, Engram)" },
3897
4152
  { value: "update", label: "Actualizar instalación" },
3898
4153
  { value: "uninstall", label: "Desinstalar" },
3899
4154
  { value: "exit", label: "Salir" }
@@ -3934,7 +4189,7 @@ async function runInteractiveMenu(scope) {
3934
4189
  fe("Cancelado.");
3935
4190
  break;
3936
4191
  }
3937
- ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
4192
+ ensureToolDirs(paths.tools, ["codegraph", "engram"]);
3938
4193
  const stackOk = await doInstallStack(paths.tools, dirname7(paths.root));
3939
4194
  if (!stackOk)
3940
4195
  process.exitCode = 1;
@@ -3958,6 +4213,7 @@ async function runInteractiveMenu(scope) {
3958
4213
  }
3959
4214
  async function runInstallCommand(scope) {
3960
4215
  we(" OpenCode Installer ");
4216
+ await ensureOpencodeInstalled();
3961
4217
  const manifest = await loadManifest();
3962
4218
  const paths = await resolveOpenCodePaths(scope ?? null);
3963
4219
  if (!paths) {
@@ -3971,6 +4227,7 @@ async function runInstallCommand(scope) {
3971
4227
  }
3972
4228
  async function runAddAgentCommand(scope) {
3973
4229
  we(" OpenCode Installer ");
4230
+ await ensureOpencodeInstalled();
3974
4231
  const manifest = await loadManifest();
3975
4232
  const paths = await resolveOpenCodePaths(scope ?? null);
3976
4233
  if (!paths) {
@@ -3983,6 +4240,7 @@ async function runAddAgentCommand(scope) {
3983
4240
  }
3984
4241
  async function runAddCommandCommand(scope) {
3985
4242
  we(" OpenCode Installer ");
4243
+ await ensureOpencodeInstalled();
3986
4244
  const manifest = await loadManifest();
3987
4245
  const paths = await resolveOpenCodePaths(scope ?? null);
3988
4246
  if (!paths) {
@@ -3995,6 +4253,7 @@ async function runAddCommandCommand(scope) {
3995
4253
  }
3996
4254
  async function runAddSkillCommand(scope) {
3997
4255
  we(" OpenCode Installer ");
4256
+ await ensureOpencodeInstalled();
3998
4257
  const manifest = await loadManifest();
3999
4258
  const paths = await resolveOpenCodePaths(scope ?? null);
4000
4259
  if (!paths) {
@@ -4007,6 +4266,7 @@ async function runAddSkillCommand(scope) {
4007
4266
  }
4008
4267
  async function runAddMcpCommand(scope) {
4009
4268
  we(" OpenCode Installer ");
4269
+ await ensureOpencodeInstalled();
4010
4270
  const manifest = await loadManifest();
4011
4271
  const paths = await resolveOpenCodePaths(scope ?? null);
4012
4272
  if (!paths) {
@@ -4018,6 +4278,7 @@ async function runAddMcpCommand(scope) {
4018
4278
  fe("Listo.");
4019
4279
  }
4020
4280
  async function runInstallStackCommand(scope) {
4281
+ await ensureOpencodeInstalled();
4021
4282
  if (scope === "global") {
4022
4283
  v2.error("install-stack requiere scope local; el stack vive en <proyecto>/.opencode/tools");
4023
4284
  fe("Usá: npx ostacky install-stack --scope local");
@@ -4037,7 +4298,7 @@ async function runInstallStackCommand(scope) {
4037
4298
  process.exitCode = 1;
4038
4299
  return;
4039
4300
  }
4040
- ensureToolDirs(paths.tools, ["codegraph", "engram", "context7"]);
4301
+ ensureToolDirs(paths.tools, ["codegraph", "engram"]);
4041
4302
  const stackOk = await doInstallStack(paths.tools, dirname7(paths.root));
4042
4303
  if (!stackOk)
4043
4304
  process.exitCode = 1;
@@ -4051,7 +4312,7 @@ async function runUninstallStackCommand(scope) {
4051
4312
  return;
4052
4313
  }
4053
4314
  const confirm = await me({
4054
- message: "¿Remover la configuración del stack (CodeGraph, Engram, Context7) del proyecto? Los binarios globales no se tocan."
4315
+ message: "¿Remover la configuración del stack (CodeGraph, Engram) del proyecto? Los binarios globales no se tocan."
4055
4316
  });
4056
4317
  onCancel(confirm);
4057
4318
  if (!confirm) {
@@ -4069,6 +4330,7 @@ async function runUninstallStackCommand(scope) {
4069
4330
  }
4070
4331
  async function runUpdateCommand(scope) {
4071
4332
  we(" OpenCode Installer ");
4333
+ await ensureOpencodeInstalled();
4072
4334
  const manifest = await loadLatestManifest();
4073
4335
  const paths = await resolveOpenCodePaths(scope ?? null);
4074
4336
  if (!paths) {
@@ -4232,18 +4494,18 @@ async function runUninstallMcpCommand(name, scope) {
4232
4494
  // src/cli.ts
4233
4495
  init_fs();
4234
4496
  import { existsSync as existsSync8, statSync as statSync3, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "node:fs";
4235
- import { join as join11, dirname as dirname8 } from "node:path";
4497
+ import { join as join10, dirname as dirname8 } from "node:path";
4236
4498
  var HELP = `
4237
4499
  ostacky — Instalador de agentes, comandos, skills y MCPs para OpenCode
4238
4500
 
4239
4501
  Uso:
4240
4502
  npx ostacky [--scope local|global|auto] Menú interactivo (instalación completa, pregunta local vs global, default local)
4241
- npx ostacky install [--scope local|global|auto] Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram + Context7)
4503
+ npx ostacky install [--scope local|global|auto] Instalar TODO (agente + skills + MCPs + CodeGraph + OpenSpec + Engram)
4242
4504
  npx ostacky add agent [--scope local|global|auto] Agregar agente(s)
4243
4505
  npx ostacky add command [--scope ...] Agregar command(s)
4244
4506
  npx ostacky add skill [--scope ...] Agregar skill(s)
4245
4507
  npx ostacky add mcp [--scope ...] Agregar MCP server(s)
4246
- npx ostacky install-stack [--scope local|auto] Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram, Context7) — global bloquea con error
4508
+ npx ostacky install-stack [--scope local|auto] Instalar solo el stack de herramientas (CodeGraph, OpenSpec, Engram) — global bloquea con error
4247
4509
  npx ostacky uninstall-stack [--scope local|global|auto] Remover la configuración del stack del proyecto
4248
4510
  npx ostacky doctor Diagnostica locks, tools, state health
4249
4511
  npx ostacky status [--json] Muestra estado del controller sin MCP
@@ -4262,7 +4524,7 @@ Scope:
4262
4524
  --scope auto Elige local si existe .opencode o .git, si no global
4263
4525
  Sin flag Pregunta interactiva local (default) vs global
4264
4526
  `.trim();
4265
- function parseScopeArg(argv = process.argv) {
4527
+ function parseScopeArg2(argv = process.argv) {
4266
4528
  for (let i = 0;i < argv.length; i++) {
4267
4529
  const arg = argv[i];
4268
4530
  if (arg === "--scope" && i + 1 < argv.length) {
@@ -4292,13 +4554,13 @@ function withoutScopeArgs(argv) {
4292
4554
  }
4293
4555
  return out;
4294
4556
  }
4295
- var scope = parseScopeArg();
4557
+ var scope = parseScopeArg2();
4296
4558
  var argvNoScope = withoutScopeArgs(process.argv);
4297
4559
  var [, , cmd, subcmd] = argvNoScope;
4298
4560
  async function runDoctorCommand() {
4299
4561
  const cwd = process.cwd();
4300
- const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4301
- const statePath = join11(opencodeDir, "ostacky-state.json");
4562
+ const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4563
+ const statePath = join10(opencodeDir, "ostacky-state.json");
4302
4564
  let hasError = false;
4303
4565
  let hasWarn = false;
4304
4566
  const check = (label, ok, warn = false) => {
@@ -4312,14 +4574,30 @@ async function runDoctorCommand() {
4312
4574
  hasError = true;
4313
4575
  }
4314
4576
  };
4577
+ const pluginPaths = [
4578
+ join10(cwd, "assets", "plugins", "ostacky-plugin.ts"),
4579
+ join10(opencodeDir, "plugins", "ostacky-plugin.ts"),
4580
+ join10(cwd, ".opencode", "plugins", "ostacky-plugin.ts"),
4581
+ join10(cwd, "assets", "plugins", "ostacky-controller.ts"),
4582
+ join10(opencodeDir, "plugins", "ostacky-controller.ts"),
4583
+ join10(cwd, ".opencode", "plugins", "ostacky-controller.ts")
4584
+ ];
4585
+ const pluginActive = pluginPaths.some((p2) => existsSync8(p2));
4315
4586
  try {
4316
4587
  if (!existsSync8(statePath)) {
4317
- check("controller: state file missing", false, true);
4588
+ if (pluginActive)
4589
+ console.log(`✅ controller: plugin active (no state yet)`);
4590
+ else
4591
+ check("controller: state file missing", false, true);
4318
4592
  } else {
4319
4593
  const stat = statSync3(statePath);
4320
4594
  const raw = readFileSync6(statePath, "utf-8");
4321
4595
  const parsed = JSON.parse(raw);
4322
- check(`controller: OK (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`, true);
4596
+ if (pluginActive) {
4597
+ console.log(`✅ controller: plugin active (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`);
4598
+ } else {
4599
+ check(`controller: OK (rev ${parsed.revision || 0} state ${parsed.state || "unknown"})`, true);
4600
+ }
4323
4601
  if (parsed.degraded) {
4324
4602
  console.log("⚠️ degraded: true (persistido)");
4325
4603
  hasWarn = true;
@@ -4375,8 +4653,8 @@ async function runDoctorCommand() {
4375
4653
  check(`controller: ${e2.message}`, false);
4376
4654
  }
4377
4655
  try {
4378
- const lockPid = join11(opencodeDir, "ostacky-state.json.lock.pid");
4379
- const lockTs = join11(opencodeDir, "ostacky-state.json.lock.timestamp");
4656
+ const lockPid = join10(opencodeDir, "ostacky-state.json.lock.pid");
4657
+ const lockTs = join10(opencodeDir, "ostacky-state.json.lock.timestamp");
4380
4658
  if (existsSync8(lockPid) || existsSync8(lockTs)) {
4381
4659
  let ageStr = "";
4382
4660
  try {
@@ -4401,15 +4679,15 @@ async function runDoctorCommand() {
4401
4679
  }
4402
4680
  const tools = ["codegraph", "engram"];
4403
4681
  for (const t of tools) {
4404
- const p2 = join11(opencodeDir, "tools", t, "bin", t);
4682
+ const p2 = join10(opencodeDir, "tools", t, "bin", t);
4405
4683
  const pExe = p2 + ".exe";
4406
4684
  check(`tool ${t}: ${existsSync8(p2) || existsSync8(pExe) ? "found" : "missing"}`, existsSync8(p2) || existsSync8(pExe), true);
4407
4685
  }
4408
4686
  try {
4409
- const manifest = JSON.parse(readFileSync6(join11(cwd, "manifest.json"), "utf-8"));
4687
+ const manifest = JSON.parse(readFileSync6(join10(cwd, "manifest.json"), "utf-8"));
4410
4688
  const expected = manifest.mcpServers?.find((x2) => x2.name === "ostacky-controller")?.sha256;
4411
4689
  if (expected) {
4412
- const actual = computeTreeHash(join11(cwd, "assets", "mcp", "ostacky-controller"));
4690
+ const actual = computeTreeHash(join10(cwd, "assets", "mcp", "ostacky-controller"));
4413
4691
  check(`manifest hash: ${expected.slice(0, 8)} vs actual ${actual.slice(0, 8)}`, expected === actual);
4414
4692
  if (expected !== actual)
4415
4693
  console.log(" Run: bun run hash:update");
@@ -4424,7 +4702,7 @@ async function runDoctorCommand() {
4424
4702
  } catch {}
4425
4703
  }
4426
4704
  try {
4427
- const cacheDir = join11(opencodeDir, "cache", "codegraph");
4705
+ const cacheDir = join10(opencodeDir, "cache", "codegraph");
4428
4706
  if (!existsSync8(cacheDir)) {
4429
4707
  console.log("ℹ️ cache: no cache dir yet (ok)");
4430
4708
  } else {
@@ -4432,7 +4710,7 @@ async function runDoctorCommand() {
4432
4710
  let total = 0;
4433
4711
  for (const f2 of files) {
4434
4712
  try {
4435
- total += statSync3(join11(cacheDir, f2)).size;
4713
+ total += statSync3(join10(cacheDir, f2)).size;
4436
4714
  } catch {}
4437
4715
  }
4438
4716
  const totalMB = (total / 1048576).toFixed(2);
@@ -4453,7 +4731,7 @@ async function runDoctorCommand() {
4453
4731
  console.log(`⚠️ cache: check failed ${e2.message}`);
4454
4732
  }
4455
4733
  try {
4456
- const secPath = join11(cwd, "src", "security.ts");
4734
+ const secPath = join10(cwd, "src", "security.ts");
4457
4735
  if (!existsSync8(secPath)) {
4458
4736
  console.log("⚠️ src/security.ts: missing (source-of-truth)");
4459
4737
  hasWarn = true;
@@ -4485,8 +4763,8 @@ async function runDoctorCommand() {
4485
4763
  async function runStatusCommand(args) {
4486
4764
  const isJson = args.includes("--json");
4487
4765
  const cwd = process.cwd();
4488
- const opencodeDir = findOpenCodeDir(cwd) || join11(cwd, ".opencode");
4489
- const statePath = join11(opencodeDir, "ostacky-state.json");
4766
+ const opencodeDir = findOpenCodeDir(cwd) || join10(cwd, ".opencode");
4767
+ const statePath = join10(opencodeDir, "ostacky-state.json");
4490
4768
  if (!existsSync8(statePath)) {
4491
4769
  console.log(isJson ? JSON.stringify({ error: "no state" }) : "No state file");
4492
4770
  return;
@@ -4509,6 +4787,10 @@ async function runStatusCommand(args) {
4509
4787
  }
4510
4788
  }
4511
4789
  async function main() {
4790
+ const needsOpencode = !cmd || cmd === "install" || cmd === "install-stack" || cmd === "update" || cmd === "add" && ["agent", "command", "skill", "mcp"].includes(subcmd ?? "");
4791
+ if (needsOpencode) {
4792
+ await ensureOpencodeInstalled();
4793
+ }
4512
4794
  switch (cmd) {
4513
4795
  case "install":
4514
4796
  await runInstallCommand(scope);