@holt-os/holt 0.11.0 → 0.12.0

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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.js +51 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -213,11 +213,13 @@ holt skill list list installed skills
213
213
  holt skill show <name> print a skill
214
214
  holt skill create <name> [--global] scaffold a new skill
215
215
  holt skill search <query> find skills in the registry
216
- holt skill add <src|name> [--global] install from a git URL, path, or registry name
216
+ holt skill add <src|name> [--global] install from a git URL, git-url#subdir, path, or registry name
217
217
  holt skill publish [<name>] prepare a skill for the registry (prints a PR entry)
218
218
  holt skill remove <name> delete a skill
219
219
  ```
220
220
 
221
+ A source may carry a `#<subdir>` suffix (split on the first `#`) that points at one skill folder inside a repo or path: `holt skill add https://github.com/holt-os/registry#skills/pm-prd`. The subdir is a path, relative to the source root, that contains a `SKILL.md`. This lets a single repo hold many skills (one monorepo), which is exactly how the community registry is laid out. The subdir is validated to stay inside the source (absolute paths and `..` traversal are refused), since a registry entry is remote-controlled. Without a `#`, behaviour is unchanged: Holt looks for a `SKILL.md` at the source root or in exactly one immediate subfolder.
222
+
221
223
  In chat, run one with `/skill <name> [your input]`. Available skills are also listed to the brain each turn, so it knows what it can be asked to follow.
222
224
 
223
225
  ### Built-in skills
@@ -256,7 +258,7 @@ The index is a JSON document:
256
258
  }
257
259
  ```
258
260
 
259
- `source` is anything `holt skill add` already accepts: a git URL (its `SKILL.md` may sit in one subfolder) or a local path. `tags` is optional. Only `name` and `source` are required per entry; malformed rows are skipped rather than failing the whole index.
261
+ `source` is anything `holt skill add` already accepts: a git URL (its `SKILL.md` may sit in one subfolder), a local path, or either of those with a `#<subdir>` suffix pointing at one skill folder inside the repo. That last form lets one repo hold many skills, so the community registry can be a single monorepo (e.g. `"source": "https://github.com/holt-os/registry#skills/pm-prd"`). `tags` is optional. Only `name` and `source` are required per entry; malformed rows are skipped rather than failing the whole index.
260
262
 
261
263
  - **Find:** `holt skill search <query>` fetches the index and lists skills whose name, description, or tags match (case-insensitive substring, name matches first). An empty query lists everything. It prints name, author, description, and source.
262
264
  - **Install by name:** `holt skill add <name>` resolves the name in the registry to its `source`, then installs through the exact same clone/copy path as `holt skill add <url|path>`. A git URL or an existing local path is still installed directly, unchanged.
package/dist/cli.js CHANGED
@@ -3702,7 +3702,7 @@ import {
3702
3702
  cpSync,
3703
3703
  mkdtempSync
3704
3704
  } from "fs";
3705
- import { join as join12, resolve as resolve3 } from "path";
3705
+ import { join as join12, resolve as resolve3, sep, isAbsolute as isAbsolute3 } from "path";
3706
3706
  import { tmpdir } from "os";
3707
3707
  import { spawnSync as spawnSync2 } from "child_process";
3708
3708
 
