sticky-note-cli 2.6.16 → 2.7.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/bin/cli.js CHANGED
@@ -116,6 +116,7 @@ function installPreCommitHook() {
116
116
  ' [ -f "$_sn_root/.sticky-note/merge-driver.js" ] && git add "$_sn_root/.sticky-note/merge-driver.js" 2>/dev/null || true',
117
117
  ' [ -d "$_sn_root/.sticky-note/audit" ] && git add "$_sn_root/.sticky-note/audit/" 2>/dev/null || true',
118
118
  ' [ -d "$_sn_root/.sticky-note/presence" ] && git add "$_sn_root/.sticky-note/presence/" 2>/dev/null || true',
119
+ ' [ -d "$_sn_root/.sticky-note/environment" ] && git add "$_sn_root/.sticky-note/environment/" 2>/dev/null || true',
119
120
  'fi',
120
121
  STICKY_PRE_COMMIT_END,
121
122
  ].join("\n");
@@ -761,6 +762,52 @@ async function cmdInit() {
761
762
  print(" [OK] .git/hooks/post-commit (auto-sync .sticky-note/ after commit)");
762
763
  }
763
764
 
765
+ // Create environment directory structure
766
+ const envDir = path.join(stickyNoteDir, "environment");
767
+ const envManifestDest = path.join(envDir, "manifest.json");
768
+ mkdirSafe(path.join(envDir, "skills"));
769
+ mkdirSafe(path.join(envDir, "agents"));
770
+ mkdirSafe(path.join(envDir, "commands"));
771
+ if (!fs.existsSync(envManifestDest)) {
772
+ // Build manifest from detected MCP servers and skills
773
+ const manifestData = { version: "1", mcp_servers: {}, permissions: [], env_vars: {} };
774
+ for (const server of mcpServers) {
775
+ const entry = { type: server.type || "stdio", description: server.name, required: false };
776
+ if (server.command) entry.command = server.command;
777
+ if (server.args) entry.args = server.args;
778
+ if (server.env) {
779
+ entry.env = {};
780
+ for (const [k, v] of Object.entries(server.env)) {
781
+ entry.env[k] = `\${${k}}`;
782
+ manifestData.env_vars[k] = { description: k, required: true };
783
+ }
784
+ }
785
+ manifestData.mcp_servers[server.name] = entry;
786
+ }
787
+ fs.writeFileSync(envManifestDest, JSON.stringify(manifestData, null, 2) + "\n");
788
+ if (mcpServers.length > 0) {
789
+ print(` [OK] Manifest populated with ${mcpServers.length} detected MCP server(s)`);
790
+ }
791
+ // Copy detected skills into environment/skills/
792
+ for (const skillName of skills) {
793
+ const claudeSkillPath = path.join(process.cwd(), ".claude", "plugins");
794
+ const possibleSkillPaths = [
795
+ path.join(claudeSkillPath, skillName, "SKILL.md"),
796
+ path.join(claudeSkillPath, skillName, "skills", skillName, "SKILL.md"),
797
+ ];
798
+ for (const sp of possibleSkillPaths) {
799
+ if (fs.existsSync(sp)) {
800
+ fs.copyFileSync(sp, path.join(envDir, "skills", skillName + ".md"));
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ if (skills.length > 0) {
806
+ print(` [OK] Copied ${skills.length} detected skill(s) to environment/skills/`);
807
+ }
808
+ }
809
+ print(" [OK] Created environment directory (.sticky-note/environment/)");
810
+
764
811
  // Force-add .claude/hooks/ and .claude/settings.json so git tracks them
765
812
  // (needed if .claude/ was previously in .gitignore)
766
813
  try {
@@ -2430,6 +2477,536 @@ function cmdSync() {
2430
2477
  print("");
2431
2478
  }
2432
2479
 
2480
+ // MCP-SERVER command — launch the stdio MCP server
2481
+ function cmdMcpServer() {
2482
+ const mcpServerPath = path.join(__dirname, "mcp-server.js");
2483
+ if (!fs.existsSync(mcpServerPath)) {
2484
+ print(" [ERR] mcp-server.js not found");
2485
+ process.exit(1);
2486
+ }
2487
+ // Forward to the MCP server script, passing remaining args + --project
2488
+ const args = ["--project", process.cwd(), ...process.argv.slice(3)];
2489
+ const { execFileSync } = require("child_process");
2490
+ try {
2491
+ execFileSync(process.execPath, [mcpServerPath, ...args], {
2492
+ stdio: "inherit",
2493
+ });
2494
+ } catch (err) {
2495
+ if (err.status) process.exit(err.status);
2496
+ }
2497
+ }
2498
+
2499
+ // ──────────────────────────────────────────────
2500
+ // bootstrap — interactive MCP server provisioning with secret resolution
2501
+ // ──────────────────────────────────────────────
2502
+
2503
+ async function cmdBootstrap() {
2504
+ const cwd = process.cwd();
2505
+ const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
2506
+ const mcpJsonPath = path.join(cwd, ".mcp.json");
2507
+ const dotEnvPath = path.join(cwd, ".env");
2508
+ const dotEnvExamplePath = path.join(cwd, ".env.example");
2509
+
2510
+ printBanner();
2511
+ print(" 🔧 Bootstrap — MCP Server Provisioning\n");
2512
+
2513
+ // 1. Read manifest
2514
+ if (!fs.existsSync(manifestPath)) {
2515
+ print(" No environment manifest found. Run `npx sticky-note init` first.");
2516
+ return;
2517
+ }
2518
+
2519
+ const manifest = readJsonSafe(manifestPath, null);
2520
+ if (!manifest) {
2521
+ print(" [ERR] Failed to parse manifest.json");
2522
+ return;
2523
+ }
2524
+
2525
+ const mcpServers = manifest.mcp_servers || {};
2526
+ const envVarsMeta = manifest.env_vars || {};
2527
+ const serverEntries = Object.entries(mcpServers);
2528
+
2529
+ if (serverEntries.length === 0) {
2530
+ print(" No MCP servers defined in manifest. Nothing to provision.");
2531
+ return;
2532
+ }
2533
+
2534
+ // 2. Find all ${ENV_VAR} placeholders across server configs
2535
+ const placeholders = new Set();
2536
+ for (const [, config] of serverEntries) {
2537
+ const envEntries = config.env || {};
2538
+ for (const val of Object.values(envEntries)) {
2539
+ if (typeof val === "string" && val.startsWith("${") && val.endsWith("}")) {
2540
+ placeholders.add(val.slice(2, -1));
2541
+ }
2542
+ }
2543
+ }
2544
+
2545
+ // 3. Parse .env file
2546
+ function parseDotEnv(filePath) {
2547
+ const vars = {};
2548
+ try {
2549
+ const content = fs.readFileSync(filePath, "utf-8");
2550
+ for (const line of content.split("\n")) {
2551
+ const trimmed = line.trim();
2552
+ if (!trimmed || trimmed.startsWith("#")) continue;
2553
+ const eqIdx = trimmed.indexOf("=");
2554
+ if (eqIdx === -1) continue;
2555
+ const key = trimmed.slice(0, eqIdx).trim();
2556
+ let value = trimmed.slice(eqIdx + 1).trim();
2557
+ // Strip surrounding quotes
2558
+ if ((value.startsWith('"') && value.endsWith('"')) ||
2559
+ (value.startsWith("'") && value.endsWith("'"))) {
2560
+ value = value.slice(1, -1);
2561
+ }
2562
+ vars[key] = value;
2563
+ }
2564
+ } catch {
2565
+ // .env doesn't exist — that's fine
2566
+ }
2567
+ return vars;
2568
+ }
2569
+
2570
+ const dotEnvVars = parseDotEnv(dotEnvPath);
2571
+
2572
+ // 4. Resolve each placeholder
2573
+ const resolved = {};
2574
+ const promptedVars = {};
2575
+
2576
+ if (placeholders.size > 0) {
2577
+ print(` Found ${placeholders.size} environment variable(s) to resolve:\n`);
2578
+
2579
+ const rl = readline.createInterface({
2580
+ input: process.stdin,
2581
+ output: process.stdout,
2582
+ });
2583
+
2584
+ for (const varName of placeholders) {
2585
+ const meta = envVarsMeta[varName] || {};
2586
+ const desc = meta.description || varName;
2587
+ const docsUrl = meta.docs_url || null;
2588
+
2589
+ // Priority: process.env → .env file → interactive prompt
2590
+ if (process.env[varName]) {
2591
+ resolved[varName] = process.env[varName];
2592
+ print(` ✅ ${varName}: found in shell environment`);
2593
+ } else if (dotEnvVars[varName]) {
2594
+ resolved[varName] = dotEnvVars[varName];
2595
+ print(` ✅ ${varName}: found in .env file`);
2596
+ } else {
2597
+ // Interactive prompt
2598
+ if (docsUrl) {
2599
+ print(` 📖 ${desc}`);
2600
+ print(` Docs: ${docsUrl}`);
2601
+ }
2602
+ const value = await ask(rl, `Enter value for ${varName}`, "");
2603
+ if (value) {
2604
+ resolved[varName] = value;
2605
+ promptedVars[varName] = value;
2606
+ print(` ✅ ${varName}: set via prompt`);
2607
+ } else {
2608
+ print(` ⚠️ ${varName}: skipped (no value provided)`);
2609
+ }
2610
+ }
2611
+ }
2612
+
2613
+ // 5. Offer to save prompted values to .env
2614
+ if (Object.keys(promptedVars).length > 0) {
2615
+ print("");
2616
+ const saveChoice = await ask(rl, "Save missing values to .env?", "yes");
2617
+ if (saveChoice.toLowerCase() === "yes" || saveChoice.toLowerCase() === "y") {
2618
+ let envContent = "";
2619
+ try {
2620
+ envContent = fs.readFileSync(dotEnvPath, "utf-8");
2621
+ if (!envContent.endsWith("\n")) envContent += "\n";
2622
+ } catch {
2623
+ // .env doesn't exist yet
2624
+ }
2625
+ for (const [key, val] of Object.entries(promptedVars)) {
2626
+ envContent += `${key}=${val}\n`;
2627
+ }
2628
+ fs.writeFileSync(dotEnvPath, envContent, "utf-8");
2629
+ print(` ✅ Saved ${Object.keys(promptedVars).length} variable(s) to .env`);
2630
+ }
2631
+ }
2632
+
2633
+ rl.close();
2634
+ }
2635
+
2636
+ // 6. Provision MCP servers to .mcp.json
2637
+ let mcpJson = { mcpServers: {} };
2638
+ try {
2639
+ mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
2640
+ mcpJson.mcpServers = mcpJson.mcpServers || {};
2641
+ } catch {
2642
+ // .mcp.json doesn't exist or is invalid — start fresh
2643
+ }
2644
+
2645
+ let provisioned = 0;
2646
+ let skipped = 0;
2647
+
2648
+ for (const [name, config] of serverEntries) {
2649
+ // Build the server entry with resolved env vars
2650
+ const serverEntry = {
2651
+ type: config.type || "stdio",
2652
+ command: config.command,
2653
+ args: config.args || [],
2654
+ };
2655
+
2656
+ if (config.env) {
2657
+ const resolvedEnv = {};
2658
+ let hasUnresolved = false;
2659
+ for (const [envKey, envVal] of Object.entries(config.env)) {
2660
+ if (typeof envVal === "string" && envVal.startsWith("${") && envVal.endsWith("}")) {
2661
+ const varName = envVal.slice(2, -1);
2662
+ if (resolved[varName]) {
2663
+ resolvedEnv[envKey] = resolved[varName];
2664
+ } else {
2665
+ hasUnresolved = true;
2666
+ }
2667
+ } else {
2668
+ resolvedEnv[envKey] = envVal;
2669
+ }
2670
+ }
2671
+ if (hasUnresolved) {
2672
+ print(` ⚠️ ${name}: skipped (unresolved secrets)`);
2673
+ skipped++;
2674
+ continue;
2675
+ }
2676
+ serverEntry.env = resolvedEnv;
2677
+ }
2678
+
2679
+ mcpJson.mcpServers[name] = serverEntry;
2680
+ provisioned++;
2681
+ }
2682
+
2683
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
2684
+
2685
+ // 7. Generate .env.example from manifest env_vars
2686
+ if (Object.keys(envVarsMeta).length > 0) {
2687
+ const lines = [
2688
+ "# Environment variables for this project",
2689
+ "# Generated by sticky-note bootstrap",
2690
+ "#",
2691
+ "# Copy this file to .env and fill in your values.",
2692
+ "",
2693
+ ];
2694
+ for (const [varName, meta] of Object.entries(envVarsMeta)) {
2695
+ if (meta.description) lines.push(`# ${meta.description}`);
2696
+ if (meta.docs_url) lines.push(`# Docs: ${meta.docs_url}`);
2697
+ if (meta.required === false) {
2698
+ lines.push(`# ${varName}=`);
2699
+ } else {
2700
+ lines.push(`${varName}=`);
2701
+ }
2702
+ lines.push("");
2703
+ }
2704
+ fs.writeFileSync(dotEnvExamplePath, lines.join("\n"), "utf-8");
2705
+ print(` ✅ Generated .env.example`);
2706
+ }
2707
+
2708
+ // 8. Print summary
2709
+ print("");
2710
+ print(" ── Summary ──");
2711
+ print(` Provisioned: ${provisioned} server(s)`);
2712
+ if (skipped > 0) print(` Skipped: ${skipped} server(s) (missing secrets)`);
2713
+ print(` Config: ${mcpJsonPath}`);
2714
+ print("");
2715
+ }
2716
+
2717
+ // ──────────────────────────────────────────────
2718
+ // env — environment management subcommands
2719
+ // ──────────────────────────────────────────────
2720
+
2721
+ async function cmdEnv() {
2722
+ const args = process.argv.slice(3);
2723
+ const subcommand = args[0];
2724
+
2725
+ switch (subcommand) {
2726
+ case "status":
2727
+ cmdEnvStatus();
2728
+ break;
2729
+ case "add-server":
2730
+ await cmdEnvAddServer();
2731
+ break;
2732
+ case "add-plugin":
2733
+ await cmdEnvAddPlugin();
2734
+ break;
2735
+ default:
2736
+ printBanner();
2737
+ print(" Usage: npx sticky-note env <subcommand>\n");
2738
+ print(" Subcommands:");
2739
+ print(" status Show what's provisioned vs missing vs needs-secrets");
2740
+ print(" add-server Interactively add an MCP server to the manifest");
2741
+ print(" add-plugin Interactively add a plugin to the manifest");
2742
+ print("");
2743
+ break;
2744
+ }
2745
+ }
2746
+
2747
+ function cmdEnvStatus() {
2748
+ const cwd = process.cwd();
2749
+ const envDir = path.join(cwd, ".sticky-note", "environment");
2750
+ const manifestPath = path.join(envDir, "manifest.json");
2751
+ const mcpJsonPath = path.join(cwd, ".mcp.json");
2752
+
2753
+ printBanner();
2754
+ print(" 📊 Environment Status\n");
2755
+
2756
+ if (!fs.existsSync(manifestPath)) {
2757
+ print(" No environment manifest found. Run `npx sticky-note init` first.");
2758
+ print("");
2759
+ return;
2760
+ }
2761
+
2762
+ const manifest = readJsonSafe(manifestPath, {});
2763
+ const mcpConfig = readJsonSafe(mcpJsonPath, { mcpServers: {} });
2764
+ const registeredServers = Object.keys(mcpConfig.mcpServers || {});
2765
+
2766
+ // MCP servers status
2767
+ const mcpServers = manifest.mcp_servers || {};
2768
+ const serverNames = Object.keys(mcpServers);
2769
+
2770
+ if (serverNames.length === 0) {
2771
+ print(" MCP Servers: (none defined)");
2772
+ } else {
2773
+ print(" MCP Servers:");
2774
+ for (const name of serverNames) {
2775
+ const config = mcpServers[name];
2776
+ const isProvisioned = registeredServers.includes(name);
2777
+
2778
+ if (isProvisioned) {
2779
+ print(` ✅ ${name} — provisioned`);
2780
+ } else {
2781
+ // Check if it needs secrets
2782
+ const envEntries = config.env || {};
2783
+ const needsSecrets = Object.values(envEntries).some(
2784
+ (val) => typeof val === "string" && val.startsWith("${") && val.endsWith("}")
2785
+ );
2786
+ if (needsSecrets) {
2787
+ print(` 🔑 ${name} — needs secrets (run \`npx sticky-note bootstrap\`)`);
2788
+ } else {
2789
+ print(` ❌ ${name} — not provisioned`);
2790
+ }
2791
+ }
2792
+ }
2793
+ }
2794
+
2795
+ // Skills, Agents, Commands counts
2796
+ const skillsDir = path.join(envDir, "skills");
2797
+ const agentsDir = path.join(envDir, "agents");
2798
+ const commandsDir = path.join(envDir, "commands");
2799
+
2800
+ function countMdFiles(dir) {
2801
+ try {
2802
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).length;
2803
+ } catch {
2804
+ return 0;
2805
+ }
2806
+ }
2807
+
2808
+ const skillsCount = countMdFiles(skillsDir);
2809
+ const agentsCount = countMdFiles(agentsDir);
2810
+ const commandsCount = countMdFiles(commandsDir);
2811
+
2812
+ print("");
2813
+ print(" Resources:");
2814
+ print(` Skills: ${skillsCount}`);
2815
+ print(` Agents: ${agentsCount}`);
2816
+ print(` Commands: ${commandsCount}`);
2817
+
2818
+ // Env vars status
2819
+ const envVars = manifest.env_vars || {};
2820
+ const envVarNames = Object.keys(envVars);
2821
+
2822
+ if (envVarNames.length > 0) {
2823
+ print("");
2824
+ print(" Environment Variables:");
2825
+
2826
+ // Parse .env file for checking
2827
+ const dotEnvPath = path.join(cwd, ".env");
2828
+ const dotEnvVars = {};
2829
+ try {
2830
+ const content = fs.readFileSync(dotEnvPath, "utf-8");
2831
+ for (const line of content.split("\n")) {
2832
+ const trimmed = line.trim();
2833
+ if (!trimmed || trimmed.startsWith("#")) continue;
2834
+ const eqIdx = trimmed.indexOf("=");
2835
+ if (eqIdx === -1) continue;
2836
+ dotEnvVars[trimmed.slice(0, eqIdx).trim()] = true;
2837
+ }
2838
+ } catch {
2839
+ // no .env
2840
+ }
2841
+
2842
+ for (const varName of envVarNames) {
2843
+ const meta = envVars[varName];
2844
+ const inEnv = !!process.env[varName];
2845
+ const inDotEnv = !!dotEnvVars[varName];
2846
+ const reqLabel = meta.required === false ? " (optional)" : "";
2847
+
2848
+ if (inEnv) {
2849
+ print(` ✅ ${varName} — set in environment${reqLabel}`);
2850
+ } else if (inDotEnv) {
2851
+ print(` ✅ ${varName} — set in .env${reqLabel}`);
2852
+ } else {
2853
+ print(` ❌ ${varName} — not found${reqLabel}`);
2854
+ }
2855
+ }
2856
+ }
2857
+
2858
+ print("");
2859
+ }
2860
+
2861
+ async function cmdEnvAddServer() {
2862
+ const cwd = process.cwd();
2863
+ const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
2864
+
2865
+ printBanner();
2866
+ print(" ➕ Add MCP Server to Manifest\n");
2867
+
2868
+ if (!fs.existsSync(manifestPath)) {
2869
+ print(" No environment manifest found. Run `npx sticky-note init` first.");
2870
+ return;
2871
+ }
2872
+
2873
+ const manifest = readJsonSafe(manifestPath, null);
2874
+ if (!manifest) {
2875
+ print(" [ERR] Failed to parse manifest.json");
2876
+ return;
2877
+ }
2878
+
2879
+ const rl = readline.createInterface({
2880
+ input: process.stdin,
2881
+ output: process.stdout,
2882
+ });
2883
+
2884
+ const name = await ask(rl, "Server name (e.g. github, context7)", "");
2885
+ if (!name) {
2886
+ print(" [ERR] Server name is required.");
2887
+ rl.close();
2888
+ return;
2889
+ }
2890
+
2891
+ const command = await ask(rl, "Command (e.g. npx, node)", "npx");
2892
+ const argsRaw = await ask(rl, "Args (comma-separated)", "");
2893
+ const description = await ask(rl, "Description", "");
2894
+ const requiredRaw = await ask(rl, "Required?", "yes");
2895
+ const required = requiredRaw.toLowerCase() === "yes" || requiredRaw.toLowerCase() === "y";
2896
+
2897
+ const args = argsRaw
2898
+ ? argsRaw.split(",").map((a) => a.trim()).filter(Boolean)
2899
+ : [];
2900
+
2901
+ const serverConfig = {
2902
+ type: "stdio",
2903
+ command,
2904
+ args,
2905
+ description,
2906
+ required,
2907
+ };
2908
+
2909
+ // Ask about env vars
2910
+ const needsEnv = await ask(rl, "Does this server need environment variables?", "no");
2911
+ if (needsEnv.toLowerCase() === "yes" || needsEnv.toLowerCase() === "y") {
2912
+ serverConfig.env = {};
2913
+ let addMore = true;
2914
+ while (addMore) {
2915
+ const envName = await ask(rl, " Env var name (e.g. GITHUB_TOKEN)", "");
2916
+ if (!envName) break;
2917
+
2918
+ const envDesc = await ask(rl, " Description", "");
2919
+ const envDocs = await ask(rl, " Docs URL (optional)", "");
2920
+ const envReqRaw = await ask(rl, " Required?", "yes");
2921
+ const envRequired = envReqRaw.toLowerCase() === "yes" || envReqRaw.toLowerCase() === "y";
2922
+
2923
+ // Add placeholder to server env
2924
+ serverConfig.env[envName] = `\${${envName}}`;
2925
+
2926
+ // Add to env_vars metadata
2927
+ manifest.env_vars = manifest.env_vars || {};
2928
+ manifest.env_vars[envName] = {
2929
+ description: envDesc || envName,
2930
+ required: envRequired,
2931
+ };
2932
+ if (envDocs) manifest.env_vars[envName].docs_url = envDocs;
2933
+
2934
+ const moreRaw = await ask(rl, " Add another env var?", "no");
2935
+ addMore = moreRaw.toLowerCase() === "yes" || moreRaw.toLowerCase() === "y";
2936
+ }
2937
+ }
2938
+
2939
+ rl.close();
2940
+
2941
+ // Write to manifest
2942
+ manifest.mcp_servers = manifest.mcp_servers || {};
2943
+ manifest.mcp_servers[name] = serverConfig;
2944
+
2945
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
2946
+
2947
+ print("");
2948
+ print(` ✅ Added server "${name}" to manifest.json`);
2949
+ print(` Run \`npx sticky-note bootstrap\` to provision it.`);
2950
+ print("");
2951
+ }
2952
+
2953
+ async function cmdEnvAddPlugin() {
2954
+ const cwd = process.cwd();
2955
+ const manifestPath = path.join(cwd, ".sticky-note", "environment", "manifest.json");
2956
+
2957
+ printBanner();
2958
+ print(" ➕ Add Plugin to Manifest\n");
2959
+
2960
+ if (!fs.existsSync(manifestPath)) {
2961
+ print(" No environment manifest found. Run `npx sticky-note init` first.");
2962
+ return;
2963
+ }
2964
+
2965
+ const manifest = readJsonSafe(manifestPath, null);
2966
+ if (!manifest) {
2967
+ print(" [ERR] Failed to parse manifest.json");
2968
+ return;
2969
+ }
2970
+
2971
+ const rl = readline.createInterface({
2972
+ input: process.stdin,
2973
+ output: process.stdout,
2974
+ });
2975
+
2976
+ const name = await ask(rl, "Plugin name (e.g. docker, eslint-helper)", "");
2977
+ if (!name) {
2978
+ print(" [ERR] Plugin name is required.");
2979
+ rl.close();
2980
+ return;
2981
+ }
2982
+
2983
+ const source = await ask(rl, "Source (e.g. registry:docker-copilot-ext, github:user/repo)", "");
2984
+ if (!source) {
2985
+ print(" [ERR] Plugin source is required.");
2986
+ rl.close();
2987
+ return;
2988
+ }
2989
+
2990
+ const description = await ask(rl, "Description", "");
2991
+ const requiredRaw = await ask(rl, "Required?", "no");
2992
+ const required = requiredRaw.toLowerCase() === "yes" || requiredRaw.toLowerCase() === "y";
2993
+
2994
+ rl.close();
2995
+
2996
+ manifest.plugins = manifest.plugins || {};
2997
+ manifest.plugins[name] = {
2998
+ source,
2999
+ description,
3000
+ required,
3001
+ };
3002
+
3003
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
3004
+
3005
+ print("");
3006
+ print(` ✅ Added plugin "${name}" to manifest.json`);
3007
+ print("");
3008
+ }
3009
+
2433
3010
  // ──────────────────────────────────────────────
2434
3011
  // Main
2435
3012
  // ──────────────────────────────────────────────
@@ -2487,6 +3064,15 @@ async function main() {
2487
3064
  case "sync":
2488
3065
  cmdSync();
2489
3066
  break;
3067
+ case "bootstrap":
3068
+ await cmdBootstrap();
3069
+ break;
3070
+ case "env":
3071
+ await cmdEnv();
3072
+ break;
3073
+ case "mcp-server":
3074
+ cmdMcpServer();
3075
+ break;
2490
3076
  case "--version":
2491
3077
  case "-v":
2492
3078
  print(`sticky-note v${VERSION}`);
@@ -2513,6 +3099,9 @@ async function main() {
2513
3099
  print(" overlap Detect file overlaps with open/stuck threads");
2514
3100
  print(" claim Claim ownership of files you're working on (--list, --clear)");
2515
3101
  print(" sync Commit .sticky-note/ changes to git (--push to also push)");
3102
+ print(" bootstrap Provision MCP servers with secrets from manifest");
3103
+ print(" env Environment management (status, add-server, add-plugin)");
3104
+ print(" mcp-server Launch the sticky-note MCP server (stdio JSON-RPC)");
2516
3105
  print("");
2517
3106
  print(" Options:");
2518
3107
  print(" --version Show version");