@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.
- package/README.md +4 -2
- package/dist/cli.js +51 -13
- 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
|
|
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(
|
|
4007
|
+
if (isGitUrl(base)) {
|
|
3993
4008
|
tempDir = mkdtempSync(join12(tmpdir(), "holt-skill-"));
|
|
3994
4009
|
console.log(c.dim(`
|
|
3995
|
-
Cloning ${
|
|
3996
|
-
const res = spawnSync2("git", ["clone", "--depth", "1",
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
5856
|
+
var VERSION = "0.12.0";
|
|
5819
5857
|
|
|
5820
5858
|
// src/commands/mcp.ts
|
|
5821
5859
|
function printSetup() {
|