@holt-os/holt 0.10.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 +20 -3
- package/dist/cli.js +82 -29
- package/package.json +2 -1
- package/skills/action-items/SKILL.md +31 -0
- package/skills/brief/SKILL.md +25 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/commit/SKILL.md +26 -0
- package/skills/decide/SKILL.md +28 -0
- package/skills/explain/SKILL.md +24 -0
- package/skills/explain-code/SKILL.md +27 -0
- package/skills/plan/SKILL.md +29 -0
- package/skills/rewrite/SKILL.md +27 -0
- package/skills/summarize/SKILL.md +27 -0
package/README.md
CHANGED
|
@@ -206,20 +206,37 @@ Wiki pages also participate in recall: each page is embedded and indexed like a
|
|
|
206
206
|
|
|
207
207
|
A skill is a folder with a `SKILL.md`: YAML frontmatter (`name`, `description`) plus Markdown instructions. Same convention as agentskills.io and Claude Code, so skills are portable both ways. Skills are prompt text only; Holt never executes their contents.
|
|
208
208
|
|
|
209
|
-
Holt looks in
|
|
209
|
+
Holt looks in three places, in precedence order (workspace shadows global shadows builtin on a name clash): `./.holt/skills/` for this folder, `~/.holt/skills/` for every folder (`--global`), and a read-only set of **built-in skills** that ship inside Holt and are available in every folder out of the box.
|
|
210
210
|
|
|
211
211
|
```
|
|
212
212
|
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
|
|
|
225
|
+
### Built-in skills
|
|
226
|
+
|
|
227
|
+
Holt ships with a small, curated set of general-purpose skills, available in every folder with no setup. They are read-only (`holt skill remove` will not delete them); to change one, create a workspace or global skill with the same name and it takes over.
|
|
228
|
+
|
|
229
|
+
- **summarize**: condense a document, article, transcript, or pasted text into its key points.
|
|
230
|
+
- **explain**: explain a concept, error message, or piece of jargon in plain language.
|
|
231
|
+
- **action-items**: pull decisions, tasks, and owners out of messy notes or a meeting transcript.
|
|
232
|
+
- **brief**: a short briefing on the state of the current folder, using recalled memory plus your input.
|
|
233
|
+
- **code-review**: review a file or a diff for correctness bugs, edge cases, and simplifications.
|
|
234
|
+
- **explain-code**: walk through what a file or function does and how it fits into the wider system.
|
|
235
|
+
- **commit**: draft a clear commit message from staged changes or a described change.
|
|
236
|
+
- **plan**: break a goal into a concrete, ordered set of steps.
|
|
237
|
+
- **rewrite**: tighten and clarify a piece of text without changing its meaning.
|
|
238
|
+
- **decide**: weigh a decision and give a clear recommendation.
|
|
239
|
+
|
|
223
240
|
### Skill registry (`holt skill search` / `publish`)
|
|
224
241
|
|
|
225
242
|
Beyond pasting URLs, Holt can discover and share skills through a **git-based registry**: a single JSON index living in a git repo. No server, no infra, in keeping with the rest of Holt.
|
|
@@ -241,7 +258,7 @@ The index is a JSON document:
|
|
|
241
258
|
}
|
|
242
259
|
```
|
|
243
260
|
|
|
244
|
-
`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.
|
|
245
262
|
|
|
246
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.
|
|
247
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
|
@@ -1032,9 +1032,15 @@ async function runApiBrain(brain, prompt, onChunk) {
|
|
|
1032
1032
|
|
|
1033
1033
|
// src/skills.ts
|
|
1034
1034
|
import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
1035
|
-
import { join as join6 } from "path";
|
|
1035
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
1036
|
+
import { fileURLToPath } from "url";
|
|
1037
|
+
function builtinSkillsRoot() {
|
|
1038
|
+
return join6(dirname2(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1039
|
+
}
|
|
1036
1040
|
function skillsRoot(scope) {
|
|
1037
|
-
|
|
1041
|
+
if (scope === "workspace") return join6(wsHoltDir(), "skills");
|
|
1042
|
+
if (scope === "builtin") return builtinSkillsRoot();
|
|
1043
|
+
return join6(GLOBAL_DIR, "skills");
|
|
1038
1044
|
}
|
|
1039
1045
|
function sanitizeName(name) {
|
|
1040
1046
|
return (name || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
@@ -1114,6 +1120,7 @@ function listScope(scope) {
|
|
|
1114
1120
|
}
|
|
1115
1121
|
function listSkills() {
|
|
1116
1122
|
const byName = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const s of listScope("builtin")) byName.set(s.name, s);
|
|
1117
1124
|
for (const s of listScope("global")) byName.set(s.name, s);
|
|
1118
1125
|
for (const s of listScope("workspace")) byName.set(s.name, s);
|
|
1119
1126
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3695,14 +3702,14 @@ import {
|
|
|
3695
3702
|
cpSync,
|
|
3696
3703
|
mkdtempSync
|
|
3697
3704
|
} from "fs";
|
|
3698
|
-
import { join as join12, resolve as resolve3 } from "path";
|
|
3705
|
+
import { join as join12, resolve as resolve3, sep, isAbsolute as isAbsolute3 } from "path";
|
|
3699
3706
|
import { tmpdir } from "os";
|
|
3700
3707
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3701
3708
|
|
|
3702
3709
|
// src/registry.ts
|
|
3703
3710
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync8, statSync as statSync4 } from "fs";
|
|
3704
3711
|
import { join as join11, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
3705
|
-
import { pathToFileURL, fileURLToPath } from "url";
|
|
3712
|
+
import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
|
|
3706
3713
|
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
|
|
3707
3714
|
var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
|
|
3708
3715
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
@@ -3767,7 +3774,7 @@ function writeCache(url, registry) {
|
|
|
3767
3774
|
function localPathFor(loc) {
|
|
3768
3775
|
if (loc.startsWith("file://")) {
|
|
3769
3776
|
try {
|
|
3770
|
-
return
|
|
3777
|
+
return fileURLToPath2(loc);
|
|
3771
3778
|
} catch {
|
|
3772
3779
|
return null;
|
|
3773
3780
|
}
|
|
@@ -3859,7 +3866,7 @@ function usage() {
|
|
|
3859
3866
|
" holt skill show <name> print a skill",
|
|
3860
3867
|
" holt skill create <name> [--global] scaffold a new skill",
|
|
3861
3868
|
" holt skill search <query> find skills in the registry",
|
|
3862
|
-
" 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",
|
|
3863
3870
|
" holt skill publish [<name>] prepare a skill for the registry (prints a PR entry)",
|
|
3864
3871
|
" holt skill remove <name> delete a skill",
|
|
3865
3872
|
"",
|
|
@@ -3895,6 +3902,20 @@ var SKILL_TEMPLATE = (name) => [
|
|
|
3895
3902
|
function isGitUrl(s) {
|
|
3896
3903
|
return s.includes("://") || s.endsWith(".git") || s.startsWith("git@");
|
|
3897
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
|
+
}
|
|
3898
3919
|
function findSkillDir(root) {
|
|
3899
3920
|
if (existsSync12(join12(root, "SKILL.md"))) return root;
|
|
3900
3921
|
let subs = [];
|
|
@@ -3921,14 +3942,15 @@ function cmdList() {
|
|
|
3921
3942
|
}
|
|
3922
3943
|
const nameW = Math.max(4, ...skills.map((s) => s.name.length));
|
|
3923
3944
|
const scopeW = Math.max(5, ...skills.map((s) => s.scope.length));
|
|
3924
|
-
console.log("\n" + c.accent("Skills") + c.dim(" (workspace shadows global on name clash)"));
|
|
3945
|
+
console.log("\n" + c.accent("Skills") + c.dim(" (workspace shadows global shadows builtin on name clash)"));
|
|
3925
3946
|
console.log(
|
|
3926
3947
|
" " + c.dim("name".padEnd(nameW)) + " " + c.dim("scope".padEnd(scopeW)) + " " + c.dim("description")
|
|
3927
3948
|
);
|
|
3928
3949
|
for (const s of skills) {
|
|
3929
3950
|
const desc = s.description.length > 68 ? s.description.slice(0, 67) + "\u2026" : s.description;
|
|
3951
|
+
const scopeCol = s.scope === "workspace" ? c.cyan(s.scope.padEnd(scopeW)) : s.scope === "builtin" ? c.accent(s.scope.padEnd(scopeW)) : c.dim(s.scope.padEnd(scopeW));
|
|
3930
3952
|
console.log(
|
|
3931
|
-
" " + c.bold(s.name.padEnd(nameW)) + " " +
|
|
3953
|
+
" " + c.bold(s.name.padEnd(nameW)) + " " + scopeCol + " " + (desc || c.dim("(no description)"))
|
|
3932
3954
|
);
|
|
3933
3955
|
}
|
|
3934
3956
|
console.log(c.dim("\n holt skill show <name> print a skill"));
|
|
@@ -3978,14 +4000,15 @@ function cmdCreate(name, global) {
|
|
|
3978
4000
|
console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
|
|
3979
4001
|
}
|
|
3980
4002
|
function installFromSource(source, scope) {
|
|
4003
|
+
const { base, subpath } = splitSubpath(source);
|
|
3981
4004
|
let fetched = "";
|
|
3982
4005
|
let tempDir = "";
|
|
3983
4006
|
try {
|
|
3984
|
-
if (isGitUrl(
|
|
4007
|
+
if (isGitUrl(base)) {
|
|
3985
4008
|
tempDir = mkdtempSync(join12(tmpdir(), "holt-skill-"));
|
|
3986
4009
|
console.log(c.dim(`
|
|
3987
|
-
Cloning ${
|
|
3988
|
-
const res = spawnSync2("git", ["clone", "--depth", "1",
|
|
4010
|
+
Cloning ${base} ...`));
|
|
4011
|
+
const res = spawnSync2("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
|
|
3989
4012
|
if (res.status !== 0) {
|
|
3990
4013
|
console.error(c.red(" Clone failed. Check the URL and that git is installed.\n"));
|
|
3991
4014
|
process.exitCode = 1;
|
|
@@ -3993,7 +4016,7 @@ function installFromSource(source, scope) {
|
|
|
3993
4016
|
}
|
|
3994
4017
|
fetched = tempDir;
|
|
3995
4018
|
} else {
|
|
3996
|
-
fetched = resolve3(
|
|
4019
|
+
fetched = resolve3(base);
|
|
3997
4020
|
if (!existsSync12(fetched)) {
|
|
3998
4021
|
console.error(c.red(`
|
|
3999
4022
|
No such path: ${fetched}
|
|
@@ -4002,9 +4025,31 @@ function installFromSource(source, scope) {
|
|
|
4002
4025
|
return false;
|
|
4003
4026
|
}
|
|
4004
4027
|
}
|
|
4005
|
-
|
|
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);
|
|
4006
4048
|
if (!skillDir) {
|
|
4007
|
-
|
|
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
|
+
`));
|
|
4008
4053
|
process.exitCode = 1;
|
|
4009
4054
|
return false;
|
|
4010
4055
|
}
|
|
@@ -4048,11 +4093,12 @@ function installFromSource(source, scope) {
|
|
|
4048
4093
|
}
|
|
4049
4094
|
async function cmdAdd(source, global, refresh) {
|
|
4050
4095
|
if (!source) {
|
|
4051
|
-
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"));
|
|
4052
4097
|
return;
|
|
4053
4098
|
}
|
|
4054
4099
|
const scope = global ? "global" : "workspace";
|
|
4055
|
-
|
|
4100
|
+
const { base } = splitSubpath(source);
|
|
4101
|
+
if (isGitUrl(base) || existsSync12(resolve3(base))) {
|
|
4056
4102
|
installFromSource(source, scope);
|
|
4057
4103
|
return;
|
|
4058
4104
|
}
|
|
@@ -4181,6 +4227,13 @@ async function cmdRemove(name, ask) {
|
|
|
4181
4227
|
const globalDir = join12(skillsRoot("global"), clean);
|
|
4182
4228
|
const target = existsSync12(wsDir) ? wsDir : existsSync12(globalDir) ? globalDir : "";
|
|
4183
4229
|
if (!target) {
|
|
4230
|
+
if (existsSync12(join12(skillsRoot("builtin"), clean))) {
|
|
4231
|
+
console.error(c.red(`
|
|
4232
|
+
"${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
|
|
4233
|
+
console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
|
|
4234
|
+
process.exitCode = 1;
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4184
4237
|
console.error(c.dim(`
|
|
4185
4238
|
No skill named "${clean}" found.
|
|
4186
4239
|
`));
|
|
@@ -4248,7 +4301,7 @@ async function skillCmd(sub, rest = []) {
|
|
|
4248
4301
|
|
|
4249
4302
|
// src/commands/graph.ts
|
|
4250
4303
|
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync13 } from "fs";
|
|
4251
|
-
import { join as join14, dirname as
|
|
4304
|
+
import { join as join14, dirname as dirname4, resolve as resolve4 } from "path";
|
|
4252
4305
|
import { spawn as spawn4 } from "child_process";
|
|
4253
4306
|
|
|
4254
4307
|
// src/communities.ts
|
|
@@ -5046,7 +5099,7 @@ function renderGraphHtml(graph2, meta) {
|
|
|
5046
5099
|
|
|
5047
5100
|
// src/ingest.ts
|
|
5048
5101
|
import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
|
|
5049
|
-
import { join as join13, relative, sep, extname, dirname as
|
|
5102
|
+
import { join as join13, relative, sep as sep2, extname, dirname as dirname3, basename as basename2 } from "path";
|
|
5050
5103
|
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
5051
5104
|
"js",
|
|
5052
5105
|
"jsx",
|
|
@@ -5097,7 +5150,7 @@ var MAX_FILES = 2e3;
|
|
|
5097
5150
|
var MAX_FILE_BYTES = 400 * 1024;
|
|
5098
5151
|
var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
5099
5152
|
function toPosix(p) {
|
|
5100
|
-
return
|
|
5153
|
+
return sep2 === "/" ? p : p.split(sep2).join("/");
|
|
5101
5154
|
}
|
|
5102
5155
|
function ingestFolder(root, want = { code: true, docs: true }) {
|
|
5103
5156
|
const files = [];
|
|
@@ -5286,7 +5339,7 @@ function resolvePySpec(f, spec, byId) {
|
|
|
5286
5339
|
return null;
|
|
5287
5340
|
}
|
|
5288
5341
|
function toPosixDir(id) {
|
|
5289
|
-
const d = toPosix(
|
|
5342
|
+
const d = toPosix(dirname3(id));
|
|
5290
5343
|
return d === "" || d === "." ? "." : d;
|
|
5291
5344
|
}
|
|
5292
5345
|
var MD_LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
@@ -5489,7 +5542,7 @@ async function graph(args = []) {
|
|
|
5489
5542
|
if (opts.report) {
|
|
5490
5543
|
const md = renderReport(g, { workspace: workspace() });
|
|
5491
5544
|
const reportPath = opts.out ? resolve4(opts.out) : join14(workspace(), "GRAPH_REPORT.md");
|
|
5492
|
-
mkdirSync10(
|
|
5545
|
+
mkdirSync10(dirname4(reportPath), { recursive: true });
|
|
5493
5546
|
writeFileSync11(reportPath, md, "utf8");
|
|
5494
5547
|
const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
|
|
5495
5548
|
console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
|
|
@@ -5511,7 +5564,7 @@ async function graph(args = []) {
|
|
|
5511
5564
|
edges: g.edges.length
|
|
5512
5565
|
});
|
|
5513
5566
|
const outPath = opts.out ? resolve4(opts.out) : join14(wsHoltDir(), "graph.html");
|
|
5514
|
-
mkdirSync10(
|
|
5567
|
+
mkdirSync10(dirname4(outPath), { recursive: true });
|
|
5515
5568
|
writeFileSync11(outPath, html, "utf8");
|
|
5516
5569
|
console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
|
|
5517
5570
|
const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
|
|
@@ -5800,7 +5853,7 @@ function holtTools() {
|
|
|
5800
5853
|
}
|
|
5801
5854
|
|
|
5802
5855
|
// src/version.ts
|
|
5803
|
-
var VERSION = "0.
|
|
5856
|
+
var VERSION = "0.12.0";
|
|
5804
5857
|
|
|
5805
5858
|
// src/commands/mcp.ts
|
|
5806
5859
|
function printSetup() {
|
|
@@ -5847,7 +5900,7 @@ async function mcp(sub, rest = []) {
|
|
|
5847
5900
|
|
|
5848
5901
|
// src/commands/hook.ts
|
|
5849
5902
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2 } from "fs";
|
|
5850
|
-
import { join as join15, dirname as
|
|
5903
|
+
import { join as join15, dirname as dirname5 } from "path";
|
|
5851
5904
|
import { homedir as homedir3 } from "os";
|
|
5852
5905
|
var EVENT = {
|
|
5853
5906
|
inject: "UserPromptSubmit",
|
|
@@ -5873,7 +5926,7 @@ function readSettings(path) {
|
|
|
5873
5926
|
}
|
|
5874
5927
|
}
|
|
5875
5928
|
function writeSettings(path, settings) {
|
|
5876
|
-
mkdirSync11(
|
|
5929
|
+
mkdirSync11(dirname5(path), { recursive: true });
|
|
5877
5930
|
writeFileSync12(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
5878
5931
|
}
|
|
5879
5932
|
function backup(path) {
|
|
@@ -6319,7 +6372,7 @@ async function run(args) {
|
|
|
6319
6372
|
// src/commands/schedule.ts
|
|
6320
6373
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
6321
6374
|
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync15 } from "fs";
|
|
6322
|
-
import { dirname as
|
|
6375
|
+
import { dirname as dirname6 } from "path";
|
|
6323
6376
|
|
|
6324
6377
|
// src/scheduler.ts
|
|
6325
6378
|
import { homedir as homedir4 } from "os";
|
|
@@ -6483,7 +6536,7 @@ function parseAdd(rest) {
|
|
|
6483
6536
|
}
|
|
6484
6537
|
function installDarwin(job, holtPath) {
|
|
6485
6538
|
const path = plistPath(job.id);
|
|
6486
|
-
mkdirSync13(
|
|
6539
|
+
mkdirSync13(dirname6(path), { recursive: true });
|
|
6487
6540
|
writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6488
6541
|
const res = spawnSync4("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6489
6542
|
if (res.status === 0) {
|
|
@@ -6636,7 +6689,7 @@ async function schedule(sub, rest = []) {
|
|
|
6636
6689
|
// src/commands/routine.ts
|
|
6637
6690
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6638
6691
|
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync16 } from "fs";
|
|
6639
|
-
import { dirname as
|
|
6692
|
+
import { dirname as dirname7, resolve as resolve6 } from "path";
|
|
6640
6693
|
|
|
6641
6694
|
// src/routines.ts
|
|
6642
6695
|
import { readFileSync as readFileSync17, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14 } from "fs";
|
|
@@ -6717,7 +6770,7 @@ function routineJobId(name) {
|
|
|
6717
6770
|
}
|
|
6718
6771
|
function installDarwin2(job, holtPath) {
|
|
6719
6772
|
const path = plistPath(job.id);
|
|
6720
|
-
mkdirSync15(
|
|
6773
|
+
mkdirSync15(dirname7(path), { recursive: true });
|
|
6721
6774
|
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6722
6775
|
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6723
6776
|
if (res.status === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holt-os/holt",
|
|
3
|
-
"version": "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",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
|
+
"skills",
|
|
35
36
|
"README.md",
|
|
36
37
|
"LICENSE"
|
|
37
38
|
],
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: action-items
|
|
3
|
+
description: Pull decisions, tasks, and owners out of messy notes or a meeting transcript. Use to turn a raw log into a clean follow-up list.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# action-items
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user pastes meeting notes, a call transcript, a chat log, or scattered notes and wants the concrete outcomes extracted so nothing gets lost.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Read the whole input and separate the substance from the chit-chat. Ignore greetings, tangents, and thinking-out-loud that led nowhere.
|
|
15
|
+
2. Sort what remains into three groups: DECISIONS (what was settled), TODOS (what someone will do), and OPEN QUESTIONS (what is unresolved).
|
|
16
|
+
3. Write each TODO as "owner: task (due date if stated)". If no owner is named, mark it "unassigned". Preserve names and dates exactly as written.
|
|
17
|
+
4. Write each DECISION as a short statement of what was chosen and, if given, why.
|
|
18
|
+
5. Keep each item to one line. Do not invent owners, dates, or actions that were not in the source.
|
|
19
|
+
6. Output the three lists in that order. Omit a list only if it is genuinely empty.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
DECISIONS
|
|
24
|
+
- Ship the beta to the waitlist on Monday, not the public.
|
|
25
|
+
|
|
26
|
+
TODOS
|
|
27
|
+
- Sam: send the waitlist export to marketing (by Fri).
|
|
28
|
+
- unassigned: write the launch FAQ.
|
|
29
|
+
|
|
30
|
+
OPEN QUESTIONS
|
|
31
|
+
- Do we gate signups behind an invite code?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brief
|
|
3
|
+
description: A short briefing on the state of the current folder, using recalled memory plus the user's input. Use to get oriented fast.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# brief
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user wants a quick read on what is going on here: what they were working on, what is decided, and what is next. This skill pairs with Holt's memory recall, which surfaces relevant past notes for the current folder.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Assume relevant past notes and facts have been injected as context above. Treat them as your source of truth for prior state; combine them with anything new in the user's input.
|
|
15
|
+
2. Synthesize the state into three threads: what is IN PROGRESS, what is DECIDED, and what is OPEN or NEXT.
|
|
16
|
+
3. Lead with the single most important thing: the item that most needs attention or that unblocks the rest.
|
|
17
|
+
4. Keep it scannable: a short intro line, then tight bullets. This is a briefing, not a report.
|
|
18
|
+
5. If the recalled context is thin or empty, say so plainly and ask the user for the one detail that would make the brief useful, rather than guessing.
|
|
19
|
+
6. Do not restate the raw notes; distill them into current status.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Most important: the migration is blocked on a schema review that has been open three days.
|
|
24
|
+
|
|
25
|
+
In progress: rewriting the importer; decided: drop the legacy CSV path; open: who signs off on the schema change.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Review a file or a diff for correctness bugs, edge cases, and simplifications. Use before merging or when a change feels risky.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# code-review
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user shares a file, a function, or a diff and wants a focused review. The goal is catching real problems, not restyling working code.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Read for real defects first: logic errors, off-by-one and boundary mistakes, unhandled error paths, null or empty cases, security holes (injection, unescaped input, secrets), and resource leaks (unclosed files, sockets, handles).
|
|
15
|
+
2. Then look for clarity and simplification: duplicated logic, dead code, overly clever constructs, and places a simpler shape would do the same job.
|
|
16
|
+
3. Then note style issues, but only if they hurt readability. Skip pure nits unless the user asks for them.
|
|
17
|
+
4. Report each finding as file:line, then what is wrong, then why it matters, then a concrete fix or the corrected snippet.
|
|
18
|
+
5. Rank findings by severity: bugs and security first, then simplifications, then minor. Put the most important at the top.
|
|
19
|
+
6. Be concise. If a change is clean, say so plainly instead of manufacturing feedback.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
- api.ts:42 (bug): the loop uses <= arr.length, so the last iteration reads arr[arr.length] which is undefined. Change to <.
|
|
24
|
+
- api.ts:88 (simplify): this if/else both return the same object with one field flipped; collapse to one return with a ternary.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: Draft a clear commit message from staged changes or a described change. Use when you want a clean, conventional message.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# commit
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user has staged changes (or describes a change in words) and wants a well-formed commit message.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Write a concise subject line in the imperative mood ("add", "fix", "remove", not "added" or "fixes"). Keep it under about 72 characters and do not end it with a period.
|
|
15
|
+
2. If the change is trivial, the subject alone is enough. Stop there.
|
|
16
|
+
3. If the change is non-trivial, add a blank line, then a short body that explains what changed and why, not how. The diff already shows how.
|
|
17
|
+
4. If unrelated changes are bundled together, note that they would be cleaner as separate commits, but still give a usable message.
|
|
18
|
+
5. Write a plain message. Do not add tool-attribution footers, "generated by" lines, or co-author trailers unless the user explicitly asks.
|
|
19
|
+
6. If the intent is ambiguous from the changes, ask one clarifying question before guessing.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Subject: fix off-by-one in pagination cursor
|
|
24
|
+
|
|
25
|
+
Body:
|
|
26
|
+
The cursor advanced past the last page because the bound used <= instead of <, returning an empty final page. Correct the comparison so the last item is included.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decide
|
|
3
|
+
description: Weigh a decision and give a clear recommendation. Use when the user is stuck between options and wants a reasoned call.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# decide
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user faces a choice (which tool, which approach, whether to do something) and wants help thinking it through and a straight recommendation, not just a list of considerations.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Restate the decision and the options on the table in one line each, so you are answering the real question.
|
|
15
|
+
2. List the factors that actually matter for this choice: cost, time, risk, reversibility, effort, fit. Drop factors that do not move the decision.
|
|
16
|
+
3. For each option, give honest pros and cons against those factors. Do not stack the deck; name the real downside of the option you prefer.
|
|
17
|
+
4. Make a clear recommendation. State the main reason it wins and the biggest risk it carries.
|
|
18
|
+
5. If the call genuinely hinges on one unknown, do not guess: name that missing fact and ask for it before committing.
|
|
19
|
+
6. Keep it decisive. The user wants a lean, not a shrug.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Decision: build the export feature now, or ship without it.
|
|
24
|
+
|
|
25
|
+
Build it: pro, unblocks the biggest customer; con, adds a week.
|
|
26
|
+
Ship without: pro, launch on time; con, that customer may churn.
|
|
27
|
+
|
|
28
|
+
Recommendation: build it. Losing the anchor customer outweighs a week's delay. Biggest risk: the week slips to two. Missing fact: has the customer given a hard deadline?
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain
|
|
3
|
+
description: Explain a concept, error message, or piece of jargon in plain language. Use when the user wants to understand something unfamiliar.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# explain
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user asks what something means: a concept, an acronym, an error message, a term of art, or a confusing sentence. They want clarity, not a lecture.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Say what it is in one plain sentence, no jargon. If you must use a technical term, define it in the same breath.
|
|
15
|
+
2. Say why it matters or where it shows up, so the user knows why they should care.
|
|
16
|
+
3. Give one concrete analogy or a small worked example that makes it click.
|
|
17
|
+
4. Match the level to the audience. If the user asks for "explain to a beginner" or "to a senior engineer", adjust depth, assumed background, and vocabulary accordingly. When unsure, aim for a smart non-expert.
|
|
18
|
+
5. Keep it short. Stop once the idea lands; offer to go deeper rather than front-loading everything.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
Request: explain what a race condition is, to a beginner.
|
|
23
|
+
|
|
24
|
+
A race condition is when the result of your program depends on which of two things happens first, and you cannot control the order. It matters because it makes bugs that appear only sometimes. Think of two people editing the same shared doc line at the same time: whoever saves last wins, and the other edit silently vanishes.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain-code
|
|
3
|
+
description: Walk through what a file or function does and how it fits into the wider system. Use to onboard onto unfamiliar code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# explain-code
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user shares a file or function they did not write (or wrote a while ago) and wants to understand what it does, how it works, and how it connects to the rest of the code.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Start with a one-line purpose: what this code is for, in plain terms.
|
|
15
|
+
2. Describe the flow and structure: the main steps or branches, in the order they run. Explain intent, not every line.
|
|
16
|
+
3. Call out key dependencies and side effects: what it imports or calls, what state it reads or mutates, what it writes to disk, network, or the console.
|
|
17
|
+
4. Flag any gotchas: assumptions it makes, inputs it does not handle, and non-obvious behavior a reader would trip on.
|
|
18
|
+
5. Match depth to size. A small helper gets a few sentences; a large module gets a section per responsibility. Do not restate the code line by line.
|
|
19
|
+
6. If something is genuinely unclear or looks buggy, say so rather than inventing a rationale.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Purpose: parses a config file and returns a typed settings object.
|
|
24
|
+
|
|
25
|
+
Flow: reads the file, tries JSON.parse, falls back to defaults on any error, then validates required keys.
|
|
26
|
+
|
|
27
|
+
Gotcha: it swallows parse errors silently, so a malformed config looks identical to a missing one.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan
|
|
3
|
+
description: Break a goal into a concrete, ordered set of steps. Use when a task is big enough to need sequencing before starting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# plan
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user states a goal or a task and wants it broken into an actionable, ordered plan they can execute or hand off.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Restate the goal in one line so the target is unambiguous.
|
|
15
|
+
2. List the steps in the order they must happen. Each step gets a clear done-condition: how you will know it is finished.
|
|
16
|
+
3. Flag dependencies explicitly (step 4 needs step 2 done first) so the sequence is not just a wish list.
|
|
17
|
+
4. Call out the riskiest or most uncertain step, the one most likely to blow up the timeline, so it can be tackled or de-risked early.
|
|
18
|
+
5. Keep it actionable, not aspirational. Prefer "write the migration script and run it on a copy" over "improve the database".
|
|
19
|
+
6. End with the single immediate next action the user should take right now.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Goal: move the app from the old auth library to the new one.
|
|
24
|
+
|
|
25
|
+
1. Inventory every call site of the old library (done: a list exists).
|
|
26
|
+
2. Write an adapter matching the old interface (done: tests pass). RISKIEST: the token refresh path differs.
|
|
27
|
+
3. Swap call sites behind a flag (done: app runs on the flag).
|
|
28
|
+
|
|
29
|
+
Next action: run the inventory grep in step 1.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rewrite
|
|
3
|
+
description: Tighten and clarify a piece of text without changing its meaning. Use for a neutral clarity pass on text the user provides.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rewrite
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user pastes a paragraph, email, or passage and wants it cleaner and clearer while keeping the same meaning. This is a neutral clarity pass, distinct from a voice-anchored writing task: keep the author's own voice unless a tone is requested.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Preserve intent and every fact. Do not add claims, drop caveats, or change the point being made.
|
|
15
|
+
2. Cut filler and hedging: "just", "really", "I think that", "in order to", throat-clearing intros, and redundant phrases.
|
|
16
|
+
3. Prefer plain words over inflated ones ("use" not "utilize", "help" not "facilitate").
|
|
17
|
+
4. Vary sentence length so it reads naturally; break up run-ons, and do not make everything staccato.
|
|
18
|
+
5. Keep the author's voice and register. Only shift tone (more formal, warmer, punchier) if the user asks for it.
|
|
19
|
+
6. Return only the rewritten text, then one short line noting what you changed (for example: "tightened, cut hedging, split two long sentences").
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Before: I just wanted to reach out in order to see if you might possibly have some availability to maybe hop on a quick call sometime.
|
|
24
|
+
|
|
25
|
+
After: Do you have time for a quick call this week?
|
|
26
|
+
|
|
27
|
+
Changed: cut hedging and filler, made it a direct ask.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: summarize
|
|
3
|
+
description: Condense a document, article, transcript, or pasted text into its key points. Use when the input is long and the user wants the gist fast.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# summarize
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
The user pastes or points at a document, article, transcript, email thread, or any long block of text and wants a shorter version that keeps what matters.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. Read the whole input first and identify its core purpose in one sentence (what it is and why it exists).
|
|
15
|
+
2. Open with a one-line TLDR that captures the single most important takeaway.
|
|
16
|
+
3. Pull the main points: arguments, decisions, numbers, dates, and named people or systems. Keep these verbatim where accuracy matters.
|
|
17
|
+
4. Drop filler: greetings, repetition, hedging, and asides that do not change the meaning.
|
|
18
|
+
5. Scale the output to the input. A few paragraphs get a short bulleted list; a long report gets tight prose grouped by theme. Do not pad.
|
|
19
|
+
6. Do not add facts, opinions, or conclusions that are not in the source.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
TLDR: The team is shipping the payments rewrite next week despite one open risk.
|
|
24
|
+
|
|
25
|
+
- Decision: launch v2 on the 14th, feature-flagged to 10 percent of users.
|
|
26
|
+
- Open risk: the refund path is untested under load.
|
|
27
|
+
- Owner: Priya to run a load test before Friday.
|