@@ -3866,7 +3866,7 @@ function usage() {
3866
3866
  " holt skill show <name> print a skill",
3867
3867
  " holt skill create <name> [--global] scaffold a new skill",
3868
3868
  " holt skill search <query> find skills in the registry",
3869
- " holt skill add <src|name> [--global] install from a git URL, path, or registry name",
3869
+ " holt skill add <src|name> [--global] install from a git URL, git-url#subdir, path, or registry name",
3870
3870
  " holt skill publish [<name>] prepare a skill for the registry (prints a PR entry)",
3871
3871
  " holt skill remove <name> delete a skill",
3872
3872
  "",
@@ -3902,6 +3902,20 @@ var SKILL_TEMPLATE = (name) => [
3902
3902
  function isGitUrl(s) {
3903
3903
  return s.includes("://") || s.endsWith(".git") || s.startsWith("git@");
3904
3904
  }
3905
+ function splitSubpath(source) {
3906
+ const i = source.indexOf("#");
3907
+ if (i === -1) return { base: source, subpath: "" };
3908
+ return { base: source.slice(0, i), subpath: source.slice(i + 1) };
3909
+ }
3910
+ function resolveSubpathDir(fetched, subpath) {
3911
+ if (isAbsolute3(subpath)) return null;
3912
+ const segments = subpath.split(/[\\/]/);
3913
+ if (segments.some((s) => s === "..")) return null;
3914
+ const target = resolve3(fetched, subpath);
3915
+ const root = resolve3(fetched);
3916
+ if (target !== root && !target.startsWith(root + sep)) return null;
3917
+ return target;
3918
+ }
3905
3919
  function findSkillDir(root) {
3906
3920
  if (existsSync12(join12(root, "SKILL.md"))) return root;
3907
3921
  let subs = [];
@@ -3986,14 +4000,15 @@ function cmdCreate(name, global) {
3986
4000
  console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
3987
4001
  }
3988
4002
  function installFromSource(source, scope) {
4003
+ const { base, subpath } = splitSubpath(source);
3989
4004
  let fetched = "";
3990
4005
  let tempDir = "";
3991
4006
  try {
3992
- if (isGitUrl(source)) {
4007
+ if (isGitUrl(base)) {
3993
4008
  tempDir = mkdtempSync(join12(tmpdir(), "holt-skill-"));
3994
4009
  console.log(c.dim(`
3995
- Cloning ${source} ...`));
3996
- const res = spawnSync2("git", ["clone", "--depth", "1", source, tempDir], { stdio: "ignore" });
4010
+ Cloning ${base} ...`));
4011
+ const res = spawnSync2("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
3997
4012
  if (res.status !== 0) {
3998
4013
  console.error(c.red(" Clone failed. Check the URL and that git is installed.\n"));
3999
4014
  process.exitCode = 1;
@@ -4001,7 +4016,7 @@ function installFromSource(source, scope) {
4001
4016
  }
4002
4017
  fetched = tempDir;
4003
4018
  } else {
4004
- fetched = resolve3(source);
4019
+ fetched = resolve3(base);
4005
4020
  if (!existsSync12(fetched)) {
4006
4021
  console.error(c.red(`
4007
4022
  No such path: ${fetched}
@@ -4010,9 +4025,31 @@ function installFromSource(source, scope) {
4010
4025
  return false;
4011
4026
  }
4012
4027
  }
4013
- const skillDir = findSkillDir(fetched);
4028
+ let searchRoot = fetched;
4029
+ if (subpath) {
4030
+ const target = resolveSubpathDir(fetched, subpath);
4031
+ if (!target) {
4032
+ console.error(c.red(`
4033
+ Refusing unsafe subpath "${subpath}": it must stay inside the source (no absolute or "..").
4034
+ `));
4035
+ process.exitCode = 1;
4036
+ return false;
4037
+ }
4038
+ if (!existsSync12(target) || !statSync5(target).isDirectory()) {
4039
+ console.error(c.red(`
4040
+ Subpath "${subpath}" does not exist in the source.
4041
+ `));
4042
+ process.exitCode = 1;
4043
+ return false;
4044
+ }
4045
+ searchRoot = target;
4046
+ }
4047
+ const skillDir = findSkillDir(searchRoot);
4014
4048
  if (!skillDir) {
4015
- console.error(c.red("\n Could not find a SKILL.md at the source root or in a single subfolder.\n"));
4049
+ const where = subpath ? `subpath "${subpath}"` : "the source root or in a single subfolder";
4050
+ console.error(c.red(`
4051
+ Could not find a SKILL.md at ${where}.
4052
+ `));
4016
4053
  process.exitCode = 1;
4017
4054
  return false;
4018
4055
  }
@@ -4056,11 +4093,12 @@ function installFromSource(source, scope) {
4056
4093
  }
4057
4094
  async function cmdAdd(source, global, refresh) {
4058
4095
  if (!source) {
4059
- console.log(c.dim("\n Usage: holt skill add <git-url|path|name> [--global]\n"));
4096
+ console.log(c.dim("\n Usage: holt skill add <git-url|git-url#subdir|path|name> [--global]\n"));
4060
4097
  return;
4061
4098
  }
4062
4099
  const scope = global ? "global" : "workspace";
4063
- if (isGitUrl(source) || existsSync12(resolve3(source))) {
4100
+ const { base } = splitSubpath(source);
4101
+ if (isGitUrl(base) || existsSync12(resolve3(base))) {
4064
4102
  installFromSource(source, scope);
4065
4103
  return;
4066
4104
  }
@@ -5061,7 +5099,7 @@ function renderGraphHtml(graph2, meta) {
5061
5099
 
5062
5100
  // src/ingest.ts
5063
5101
  import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
5064
- import { join as join13, relative, sep, extname, dirname as dirname3, basename as basename2 } from "path";
5102
+ import { join as join13, relative, sep as sep2, extname, dirname as dirname3, basename as basename2 } from "path";
5065
5103
  var CODE_EXT = /* @__PURE__ */ new Set([
5066
5104
  "js",
5067
5105
  "jsx",
@@ -5112,7 +5150,7 @@ var MAX_FILES = 2e3;
5112
5150
  var MAX_FILE_BYTES = 400 * 1024;
5113
5151
  var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
5114
5152
  function toPosix(p) {
5115
- return sep === "/" ? p : p.split(sep).join("/");
5153
+ return sep2 === "/" ? p : p.split(sep2).join("/");
5116
5154
  }
5117
5155
  function ingestFolder(root, want = { code: true, docs: true }) {
5118
5156
  const files = [];
@@ -5815,7 +5853,7 @@ function holtTools() {
5815
5853
  }
5816
5854
 
5817
5855
  // src/version.ts
5818
- var VERSION = "0.11.0";
5856
+ var VERSION = "0.12.0";
5819
5857
 
5820
5858
  // src/commands/mcp.ts
5821
5859
  function printSetup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "An open-source personal agent OS: any LLM, private memory you can see and walk.",
5
5
  "type": "module",
6
6
  "license": "MIT",