@holt-os/holt 0.5.2 → 0.6.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 -1
- package/dist/cli.js +209 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ The point of `/brain`: Holt owns the transcript, so you can start a thread on on
|
|
|
63
63
|
|
|
64
64
|
Every exchange is saved to `<folder>/.holt/memory/turns.jsonl`, private and local. On each message, Holt recalls the most relevant moments from your *past* sessions in that folder and hands them to the brain, so it remembers what you told it last week.
|
|
65
65
|
|
|
66
|
+
When you end a chat, Holt asks the brain to distill 1 to 5 durable facts from the session (decisions, preferences, key names, numbers) and writes them to a human-readable `<folder>/.holt/memory/facts.md` you can read and edit. Those facts are also embedded and ranked slightly higher than raw turns during recall, so the signal rises to the top over time. See them with `holt memory facts`. Turn it off by setting `memory.extractFacts` to `false` in `config.json`.
|
|
67
|
+
|
|
66
68
|
Two recall modes, picked automatically:
|
|
67
69
|
|
|
68
70
|
- **Semantic** (best): a local [Ollama](https://ollama.com) with an embedding model, which `holt init` offers to set up for you. Recall matches by meaning: asking "who owns my apartment" finds "my landlord is called Pieter". No API keys, nothing leaves your machine.
|
|
@@ -72,6 +74,7 @@ Inspect it any time:
|
|
|
72
74
|
|
|
73
75
|
```bash
|
|
74
76
|
holt memory # stats for this folder
|
|
77
|
+
holt memory facts # show the distilled facts (facts.md)
|
|
75
78
|
holt memory search <query> # find remembered moments
|
|
76
79
|
holt memory embed # embed older moments for semantic recall
|
|
77
80
|
holt memory clear # wipe this folder's memory
|
|
@@ -155,7 +158,7 @@ Small strongly-typed **TypeScript core**, zero runtime dependencies: command dis
|
|
|
155
158
|
Built in always-shippable phases toward a full-vision v1:
|
|
156
159
|
|
|
157
160
|
0. **Skeleton and chat**: trust, init with install and sign-in, chat, brain switching with kept context *(shipped)*
|
|
158
|
-
1. **Memory**: per-folder store, semantic recall
|
|
161
|
+
1. **Memory**: per-folder store, semantic recall, streaming replies, and distilled facts per session *(shipped)*
|
|
159
162
|
2. **Any LLM directly**: API brains with your own key, HTML or Markdown output *(shipped)*
|
|
160
163
|
3. **Skills**: portable SKILL.md skills, create/add/run *(shipped)*
|
|
161
164
|
4. **Knowledge graph**: see and navigate your own memory with `holt graph` *(shipped)*
|
package/dist/cli.js
CHANGED
|
@@ -121,7 +121,7 @@ function defaultConfig() {
|
|
|
121
121
|
const d = BRAIN_DEFS[id];
|
|
122
122
|
brains[id] = { id, label: d.label, command: d.command, args: [...d.args], enabled: false };
|
|
123
123
|
}
|
|
124
|
-
return { version:
|
|
124
|
+
return { version: 4, defaultBrain: null, brains, apiBrains: [], outputFormat: "markdown", memory: { extractFacts: true } };
|
|
125
125
|
}
|
|
126
126
|
function loadConfig() {
|
|
127
127
|
const path = wsConfigPath();
|
|
@@ -132,11 +132,14 @@ function loadConfig() {
|
|
|
132
132
|
const brains = cfg.brains ?? {};
|
|
133
133
|
for (const id of BRAIN_IDS) if (!brains[id]) brains[id] = base.brains[id];
|
|
134
134
|
return {
|
|
135
|
-
version:
|
|
135
|
+
version: 4,
|
|
136
136
|
defaultBrain: cfg.defaultBrain ?? null,
|
|
137
137
|
brains,
|
|
138
138
|
apiBrains: Array.isArray(cfg.apiBrains) ? cfg.apiBrains : [],
|
|
139
|
-
outputFormat: cfg.outputFormat === "html" ? "html" : "markdown"
|
|
139
|
+
outputFormat: cfg.outputFormat === "html" ? "html" : "markdown",
|
|
140
|
+
memory: {
|
|
141
|
+
extractFacts: cfg.memory && typeof cfg.memory.extractFacts === "boolean" ? cfg.memory.extractFacts : true
|
|
142
|
+
}
|
|
140
143
|
};
|
|
141
144
|
} catch {
|
|
142
145
|
return null;
|
|
@@ -415,6 +418,9 @@ function memDir() {
|
|
|
415
418
|
function memPath() {
|
|
416
419
|
return join4(memDir(), "turns.jsonl");
|
|
417
420
|
}
|
|
421
|
+
function factsMdPath() {
|
|
422
|
+
return join4(memDir(), "facts.md");
|
|
423
|
+
}
|
|
418
424
|
function newSessionId() {
|
|
419
425
|
return randomUUID().slice(0, 8);
|
|
420
426
|
}
|
|
@@ -470,12 +476,58 @@ function appendTurn(t) {
|
|
|
470
476
|
function clearMemory() {
|
|
471
477
|
if (existsSync4(memPath())) rmSync2(memPath());
|
|
472
478
|
}
|
|
479
|
+
function normalizeFact(s) {
|
|
480
|
+
return s.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/, "");
|
|
481
|
+
}
|
|
482
|
+
var FACTS_HEADER = "# Holt facts\n\nDistilled memories for this folder. Safe to edit by hand.\n";
|
|
483
|
+
async function saveFact(content, session) {
|
|
484
|
+
const clean = content.trim();
|
|
485
|
+
if (!clean) return false;
|
|
486
|
+
const norm2 = normalizeFact(clean);
|
|
487
|
+
for (const t of loadTurns()) {
|
|
488
|
+
if (t.role === "fact" && normalizeFact(t.content) === norm2) return false;
|
|
489
|
+
}
|
|
490
|
+
if (existsSync4(factsMdPath())) {
|
|
491
|
+
for (const line of readFileSync4(factsMdPath(), "utf8").split("\n")) {
|
|
492
|
+
const l = line.trim();
|
|
493
|
+
if (!l.startsWith("- ")) continue;
|
|
494
|
+
if (normalizeFact(l.slice(2)) === norm2) return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
mkdirSync4(memDir(), { recursive: true });
|
|
498
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
499
|
+
let existing = existsSync4(factsMdPath()) ? readFileSync4(factsMdPath(), "utf8") : "";
|
|
500
|
+
if (!existing) existing = FACTS_HEADER;
|
|
501
|
+
const headings = existing.match(/^## (\d{4}-\d{2}-\d{2})/gm);
|
|
502
|
+
const lastHeading = headings && headings.length ? headings[headings.length - 1] : null;
|
|
503
|
+
const needsHeading = lastHeading !== `## ${today}`;
|
|
504
|
+
let addition = "";
|
|
505
|
+
if (needsHeading) {
|
|
506
|
+
if (!existing.endsWith("\n")) addition += "\n";
|
|
507
|
+
addition += `
|
|
508
|
+
## ${today}
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
addition += `- ${clean}
|
|
512
|
+
`;
|
|
513
|
+
writeFileSync4(factsMdPath(), existing + addition, "utf8");
|
|
514
|
+
appendTurn({
|
|
515
|
+
id: randomUUID().slice(0, 8),
|
|
516
|
+
ts: Date.now(),
|
|
517
|
+
session,
|
|
518
|
+
role: "fact",
|
|
519
|
+
content: clean,
|
|
520
|
+
emb: await embed(clean) ?? void 0
|
|
521
|
+
});
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
473
524
|
function memStats() {
|
|
474
525
|
const turns = loadTurns();
|
|
475
526
|
const sessions = new Set(turns.map((t) => t.session)).size;
|
|
476
527
|
const withEmbeddings = turns.filter((t) => Array.isArray(t.emb)).length;
|
|
528
|
+
const facts = turns.filter((t) => t.role === "fact").length;
|
|
477
529
|
const bytes = existsSync4(memPath()) ? statSync(memPath()).size : 0;
|
|
478
|
-
return { turns: turns.length, sessions, withEmbeddings, bytes };
|
|
530
|
+
return { turns: turns.length, facts, sessions, withEmbeddings, bytes };
|
|
479
531
|
}
|
|
480
532
|
function cosine(a, b) {
|
|
481
533
|
let dot = 0;
|
|
@@ -503,6 +555,7 @@ function keywordScore(q, text) {
|
|
|
503
555
|
for (const w of q) if (t.has(w)) hit++;
|
|
504
556
|
return hit / q.size;
|
|
505
557
|
}
|
|
558
|
+
var FACT_BOOST = 1.15;
|
|
506
559
|
async function recall(query, currentSession, k = 4) {
|
|
507
560
|
const past = loadTurns().filter((t) => t.session !== currentSession);
|
|
508
561
|
if (past.length === 0) return [];
|
|
@@ -513,7 +566,10 @@ async function recall(query, currentSession, k = 4) {
|
|
|
513
566
|
let score = 0;
|
|
514
567
|
if (qEmb && Array.isArray(turn.emb)) score = cosine(qEmb, turn.emb);
|
|
515
568
|
else score = keywordScore(qTok, turn.content);
|
|
516
|
-
if (score > (qEmb && Array.isArray(turn.emb) ? 0.35 : 0.15))
|
|
569
|
+
if (score > (qEmb && Array.isArray(turn.emb) ? 0.35 : 0.15)) {
|
|
570
|
+
if (turn.role === "fact") score *= FACT_BOOST;
|
|
571
|
+
scored.push({ turn, score });
|
|
572
|
+
}
|
|
517
573
|
}
|
|
518
574
|
scored.sort((a, b) => b.score - a.score);
|
|
519
575
|
return scored.slice(0, k);
|
|
@@ -975,6 +1031,115 @@ async function runApiBrain(brain, prompt, onChunk) {
|
|
|
975
1031
|
return { ok: false, text: `${brain.provider} returned an empty reply.` };
|
|
976
1032
|
}
|
|
977
1033
|
|
|
1034
|
+
// src/facts.ts
|
|
1035
|
+
var MIN_EXCHANGES_FOR_EXTRACTION = 3;
|
|
1036
|
+
var MAX_TRANSCRIPT_TURNS = 20;
|
|
1037
|
+
var MAX_TURN_CHARS = 1e3;
|
|
1038
|
+
function buildExtractionPrompt(history) {
|
|
1039
|
+
const recent = history.slice(-MAX_TRANSCRIPT_TURNS);
|
|
1040
|
+
const transcript = recent.map((t) => `${t.role === "user" ? "USER" : "ASSISTANT"}: ${t.content.slice(0, MAX_TURN_CHARS)}`).join("\n\n");
|
|
1041
|
+
const lines = [
|
|
1042
|
+
"Given this conversation, extract 1 to 5 durable facts worth remembering long-term.",
|
|
1043
|
+
"Output ONLY a JSON array of strings, no prose, no code fences.",
|
|
1044
|
+
"",
|
|
1045
|
+
"Rules:",
|
|
1046
|
+
"- Keep decisions, outcomes, stable preferences, key names, dates, and numbers.",
|
|
1047
|
+
"- Skip pleasantries, one-off questions, and generic advice.",
|
|
1048
|
+
"- Make each fact self-contained so it reads on its own in a future session.",
|
|
1049
|
+
"- If nothing is worth saving, output exactly []",
|
|
1050
|
+
"",
|
|
1051
|
+
"Good examples:",
|
|
1052
|
+
'- "User decided to target the Netherlands as the primary job market, HSM visa route."',
|
|
1053
|
+
'- "HDFC EMI is 17,394 rupees per month with 29 payments remaining as of March 2026."',
|
|
1054
|
+
"",
|
|
1055
|
+
"Bad examples (do NOT output these):",
|
|
1056
|
+
'- "User asked a question about their finances."',
|
|
1057
|
+
'- "The assistant gave some helpful advice."',
|
|
1058
|
+
"",
|
|
1059
|
+
"Conversation:",
|
|
1060
|
+
transcript,
|
|
1061
|
+
"",
|
|
1062
|
+
"JSON array:"
|
|
1063
|
+
];
|
|
1064
|
+
return lines.join("\n");
|
|
1065
|
+
}
|
|
1066
|
+
function firstBalancedArray(s) {
|
|
1067
|
+
const start = s.indexOf("[");
|
|
1068
|
+
if (start < 0) return null;
|
|
1069
|
+
let depth = 0;
|
|
1070
|
+
let inStr = false;
|
|
1071
|
+
let esc = false;
|
|
1072
|
+
for (let i = start; i < s.length; i++) {
|
|
1073
|
+
const ch = s[i];
|
|
1074
|
+
if (inStr) {
|
|
1075
|
+
if (esc) esc = false;
|
|
1076
|
+
else if (ch === "\\") esc = true;
|
|
1077
|
+
else if (ch === '"') inStr = false;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (ch === '"') inStr = true;
|
|
1081
|
+
else if (ch === "[") depth++;
|
|
1082
|
+
else if (ch === "]") {
|
|
1083
|
+
depth--;
|
|
1084
|
+
if (depth === 0) {
|
|
1085
|
+
const slice = s.slice(start, i + 1);
|
|
1086
|
+
try {
|
|
1087
|
+
const parsed = JSON.parse(slice);
|
|
1088
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
function norm(s) {
|
|
1098
|
+
return s.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/, "");
|
|
1099
|
+
}
|
|
1100
|
+
function parseFacts(raw) {
|
|
1101
|
+
let s = (raw || "").trim();
|
|
1102
|
+
s = s.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
1103
|
+
const m = s.match(/\[[\s\S]*\]/);
|
|
1104
|
+
if (!m) return [];
|
|
1105
|
+
let arr = null;
|
|
1106
|
+
try {
|
|
1107
|
+
arr = JSON.parse(m[0]);
|
|
1108
|
+
} catch {
|
|
1109
|
+
arr = firstBalancedArray(s);
|
|
1110
|
+
}
|
|
1111
|
+
if (!Array.isArray(arr)) return [];
|
|
1112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1113
|
+
const out = [];
|
|
1114
|
+
for (const item of arr) {
|
|
1115
|
+
if (typeof item !== "string") continue;
|
|
1116
|
+
const v = String(item).trim();
|
|
1117
|
+
if (v.length <= 10) continue;
|
|
1118
|
+
const key = norm(v);
|
|
1119
|
+
if (seen.has(key)) continue;
|
|
1120
|
+
seen.add(key);
|
|
1121
|
+
out.push(v);
|
|
1122
|
+
if (out.length >= 5) break;
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
1126
|
+
async function extractAndSaveFacts(active, cfg, history, session) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (Math.floor(history.length / 2) < MIN_EXCHANGES_FOR_EXTRACTION) return 0;
|
|
1129
|
+
const prompt = buildExtractionPrompt(history);
|
|
1130
|
+
const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt) : await runApiBrain(active.brain, prompt);
|
|
1131
|
+
if (!res.ok) return 0;
|
|
1132
|
+
const facts = parseFacts(res.text);
|
|
1133
|
+
let saved = 0;
|
|
1134
|
+
for (const f of facts) {
|
|
1135
|
+
if (await saveFact(f, session)) saved++;
|
|
1136
|
+
}
|
|
1137
|
+
return saved;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return 0;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
978
1143
|
// src/output.ts
|
|
979
1144
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
980
1145
|
import { join as join5 } from "path";
|
|
@@ -1301,7 +1466,7 @@ async function chat() {
|
|
|
1301
1466
|
const stats = memStats();
|
|
1302
1467
|
console.log("\n" + c.accent("Holt") + c.dim(` brain: ${active.label}`));
|
|
1303
1468
|
console.log(c.dim(
|
|
1304
|
-
`Memory: ${stats.turns} moments from ${stats.sessions} session${stats.sessions === 1 ? "" : "s"} in this folder (recall: ${embedOk ? "embeddings via local Ollama" : "keyword match"}).`
|
|
1469
|
+
`Memory: ${stats.turns} moments (${stats.facts} facts) from ${stats.sessions} session${stats.sessions === 1 ? "" : "s"} in this folder (recall: ${embedOk ? "embeddings via local Ollama" : "keyword match"}).`
|
|
1305
1470
|
));
|
|
1306
1471
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
1307
1472
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
@@ -1343,7 +1508,7 @@ async function chat() {
|
|
|
1343
1508
|
else for (const h of hits) console.log(c.dim(` ${h.score.toFixed(2)} (${h.turn.role}) ${h.turn.content.slice(0, 110).replace(/\s+/g, " ")}`));
|
|
1344
1509
|
} else {
|
|
1345
1510
|
const s = memStats();
|
|
1346
|
-
console.log(c.dim(` ${s.turns} moments, ${s.sessions} sessions, ${s.withEmbeddings} embedded, ${(s.bytes / 1024).toFixed(1)} KB in ./.holt/memory/`));
|
|
1511
|
+
console.log(c.dim(` ${s.turns} moments, ${s.facts} facts, ${s.sessions} sessions, ${s.withEmbeddings} embedded, ${(s.bytes / 1024).toFixed(1)} KB in ./.holt/memory/`));
|
|
1347
1512
|
console.log(c.dim(' usage: /memory <query> to preview recall, or "holt memory clear" to wipe.'));
|
|
1348
1513
|
}
|
|
1349
1514
|
continue;
|
|
@@ -1451,6 +1616,14 @@ async function chat() {
|
|
|
1451
1616
|
console.log(c.red("\n " + res.text + "\n"));
|
|
1452
1617
|
}
|
|
1453
1618
|
}
|
|
1619
|
+
if (cfg.memory?.extractFacts !== false) {
|
|
1620
|
+
try {
|
|
1621
|
+
const arg = active.kind === "cli" ? { kind: "cli", id: active.id } : { kind: "api", brain: active.brain };
|
|
1622
|
+
const n = await extractAndSaveFacts(arg, cfg, history, session);
|
|
1623
|
+
if (n > 0) console.log(c.dim(` distilled ${n} fact${n === 1 ? "" : "s"} from this session.`));
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1454
1627
|
close();
|
|
1455
1628
|
console.log(c.dim("\nBye.\n"));
|
|
1456
1629
|
}
|
|
@@ -1472,6 +1645,7 @@ async function login(which) {
|
|
|
1472
1645
|
}
|
|
1473
1646
|
|
|
1474
1647
|
// src/commands/memory.ts
|
|
1648
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
1475
1649
|
async function memoryCmd(sub, rest = []) {
|
|
1476
1650
|
const { ask, close } = createReader();
|
|
1477
1651
|
if (!await ensureTrusted(ask)) {
|
|
@@ -1505,7 +1679,7 @@ async function memoryCmd(sub, rest = []) {
|
|
|
1505
1679
|
}
|
|
1506
1680
|
const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
|
|
1507
1681
|
if (missing === 0) {
|
|
1508
|
-
console.log(c.dim("\n All
|
|
1682
|
+
console.log(c.dim("\n All memories already have embeddings.\n"));
|
|
1509
1683
|
close();
|
|
1510
1684
|
return;
|
|
1511
1685
|
}
|
|
@@ -1513,7 +1687,18 @@ async function memoryCmd(sub, rest = []) {
|
|
|
1513
1687
|
const r = await backfillEmbeddings((done, total) => {
|
|
1514
1688
|
process.stdout.write(`\r embedding ${done}/${total}...`);
|
|
1515
1689
|
});
|
|
1516
|
-
console.log("\n" + c.green(` Done. ${r.embedded} of ${r.total}
|
|
1690
|
+
console.log("\n" + c.green(` Done. ${r.embedded} of ${r.total} memories embedded.`) + "\n");
|
|
1691
|
+
close();
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (action === "facts") {
|
|
1695
|
+
if (!existsSync6(factsMdPath())) {
|
|
1696
|
+
console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
|
|
1697
|
+
close();
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const body = readFileSync6(factsMdPath(), "utf8").trim();
|
|
1701
|
+
console.log("\n" + body + "\n");
|
|
1517
1702
|
close();
|
|
1518
1703
|
return;
|
|
1519
1704
|
}
|
|
@@ -1540,6 +1725,7 @@ async function memoryCmd(sub, rest = []) {
|
|
|
1540
1725
|
const sessions = new Set(loadTurns().map((t) => t.session)).size;
|
|
1541
1726
|
console.log("\n" + c.accent("Holt memory") + c.dim(" (this folder)"));
|
|
1542
1727
|
console.log(` moments ${s.turns}`);
|
|
1728
|
+
console.log(` facts ${s.facts} (./.holt/memory/facts.md)`);
|
|
1543
1729
|
console.log(` sessions ${sessions}`);
|
|
1544
1730
|
console.log(` embedded ${s.withEmbeddings} of ${s.turns}`);
|
|
1545
1731
|
console.log(` size ${(s.bytes / 1024).toFixed(1)} KB (./.holt/memory/turns.jsonl)`);
|
|
@@ -1549,7 +1735,8 @@ async function memoryCmd(sub, rest = []) {
|
|
|
1549
1735
|
${s.turns - s.withEmbeddings} moments lack embeddings. Run "holt memory embed" to upgrade them to semantic recall.`));
|
|
1550
1736
|
}
|
|
1551
1737
|
console.log(c.dim("\n holt memory search <query> find remembered moments"));
|
|
1552
|
-
console.log(c.dim(" holt memory
|
|
1738
|
+
console.log(c.dim(" holt memory facts show distilled facts (facts.md)"));
|
|
1739
|
+
console.log(c.dim(" holt memory embed embed older memories for semantic recall"));
|
|
1553
1740
|
console.log(c.dim(" holt memory clear wipe this folder's memory\n"));
|
|
1554
1741
|
close();
|
|
1555
1742
|
}
|
|
@@ -1558,8 +1745,8 @@ async function memoryCmd(sub, rest = []) {
|
|
|
1558
1745
|
import {
|
|
1559
1746
|
mkdirSync as mkdirSync5,
|
|
1560
1747
|
writeFileSync as writeFileSync6,
|
|
1561
|
-
readFileSync as
|
|
1562
|
-
existsSync as
|
|
1748
|
+
readFileSync as readFileSync7,
|
|
1749
|
+
existsSync as existsSync7,
|
|
1563
1750
|
readdirSync as readdirSync2,
|
|
1564
1751
|
statSync as statSync3,
|
|
1565
1752
|
rmSync as rmSync3,
|
|
@@ -1612,7 +1799,7 @@ function isGitUrl(s) {
|
|
|
1612
1799
|
return s.includes("://") || s.endsWith(".git") || s.startsWith("git@");
|
|
1613
1800
|
}
|
|
1614
1801
|
function findSkillDir(root) {
|
|
1615
|
-
if (
|
|
1802
|
+
if (existsSync7(join7(root, "SKILL.md"))) return root;
|
|
1616
1803
|
let subs = [];
|
|
1617
1804
|
try {
|
|
1618
1805
|
subs = readdirSync2(root).filter((f) => {
|
|
@@ -1625,7 +1812,7 @@ function findSkillDir(root) {
|
|
|
1625
1812
|
} catch {
|
|
1626
1813
|
return null;
|
|
1627
1814
|
}
|
|
1628
|
-
const withSkill = subs.filter((f) =>
|
|
1815
|
+
const withSkill = subs.filter((f) => existsSync7(join7(root, f, "SKILL.md")));
|
|
1629
1816
|
if (withSkill.length === 1) return join7(root, withSkill[0]);
|
|
1630
1817
|
return null;
|
|
1631
1818
|
}
|
|
@@ -1678,7 +1865,7 @@ function cmdCreate(name, global) {
|
|
|
1678
1865
|
}
|
|
1679
1866
|
const scope = global ? "global" : "workspace";
|
|
1680
1867
|
const dir = join7(skillsRoot(scope), clean);
|
|
1681
|
-
if (
|
|
1868
|
+
if (existsSync7(dir)) {
|
|
1682
1869
|
console.log(c.red(`
|
|
1683
1870
|
A ${scope} skill named "${clean}" already exists.`));
|
|
1684
1871
|
console.log(c.dim(" " + dir + "\n"));
|
|
@@ -1712,7 +1899,7 @@ function cmdAdd(source, global) {
|
|
|
1712
1899
|
fetched = tempDir;
|
|
1713
1900
|
} else {
|
|
1714
1901
|
fetched = resolve(source);
|
|
1715
|
-
if (!
|
|
1902
|
+
if (!existsSync7(fetched)) {
|
|
1716
1903
|
console.log(c.red(`
|
|
1717
1904
|
No such path: ${fetched}
|
|
1718
1905
|
`));
|
|
@@ -1726,7 +1913,7 @@ function cmdAdd(source, global) {
|
|
|
1726
1913
|
}
|
|
1727
1914
|
let skillName = "";
|
|
1728
1915
|
try {
|
|
1729
|
-
const { data } = parseFrontmatter(
|
|
1916
|
+
const { data } = parseFrontmatter(readFileSync7(join7(skillDir, "SKILL.md"), "utf8"));
|
|
1730
1917
|
skillName = sanitizeName(data.name || "");
|
|
1731
1918
|
} catch {
|
|
1732
1919
|
skillName = "";
|
|
@@ -1737,7 +1924,7 @@ function cmdAdd(source, global) {
|
|
|
1737
1924
|
return;
|
|
1738
1925
|
}
|
|
1739
1926
|
const dest = join7(skillsRoot(scope), skillName);
|
|
1740
|
-
if (
|
|
1927
|
+
if (existsSync7(dest)) {
|
|
1741
1928
|
console.log(c.red(`
|
|
1742
1929
|
A ${scope} skill named "${skillName}" already exists.`));
|
|
1743
1930
|
console.log(c.dim(" " + dest + "\n"));
|
|
@@ -1746,12 +1933,12 @@ function cmdAdd(source, global) {
|
|
|
1746
1933
|
mkdirSync5(skillsRoot(scope), { recursive: true });
|
|
1747
1934
|
cpSync(skillDir, dest, { recursive: true });
|
|
1748
1935
|
const nestedGit = join7(dest, ".git");
|
|
1749
|
-
if (
|
|
1936
|
+
if (existsSync7(nestedGit)) rmSync3(nestedGit, { recursive: true, force: true });
|
|
1750
1937
|
console.log(c.green(`
|
|
1751
1938
|
Installed ${scope} skill "${skillName}".`));
|
|
1752
1939
|
console.log(c.dim(" " + join7(dest, "SKILL.md") + "\n"));
|
|
1753
1940
|
} finally {
|
|
1754
|
-
if (tempDir &&
|
|
1941
|
+
if (tempDir && existsSync7(tempDir)) {
|
|
1755
1942
|
try {
|
|
1756
1943
|
rmSync3(tempDir, { recursive: true, force: true });
|
|
1757
1944
|
} catch {
|
|
@@ -1767,7 +1954,7 @@ async function cmdRemove(name, ask) {
|
|
|
1767
1954
|
}
|
|
1768
1955
|
const wsDir = join7(skillsRoot("workspace"), clean);
|
|
1769
1956
|
const globalDir = join7(skillsRoot("global"), clean);
|
|
1770
|
-
const target =
|
|
1957
|
+
const target = existsSync7(wsDir) ? wsDir : existsSync7(globalDir) ? globalDir : "";
|
|
1771
1958
|
if (!target) {
|
|
1772
1959
|
console.log(c.dim(`
|
|
1773
1960
|
No skill named "${clean}" found.
|
|
@@ -2506,7 +2693,7 @@ async function graph(args = []) {
|
|
|
2506
2693
|
}
|
|
2507
2694
|
|
|
2508
2695
|
// src/cli.ts
|
|
2509
|
-
var VERSION = "0.
|
|
2696
|
+
var VERSION = "0.6.0";
|
|
2510
2697
|
var BANNER = `
|
|
2511
2698
|
\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
2512
2699
|
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D
|