set-prompt 0.8.1 → 0.8.3

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,36 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.8.3] - 2026-05-02
8
+
9
+ ### Changed
10
+ - **Hermes plugin renamed `set-prompt` → `sppt`** for ergonomics. Plugin skills are loaded via `skill_view("sppt:<name>")` instead of the longer `skill_view("set-prompt:<name>")`. Internally aligned with the rest of set-prompt where every other agent already uses `PLUGIN_NAME = "sppt"` as the plugin identifier (Claude Code, Codex, etc.) — Hermes was the lone outlier still using `MARKET_NAME = "set-prompt"`.
11
+ - Plugin directory: `~/.hermes/plugins/set-prompt/` → `~/.hermes/plugins/sppt/`
12
+ - `plugin.yaml` `name`: `set-prompt` → `sppt`
13
+ - `~/.hermes/config.yaml` `plugins.enabled` entry: `- set-prompt` → `- sppt`
14
+
15
+ ### Added
16
+ - **Post-link guidance** — `set-prompt link hermes` now prints a hint explaining that plugin skills don't appear in `/skills list` (Hermes policy) and shows the explicit qualified-load form: `skill_view("sppt:<skill-name>")`. This addresses the common confusion of "the link succeeded but my skills aren't showing up" — they're registered, just opt-in via qualified namespace as documented by Hermes.
17
+
18
+ ---
19
+
20
+ ## [0.8.2] - 2026-05-02
21
+
22
+ ### Fixed
23
+ - **Hermes `register_skill` argument types** — fixed `'str' object has no attribute 'exists'` error that surfaced after enabling the plugin in Hermes. The generated `__init__.py` was passing `str(skill_path)` (the skill directory as a string), but Hermes's `ctx.register_skill(name, path)` expects:
24
+ 1. A `pathlib.Path` object (not a string)
25
+ 2. The path to `SKILL.md` itself (not the parent directory)
26
+ Verified against the official "Build a Hermes plugin" guide example. Now passes `skill_path / "SKILL.md"` as a `Path`.
27
+ - **Hermes command handler signature** — handler is now defined as `handler(raw_args: str = "") -> Optional[str]` matching the documented Hermes signature (previously `handler(_args=None)`).
28
+ - **Slash commands silent in gateway/TUI mode** — `ctx.inject_message()` is documented as **CLI-mode only** (returns `False` in gateway mode). The handler now falls back to **returning the markdown body** as the command's response when `inject_message` is unavailable, so the content is still visible to the user instead of being silently dropped.
29
+ - **`register_command` now uses keyword arguments** (`handler=`, `description=`) to match the official Hermes plugin examples (e.g. `disk-cleanup`) and stay robust if Hermes reorders positional parameters.
30
+
31
+ ### Notes
32
+ - After upgrading, re-run `set-prompt link hermes` to regenerate `__init__.py`, then restart Hermes. The plugin should now load with `✓ sppt v1.0` (renamed from `set-prompt` in 0.8.3) and skills become loadable via `skill_view("sppt:<name>")` (plugin skills are opt-in explicit loads — they do not appear in Hermes's auto-listed `available_skills` index).
33
+ - Hook callbacks remain `def runner(*args, **kwargs)` — Hermes documents that all hook callbacks should accept `**kwargs` for forward compatibility, and the bridge's role is purely to forward the payload to the user-defined hook command via stdin JSON.
34
+
35
+ ---
36
+
7
37
  ## [0.8.1] - 2026-05-02
8
38
 
9
39
  ### Changed
package/README.md CHANGED
@@ -109,7 +109,7 @@ The interactive mode shows all agents with their current state. **Check to link,
109
109
  | Cursor | dir symlinks into `~/.cursor/` | `skills/`, `agents/`, `commands/`, `hooks/`, `mcp.json` (hardlink) |
110
110
  | OpenCode | dir symlinks into `~/.config/opencode/` | `skills/`, `commands/`, `agents/` |
111
111
  | Gemini CLI | dir symlinks into `~/.gemini/` | `skills/`, `commands/`, `agents/` |
112
- | Hermes | Python plugin adapter at `~/.hermes/plugins/set-prompt/` | `skills/`, `commands/`, `hooks/hooks.json` (read directly from repo at startup) |
112
+ | Hermes | Python plugin adapter at `~/.hermes/plugins/sppt/` | `skills/`, `commands/`, `hooks/hooks.json` (read directly from repo at startup) |
113
113
 
114
114
  > **Note on Claude Code**: Operates as a plugin. Restart Claude Code after linking for the plugin to be recognized.
115
115
 
@@ -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 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}`.
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/sppt/__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 `sppt` 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). **Plugin skills don't appear in `/skills list`** (Hermes policy — plugin skills are opt-in explicit loads, not part of the auto-listed `available_skills` index). Load them with the qualified namespace: `skill_view("sppt:<name>")` — e.g. `skill_view("sppt:dev-base")`. 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
 
@@ -237,8 +237,8 @@ set-prompt uninstall
237
237
  └── antigravity/ (Antigravity's own subtree, unrelated)
238
238
 
239
239
  ~/.hermes/ # Hermes (Python plugin adapter, no symlinks)
240
- ├── config.yaml # plugins.enabled: [set-prompt] (created if absent)
241
- └── plugins/set-prompt/
240
+ ├── config.yaml # plugins.enabled: [sppt] (created if absent)
241
+ └── plugins/sppt/
242
242
  ├── plugin.yaml # Hermes manifest
243
243
  └── __init__.py # REPO_DIR baked in; reads <repo>/{skills,commands,hooks/hooks.json} at startup
244
244
  ```
