set-prompt 0.8.0 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.8.1] - 2026-05-02
8
+
9
+ ### Changed
10
+ - **Hermes auto-enable** — `link hermes` now writes `set-prompt` into the existing `~/.hermes/config.yaml` automatically, matching Claude Code / Codex behavior. Previously the file was left untouched and only a manual-merge snippet was printed, which left the plugin detected-but-disabled (`× set-prompt — not enabled in config`).
11
+ - Existing `plugins.enabled` list → `set-prompt` is appended (other entries preserved).
12
+ - `plugins` map exists but no `enabled` key → `enabled: [set-prompt]` is added.
13
+ - `plugins` key missing entirely → `plugins.enabled: [set-prompt]` is added.
14
+ - YAML comments and formatting are preserved (AST-based modification via `yaml` package).
15
+ - Atomic write with timestamped backup + rollback on failure (same pattern as `link claudecode` / `link codex`).
16
+ - **`unlinkHermes` surgical removal** — for user-authored `config.yaml` (no `# Generated by set-prompt` header), only the `- set-prompt` entry is removed; the rest of the file is preserved. Auto-generated configs are still deleted whole.
17
+
18
+ ### Added
19
+ - `yaml` (eemeli/yaml) dependency for AST-based YAML editing.
20
+ - Tests for the new auto-enable branches: `plugins` key absent, `plugins.enabled` absent, comment preservation, and `unlinkHermes` surgical removal (17 cases total in `link-hermes.test.ts`).
21
+
22
+ ---
23
+
7
24
  ## [0.8.0] - 2026-05-02
8
25
 
9
26
  ### Added
package/README.md CHANGED
@@ -123,7 +123,7 @@ The interactive mode shows all agents with their current state. **Check to link,
123
123
 
124
124
  > **Note on Gemini CLI**: Skills follow the standard `skills/<name>/SKILL.md` pattern. Commands use `.toml` format (not `.md`) and agents use `.md` with YAML frontmatter. Files in your repo's `commands/` must be TOML for Gemini CLI to recognize them. **Agents have strict frontmatter validation** — unknown keys (e.g. `allowed-tools`, `color`, `mode` from other platforms) cause Gemini CLI to reject the agent. See `SET_PROMPT_GUIDE.md` for the allowed keys.
125
125
 
126
- > **Note on Hermes**: Hermes plugins must register skills/commands/hooks programmatically — directory drop-ins are not auto-discovered. set-prompt generates a small Python adapter (`~/.hermes/plugins/set-prompt/__init__.py`) with the repo's absolute path baked in, so no symlinks are needed. **Restart Hermes after `link` (or after adding/removing a skill in your repo)** — `register()` runs only once at Hermes startup. Activation requires `set-prompt` listed under `plugins.enabled` in `~/.hermes/config.yaml` — set-prompt creates this file if absent but never modifies an existing one (it prints the snippet to add manually). Hooks are observation-only on the Hermes side: the same `hooks/hooks.json` is reused, but Hermes events (`pre_tool_call`, `on_session_start`, etc.) are picked up while Claude/Cursor keys are ignored. Hook scripts can reference `${SET_PROMPT_REPO}`.
126
+ > **Note on Hermes**: Hermes plugins must register skills/commands/hooks programmatically — directory drop-ins are not auto-discovered. set-prompt generates a small Python adapter (`~/.hermes/plugins/set-prompt/__init__.py`) with the repo's absolute path baked in, so no symlinks are needed. **Restart Hermes after `link` (or after adding/removing a skill in your repo)** — `register()` runs only once at Hermes startup. Activation requires `set-prompt` listed under `plugins.enabled` in `~/.hermes/config.yaml` — set-prompt writes this entry automatically (creating the file when absent, or appending to the existing list while preserving other entries and comments via AST-based YAML editing with backup/rollback). Hooks are observation-only on the Hermes side: the same `hooks/hooks.json` is reused, but Hermes events (`pre_tool_call`, `on_session_start`, etc.) are picked up while Claude/Cursor keys are ignored. Hook scripts can reference `${SET_PROMPT_REPO}`.
127
127
 
128
128
  ---
129
129
 
@@ -255,7 +255,7 @@ set-prompt uninstall
255
255
  - **Cursor** — replaces directories and `mcp.json` in `~/.cursor/`
256
256
  - **OpenCode** — replaces directories in `~/.config/opencode/`
257
257
  - **Gemini CLI** — replaces directories in `~/.gemini/` (`antigravity/` subtree untouched)
