syntaur 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -83,6 +83,31 @@ var init_fs = __esm({
83
83
  }
84
84
  });
85
85
 
86
+ // src/templates/config.ts
87
+ function renderConfig(params) {
88
+ return `---
89
+ version: "1.0"
90
+ defaultMissionDir: ${params.defaultMissionDir}
91
+ agentDefaults:
92
+ trustLevel: medium
93
+ autoApprove: false
94
+ sync:
95
+ enabled: false
96
+ endpoint: null
97
+ interval: 300
98
+ ---
99
+
100
+ # Syntaur Configuration
101
+
102
+ Global configuration for the Syntaur CLI.
103
+ `;
104
+ }
105
+ var init_config = __esm({
106
+ "src/templates/config.ts"() {
107
+ "use strict";
108
+ }
109
+ });
110
+
86
111
  // src/dashboard/parser.ts
87
112
  function extractFrontmatter(fileContent) {
88
113
  const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
@@ -469,6 +494,58 @@ function serializeStatusConfig(statuses) {
469
494
  }
470
495
  return lines.join("\n");
471
496
  }
497
+ function serializeIntegrationConfig(integrations) {
498
+ const lines = [];
499
+ if (integrations.claudePluginDir) {
500
+ lines.push(` claudePluginDir: ${integrations.claudePluginDir}`);
501
+ }
502
+ if (integrations.codexPluginDir) {
503
+ lines.push(` codexPluginDir: ${integrations.codexPluginDir}`);
504
+ }
505
+ if (integrations.codexMarketplacePath) {
506
+ lines.push(` codexMarketplacePath: ${integrations.codexMarketplacePath}`);
507
+ }
508
+ if (lines.length === 0) {
509
+ return null;
510
+ }
511
+ return ["integrations:", ...lines].join("\n");
512
+ }
513
+ function stripTopLevelBlock(fmBlock, key) {
514
+ const blockStart = fmBlock.match(new RegExp(`^${key}:\\s*$`, "m"));
515
+ if (!blockStart) {
516
+ return fmBlock.replace(/\n+$/, "");
517
+ }
518
+ const startIdx = fmBlock.indexOf(blockStart[0]);
519
+ const before = fmBlock.slice(0, startIdx);
520
+ const after = fmBlock.slice(startIdx + blockStart[0].length);
521
+ const remaining = after.split("\n");
522
+ let endIdx = 0;
523
+ for (let i = 0; i < remaining.length; i++) {
524
+ const line = remaining[i];
525
+ if (line.trim() === "") {
526
+ endIdx = i + 1;
527
+ continue;
528
+ }
529
+ if (line.length > 0 && line[0] !== " ") {
530
+ break;
531
+ }
532
+ endIdx = i + 1;
533
+ }
534
+ return (before + remaining.slice(endIdx).join("\n")).replace(/\n+$/, "");
535
+ }
536
+ function parseOptionalAbsolutePath(value, fieldName) {
537
+ if (!value) {
538
+ return null;
539
+ }
540
+ const expanded = expandHome(String(value));
541
+ if (!isAbsolute(expanded)) {
542
+ console.warn(
543
+ `Warning: config.md ${fieldName} is not an absolute path ("${value}"), ignoring it`
544
+ );
545
+ return null;
546
+ }
547
+ return resolve4(expanded);
548
+ }
472
549
  async function writeStatusConfig(statuses) {
473
550
  const configPath = resolve4(syntaurRoot(), "config.md");
474
551
  const statusBlock = serializeStatusConfig(statuses);
@@ -531,28 +608,42 @@ async function deleteStatusConfig() {
531
608
  if (!fmMatch) return;
532
609
  const fmBlock = fmMatch[2];
533
610
  const afterFrontmatter = existing.slice(fmMatch[0].length);
534
- const statusesStart = fmBlock.match(/^statuses:\s*$/m);
535
- if (!statusesStart) return;
536
- const startIdx = fmBlock.indexOf(statusesStart[0]);
537
- const before = fmBlock.slice(0, startIdx);
538
- const after = fmBlock.slice(startIdx + statusesStart[0].length);
539
- const remaining = after.split("\n");
540
- let endIdx = 0;
541
- for (let i = 0; i < remaining.length; i++) {
542
- const line = remaining[i];
543
- if (line.trim() === "") {
544
- endIdx = i + 1;
545
- continue;
546
- }
547
- if (line.length > 0 && line[0] !== " ") break;
548
- endIdx = i + 1;
549
- }
550
- const cleanedFm = (before + remaining.slice(endIdx).join("\n")).replace(/\n+$/, "");
611
+ const cleanedFm = stripTopLevelBlock(fmBlock, "statuses");
551
612
  const newContent = `---
552
613
  ${cleanedFm}
553
614
  ---${afterFrontmatter}`;
554
615
  await writeFileForce(configPath, newContent);
555
616
  }
617
+ async function updateIntegrationConfig(integrations) {
618
+ const configPath = resolve4(syntaurRoot(), "config.md");
619
+ const nextIntegrations = {
620
+ ...(await readConfig()).integrations,
621
+ ...integrations
622
+ };
623
+ const integrationBlock = serializeIntegrationConfig(nextIntegrations);
624
+ const existing = await fileExists(configPath) ? await readFile3(configPath, "utf-8") : renderConfig({ defaultMissionDir: defaultMissionDir() });
625
+ const fmMatch = existing.match(/^(---\n)([\s\S]*?)\n(---)/);
626
+ if (!fmMatch) {
627
+ const content = `---
628
+ version: "1.0"
629
+ defaultMissionDir: ${defaultMissionDir()}
630
+ ${integrationBlock ?? ""}
631
+ ---
632
+ ${existing}`;
633
+ await writeFileForce(configPath, content.replace(/\n\n---/, "\n---"));
634
+ return;
635
+ }
636
+ const fmBlock = fmMatch[2];
637
+ const afterFrontmatter = existing.slice(fmMatch[0].length);
638
+ const cleanedFm = stripTopLevelBlock(fmBlock, "integrations");
639
+ const newFm = integrationBlock ? `${cleanedFm}
640
+ ${integrationBlock}`.replace(/^\n+/, "") : cleanedFm;
641
+ const normalizedFm = newFm.replace(/\n+$/, "");
642
+ const newContent = `---
643
+ ${normalizedFm}
644
+ ---${afterFrontmatter}`;
645
+ await writeFileForce(configPath, newContent);
646
+ }
556
647
  async function readConfig() {
557
648
  const configPath = resolve4(syntaurRoot(), "config.md");
558
649
  if (!await fileExists(configPath)) {
@@ -578,15 +669,30 @@ async function readConfig() {
578
669
  trustLevel: fm["agentDefaults.trustLevel"] || DEFAULT_CONFIG.agentDefaults.trustLevel,
579
670
  autoApprove: fm["agentDefaults.autoApprove"] === "true" || DEFAULT_CONFIG.agentDefaults.autoApprove
580
671
  },
672
+ integrations: {
673
+ claudePluginDir: parseOptionalAbsolutePath(
674
+ fm["integrations.claudePluginDir"],
675
+ "integrations.claudePluginDir"
676
+ ),
677
+ codexPluginDir: parseOptionalAbsolutePath(
678
+ fm["integrations.codexPluginDir"],
679
+ "integrations.codexPluginDir"
680
+ ),
681
+ codexMarketplacePath: parseOptionalAbsolutePath(
682
+ fm["integrations.codexMarketplacePath"],
683
+ "integrations.codexMarketplacePath"
684
+ )
685
+ },
581
686
  statuses: parseStatusConfig(content)
582
687
  };
583
688
  }
584
689
  var DEFAULT_CONFIG;
585
- var init_config = __esm({
690
+ var init_config2 = __esm({
586
691
  "src/utils/config.ts"() {
587
692
  "use strict";
588
693
  init_paths();
589
694
  init_fs();
695
+ init_config();
590
696
  DEFAULT_CONFIG = {
591
697
  version: "1.0",
592
698
  defaultMissionDir: defaultMissionDir(),
@@ -594,6 +700,11 @@ var init_config = __esm({
594
700
  trustLevel: "medium",
595
701
  autoApprove: false
596
702
  },
703
+ integrations: {
704
+ claudePluginDir: null,
705
+ codexPluginDir: null,
706
+ codexMarketplacePath: null
707
+ },
597
708
  statuses: null
598
709
  };
599
710
  }
@@ -1222,13 +1333,13 @@ var init_help = __esm({
1222
1333
  // --- Plugin & adapter setup (indices 13-16) ---
1223
1334
  {
1224
1335
  command: "syntaur install-plugin",
1225
- description: "Install the Syntaur Claude Code plugin into your home plugin directory.",
1226
- example: "syntaur install-plugin"
1336
+ description: "Install the Syntaur Claude Code plugin, detecting the local Claude marketplace when available and prompting for the target directory when interactive.",
1337
+ example: "syntaur install-plugin --target-dir ~/.claude/plugins/marketplaces/user-plugins/plugins/syntaur"
1227
1338
  },
1228
1339
  {
1229
1340
  command: "syntaur install-codex-plugin",
1230
- description: "Install the Syntaur Codex plugin and register its local marketplace entry.",
1231
- example: "syntaur install-codex-plugin"
1341
+ description: "Install the Syntaur Codex plugin and register its marketplace entry, prompting for both paths when interactive.",
1342
+ example: "syntaur install-codex-plugin --target-dir ~/plugins/syntaur --marketplace-path ~/.agents/plugins/marketplace.json"
1232
1343
  },
1233
1344
  {
1234
1345
  command: "syntaur uninstall",
@@ -2684,7 +2795,7 @@ var init_api = __esm({
2684
2795
  "use strict";
2685
2796
  init_lifecycle();
2686
2797
  init_fs();
2687
- init_config();
2798
+ init_config2();
2688
2799
  init_parser();
2689
2800
  init_help();
2690
2801
  STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -3606,30 +3717,11 @@ import { Command as Command2 } from "commander";
3606
3717
  // src/commands/init.ts
3607
3718
  init_paths();
3608
3719
  init_fs();
3720
+ init_config();
3609
3721
  import { resolve as resolve3, dirname as dirname2 } from "path";
3610
3722
  import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
3611
3723
  import { fileURLToPath } from "url";
3612
3724
 
3613
- // src/templates/config.ts
3614
- function renderConfig(params) {
3615
- return `---
3616
- version: "1.0"
3617
- defaultMissionDir: ${params.defaultMissionDir}
3618
- agentDefaults:
3619
- trustLevel: medium
3620
- autoApprove: false
3621
- sync:
3622
- enabled: false
3623
- endpoint: null
3624
- interval: 300
3625
- ---
3626
-
3627
- # Syntaur Configuration
3628
-
3629
- Global configuration for the Syntaur CLI.
3630
- `;
3631
- }
3632
-
3633
3725
  // src/utils/playbooks.ts
3634
3726
  init_fs();
3635
3727
  init_parser();
@@ -3752,6 +3844,9 @@ function generateId() {
3752
3844
  // src/commands/create-mission.ts
3753
3845
  init_paths();
3754
3846
  init_fs();
3847
+ init_config2();
3848
+
3849
+ // src/templates/index.ts
3755
3850
  init_config();
3756
3851
 
3757
3852
  // src/templates/manifest.ts
@@ -4527,7 +4622,7 @@ import { resolve as resolve6 } from "path";
4527
4622
  init_timestamp();
4528
4623
  init_paths();
4529
4624
  init_fs();
4530
- init_config();
4625
+ init_config2();
4531
4626
  async function createAssignmentCommand(title, options) {
4532
4627
  if (!title.trim()) {
4533
4628
  throw new Error("Assignment title cannot be empty.");
@@ -4676,7 +4771,7 @@ Use --slug to specify a different slug.`
4676
4771
  }
4677
4772
 
4678
4773
  // src/commands/dashboard.ts
4679
- init_config();
4774
+ init_config2();
4680
4775
  import { spawn } from "child_process";
4681
4776
  import { resolve as resolve18, dirname as dirname5 } from "path";
4682
4777
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -4836,7 +4931,7 @@ function createWatcher(options) {
4836
4931
 
4837
4932
  // src/dashboard/server.ts
4838
4933
  init_fs();
4839
- init_config();
4934
+ init_config2();
4840
4935
 
4841
4936
  // src/dashboard/api-write.ts
4842
4937
  init_lifecycle();
@@ -7212,7 +7307,7 @@ async function dashboardCommand(options) {
7212
7307
  // src/commands/assign.ts
7213
7308
  init_paths();
7214
7309
  init_fs();
7215
- init_config();
7310
+ init_config2();
7216
7311
  import { resolve as resolve19 } from "path";
7217
7312
  init_lifecycle();
7218
7313
  async function assignCommand(assignment, options) {
@@ -7251,7 +7346,7 @@ async function assignCommand(assignment, options) {
7251
7346
  // src/commands/start.ts
7252
7347
  init_paths();
7253
7348
  init_fs();
7254
- init_config();
7349
+ init_config2();
7255
7350
  import { resolve as resolve20 } from "path";
7256
7351
  init_lifecycle();
7257
7352
  async function startCommand(assignment, options) {
@@ -7294,7 +7389,7 @@ async function startCommand(assignment, options) {
7294
7389
  // src/commands/complete.ts
7295
7390
  init_paths();
7296
7391
  init_fs();
7297
- init_config();
7392
+ init_config2();
7298
7393
  import { resolve as resolve21 } from "path";
7299
7394
  init_lifecycle();
7300
7395
  async function completeCommand(assignment, options) {
@@ -7330,7 +7425,7 @@ async function completeCommand(assignment, options) {
7330
7425
  // src/commands/block.ts
7331
7426
  init_paths();
7332
7427
  init_fs();
7333
- init_config();
7428
+ init_config2();
7334
7429
  import { resolve as resolve22 } from "path";
7335
7430
  init_lifecycle();
7336
7431
  async function blockCommand(assignment, options) {
@@ -7368,7 +7463,7 @@ async function blockCommand(assignment, options) {
7368
7463
  // src/commands/unblock.ts
7369
7464
  init_paths();
7370
7465
  init_fs();
7371
- init_config();
7466
+ init_config2();
7372
7467
  import { resolve as resolve23 } from "path";
7373
7468
  init_lifecycle();
7374
7469
  async function unblockCommand(assignment, options) {
@@ -7404,7 +7499,7 @@ async function unblockCommand(assignment, options) {
7404
7499
  // src/commands/review.ts
7405
7500
  init_paths();
7406
7501
  init_fs();
7407
- init_config();
7502
+ init_config2();
7408
7503
  import { resolve as resolve24 } from "path";
7409
7504
  init_lifecycle();
7410
7505
  async function reviewCommand(assignment, options) {
@@ -7440,7 +7535,7 @@ async function reviewCommand(assignment, options) {
7440
7535
  // src/commands/fail.ts
7441
7536
  init_paths();
7442
7537
  init_fs();
7443
- init_config();
7538
+ init_config2();
7444
7539
  import { resolve as resolve25 } from "path";
7445
7540
  init_lifecycle();
7446
7541
  async function failCommand(assignment, options) {
@@ -7476,7 +7571,7 @@ async function failCommand(assignment, options) {
7476
7571
  // src/commands/reopen.ts
7477
7572
  init_paths();
7478
7573
  init_fs();
7479
- init_config();
7574
+ init_config2();
7480
7575
  import { resolve as resolve26 } from "path";
7481
7576
  init_lifecycle();
7482
7577
  async function reopenCommand(assignment, options) {
@@ -7509,10 +7604,15 @@ async function reopenCommand(assignment, options) {
7509
7604
  console.log(result.message);
7510
7605
  }
7511
7606
 
7607
+ // src/commands/install-plugin.ts
7608
+ init_config2();
7609
+
7512
7610
  // src/utils/install.ts
7611
+ init_config2();
7513
7612
  init_fs();
7514
7613
  import {
7515
7614
  cp,
7615
+ readdir as readdir8,
7516
7616
  symlink,
7517
7617
  lstat,
7518
7618
  readFile as readFile12,
@@ -7523,7 +7623,7 @@ import {
7523
7623
  } from "fs/promises";
7524
7624
  import { existsSync } from "fs";
7525
7625
  import { homedir as homedir3 } from "os";
7526
- import { basename, dirname as dirname7, resolve as resolve28 } from "path";
7626
+ import { basename, dirname as dirname7, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve28 } from "path";
7527
7627
 
7528
7628
  // src/utils/package-root.ts
7529
7629
  init_fs();
@@ -7555,10 +7655,22 @@ function getPluginRelativePath(pluginKind) {
7555
7655
  function getPluginManifestRelativePath(pluginKind) {
7556
7656
  return pluginKind === "claude" ? ".claude-plugin/plugin.json" : ".codex-plugin/plugin.json";
7557
7657
  }
7558
- function getPluginTargetDir(pluginKind) {
7658
+ function getDefaultPluginTargetDir(pluginKind) {
7559
7659
  const home = homedir3();
7560
7660
  return pluginKind === "claude" ? resolve28(home, ".claude", "plugins", "syntaur") : resolve28(home, "plugins", "syntaur");
7561
7661
  }
7662
+ function getDefaultMarketplacePath() {
7663
+ return resolve28(homedir3(), ".agents", "plugins", "marketplace.json");
7664
+ }
7665
+ function getClaudeMarketplacesRoot() {
7666
+ return resolve28(homedir3(), ".claude", "plugins", "marketplaces");
7667
+ }
7668
+ function getClaudeKnownMarketplacesPath() {
7669
+ return resolve28(homedir3(), ".claude", "plugins", "known_marketplaces.json");
7670
+ }
7671
+ function getClaudeInstalledPluginsPath() {
7672
+ return resolve28(homedir3(), ".claude", "plugins", "installed_plugins.json");
7673
+ }
7562
7674
  function getInstallMarkerPath(targetDir) {
7563
7675
  return resolve28(targetDir, INSTALL_MARKER_FILENAME);
7564
7676
  }
@@ -7566,6 +7678,22 @@ async function readPackageManifest(packageRoot2) {
7566
7678
  const raw = await readFile12(resolve28(packageRoot2, "package.json"), "utf-8");
7567
7679
  return JSON.parse(raw);
7568
7680
  }
7681
+ async function readJsonFileIfExists(pathValue) {
7682
+ if (!await fileExists(pathValue)) {
7683
+ return null;
7684
+ }
7685
+ try {
7686
+ const raw = await readFile12(pathValue, "utf-8");
7687
+ return JSON.parse(raw);
7688
+ } catch {
7689
+ return null;
7690
+ }
7691
+ }
7692
+ async function readClaudePluginManifest(pluginDir) {
7693
+ return await readJsonFileIfExists(
7694
+ resolve28(pluginDir, ".claude-plugin", "plugin.json")
7695
+ ) ?? {};
7696
+ }
7569
7697
  async function readPluginManifestName(targetDir, pluginKind) {
7570
7698
  const manifestPath = resolve28(targetDir, getPluginManifestRelativePath(pluginKind));
7571
7699
  if (!await fileExists(manifestPath)) {
@@ -7649,22 +7777,325 @@ async function removeInstallMarker(targetDir) {
7649
7777
  });
7650
7778
  }
7651
7779
  }
7652
- async function resolvePluginPaths(pluginKind) {
7780
+ function normalizeAbsoluteInstallPath(pathValue, label) {
7781
+ const expanded = expandHome(pathValue.trim());
7782
+ if (!isAbsolute2(expanded)) {
7783
+ throw new Error(`${label} must be an absolute path.`);
7784
+ }
7785
+ return resolve28(expanded);
7786
+ }
7787
+ async function resolvePluginPaths(pluginKind, targetDir) {
7653
7788
  const packageRoot2 = await findPackageRoot(getPluginRelativePath(pluginKind));
7654
7789
  return {
7655
7790
  packageRoot: packageRoot2,
7656
7791
  sourceDir: resolve28(packageRoot2, getPluginRelativePath(pluginKind)),
7657
- targetDir: getPluginTargetDir(pluginKind)
7792
+ targetDir: targetDir ?? getDefaultPluginTargetDir(pluginKind)
7658
7793
  };
7659
7794
  }
7660
- async function installManagedPlugin(options) {
7661
- const { pluginKind, force = false, link = false } = options;
7662
- const paths = await resolvePluginPaths(pluginKind);
7663
- if (!await fileExists(paths.sourceDir)) {
7795
+ async function readInstalledClaudeMarketplaceNames() {
7796
+ const parsed = await readJsonFileIfExists(getClaudeInstalledPluginsPath());
7797
+ const names = /* @__PURE__ */ new Set();
7798
+ for (const key of Object.keys(parsed?.plugins ?? {})) {
7799
+ const atIndex = key.lastIndexOf("@");
7800
+ if (atIndex > 0 && atIndex < key.length - 1) {
7801
+ names.add(key.slice(atIndex + 1));
7802
+ }
7803
+ }
7804
+ return names;
7805
+ }
7806
+ async function readKnownClaudeMarketplaceRecords() {
7807
+ const parsed = await readJsonFileIfExists(
7808
+ getClaudeKnownMarketplacesPath()
7809
+ );
7810
+ return new Map(Object.entries(parsed ?? {}));
7811
+ }
7812
+ async function readClaudeMarketplaceFile(manifestPath) {
7813
+ const parsed = await readJsonFileIfExists(manifestPath);
7814
+ if (!parsed || typeof parsed !== "object") {
7815
+ return { plugins: [] };
7816
+ }
7817
+ return {
7818
+ ...parsed,
7819
+ plugins: Array.isArray(parsed.plugins) ? parsed.plugins.filter(
7820
+ (plugin) => typeof plugin === "object" && plugin !== null
7821
+ ) : []
7822
+ };
7823
+ }
7824
+ async function writeClaudeMarketplaceFile(manifestPath, marketplace) {
7825
+ await ensureDir(dirname7(manifestPath));
7826
+ await writeFile4(manifestPath, `${JSON.stringify(marketplace, null, 2)}
7827
+ `, "utf-8");
7828
+ }
7829
+ function buildClaudeMarketplaceSourcePath(pluginTargetDir, marketplaceRootDir) {
7830
+ const relPath = relative2(marketplaceRootDir, pluginTargetDir).replaceAll("\\", "/");
7831
+ if (relPath === "") {
7832
+ return ".";
7833
+ }
7834
+ return relPath.startsWith(".") ? relPath : `./${relPath}`;
7835
+ }
7836
+ function normalizeClaudeAuthor(author) {
7837
+ if (!author) {
7838
+ return void 0;
7839
+ }
7840
+ if (typeof author === "string") {
7841
+ return { name: author };
7842
+ }
7843
+ return {
7844
+ name: author.name,
7845
+ email: author.email
7846
+ };
7847
+ }
7848
+ function buildClaudeMarketplaceEntry(pluginTargetDir, marketplaceRootDir, manifest) {
7849
+ return {
7850
+ name: manifest.name ?? "syntaur",
7851
+ description: manifest.description,
7852
+ version: manifest.version,
7853
+ author: normalizeClaudeAuthor(manifest.author),
7854
+ source: buildClaudeMarketplaceSourcePath(pluginTargetDir, marketplaceRootDir),
7855
+ category: "development"
7856
+ };
7857
+ }
7858
+ function scoreClaudeMarketplaceCandidate(candidate) {
7859
+ if (candidate.hasSyntaur) {
7860
+ return 100;
7861
+ }
7862
+ if (candidate.active && candidate.isUserPlugins) {
7863
+ return 90;
7864
+ }
7865
+ if (candidate.isUserPlugins) {
7866
+ return 80;
7867
+ }
7868
+ if (candidate.active && candidate.isDirectorySource) {
7869
+ return 70;
7870
+ }
7871
+ if (candidate.isDirectorySource) {
7872
+ return 60;
7873
+ }
7874
+ if (candidate.active) {
7875
+ return 50;
7876
+ }
7877
+ return 10;
7878
+ }
7879
+ async function listClaudeMarketplaceCandidates() {
7880
+ const rootDir = getClaudeMarketplacesRoot();
7881
+ if (!await fileExists(rootDir)) {
7882
+ return [];
7883
+ }
7884
+ const [knownMarketplaces, activeMarketplaceNames, entries] = await Promise.all([
7885
+ readKnownClaudeMarketplaceRecords(),
7886
+ readInstalledClaudeMarketplaceNames(),
7887
+ readdir8(rootDir, { withFileTypes: true })
7888
+ ]);
7889
+ const candidates = [];
7890
+ const seen = /* @__PURE__ */ new Set();
7891
+ for (const entry of entries) {
7892
+ if (!entry.isDirectory()) {
7893
+ continue;
7894
+ }
7895
+ const candidateRoot = resolve28(rootDir, entry.name);
7896
+ const manifestPath = resolve28(candidateRoot, ".claude-plugin", "marketplace.json");
7897
+ if (!await fileExists(manifestPath)) {
7898
+ continue;
7899
+ }
7900
+ const parsed = await readClaudeMarketplaceFile(manifestPath);
7901
+ const candidateName = typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : entry.name;
7902
+ const known = knownMarketplaces.get(candidateName);
7903
+ const hasSyntaur = parsed.plugins.some(
7904
+ (plugin) => plugin.name === "syntaur" && typeof plugin.source === "string"
7905
+ );
7906
+ const isUserPlugins = candidateName === "user-plugins" || entry.name === "user-plugins";
7907
+ candidates.push({
7908
+ name: candidateName,
7909
+ rootDir: candidateRoot,
7910
+ manifestPath,
7911
+ active: activeMarketplaceNames.has(candidateName),
7912
+ hasSyntaur,
7913
+ isUserPlugins,
7914
+ isDirectorySource: known?.source?.source === "directory" || isUserPlugins
7915
+ });
7916
+ seen.add(candidateRoot);
7917
+ }
7918
+ for (const [candidateName, known] of knownMarketplaces.entries()) {
7919
+ const installLocation = known.installLocation;
7920
+ if (!installLocation) {
7921
+ continue;
7922
+ }
7923
+ const candidateRoot = resolve28(expandHome(installLocation));
7924
+ if (seen.has(candidateRoot)) {
7925
+ continue;
7926
+ }
7927
+ const manifestPath = resolve28(candidateRoot, ".claude-plugin", "marketplace.json");
7928
+ if (!await fileExists(manifestPath)) {
7929
+ continue;
7930
+ }
7931
+ const parsed = await readClaudeMarketplaceFile(manifestPath);
7932
+ candidates.push({
7933
+ name: typeof parsed.name === "string" && parsed.name.trim() !== "" ? parsed.name : candidateName,
7934
+ rootDir: candidateRoot,
7935
+ manifestPath,
7936
+ active: activeMarketplaceNames.has(candidateName),
7937
+ hasSyntaur: parsed.plugins.some(
7938
+ (plugin) => plugin.name === "syntaur" && typeof plugin.source === "string"
7939
+ ),
7940
+ isUserPlugins: candidateName === "user-plugins",
7941
+ isDirectorySource: known.source?.source === "directory" || candidateName === "user-plugins"
7942
+ });
7943
+ }
7944
+ return candidates.sort((a, b) => scoreClaudeMarketplaceCandidate(b) - scoreClaudeMarketplaceCandidate(a));
7945
+ }
7946
+ async function getPreferredClaudeMarketplace() {
7947
+ const candidate = (await listClaudeMarketplaceCandidates())[0];
7948
+ if (!candidate) {
7949
+ return null;
7950
+ }
7951
+ return {
7952
+ name: candidate.name,
7953
+ rootDir: candidate.rootDir,
7954
+ manifestPath: candidate.manifestPath,
7955
+ targetDir: resolve28(candidate.rootDir, "plugins", "syntaur")
7956
+ };
7957
+ }
7958
+ async function detectClaudeMarketplaceForTarget(targetDir) {
7959
+ const normalizedTargetDir = normalizeAbsoluteInstallPath(targetDir, "Claude plugin target");
7960
+ const pluginsDir = dirname7(normalizedTargetDir);
7961
+ if (basename(pluginsDir) !== "plugins") {
7962
+ return null;
7963
+ }
7964
+ const rootDir = dirname7(pluginsDir);
7965
+ const manifestPath = resolve28(rootDir, ".claude-plugin", "marketplace.json");
7966
+ if (!await fileExists(manifestPath)) {
7967
+ return null;
7968
+ }
7969
+ const marketplace = await readClaudeMarketplaceFile(manifestPath);
7970
+ const name = typeof marketplace.name === "string" && marketplace.name.trim() !== "" ? marketplace.name : basename(rootDir);
7971
+ return {
7972
+ name,
7973
+ rootDir,
7974
+ manifestPath,
7975
+ targetDir: normalizedTargetDir
7976
+ };
7977
+ }
7978
+ async function findManagedClaudeMarketplacePluginDir() {
7979
+ const marketplaces = await listClaudeMarketplaceCandidates();
7980
+ for (const marketplace of marketplaces) {
7981
+ const targetDir = resolve28(marketplace.rootDir, "plugins", "syntaur");
7982
+ const status = await getInstallStatus(targetDir, "claude");
7983
+ if (status.exists && status.managed) {
7984
+ return targetDir;
7985
+ }
7986
+ }
7987
+ return null;
7988
+ }
7989
+ async function ensureClaudeMarketplaceEntry(options) {
7990
+ const marketplaceRootDir = normalizeAbsoluteInstallPath(
7991
+ options.marketplaceRootDir,
7992
+ "Claude marketplace root"
7993
+ );
7994
+ const manifestPath = normalizeAbsoluteInstallPath(
7995
+ options.manifestPath,
7996
+ "Claude marketplace manifest"
7997
+ );
7998
+ const pluginTargetDir = normalizeAbsoluteInstallPath(
7999
+ options.pluginTargetDir,
8000
+ "Claude plugin target"
8001
+ );
8002
+ const marketplace = await readClaudeMarketplaceFile(manifestPath);
8003
+ const pluginManifest = await readClaudePluginManifest(pluginTargetDir);
8004
+ const entry = buildClaudeMarketplaceEntry(pluginTargetDir, marketplaceRootDir, pluginManifest);
8005
+ const expectedSource = entry.source;
8006
+ const existingIndex = marketplace.plugins.findIndex(
8007
+ (plugin) => plugin.name === entry.name && plugin.source === expectedSource
8008
+ );
8009
+ if (existingIndex >= 0) {
8010
+ const existing = marketplace.plugins[existingIndex];
8011
+ const changed = JSON.stringify(existing) !== JSON.stringify(entry);
8012
+ if (changed) {
8013
+ marketplace.plugins[existingIndex] = entry;
8014
+ await writeClaudeMarketplaceFile(manifestPath, marketplace);
8015
+ }
8016
+ return { manifestPath, changed };
8017
+ }
8018
+ const conflictingIndex = marketplace.plugins.findIndex((plugin) => plugin.name === entry.name);
8019
+ if (conflictingIndex >= 0) {
8020
+ const expectedExistingSource = options.expectedExistingPluginTargetDir ? buildClaudeMarketplaceSourcePath(
8021
+ normalizeAbsoluteInstallPath(
8022
+ options.expectedExistingPluginTargetDir,
8023
+ "Existing Claude plugin target"
8024
+ ),
8025
+ marketplaceRootDir
8026
+ ) : null;
8027
+ const existing = marketplace.plugins[conflictingIndex];
8028
+ if (expectedExistingSource && existing.source === expectedExistingSource) {
8029
+ marketplace.plugins[conflictingIndex] = entry;
8030
+ await writeClaudeMarketplaceFile(manifestPath, marketplace);
8031
+ return { manifestPath, changed: true };
8032
+ }
7664
8033
  throw new Error(
7665
- `Plugin source directory not found at ${paths.sourceDir}.`
8034
+ `Marketplace entry "${entry.name}" already exists with different settings in ${manifestPath}. Remove it manually before installing the Claude plugin.`
7666
8035
  );
7667
8036
  }
8037
+ marketplace.plugins.push(entry);
8038
+ await writeClaudeMarketplaceFile(manifestPath, marketplace);
8039
+ return { manifestPath, changed: true };
8040
+ }
8041
+ async function removeClaudeMarketplaceEntry(options) {
8042
+ const manifestPath = normalizeAbsoluteInstallPath(
8043
+ options.manifestPath,
8044
+ "Claude marketplace manifest"
8045
+ );
8046
+ if (!await fileExists(manifestPath)) {
8047
+ return { manifestPath, removed: false };
8048
+ }
8049
+ const marketplaceRootDir = normalizeAbsoluteInstallPath(
8050
+ options.marketplaceRootDir,
8051
+ "Claude marketplace root"
8052
+ );
8053
+ const expectedSource = options.pluginTargetDir ? buildClaudeMarketplaceSourcePath(
8054
+ normalizeAbsoluteInstallPath(options.pluginTargetDir, "Claude plugin target"),
8055
+ marketplaceRootDir
8056
+ ) : null;
8057
+ const marketplace = await readClaudeMarketplaceFile(manifestPath);
8058
+ const beforeCount = marketplace.plugins.length;
8059
+ marketplace.plugins = marketplace.plugins.filter((plugin) => {
8060
+ if (plugin.name !== "syntaur") {
8061
+ return true;
8062
+ }
8063
+ if (!expectedSource) {
8064
+ return false;
8065
+ }
8066
+ return plugin.source !== expectedSource;
8067
+ });
8068
+ if (marketplace.plugins.length === beforeCount) {
8069
+ return { manifestPath, removed: false };
8070
+ }
8071
+ await writeClaudeMarketplaceFile(manifestPath, marketplace);
8072
+ return { manifestPath, removed: true };
8073
+ }
8074
+ async function inspectInstallPath(pluginKind, targetDir) {
8075
+ const normalizedTarget = normalizeAbsoluteInstallPath(targetDir, `${getPluginDisplayName(pluginKind)} target`);
8076
+ const status = await getInstallStatus(normalizedTarget, pluginKind);
8077
+ return {
8078
+ exists: status.exists,
8079
+ managed: status.managed,
8080
+ installMode: status.installMode,
8081
+ targetDir: normalizedTarget
8082
+ };
8083
+ }
8084
+ async function installManagedPlugin(options) {
8085
+ const {
8086
+ pluginKind,
8087
+ force = false,
8088
+ link = false,
8089
+ targetDir = getDefaultPluginTargetDir(pluginKind)
8090
+ } = options;
8091
+ const normalizedTargetDir = normalizeAbsoluteInstallPath(
8092
+ targetDir,
8093
+ `${getPluginDisplayName(pluginKind)} target`
8094
+ );
8095
+ const paths = await resolvePluginPaths(pluginKind, normalizedTargetDir);
8096
+ if (!await fileExists(paths.sourceDir)) {
8097
+ throw new Error(`Plugin source directory not found at ${paths.sourceDir}.`);
8098
+ }
7668
8099
  const desiredMode = link ? "link" : "copy";
7669
8100
  const existing = await getInstallStatus(paths.targetDir, pluginKind);
7670
8101
  if (existing.exists && !existing.managed) {
@@ -7695,12 +8126,19 @@ async function installManagedPlugin(options) {
7695
8126
  changed: true
7696
8127
  };
7697
8128
  }
7698
- function buildSyntaurMarketplaceEntry() {
8129
+ function buildMarketplaceSourcePath(pluginTargetDir, marketplacePath) {
8130
+ const relPath = relative2(dirname7(marketplacePath), pluginTargetDir).replaceAll("\\", "/");
8131
+ if (relPath === "") {
8132
+ return ".";
8133
+ }
8134
+ return relPath.startsWith(".") ? relPath : `./${relPath}`;
8135
+ }
8136
+ function buildSyntaurMarketplaceEntry(pluginTargetDir, marketplacePath) {
7699
8137
  return {
7700
8138
  name: "syntaur",
7701
8139
  source: {
7702
8140
  source: "local",
7703
- path: "./plugins/syntaur"
8141
+ path: buildMarketplaceSourcePath(pluginTargetDir, marketplacePath)
7704
8142
  },
7705
8143
  policy: {
7706
8144
  installation: "AVAILABLE",
@@ -7709,11 +8147,7 @@ function buildSyntaurMarketplaceEntry() {
7709
8147
  category: "Coding"
7710
8148
  };
7711
8149
  }
7712
- function getMarketplacePath() {
7713
- return resolve28(homedir3(), ".agents", "plugins", "marketplace.json");
7714
- }
7715
- async function readMarketplaceFile() {
7716
- const marketplacePath = getMarketplacePath();
8150
+ async function readMarketplaceFile(marketplacePath) {
7717
8151
  if (!await fileExists(marketplacePath)) {
7718
8152
  return {
7719
8153
  name: "local",
@@ -7729,50 +8163,90 @@ async function readMarketplaceFile() {
7729
8163
  plugins: Array.isArray(parsed.plugins) ? parsed.plugins : []
7730
8164
  };
7731
8165
  }
7732
- async function writeMarketplaceFile(marketplace) {
7733
- const marketplacePath = getMarketplacePath();
8166
+ async function writeMarketplaceFile(marketplacePath, marketplace) {
7734
8167
  await ensureDir(dirname7(marketplacePath));
7735
8168
  await writeFile4(marketplacePath, `${JSON.stringify(marketplace, null, 2)}
7736
8169
  `, "utf-8");
7737
8170
  }
7738
- async function ensureMarketplaceEntry() {
7739
- const marketplace = await readMarketplaceFile();
7740
- const entry = buildSyntaurMarketplaceEntry();
8171
+ async function hasAnySyntaurMarketplaceEntry(marketplacePath) {
8172
+ const marketplace = await readMarketplaceFile(marketplacePath);
8173
+ return marketplace.plugins.some(
8174
+ (plugin) => plugin.name === "syntaur" && plugin.source?.source === "local"
8175
+ );
8176
+ }
8177
+ async function hasSyntaurMarketplaceEntry(marketplacePath, pluginTargetDir) {
8178
+ const marketplace = await readMarketplaceFile(marketplacePath);
8179
+ const expectedPath = buildMarketplaceSourcePath(pluginTargetDir, marketplacePath);
8180
+ return marketplace.plugins.some(
8181
+ (plugin) => plugin.name === "syntaur" && plugin.source?.source === "local" && plugin.source?.path === expectedPath
8182
+ );
8183
+ }
8184
+ async function ensureMarketplaceEntry(options) {
8185
+ const marketplacePath = normalizeAbsoluteInstallPath(
8186
+ options.marketplacePath,
8187
+ "Codex marketplace path"
8188
+ );
8189
+ const pluginTargetDir = normalizeAbsoluteInstallPath(
8190
+ options.pluginTargetDir,
8191
+ "Codex plugin target"
8192
+ );
8193
+ const marketplace = await readMarketplaceFile(marketplacePath);
8194
+ const entry = buildSyntaurMarketplaceEntry(pluginTargetDir, marketplacePath);
7741
8195
  const existingIndex = marketplace.plugins.findIndex(
7742
8196
  (plugin) => plugin.name === entry.name && plugin.source?.source === entry.source.source && plugin.source?.path === entry.source.path
7743
8197
  );
7744
8198
  if (existingIndex >= 0) {
7745
- return {
7746
- marketplacePath: getMarketplacePath(),
7747
- changed: false
7748
- };
8199
+ return { marketplacePath, changed: false };
7749
8200
  }
7750
8201
  const conflictingIndex = marketplace.plugins.findIndex((plugin) => plugin.name === entry.name);
7751
8202
  if (conflictingIndex >= 0) {
8203
+ const existing = marketplace.plugins[conflictingIndex];
8204
+ const expectedExistingPath = options.expectedExistingPluginTargetDir ? buildMarketplaceSourcePath(
8205
+ normalizeAbsoluteInstallPath(
8206
+ options.expectedExistingPluginTargetDir,
8207
+ "Existing Codex plugin target"
8208
+ ),
8209
+ marketplacePath
8210
+ ) : null;
8211
+ if (existing.source?.source === "local" && expectedExistingPath && existing.source?.path === expectedExistingPath) {
8212
+ marketplace.plugins[conflictingIndex] = entry;
8213
+ await writeMarketplaceFile(marketplacePath, marketplace);
8214
+ return { marketplacePath, changed: true };
8215
+ }
7752
8216
  throw new Error(
7753
- `Marketplace entry "${entry.name}" already exists with different settings. Remove it manually before installing the Codex plugin.`
8217
+ `Marketplace entry "${entry.name}" already exists with different settings in ${marketplacePath}. Remove it manually before installing the Codex plugin.`
7754
8218
  );
7755
8219
  }
7756
8220
  marketplace.plugins.push(entry);
7757
- await writeMarketplaceFile(marketplace);
7758
- return {
7759
- marketplacePath: getMarketplacePath(),
7760
- changed: true
7761
- };
8221
+ await writeMarketplaceFile(marketplacePath, marketplace);
8222
+ return { marketplacePath, changed: true };
7762
8223
  }
7763
8224
  function isDefaultMarketplaceShell(marketplace) {
7764
8225
  return marketplace.name === "local" && (marketplace.interface?.displayName ?? "Local Plugins") === "Local Plugins";
7765
8226
  }
7766
- async function removeMarketplaceEntry() {
7767
- const marketplacePath = getMarketplacePath();
8227
+ async function removeMarketplaceEntry(options) {
8228
+ const marketplacePath = normalizeAbsoluteInstallPath(
8229
+ options.marketplacePath,
8230
+ "Codex marketplace path"
8231
+ );
7768
8232
  if (!await fileExists(marketplacePath)) {
7769
8233
  return { marketplacePath, removed: false };
7770
8234
  }
7771
- const marketplace = await readMarketplaceFile();
8235
+ const expectedSourcePath = options.expectedSourcePath ?? (options.pluginTargetDir ? buildMarketplaceSourcePath(
8236
+ normalizeAbsoluteInstallPath(options.pluginTargetDir, "Codex plugin target"),
8237
+ marketplacePath
8238
+ ) : null);
8239
+ const marketplace = await readMarketplaceFile(marketplacePath);
7772
8240
  const beforeCount = marketplace.plugins.length;
7773
- marketplace.plugins = marketplace.plugins.filter(
7774
- (plugin) => !(plugin.name === "syntaur" && plugin.source?.source === "local" && plugin.source?.path === "./plugins/syntaur")
7775
- );
8241
+ marketplace.plugins = marketplace.plugins.filter((plugin) => {
8242
+ if (plugin.name !== "syntaur" || plugin.source?.source !== "local") {
8243
+ return true;
8244
+ }
8245
+ if (!expectedSourcePath) {
8246
+ return false;
8247
+ }
8248
+ return plugin.source.path !== expectedSourcePath;
8249
+ });
7776
8250
  if (marketplace.plugins.length === beforeCount) {
7777
8251
  return { marketplacePath, removed: false };
7778
8252
  }
@@ -7780,23 +8254,66 @@ async function removeMarketplaceEntry() {
7780
8254
  await rm2(marketplacePath, { force: true });
7781
8255
  return { marketplacePath, removed: true };
7782
8256
  }
7783
- await writeMarketplaceFile(marketplace);
8257
+ await writeMarketplaceFile(marketplacePath, marketplace);
7784
8258
  return { marketplacePath, removed: true };
7785
8259
  }
7786
- async function uninstallManagedPlugin(pluginKind) {
7787
- const targetDir = getPluginTargetDir(pluginKind);
7788
- const existing = await getInstallStatus(targetDir, pluginKind);
8260
+ async function uninstallManagedPlugin(pluginKind, targetDir = getDefaultPluginTargetDir(pluginKind)) {
8261
+ const normalizedTarget = normalizeAbsoluteInstallPath(
8262
+ targetDir,
8263
+ `${getPluginDisplayName(pluginKind)} target`
8264
+ );
8265
+ const existing = await getInstallStatus(normalizedTarget, pluginKind);
7789
8266
  if (!existing.exists) {
7790
- return { removed: false, targetDir };
8267
+ return { removed: false, targetDir: normalizedTarget };
7791
8268
  }
7792
8269
  if (!existing.managed) {
7793
8270
  throw new Error(
7794
- `${targetDir} exists but is not a Syntaur-managed install. Remove it manually if you want to replace it.`
8271
+ `${normalizedTarget} exists but is not a Syntaur-managed install. Remove it manually if you want to replace it.`
7795
8272
  );
7796
8273
  }
7797
- await removeInstallMarker(targetDir);
7798
- await rm2(targetDir, { recursive: true, force: true });
7799
- return { removed: true, targetDir };
8274
+ await removeInstallMarker(normalizedTarget);
8275
+ await rm2(normalizedTarget, { recursive: true, force: true });
8276
+ return { removed: true, targetDir: normalizedTarget };
8277
+ }
8278
+ async function getConfiguredOrLegacyManagedPluginDir(pluginKind) {
8279
+ const config = await readConfig();
8280
+ const configuredPath = pluginKind === "claude" ? config.integrations.claudePluginDir : config.integrations.codexPluginDir;
8281
+ if (configuredPath) {
8282
+ return configuredPath;
8283
+ }
8284
+ const defaultTarget = getDefaultPluginTargetDir(pluginKind);
8285
+ const status = await getInstallStatus(defaultTarget, pluginKind);
8286
+ if (status.exists && status.managed) {
8287
+ return defaultTarget;
8288
+ }
8289
+ if (pluginKind === "claude") {
8290
+ return findManagedClaudeMarketplacePluginDir();
8291
+ }
8292
+ return null;
8293
+ }
8294
+ async function getConfiguredOrLegacyMarketplacePath() {
8295
+ const config = await readConfig();
8296
+ if (config.integrations.codexMarketplacePath) {
8297
+ return config.integrations.codexMarketplacePath;
8298
+ }
8299
+ const defaultMarketplacePath = getDefaultMarketplacePath();
8300
+ return await hasAnySyntaurMarketplaceEntry(defaultMarketplacePath) ? defaultMarketplacePath : null;
8301
+ }
8302
+ async function recommendPluginTargetDir(pluginKind) {
8303
+ const configuredOrManaged = await getConfiguredOrLegacyManagedPluginDir(pluginKind);
8304
+ if (pluginKind !== "claude") {
8305
+ return configuredOrManaged ?? getDefaultPluginTargetDir(pluginKind);
8306
+ }
8307
+ const preferredMarketplace = await getPreferredClaudeMarketplace();
8308
+ const legacyTarget = getDefaultPluginTargetDir("claude");
8309
+ if (configuredOrManaged) {
8310
+ return configuredOrManaged === legacyTarget && preferredMarketplace ? preferredMarketplace.targetDir : configuredOrManaged;
8311
+ }
8312
+ return preferredMarketplace?.targetDir ?? legacyTarget;
8313
+ }
8314
+ async function recommendMarketplacePath() {
8315
+ const configuredOrManaged = await getConfiguredOrLegacyMarketplacePath();
8316
+ return configuredOrManaged ?? getDefaultMarketplacePath();
7800
8317
  }
7801
8318
  async function isSyntaurDataInstalled() {
7802
8319
  return fileExists(resolve28(syntaurRoot(), "config.md"));
@@ -7808,52 +8325,224 @@ async function removeSyntaurData() {
7808
8325
  await rm2(syntaurRoot(), { recursive: true, force: true });
7809
8326
  }
7810
8327
  async function getConfiguredMissionDir() {
7811
- const configPath = resolve28(syntaurRoot(), "config.md");
7812
- if (!await fileExists(configPath)) {
7813
- return null;
7814
- }
7815
- const raw = await readFile12(configPath, "utf-8");
7816
- const match = raw.match(/^defaultMissionDir:\s*(.+)$/m);
7817
- if (!match) {
8328
+ if (!await fileExists(resolve28(syntaurRoot(), "config.md"))) {
7818
8329
  return null;
7819
8330
  }
7820
- return expandHome(match[1].trim().replace(/^["']|["']$/g, ""));
8331
+ return (await readConfig()).defaultMissionDir;
8332
+ }
8333
+ function getPluginDisplayName(pluginKind) {
8334
+ return pluginKind === "claude" ? "Claude Code plugin" : "Codex plugin";
7821
8335
  }
7822
8336
  function getPluginInstallCommand(pluginKind) {
7823
8337
  return pluginKind === "claude" ? "syntaur install-plugin" : "syntaur install-codex-plugin";
7824
8338
  }
7825
8339
 
8340
+ // src/utils/prompt.ts
8341
+ import { stdin as input, stdout as output } from "process";
8342
+ import { createInterface } from "readline/promises";
8343
+ function isInteractiveTerminal() {
8344
+ return Boolean(input.isTTY && output.isTTY);
8345
+ }
8346
+ async function confirmPrompt(question, defaultValue = false) {
8347
+ if (!isInteractiveTerminal()) {
8348
+ throw new Error("Interactive confirmation requires a TTY.");
8349
+ }
8350
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
8351
+ const rl = createInterface({ input, output });
8352
+ try {
8353
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
8354
+ if (answer === "") {
8355
+ return defaultValue;
8356
+ }
8357
+ return answer === "y" || answer === "yes";
8358
+ } finally {
8359
+ rl.close();
8360
+ }
8361
+ }
8362
+ async function textPrompt(question, defaultValue) {
8363
+ if (!isInteractiveTerminal()) {
8364
+ throw new Error("Interactive input requires a TTY.");
8365
+ }
8366
+ const suffix = defaultValue !== void 0 ? ` [${defaultValue}] ` : " ";
8367
+ const rl = createInterface({ input, output });
8368
+ try {
8369
+ const answer = (await rl.question(`${question}${suffix}`)).trim();
8370
+ if (answer === "" && defaultValue !== void 0) {
8371
+ return defaultValue;
8372
+ }
8373
+ return answer;
8374
+ } finally {
8375
+ rl.close();
8376
+ }
8377
+ }
8378
+
7826
8379
  // src/commands/install-plugin.ts
8380
+ async function promptForInstallPath(question, recommendedPath) {
8381
+ while (true) {
8382
+ const answer = await textPrompt(question, recommendedPath);
8383
+ try {
8384
+ return normalizeAbsoluteInstallPath(answer, question);
8385
+ } catch (error) {
8386
+ console.error(error instanceof Error ? error.message : String(error));
8387
+ }
8388
+ }
8389
+ }
7827
8390
  async function installPluginCommand(options) {
8391
+ const shouldPromptForTarget = Boolean(
8392
+ options.promptForTarget !== false && isInteractiveTerminal() && !options.targetDir
8393
+ );
8394
+ const recommendedTargetDir = await recommendPluginTargetDir("claude");
8395
+ const targetDir = options.targetDir ? normalizeAbsoluteInstallPath(options.targetDir, "Claude plugin target") : shouldPromptForTarget ? await promptForInstallPath("Claude plugin directory", recommendedTargetDir) : recommendedTargetDir;
8396
+ const previousTargetDir = await getConfiguredOrLegacyManagedPluginDir("claude");
8397
+ const migrating = Boolean(previousTargetDir && previousTargetDir !== targetDir);
8398
+ let previousInstall = previousTargetDir ? await inspectInstallPath("claude", previousTargetDir) : null;
8399
+ const previousMarketplace = previousTargetDir ? await detectClaudeMarketplaceForTarget(previousTargetDir) : null;
8400
+ const legacyTargetDir = getDefaultPluginTargetDir("claude");
8401
+ const legacyInstall = targetDir !== legacyTargetDir ? await inspectInstallPath("claude", legacyTargetDir) : null;
8402
+ if (migrating && previousInstall?.exists && !previousInstall.managed) {
8403
+ throw new Error(
8404
+ `${previousTargetDir} exists but is not a Syntaur-managed install. Remove it manually before changing the Claude plugin location.`
8405
+ );
8406
+ }
8407
+ if (targetDir !== legacyTargetDir && legacyInstall?.exists && !legacyInstall.managed && (!previousTargetDir || previousTargetDir !== legacyTargetDir)) {
8408
+ console.warn(
8409
+ `Warning: ${legacyTargetDir} already exists and is not a Syntaur-managed install. Syntaur will use ${targetDir} instead.`
8410
+ );
8411
+ }
8412
+ if (migrating && previousInstall?.exists && previousInstall.managed && isInteractiveTerminal()) {
8413
+ const confirmed = await confirmPrompt(
8414
+ `Move the Claude Code plugin from ${previousTargetDir} to ${targetDir} and remove the old install?`,
8415
+ true
8416
+ );
8417
+ if (!confirmed) {
8418
+ throw new Error("Install cancelled.");
8419
+ }
8420
+ }
7828
8421
  const result = await installManagedPlugin({
7829
8422
  pluginKind: "claude",
7830
8423
  force: options.force,
7831
- link: options.link
8424
+ link: options.link,
8425
+ targetDir
7832
8426
  });
7833
- console.log(`Installed Syntaur plugin:`);
8427
+ const currentMarketplace = await detectClaudeMarketplaceForTarget(result.targetDir);
8428
+ if (currentMarketplace) {
8429
+ await ensureClaudeMarketplaceEntry({
8430
+ marketplaceRootDir: currentMarketplace.rootDir,
8431
+ manifestPath: currentMarketplace.manifestPath,
8432
+ pluginTargetDir: result.targetDir,
8433
+ expectedExistingPluginTargetDir: previousMarketplace && previousMarketplace.manifestPath === currentMarketplace.manifestPath ? previousTargetDir : null
8434
+ });
8435
+ }
8436
+ await updateIntegrationConfig({ claudePluginDir: result.targetDir });
8437
+ if (previousMarketplace && previousTargetDir && (!currentMarketplace || currentMarketplace.manifestPath !== previousMarketplace.manifestPath)) {
8438
+ const removedMarketplaceEntry = await removeClaudeMarketplaceEntry({
8439
+ manifestPath: previousMarketplace.manifestPath,
8440
+ marketplaceRootDir: previousMarketplace.rootDir,
8441
+ pluginTargetDir: previousTargetDir
8442
+ });
8443
+ if (removedMarketplaceEntry.removed) {
8444
+ console.log(`Removed previous Claude marketplace entry from ${removedMarketplaceEntry.manifestPath}`);
8445
+ }
8446
+ }
8447
+ if (migrating && previousInstall?.exists && previousInstall.managed && previousTargetDir) {
8448
+ const removed = await uninstallManagedPlugin("claude", previousTargetDir);
8449
+ if (removed.removed) {
8450
+ console.log(`Removed previous Claude Code plugin from ${removed.targetDir}`);
8451
+ }
8452
+ previousInstall = null;
8453
+ }
8454
+ console.log("Installed Syntaur plugin:");
7834
8455
  console.log(` target: ${result.targetDir}`);
7835
8456
  console.log(` source: ${result.sourceDir}`);
7836
8457
  console.log(` mode: ${result.mode}`);
7837
- console.log(`
7838
- The plugin is now available in Claude Code.`);
7839
- console.log(` Skills: /grab-assignment, /plan-assignment, /complete-assignment`);
7840
- console.log(` Background: syntaur-protocol (auto-invoked)`);
7841
- console.log(` Hook: write boundary enforcement (PreToolUse)`);
8458
+ if (currentMarketplace) {
8459
+ console.log(` marketplace: ${currentMarketplace.manifestPath}`);
8460
+ }
8461
+ console.log("\nThe plugin is now available in Claude Code.");
8462
+ console.log(" Skills: /grab-assignment, /plan-assignment, /complete-assignment");
8463
+ console.log(" Background: syntaur-protocol (auto-invoked)");
8464
+ console.log(" Hook: write boundary enforcement (PreToolUse)");
7842
8465
  }
7843
8466
 
7844
8467
  // src/commands/install-codex-plugin.ts
8468
+ init_config2();
8469
+ async function promptForInstallPath2(question, recommendedPath) {
8470
+ while (true) {
8471
+ const answer = await textPrompt(question, recommendedPath);
8472
+ try {
8473
+ return normalizeAbsoluteInstallPath(answer, question);
8474
+ } catch (error) {
8475
+ console.error(error instanceof Error ? error.message : String(error));
8476
+ }
8477
+ }
8478
+ }
7845
8479
  async function installCodexPluginCommand(options) {
8480
+ const promptForTarget = Boolean(
8481
+ options.promptForTarget !== false && isInteractiveTerminal() && (!options.targetDir || !options.marketplacePath)
8482
+ );
8483
+ const recommendedTargetDir = await recommendPluginTargetDir("codex");
8484
+ const recommendedMarketplacePath = await recommendMarketplacePath();
8485
+ const targetDir = options.targetDir ? normalizeAbsoluteInstallPath(options.targetDir, "Codex plugin target") : promptForTarget ? await promptForInstallPath2("Codex plugin directory", recommendedTargetDir) : recommendedTargetDir;
8486
+ const marketplacePath = options.marketplacePath ? normalizeAbsoluteInstallPath(options.marketplacePath, "Codex marketplace path") : promptForTarget ? await promptForInstallPath2("Codex marketplace file path", recommendedMarketplacePath) : recommendedMarketplacePath;
8487
+ const previousTargetDir = await getConfiguredOrLegacyManagedPluginDir("codex");
8488
+ const previousMarketplacePath = await getConfiguredOrLegacyMarketplacePath();
8489
+ const pluginPathChanged = Boolean(previousTargetDir && previousTargetDir !== targetDir);
8490
+ const marketplacePathChanged = Boolean(
8491
+ previousMarketplacePath && previousMarketplacePath !== marketplacePath
8492
+ );
8493
+ const previousInstall = previousTargetDir ? await inspectInstallPath("codex", previousTargetDir) : null;
8494
+ const previousMarketplaceEntryExists = Boolean(
8495
+ previousMarketplacePath && previousTargetDir && await hasSyntaurMarketplaceEntry(previousMarketplacePath, previousTargetDir)
8496
+ );
8497
+ if (pluginPathChanged && previousInstall?.exists && !previousInstall.managed) {
8498
+ throw new Error(
8499
+ `${previousTargetDir} exists but is not a Syntaur-managed install. Remove it manually before changing the Codex plugin location.`
8500
+ );
8501
+ }
8502
+ if ((pluginPathChanged || marketplacePathChanged) && (previousInstall?.exists || previousMarketplaceEntryExists) && isInteractiveTerminal()) {
8503
+ const confirmed = await confirmPrompt(
8504
+ `Move the Codex integration to ${targetDir} and ${marketplacePath} and remove the previous Syntaur-managed integration?`,
8505
+ true
8506
+ );
8507
+ if (!confirmed) {
8508
+ throw new Error("Install cancelled.");
8509
+ }
8510
+ }
7846
8511
  const result = await installManagedPlugin({
7847
8512
  pluginKind: "codex",
7848
8513
  force: options.force,
7849
- link: options.link
8514
+ link: options.link,
8515
+ targetDir
8516
+ });
8517
+ const marketplace = await ensureMarketplaceEntry({
8518
+ marketplacePath,
8519
+ pluginTargetDir: result.targetDir,
8520
+ expectedExistingPluginTargetDir: previousMarketplacePath === marketplacePath ? previousTargetDir : null
8521
+ });
8522
+ await updateIntegrationConfig({
8523
+ codexPluginDir: result.targetDir,
8524
+ codexMarketplacePath: marketplace.marketplacePath
7850
8525
  });
7851
- const marketplace = await ensureMarketplaceEntry();
8526
+ if (pluginPathChanged && previousInstall?.exists && previousInstall.managed && previousTargetDir) {
8527
+ const removed = await uninstallManagedPlugin("codex", previousTargetDir);
8528
+ if (removed.removed) {
8529
+ console.log(`Removed previous Codex plugin from ${removed.targetDir}`);
8530
+ }
8531
+ }
8532
+ if (previousMarketplacePath && previousTargetDir && previousMarketplacePath !== marketplace.marketplacePath) {
8533
+ const removedMarketplace = await removeMarketplaceEntry({
8534
+ marketplacePath: previousMarketplacePath,
8535
+ pluginTargetDir: previousTargetDir
8536
+ });
8537
+ if (removedMarketplace.removed) {
8538
+ console.log(`Removed previous Codex marketplace entry from ${removedMarketplace.marketplacePath}`);
8539
+ }
8540
+ }
7852
8541
  console.log("Installed Syntaur Codex plugin:");
7853
8542
  console.log(` target: ${result.targetDir}`);
7854
8543
  console.log(` source: ${result.sourceDir}`);
7855
8544
  console.log(` mode: ${result.mode}`);
7856
- console.log(` marketplace: ${marketplace.changed ? marketplace.marketplacePath : getMarketplacePath()}`);
8545
+ console.log(` marketplace: ${marketplace.marketplacePath}`);
7857
8546
  console.log("\nThe plugin is now available to Codex.");
7858
8547
  console.log(
7859
8548
  " Skills: syntaur-protocol, create-mission, create-assignment, grab-assignment, plan-assignment, complete-assignment, track-session"
@@ -7862,36 +8551,13 @@ async function installCodexPluginCommand(options) {
7862
8551
  console.log(" Hooks: write boundary enforcement, session cleanup");
7863
8552
  }
7864
8553
 
7865
- // src/utils/prompt.ts
7866
- import { stdin as input, stdout as output } from "process";
7867
- import { createInterface } from "readline/promises";
7868
- function isInteractiveTerminal() {
7869
- return Boolean(input.isTTY && output.isTTY);
7870
- }
7871
- async function confirmPrompt(question, defaultValue = false) {
7872
- if (!isInteractiveTerminal()) {
7873
- throw new Error("Interactive confirmation requires a TTY.");
7874
- }
7875
- const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
7876
- const rl = createInterface({ input, output });
7877
- try {
7878
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
7879
- if (answer === "") {
7880
- return defaultValue;
7881
- }
7882
- return answer === "y" || answer === "yes";
7883
- } finally {
7884
- rl.close();
7885
- }
7886
- }
7887
-
7888
8554
  // src/commands/setup.ts
7889
8555
  function printNonInteractiveSetupHelp() {
7890
8556
  console.error("Syntaur setup needs confirmation for optional steps when no TTY is available.");
7891
8557
  console.error("Run one of these commands instead:");
7892
8558
  console.error(" npx syntaur@latest setup --yes");
7893
- console.error(` npx syntaur@latest setup --yes --claude`);
7894
- console.error(` npx syntaur@latest setup --yes --codex`);
8559
+ console.error(" npx syntaur@latest setup --yes --claude");
8560
+ console.error(" npx syntaur@latest setup --yes --codex");
7895
8561
  console.error(` npx syntaur@latest setup --yes --dashboard`);
7896
8562
  }
7897
8563
  async function setupCommand(options) {
@@ -7921,12 +8587,19 @@ async function setupCommand(options) {
7921
8587
  }
7922
8588
  }
7923
8589
  if (installClaude) {
7924
- await installPluginCommand({});
8590
+ await installPluginCommand({
8591
+ targetDir: options.claudeDir,
8592
+ promptForTarget: !options.yes
8593
+ });
7925
8594
  } else if (!interactive && !options.yes && !initialized) {
7926
8595
  console.log(`Skip Claude plugin for now. Install later with: ${getPluginInstallCommand("claude")}`);
7927
8596
  }
7928
8597
  if (installCodex) {
7929
- await installCodexPluginCommand({});
8598
+ await installCodexPluginCommand({
8599
+ targetDir: options.codexDir,
8600
+ marketplacePath: options.codexMarketplacePath,
8601
+ promptForTarget: !options.yes
8602
+ });
7930
8603
  } else if (!interactive && !options.yes && !initialized) {
7931
8604
  console.log(`Skip Codex plugin for now. Install later with: ${getPluginInstallCommand("codex")}`);
7932
8605
  }
@@ -7984,19 +8657,44 @@ async function uninstallCommand(options) {
7984
8657
  }
7985
8658
  }
7986
8659
  if (targets.claude) {
7987
- const result = await uninstallManagedPlugin("claude");
8660
+ const claudeTargetDir = await getConfiguredOrLegacyManagedPluginDir("claude");
8661
+ const claudeMarketplace = claudeTargetDir ? await detectClaudeMarketplaceForTarget(claudeTargetDir) : null;
8662
+ const result = await uninstallManagedPlugin(
8663
+ "claude",
8664
+ claudeTargetDir ?? void 0
8665
+ );
7988
8666
  console.log(
7989
8667
  result.removed ? `Removed Claude Code plugin from ${result.targetDir}` : `Claude Code plugin not installed at ${result.targetDir}`
7990
8668
  );
8669
+ if (claudeMarketplace) {
8670
+ const removedMarketplaceEntry = await removeClaudeMarketplaceEntry({
8671
+ manifestPath: claudeMarketplace.manifestPath,
8672
+ marketplaceRootDir: claudeMarketplace.rootDir,
8673
+ pluginTargetDir: claudeTargetDir ?? void 0
8674
+ });
8675
+ if (removedMarketplaceEntry.removed) {
8676
+ console.log(`Removed Claude marketplace entry from ${removedMarketplaceEntry.manifestPath}`);
8677
+ }
8678
+ }
7991
8679
  }
7992
8680
  if (targets.codex) {
7993
- const result = await uninstallManagedPlugin("codex");
8681
+ const codexTargetDir = await getConfiguredOrLegacyManagedPluginDir("codex");
8682
+ const marketplacePath = await getConfiguredOrLegacyMarketplacePath();
8683
+ const result = await uninstallManagedPlugin(
8684
+ "codex",
8685
+ codexTargetDir ?? void 0
8686
+ );
7994
8687
  console.log(
7995
8688
  result.removed ? `Removed Codex plugin from ${result.targetDir}` : `Codex plugin not installed at ${result.targetDir}`
7996
8689
  );
7997
- const marketplace = await removeMarketplaceEntry();
7998
- if (marketplace.removed) {
7999
- console.log(`Removed Codex marketplace entry from ${marketplace.marketplacePath}`);
8690
+ if (marketplacePath) {
8691
+ const marketplace = await removeMarketplaceEntry({
8692
+ marketplacePath,
8693
+ pluginTargetDir: codexTargetDir ?? void 0
8694
+ });
8695
+ if (marketplace.removed) {
8696
+ console.log(`Removed Codex marketplace entry from ${marketplace.marketplacePath}`);
8697
+ }
8000
8698
  }
8001
8699
  }
8002
8700
  if (targets.data) {
@@ -8017,7 +8715,7 @@ async function uninstallCommand(options) {
8017
8715
  // src/commands/setup-adapter.ts
8018
8716
  init_paths();
8019
8717
  init_fs();
8020
- init_config();
8718
+ init_config2();
8021
8719
  import { resolve as resolve30 } from "path";
8022
8720
  var SUPPORTED_FRAMEWORKS = ["cursor", "codex", "opencode"];
8023
8721
  async function setupAdapterCommand(framework, options) {
@@ -8116,7 +8814,7 @@ async function setupAdapterCommand(framework, options) {
8116
8814
  // src/commands/track-session.ts
8117
8815
  init_paths();
8118
8816
  init_fs();
8119
- init_config();
8817
+ init_config2();
8120
8818
  import { resolve as resolve31 } from "path";
8121
8819
  import { randomUUID as randomUUID3 } from "crypto";
8122
8820
  async function trackSessionCommand(options) {
@@ -8153,7 +8851,7 @@ async function trackSessionCommand(options) {
8153
8851
  }
8154
8852
 
8155
8853
  // src/commands/browse.ts
8156
- init_config();
8854
+ init_config2();
8157
8855
  async function browseCommand(options) {
8158
8856
  const config = await readConfig();
8159
8857
  const missionsDir = config.defaultMissionDir;
@@ -8215,7 +8913,7 @@ Use --slug to specify a different slug.`
8215
8913
  init_paths();
8216
8914
  init_fs();
8217
8915
  init_parser();
8218
- import { readdir as readdir8, readFile as readFile13 } from "fs/promises";
8916
+ import { readdir as readdir9, readFile as readFile13 } from "fs/promises";
8219
8917
  import { resolve as resolve34 } from "path";
8220
8918
  async function listPlaybooksCommand() {
8221
8919
  const dir = playbooksDir();
@@ -8223,7 +8921,7 @@ async function listPlaybooksCommand() {
8223
8921
  console.log('No playbooks directory found. Run "syntaur init" first.');
8224
8922
  return;
8225
8923
  }
8226
- const entries = await readdir8(dir, { withFileTypes: true });
8924
+ const entries = await readdir9(dir, { withFileTypes: true });
8227
8925
  const mdFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_") && e.name !== "manifest.md");
8228
8926
  if (mdFiles.length === 0) {
8229
8927
  console.log('No playbooks found. Create one with "syntaur create-playbook <name>".');
@@ -8785,7 +9483,7 @@ program.command("reopen").description("Reopen a completed or failed assignment")
8785
9483
  process.exit(1);
8786
9484
  }
8787
9485
  });
8788
- program.command("setup").description("Initialize Syntaur and optionally install plugins or launch the dashboard").option("--yes", "Skip interactive prompts and perform only the requested flags").option("--claude", "Install the Claude Code plugin").option("--codex", "Install the Codex plugin").option("--dashboard", "Launch the dashboard after setup").action(async (options) => {
9486
+ program.command("setup").description("Initialize Syntaur and optionally install plugins or launch the dashboard").option("--yes", "Skip interactive prompts and perform only the requested flags").option("--claude", "Install the Claude Code plugin").option("--codex", "Install the Codex plugin").option("--claude-dir <path>", "Install the Claude Code plugin at a specific path").option("--codex-dir <path>", "Install the Codex plugin at a specific path").option("--codex-marketplace-path <path>", "Write the Codex marketplace entry to a specific file").option("--dashboard", "Launch the dashboard after setup").action(async (options) => {
8789
9487
  try {
8790
9488
  await setupCommand(options);
8791
9489
  } catch (error) {
@@ -8796,9 +9494,9 @@ program.command("setup").description("Initialize Syntaur and optionally install
8796
9494
  process.exit(1);
8797
9495
  }
8798
9496
  });
8799
- program.command("install-plugin").description("Install the Syntaur Claude Code plugin").option("--force", "Overwrite an existing Syntaur-managed install").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
9497
+ program.command("install-plugin").description("Install the Syntaur Claude Code plugin").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
8800
9498
  try {
8801
- await installPluginCommand(options);
9499
+ await installPluginCommand({ ...options, promptForTarget: true });
8802
9500
  } catch (error) {
8803
9501
  console.error(
8804
9502
  "Error:",
@@ -8807,9 +9505,9 @@ program.command("install-plugin").description("Install the Syntaur Claude Code p
8807
9505
  process.exit(1);
8808
9506
  }
8809
9507
  });
8810
- program.command("install-codex-plugin").description("Install the Syntaur Codex plugin and home marketplace entry").option("--force", "Overwrite an existing Syntaur-managed install").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
9508
+ program.command("install-codex-plugin").description("Install the Syntaur Codex plugin and marketplace entry").option("--force", "Overwrite an existing Syntaur-managed install").option("--target-dir <path>", "Install the plugin at a specific directory").option("--marketplace-path <path>", "Write the marketplace entry to a specific file").option("--link", "Use a symlink instead of copying files (repo-local dev only)").action(async (options) => {
8811
9509
  try {
8812
- await installCodexPluginCommand(options);
9510
+ await installCodexPluginCommand({ ...options, promptForTarget: true });
8813
9511
  } catch (error) {
8814
9512
  console.error(
8815
9513
  "Error:",