@@ -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 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)
258
+ - **Hermes** — generates `~/.hermes/plugins/sppt/{plugin.yaml, __init__.py}` and adds `sppt` 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
@@ -39,7 +39,7 @@ var OPENCODE_BACKUP_DIR = path.join(OPENCODE_DIR, "SET_PROMPT_BACKUP");
39
39
  var GEMINICLI_DIR = path.join(os.homedir(), ".gemini");
40
40
  var GEMINICLI_BACKUP_DIR = path.join(GEMINICLI_DIR, "SET_PROMPT_BACKUP");
41
41
  var HERMES_DIR = path.join(os.homedir(), ".hermes");
42
- var HERMES_PLUGIN_DIR = path.join(HERMES_DIR, "plugins", MARKET_NAME);
42
+ var HERMES_PLUGIN_DIR = path.join(HERMES_DIR, "plugins", PLUGIN_NAME);
43
43
  var HERMES_CONFIG_PATH = path.join(HERMES_DIR, "config.yaml");
44
44
  var PROMPT_DIR_NAMES = ["skills", "commands", "hooks", "agents", "rules"];
45
45
  var AGENT_PROMPT_DIRS = {
@@ -549,7 +549,7 @@ required_environment_variables:
549
549
 
550
550
  > **Gemini CLI note**: Only \`name\` and \`description\` are recognized. \`name\` must be lowercase with hyphens and match the directory name.
551
551
 
552
- > **Hermes note (set-prompt integration)**: Hermes does not auto-discover files in standard directories \u2014 plugins must register skills programmatically. set-prompt generates \`~/.hermes/plugins/set-prompt/{plugin.yaml, __init__.py}\` on \`set-prompt link hermes\`. The \`__init__.py\` reads \`<repo>/skills/<skill-name>/SKILL.md\` directly (REPO_DIR is baked in at link time) and calls \`ctx.register_skill()\` at Hermes startup. The skill directory layout is the same as other platforms \u2014 no nested category folder.
552
+ > **Hermes note (set-prompt integration)**: Hermes does not auto-discover files in standard directories \u2014 plugins must register skills programmatically. set-prompt generates \`~/.hermes/plugins/sppt/{plugin.yaml, __init__.py}\` on \`set-prompt link hermes\`. The \`__init__.py\` reads \`<repo>/skills/<skill-name>/SKILL.md\` directly (REPO_DIR is baked in at link time) and calls \`ctx.register_skill(name, Path(SKILL.md))\` at Hermes startup. The skill directory layout is the same as other platforms \u2014 no nested category folder. **Plugin skills don't appear in \`/skills list\`** (Hermes policy \u2014 plugin skills are opt-in explicit loads). Load with the qualified namespace: \`skill_view("sppt:<skill-name>")\` \u2014 e.g. \`skill_view("sppt:dev-base")\`.
553
553
 
554
554
  | Field | Required | Platform | Description |
555
555
  |-------|----------|----------|-------------|
@@ -659,7 +659,7 @@ Include: 1) Refactored code. 2) Explanation of changes.
659
659
 
660
660
  #### Hermes commands (set-prompt integration)
661
661
 
662
- Hermes commands are registered programmatically. set-prompt's generated \`__init__.py\` walks \`<repo>/commands/*.md\`, parses each file's YAML frontmatter for \`name\` / \`description\`, and calls \`ctx.register_command(name, handler, description)\`. The handler injects the markdown body as a user message via \`ctx.inject_message(body, role="user")\` when the user invokes the slash command. Hermes does **not** read \`allowed-tools\`, \`model\`, \`agent\`, or other platform-specific frontmatter \u2014 only \`name\` and \`description\` are honored.
662
+ Hermes commands are registered programmatically. set-prompt's generated \`__init__.py\` walks \`<repo>/commands/*.md\`, parses each file's YAML frontmatter for \`name\` / \`description\`, and calls \`ctx.register_command(name, handler=..., description=...)\`. The handler tries to inject the markdown body as a user message via \`ctx.inject_message(body, role="user")\` when the slash command is invoked. **\`inject_message\` is CLI-mode only** \u2014 in gateway/TUI mode it returns \`False\`, and the handler falls back to **returning the body** as the command's response so the content is still visible. Hermes does **not** read \`allowed-tools\`, \`model\`, \`agent\`, or other platform-specific frontmatter \u2014 only \`name\` and \`description\` are honored.
663
663
 
664
664
  | Field | Required | Platform | Description |
665
665
  |-------|----------|----------|-------------|
@@ -2491,7 +2491,7 @@ import fs13 from "fs";
2491
2491
  import chalk14 from "chalk";
2492
2492
  import { confirm as confirm11 } from "@inquirer/prompts";
2493
2493
  import YAML from "yaml";
2494
- var PLUGIN_YAML = `name: ${MARKET_NAME}
2494
+ var PLUGIN_YAML = `name: ${PLUGIN_NAME}
2495
2495
  version: "1.0"
2496
2496
  description: Managed by set-prompt
2497
2497
  `;
@@ -2537,8 +2537,18 @@ def _parse_frontmatter(text):
2537
2537
 
2538
2538
 
2539
2539
  def _make_command_handler(ctx, body):
2540
- def handler(_args=None):
2541
- ctx.inject_message(body, role="user")
2540
+ # Hermes calls handlers as handler(raw_args: str) -> Optional[str].
2541
+ # Primary path: inject the markdown body as a user message (CLI mode).
2542
+ # Fallback: in gateway mode inject_message returns False \u2014 return the body
2543
+ # as the handler response so the content is at least visible to the user.
2544
+ def handler(raw_args=""):
2545
+ try:
2546
+ ok = ctx.inject_message(body, role="user")
2547
+ except Exception:
2548
+ ok = False
2549
+ if not ok:
2550
+ return body
2551
+ return None
2542
2552
  return handler
2543
2553
 
2544
2554
 
@@ -2607,9 +2617,11 @@ def register(ctx):
2607
2617
  for skill_path in sorted(skills_dir.iterdir()):
2608
2618
  if not skill_path.is_dir():
2609
2619
  continue
2610
- if not (skill_path / "SKILL.md").exists():
2620
+ skill_md = skill_path / "SKILL.md"
2621
+ if not skill_md.exists():
2611
2622
  continue
2612
- ctx.register_skill(skill_path.name, str(skill_path))
2623
+ # Hermes expects (name, Path-to-SKILL.md), not (name, dir-as-str).
2624
+ ctx.register_skill(skill_path.name, skill_md)
2613
2625
 
2614
2626
  commands_dir = REPO_DIR / "commands"
2615
2627
  if commands_dir.exists():
@@ -2621,7 +2633,11 @@ def register(ctx):
2621
2633
  meta, body = _parse_frontmatter(text)
2622
2634
  name = meta.get("name") or cmd_file.stem
2623
2635
  description = meta.get("description") or ""
2624
- ctx.register_command(name, _make_command_handler(ctx, body), description)
2636
+ ctx.register_command(
2637
+ name,
2638
+ handler=_make_command_handler(ctx, body),
2639
+ description=description,
2640
+ )
2625
2641
 
2626
2642
  _register_hooks(ctx)
2627
2643
  `;
@@ -2629,7 +2645,7 @@ def register(ctx):
2629
2645
  var FRESH_CONFIG_YAML = `# Generated by set-prompt
2630
2646
  plugins:
2631
2647
  enabled:
2632
- - ${MARKET_NAME}
2648
+ - ${PLUGIN_NAME}
2633
2649
  `;
2634
2650
  var writePluginManifest = (repoPath) => {
2635
2651
  fs13.writeFileSync(path12.join(HERMES_PLUGIN_DIR, "plugin.yaml"), PLUGIN_YAML, "utf-8");
@@ -2691,7 +2707,7 @@ var ensureHermesConfigEnabled = () => {
2691
2707
  }
2692
2708
  let plugins = doc.get("plugins");
2693
2709
  if (plugins == null) {
2694
- doc.set("plugins", doc.createNode({ enabled: [MARKET_NAME] }));
2710
+ doc.set("plugins", doc.createNode({ enabled: [PLUGIN_NAME] }));
2695
2711
  writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2696
2712
  console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
2697
2713
  return;
@@ -2703,7 +2719,7 @@ var ensureHermesConfigEnabled = () => {
2703
2719
  }
2704
2720
  let enabled = plugins.get("enabled");
2705
2721
  if (enabled == null) {
2706
- plugins.set("enabled", doc.createNode([MARKET_NAME]));
2722
+ plugins.set("enabled", doc.createNode([PLUGIN_NAME]));
2707
2723
  writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2708
2724
  console.log(chalk14.green(" \u2713 ") + chalk14.dim(`added plugins.enabled to ${HERMES_CONFIG_PATH}`));
2709
2725
  return;
@@ -2715,15 +2731,15 @@ var ensureHermesConfigEnabled = () => {
2715
2731
  }
2716
2732
  const alreadyListed = enabled.items.some((item) => {
2717
2733
  const v = YAML.isScalar(item) ? item.value : item;
2718
- return v === MARKET_NAME;
2734
+ return v === PLUGIN_NAME;
2719
2735
  });
2720
2736
  if (alreadyListed) {
2721
- console.log(chalk14.dim(` \u2713 ${MARKET_NAME} already listed in ${HERMES_CONFIG_PATH}`));
2737
+ console.log(chalk14.dim(` \u2713 ${PLUGIN_NAME} already listed in ${HERMES_CONFIG_PATH}`));
2722
2738
  return;
2723
2739
  }
2724
- enabled.add(MARKET_NAME);
2740
+ enabled.add(PLUGIN_NAME);
2725
2741
  writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2726
- console.log(chalk14.green(" \u2713 ") + chalk14.dim(`enabled "${MARKET_NAME}" in ${HERMES_CONFIG_PATH}`));
2742
+ console.log(chalk14.green(" \u2713 ") + chalk14.dim(`enabled "${PLUGIN_NAME}" in ${HERMES_CONFIG_PATH}`));
2727
2743
  };
2728
2744
  var linkHermes = async () => {
2729
2745
  const repoPath = resolveRepoPath();
@@ -2743,6 +2759,8 @@ Setting up Hermes plugin...`));
2743
2759
  configManager.save();