258
- - **Hermes** — generates `~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}` and writes `~/.hermes/config.yaml` only when absent (existing files are never auto-modified)
258
+ - **Hermes** — generates `~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}` and adds `set-prompt` to `~/.hermes/config.yaml` under `plugins.enabled` (creates the file when absent; appends to the existing list while preserving other entries and comments)
259
259
 
260
260
  Before making any changes, `set-prompt` creates a backup and rolls back automatically on failure. However, you should be aware that:
261
261
 
package/dist/index.js CHANGED
@@ -2490,6 +2490,7 @@ import path12 from "path";
2490
2490
  import fs13 from "fs";
2491
2491
  import chalk14 from "chalk";
2492
2492
  import { confirm as confirm11 } from "@inquirer/prompts";
2493
+ import YAML from "yaml";
2493
2494
  var PLUGIN_YAML = `name: ${MARKET_NAME}
2494
2495
  version: "1.0"
2495
2496
  description: Managed by set-prompt
@@ -2630,13 +2631,43 @@ plugins:
2630
2631
  enabled:
2631
2632
  - ${MARKET_NAME}
2632
2633
  `;
2633
- var ENABLE_SNIPPET = `plugins:
2634
- enabled:
2635
- - ${MARKET_NAME}`;
2636
2634
  var writePluginManifest = (repoPath) => {
2637
2635
  fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "plugin.yaml"), PLUGIN_YAML, "utf-8");
2638
2636
  fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "__init__.py"), buildInitPy(repoPath), "utf-8");
2639
2637
  };
2638
+ var writeWithBackup = (filePath, content) => {
2639
+ let backupPath = null;
2640
+ if (fs13.existsSync(filePath)) {
2641
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2642
+ backupPath = `${filePath}.bak.${timestamp}`;
2643
+ try {
2644
+ fs13.copyFileSync(filePath, backupPath);
2645
+ } catch {
2646
+ backupPath = null;
2647
+ }
2648
+ }
2649
+ try {
2650
+ fs13.mkdirSync(path12.dirname(filePath), { recursive: true });
2651
+ fs13.writeFileSync(filePath, content, "utf-8");
2652
+ } catch (ex) {
2653
+ if (backupPath !== null) {
2654
+ try {
2655
+ fs13.copyFileSync(backupPath, filePath);
2656
+ fs13.unlinkSync(backupPath);
2657
+ console.warn(chalk14.yellow(" \u26A0 Write failed \u2014 rolled back to original."));
2658
+ } catch {
2659
+ console.error(chalk14.red(` \u274C Rollback failed. Backup preserved at: ${backupPath}`));
2660
+ }
2661
+ }
2662
+ throw ex;
2663
+ }
2664
+ if (backupPath !== null) {
2665
+ try {
2666
+ fs13.unlinkSync(backupPath);
2667
+ } catch {
2668
+ }
2669
+ }
2670
+ };
2640
2671
  var ensureHermesConfigEnabled = () => {
2641
2672
  if (fs13.existsSync(HERMES_CONFIG_PATH) === false) {
2642
2673
  fs13.mkdirSync(path12.dirname(HERMES_CONFIG_PATH), { recursive: true });
@@ -2645,17 +2676,54 @@ var ensureHermesConfigEnabled = () => {
2645
2676
  return;
2646
2677
  }
2647
2678
  const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
2648
- const enabledPattern = new RegExp(`^\\s*-\\s+${MARKET_NAME}\\s*$`, "m");
2649
- if (enabledPattern.test(content)) {
2679
+ let doc;
2680
+ try {
2681
+ doc = YAML.parseDocument(content);
2682
+ } catch (ex) {
2683
+ console.warn(chalk14.yellow(` \u26A0 Failed to parse Hermes config (${ex.message}) \u2014 leaving file untouched.`));
2684
+ console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
2685
+ return;
2686
+ }
2687
+ if (doc.errors.length > 0) {
2688
+ console.warn(chalk14.yellow(` \u26A0 Hermes config has YAML errors \u2014 leaving file untouched.`));
2689
+ console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
2690
+ return;
2691
+ }
2692
+ let plugins = doc.get("plugins");
2693
+ if (plugins == null) {
2694
+ doc.set("plugins", doc.createNode({ enabled: [MARKET_NAME] }));
2695
+ writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2696
+ console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
2697
+ return;
2698
+ }
2699
+ if (!YAML.isMap(plugins)) {
2700
+ console.warn(chalk14.yellow(` \u26A0 "plugins" is not a map \u2014 leaving file untouched.`));
2701
+ console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
2702
+ return;
2703
+ }
2704
+ let enabled = plugins.get("enabled");
2705
+ if (enabled == null) {
2706
+ plugins.set("enabled", doc.createNode([MARKET_NAME]));
2707
+ writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2708
+ console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
2709
+ return;
2710
+ }
2711
+ if (!YAML.isSeq(enabled)) {
2712
+ console.warn(chalk14.yellow(` \u26A0 "plugins.enabled" is not a list \u2014 leaving file untouched.`));
2713
+ console.log(chalk14.dim(` File: ${HERMES_CONFIG_PATH}`));
2714
+ return;
2715
+ }
2716
+ const alreadyListed = enabled.items.some((item) => {
2717
+ const v = YAML.isScalar(item) ? item.value : item;
2718
+ return v === MARKET_NAME;
2719
+ });
2720
+ if (alreadyListed) {
2650
2721
  console.log(chalk14.dim(` \u2713 ${MARKET_NAME} already listed in ${HERMES_CONFIG_PATH}`));
2651
2722
  return;
2652
2723
  }
2653
- console.log(chalk14.yellow(`
2654
- \u26A0 Hermes config exists but does not list "${MARKET_NAME}" as enabled.`));
2655
- console.log(chalk14.dim(" Add the following entry under plugins.enabled to activate this plugin:\n"));
2656
- console.log(chalk14.cyan(ENABLE_SNIPPET.split("\n").map((l) => ` ${l}`).join("\n")));
2657
- console.log(chalk14.dim(`
2658
- File: ${HERMES_CONFIG_PATH}`));
2724
+ enabled.add(MARKET_NAME);
2725
+ writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2726
+ console.log(chalk14.green(" \u2713 ") + chalk14.dim(`enabled "${MARKET_NAME}" in ${HERMES_CONFIG_PATH}`));
2659
2727
  };
2660
2728
  var linkHermes = async () => {
2661
2729
  const repoPath = resolveRepoPath();
@@ -2702,14 +2770,44 @@ Removing Hermes plugin...`));
2702
2770
  if (content.trimStart().startsWith("# Generated by set-prompt")) {
2703
2771
  fs13.unlinkSync(HERMES_CONFIG_PATH);
2704
2772
  console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_CONFIG_PATH}`));
2705
- } else if (new RegExp(`^\\s*-\\s+${MARKET_NAME}\\s*$`, "m").test(content)) {
2706
- console.log(chalk14.yellow(` \u26A0 Hermes config still lists "${MARKET_NAME}" \u2014 remove it manually:`));
2707
- console.log(chalk14.dim(` ${HERMES_CONFIG_PATH}`));
2773
+ } else {
2774
+ removeMarketEntryFromConfig();
2708
2775
  }
2709
2776
  }
2710
2777
  configManager.hermes = null;
2711
2778
  configManager.save();
2712
2779
  };
2780
+ var removeMarketEntryFromConfig = () => {
2781
+ const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
2782
+ let doc;
2783
+ try {
2784
+ doc = YAML.parseDocument(content);
2785
+ } catch (ex) {
2786
+ console.warn(chalk14.yellow(` \u26A0 Failed to parse Hermes config (${ex.message}) \u2014 leaving file untouched.`));
2787
+ return;
2788
+ }
2789
+ if (doc.errors.length > 0) {
2790
+ return;
2791
+ }
2792
+ const plugins = doc.get("plugins");
2793
+ if (!YAML.isMap(plugins)) {
2794
+ return;
2795
+ }
2796
+ const enabled = plugins.get("enabled");
2797
+ if (!YAML.isSeq(enabled)) {
2798
+ return;
2799
+ }
2800
+ const idx = enabled.items.findIndex((item) => {
2801
+ const v = YAML.isScalar(item) ? item.value : item;
2802
+ return v === MARKET_NAME;
2803
+ });
2804
+ if (idx === -1) {
2805
+ return;
2806
+ }
2807
+ enabled.delete(idx);
2808
+ writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2809
+ console.log(chalk14.red(" removed") + chalk14.dim(` "${MARKET_NAME}" from: ${HERMES_CONFIG_PATH}`));
2810
+ };
2713
2811
 
2714
2812
  // src/commands/link-command.ts
2715
2813
  var LINK_MAP = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "set-prompt",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "One repo. Every AI coding tool. Always in sync.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -52,6 +52,7 @@
52
52
  "ora": "^9.3.0",
53
53
  "smol-toml": "^1.6.0",
54
54
  "typia": "^11.0.3",
55
+ "yaml": "^2.8.4",
55
56
  "zod": "^4.3.6"
56
57
  },
57
58
  "devDependencies": {