2744
2760
  console.log(chalk14.dim(`
2745
2761
  \u2139 Hermes loads plugins at startup \u2014 restart Hermes to pick up new skills/commands.`));
2762
+ console.log(chalk14.dim(` \u2139 Plugin skills don't appear in /skills list (Hermes policy).`));
2763
+ console.log(chalk14.dim(` Load explicitly: `) + chalk14.cyan(`skill_view("${PLUGIN_NAME}:<skill-name>")`) + chalk14.dim(` \u2014 e.g. `) + chalk14.cyan(`skill_view("${PLUGIN_NAME}:dev-base")`));
2746
2764
  } catch (ex) {
2747
2765
  console.error(chalk14.red(`\u274C Failed to set up Hermes plugin: ${ex.message}`));
2748
2766
  }
@@ -2771,42 +2789,46 @@ Removing Hermes plugin...`));
2771
2789
  fs13.unlinkSync(HERMES_CONFIG_PATH);
2772
2790
  console.log(chalk14.red(" removed") + chalk14.dim(`: ${HERMES_CONFIG_PATH}`));
2773
2791
  } else {
2774
- removeMarketEntryFromConfig();
2792
+ removeEntryFromConfig(PLUGIN_NAME);
2775
2793
  }
2776
2794
  }
2777
2795
  configManager.hermes = null;
2778
2796
  configManager.save();
2779
2797
  };
2780
- var removeMarketEntryFromConfig = () => {
2798
+ var removeEntryFromConfig = (entryName) => {
2799
+ if (fs13.existsSync(HERMES_CONFIG_PATH) === false) {
2800
+ return false;
2801
+ }
2781
2802
  const content = fs13.readFileSync(HERMES_CONFIG_PATH, "utf-8");
2782
2803
  let doc;
2783
2804
  try {
2784
2805
  doc = YAML.parseDocument(content);
2785
2806
  } catch (ex) {
2786
2807
  console.warn(chalk14.yellow(` \u26A0 Failed to parse Hermes config (${ex.message}) \u2014 leaving file untouched.`));
2787
- return;
2808
+ return false;
2788
2809
  }
2789
2810
  if (doc.errors.length > 0) {
2790
- return;
2811
+ return false;
2791
2812
  }
2792
2813
  const plugins = doc.get("plugins");
2793
2814
  if (!YAML.isMap(plugins)) {
2794
- return;
2815
+ return false;
2795
2816
  }
2796
2817
  const enabled = plugins.get("enabled");
2797
2818
  if (!YAML.isSeq(enabled)) {
2798
- return;
2819
+ return false;
2799
2820
  }
2800
2821
  const idx = enabled.items.findIndex((item) => {
2801
2822
  const v = YAML.isScalar(item) ? item.value : item;
2802
- return v === MARKET_NAME;
2823
+ return v === entryName;
2803
2824
  });
2804
2825
  if (idx === -1) {
2805
- return;
2826
+ return false;
2806
2827
  }
2807
2828
  enabled.delete(idx);
2808
2829
  writeWithBackup(HERMES_CONFIG_PATH, String(doc));
2809
- console.log(chalk14.red(" removed") + chalk14.dim(` "${MARKET_NAME}" from: ${HERMES_CONFIG_PATH}`));
2830
+ console.log(chalk14.red(" removed") + chalk14.dim(` "${entryName}" from: ${HERMES_CONFIG_PATH}`));
2831
+ return true;
2810
2832
  };
2811
2833
 
2812
2834
  // src/commands/link-command.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "set-prompt",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "One repo. Every AI coding tool. Always in sync.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -31,7 +31,8 @@
31
31
  "cursor",
32
32
  "codex",
33
33
  "opencode",
34
- "gemini-cli"
34
+ "gemini-cli",
35
+ "hermes"
35
36
  ],
36
37
  "author": "juncha9 (https://github.com/juncha9)",
37
38
  "license": "MIT",