@vectorize-io/self-driving-agents 0.0.23 → 0.0.26
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 +486 -87
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +110 -35
- package/dist/skill/SKILL.md +3 -1
- package/dist/tests/cli.test.js +88 -0
- package/dist/tests/lint.test.d.ts +1 -0
- package/dist/tests/lint.test.js +85 -0
- package/hermes-plugin/__init__.py +470 -0
- package/hermes-plugin/plugin.yaml +7 -0
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,17 @@ function isLocalPath(input) {
|
|
|
50
50
|
input.startsWith("/") ||
|
|
51
51
|
input.startsWith("~"));
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a string is a valid agent name. Used by --empty mode where the
|
|
55
|
+
* first positional arg becomes the agent name (no path, no GitHub fetch).
|
|
56
|
+
*
|
|
57
|
+
* Rules: starts with [a-z0-9], then lowercase alphanumerics or hyphens, max
|
|
58
|
+
* 64 chars. Matches the lowercase-with-hyphens convention the create-agent
|
|
59
|
+
* skill expects.
|
|
60
|
+
*/
|
|
61
|
+
export function isValidAgentName(name) {
|
|
62
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(name) && name.length <= 64;
|
|
63
|
+
}
|
|
53
64
|
/**
|
|
54
65
|
* Resolve the agent specifier to a local directory.
|
|
55
66
|
*
|
|
@@ -649,7 +660,9 @@ curl -s -X POST ${apiUrl}/v1/default/banks/${bankId}/memories/retain \\
|
|
|
649
660
|
execSync(`cd "${outDir}" && zip -j "${zipPath}" SKILL.md`, { stdio: "pipe" });
|
|
650
661
|
return zipPath;
|
|
651
662
|
}
|
|
652
|
-
async function promptClaudeConfig(agentId, opts = {
|
|
663
|
+
async function promptClaudeConfig(agentId, opts = {}) {
|
|
664
|
+
const askBankId = opts.askBankId ?? true;
|
|
665
|
+
const allowLocalhost = opts.allowLocalhost ?? false;
|
|
653
666
|
const deploymentType = await p.select({
|
|
654
667
|
message: "Hindsight deployment:",
|
|
655
668
|
options: [
|
|
@@ -668,11 +681,14 @@ async function promptClaudeConfig(agentId, opts = { askBankId: true }) {
|
|
|
668
681
|
else {
|
|
669
682
|
const urlInput = await p.text({
|
|
670
683
|
message: "Hindsight API URL:",
|
|
671
|
-
placeholder:
|
|
684
|
+
placeholder: allowLocalhost
|
|
685
|
+
? "http://localhost:9077 or https://your-hindsight.example.com"
|
|
686
|
+
: "https://your-hindsight.example.com",
|
|
672
687
|
validate: (val) => {
|
|
673
688
|
if (!val)
|
|
674
689
|
return "URL is required";
|
|
675
|
-
if (
|
|
690
|
+
if (!allowLocalhost &&
|
|
691
|
+
(val.startsWith("http://localhost") || val.startsWith("http://127.0.0.1"))) {
|
|
676
692
|
return "Claude cannot reach localhost. Use a publicly accessible URL.";
|
|
677
693
|
}
|
|
678
694
|
return undefined;
|
|
@@ -683,7 +699,9 @@ async function promptClaudeConfig(agentId, opts = { askBankId: true }) {
|
|
|
683
699
|
process.exit(0);
|
|
684
700
|
}
|
|
685
701
|
apiUrl = urlInput;
|
|
686
|
-
|
|
702
|
+
if (!allowLocalhost) {
|
|
703
|
+
p.log.warn("Make sure your Hindsight instance is publicly accessible from Claude's servers.");
|
|
704
|
+
}
|
|
687
705
|
}
|
|
688
706
|
const tokenInput = await p.password({ message: "Hindsight API token:" });
|
|
689
707
|
if (p.isCancel(tokenInput)) {
|
|
@@ -692,7 +710,7 @@ async function promptClaudeConfig(agentId, opts = { askBankId: true }) {
|
|
|
692
710
|
}
|
|
693
711
|
const apiToken = tokenInput || undefined;
|
|
694
712
|
let bankId = agentId;
|
|
695
|
-
if (
|
|
713
|
+
if (askBankId) {
|
|
696
714
|
const bankInput = await p.text({
|
|
697
715
|
message: "Bank ID:",
|
|
698
716
|
initialValue: agentId,
|
|
@@ -713,16 +731,19 @@ async function main() {
|
|
|
713
731
|
${color.bold("self-driving-agents")} — install a self-driving agent
|
|
714
732
|
|
|
715
733
|
${color.dim("Usage:")}
|
|
716
|
-
npx @vectorize-io/self-driving-agents install <agent> --harness <
|
|
734
|
+
npx @vectorize-io/self-driving-agents install <agent> --harness <h> ${color.dim("# from path or GitHub")}
|
|
735
|
+
npx @vectorize-io/self-driving-agents install <name> --harness <h> --empty ${color.dim("# blank agent")}
|
|
717
736
|
|
|
718
|
-
${color.dim("Agent sources:")}
|
|
737
|
+
${color.dim("Agent sources (without --empty):")}
|
|
719
738
|
${color.cyan("marketing-agent")} → ${DEFAULT_REPO}/marketing-agent
|
|
720
739
|
${color.cyan("org/repo/my-agent")} → org/repo/my-agent on GitHub
|
|
721
740
|
${color.cyan("./local-dir")} → local directory
|
|
722
741
|
|
|
723
742
|
${color.dim("Options:")}
|
|
724
743
|
${color.cyan("--harness <h>")} Required. openclaw | nemoclaw | hermes | claude | claude-code
|
|
725
|
-
${color.cyan("--
|
|
744
|
+
${color.cyan("--empty")} Create a blank agent. First positional becomes the agent name;
|
|
745
|
+
no content fetched, no bank-template imported from disk.
|
|
746
|
+
${color.cyan("--agent <name>")} Override the agent name (defaults to directory name)
|
|
726
747
|
${color.cyan("--sandbox <name>")} NemoClaw sandbox (auto-detected if only one exists)
|
|
727
748
|
`);
|
|
728
749
|
process.exit(0);
|
|
@@ -736,6 +757,7 @@ async function main() {
|
|
|
736
757
|
let harness;
|
|
737
758
|
let agentName;
|
|
738
759
|
let sandbox;
|
|
760
|
+
let isEmpty = false;
|
|
739
761
|
for (let i = 0; i < restArgs.length; i++) {
|
|
740
762
|
if (restArgs[i] === "--harness" && restArgs[i + 1])
|
|
741
763
|
harness = restArgs[++i];
|
|
@@ -743,6 +765,8 @@ async function main() {
|
|
|
743
765
|
agentName = restArgs[++i];
|
|
744
766
|
else if (restArgs[i] === "--sandbox" && restArgs[i + 1])
|
|
745
767
|
sandbox = restArgs[++i];
|
|
768
|
+
else if (restArgs[i] === "--empty")
|
|
769
|
+
isEmpty = true;
|
|
746
770
|
}
|
|
747
771
|
if (!harness) {
|
|
748
772
|
p.cancel("--harness required (openclaw | nemoclaw | hermes | claude | claude-code)");
|
|
@@ -752,9 +776,30 @@ async function main() {
|
|
|
752
776
|
sandbox = await detectNemoClawSandbox();
|
|
753
777
|
}
|
|
754
778
|
p.intro(color.bgCyan(color.black(` self-driving-agents `)));
|
|
755
|
-
// Step 0: Resolve agent directory (local or GitHub)
|
|
779
|
+
// Step 0: Resolve agent directory (local or GitHub) — skipped when --empty
|
|
780
|
+
// is set; in that case the first positional becomes the agent name and
|
|
781
|
+
// there's no source content to ingest.
|
|
756
782
|
const spin = p.spinner();
|
|
757
|
-
|
|
783
|
+
let dir = "";
|
|
784
|
+
let source;
|
|
785
|
+
let defaultName;
|
|
786
|
+
let cleanup;
|
|
787
|
+
if (isEmpty) {
|
|
788
|
+
if (!dirArg || dirArg.startsWith("-")) {
|
|
789
|
+
p.cancel("--empty needs an agent name as the first positional argument.\n" +
|
|
790
|
+
" e.g. install my-agent --harness claude-code --empty");
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
if (!isValidAgentName(dirArg)) {
|
|
794
|
+
p.cancel(`Invalid agent name '${dirArg}'. Use lowercase letters, digits, and hyphens (max 64 chars), e.g. my-agent.`);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
source = "<empty>";
|
|
798
|
+
defaultName = dirArg;
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
({ dir, source, defaultName, cleanup } = await resolveAgentDir(dirArg, spin));
|
|
802
|
+
}
|
|
758
803
|
try {
|
|
759
804
|
let agentId;
|
|
760
805
|
if (agentName) {
|
|
@@ -954,9 +999,29 @@ async function main() {
|
|
|
954
999
|
catch { /* ignore */ }
|
|
955
1000
|
}
|
|
956
1001
|
const hasConnection = ccConfig.hindsightApiUrl || ccConfig.llmProvider;
|
|
957
|
-
|
|
1002
|
+
let reconfigure = !hasConnection;
|
|
1003
|
+
if (hasConnection && process.stdin.isTTY) {
|
|
1004
|
+
const summary = ccConfig.hindsightApiUrl === HINDSIGHT_CLOUD_API_URL
|
|
1005
|
+
? "Cloud (api.hindsight.vectorize.io)"
|
|
1006
|
+
: ccConfig.hindsightApiUrl
|
|
1007
|
+
? `External: ${ccConfig.hindsightApiUrl}`
|
|
1008
|
+
: `LLM provider: ${ccConfig.llmProvider}`;
|
|
1009
|
+
const ok = await p.confirm({
|
|
1010
|
+
message: `Hindsight: ${color.cyan(summary)}. Use this?\n${color.dim(" Changing this will affect all existing Claude Code agents — one Claude Code install shares a single Hindsight connection.")}`,
|
|
1011
|
+
});
|
|
1012
|
+
if (p.isCancel(ok)) {
|
|
1013
|
+
p.cancel("Cancelled.");
|
|
1014
|
+
process.exit(0);
|
|
1015
|
+
}
|
|
1016
|
+
if (!ok)
|
|
1017
|
+
reconfigure = true;
|
|
1018
|
+
}
|
|
1019
|
+
if (reconfigure && process.stdin.isTTY) {
|
|
958
1020
|
// Bank ID is derived from agent + cwd at runtime — don't ask for it
|
|
959
|
-
const claudeConfig = await promptClaudeConfig(agentId, {
|
|
1021
|
+
const claudeConfig = await promptClaudeConfig(agentId, {
|
|
1022
|
+
askBankId: false,
|
|
1023
|
+
allowLocalhost: true,
|
|
1024
|
+
});
|
|
960
1025
|
ccConfig.hindsightApiUrl = claudeConfig.apiUrl;
|
|
961
1026
|
ccConfig.hindsightApiToken = claudeConfig.apiToken;
|
|
962
1027
|
}
|
|
@@ -993,22 +1058,24 @@ async function main() {
|
|
|
993
1058
|
mkdirSync(ccConfigDir, { recursive: true });
|
|
994
1059
|
writeFileSync(ccConfigPath, JSON.stringify(ccConfig, null, 2) + "\n");
|
|
995
1060
|
p.log.success(`Plugin config: ${color.dim(ccConfigPath)}`);
|
|
996
|
-
// Step 4: Save content locally for the agent
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1061
|
+
// Step 4: Save content locally for the agent (skipped when --empty —
|
|
1062
|
+
// the create-agent skill goes interactive without a `from <path>`).
|
|
1063
|
+
let contentDir = null;
|
|
1064
|
+
if (!isEmpty) {
|
|
1065
|
+
contentDir = join(homedir(), ".self-driving-agents", "claude-code", agentId);
|
|
1066
|
+
mkdirSync(contentDir, { recursive: true });
|
|
1067
|
+
const contentFiles = findContentFiles(dir);
|
|
1068
|
+
for (const relPath of contentFiles) {
|
|
1069
|
+
const destPath = join(contentDir, relPath);
|
|
1070
|
+
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
1071
|
+
writeFileSync(destPath, readFileSync(join(dir, relPath), "utf-8"));
|
|
1072
|
+
}
|
|
1073
|
+
const templateSrc = join(dir, "bank-template.json");
|
|
1074
|
+
if (existsSync(templateSrc)) {
|
|
1075
|
+
writeFileSync(join(contentDir, "bank-template.json"), readFileSync(templateSrc, "utf-8"));
|
|
1076
|
+
}
|
|
1077
|
+
p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
|
|
1010
1078
|
}
|
|
1011
|
-
p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
|
|
1012
1079
|
// Auto-approve hindsight MCP tools and skills in user settings
|
|
1013
1080
|
const userSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
1014
1081
|
let userSettings = {};
|
|
@@ -1041,7 +1108,11 @@ async function main() {
|
|
|
1041
1108
|
writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2) + "\n");
|
|
1042
1109
|
p.log.success("Auto-approved hindsight tools in Claude Code");
|
|
1043
1110
|
}
|
|
1044
|
-
|
|
1111
|
+
// With --empty the skill runs interactively (Mode B); otherwise it
|
|
1112
|
+
// ingests from the staged content directory (Mode A).
|
|
1113
|
+
const prompt = contentDir
|
|
1114
|
+
? `/hindsight-memory:create-agent ${agentId} from ${contentDir}`
|
|
1115
|
+
: `/hindsight-memory:create-agent ${agentId}`;
|
|
1045
1116
|
p.note([
|
|
1046
1117
|
`${color.yellow("⚠")} ${color.bold(`Important:`)} the agent's memory is scoped to the directory where you start ${color.cyan("claude")}.`,
|
|
1047
1118
|
` Always start your Claude Code sessions from the same project directory.`,
|
|
@@ -1089,20 +1160,24 @@ async function main() {
|
|
|
1089
1160
|
p.cancel(`Cannot reach Hindsight at ${apiUrl}\nStart the server or reconfigure the plugin.`);
|
|
1090
1161
|
process.exit(1);
|
|
1091
1162
|
}
|
|
1092
|
-
// Step 4: Import bank template
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1163
|
+
// Step 4: Import bank template — or, with --empty, provision a blank
|
|
1164
|
+
// bank so later writes from the harness have somewhere to land.
|
|
1165
|
+
const templatePath = isEmpty ? "" : join(dir, "bank-template.json");
|
|
1166
|
+
const hasTemplate = !isEmpty && existsSync(templatePath);
|
|
1167
|
+
if (isEmpty || hasTemplate) {
|
|
1168
|
+
spin.start(isEmpty ? "Provisioning bank..." : "Importing bank template...");
|
|
1169
|
+
const template = hasTemplate
|
|
1170
|
+
? JSON.parse(readFileSync(templatePath, "utf-8"))
|
|
1171
|
+
: { version: "1" };
|
|
1097
1172
|
await sdk.importBankTemplate({
|
|
1098
1173
|
client: lowLevel,
|
|
1099
1174
|
path: { bank_id: bankId },
|
|
1100
1175
|
body: template,
|
|
1101
1176
|
});
|
|
1102
|
-
spin.stop("Bank template imported");
|
|
1177
|
+
spin.stop(isEmpty ? "Bank provisioned" : "Bank template imported");
|
|
1103
1178
|
}
|
|
1104
1179
|
// Step 5: Ingest content (recursive — all text files except bank-template.json)
|
|
1105
|
-
const contentFiles = findContentFiles(dir);
|
|
1180
|
+
const contentFiles = isEmpty ? [] : findContentFiles(dir);
|
|
1106
1181
|
if (contentFiles.length > 0) {
|
|
1107
1182
|
spin.start(`Ingesting ${contentFiles.length} file(s)...`);
|
|
1108
1183
|
for (const relPath of contentFiles) {
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -41,7 +41,9 @@ Use when pages don't cover what you need.
|
|
|
41
41
|
|
|
42
42
|
## Ingesting documents
|
|
43
43
|
|
|
44
|
-
`agent_knowledge_ingest(title, content)` — upload raw content into memory. Never summarize before ingesting.
|
|
44
|
+
`agent_knowledge_ingest(title, content)` — upload raw content into memory. Never summarize before ingesting. Pass the full text inline.
|
|
45
|
+
|
|
46
|
+
`agent_knowledge_ingest_files(paths)` — ingest one or more files straight from disk. `paths` is a list of file paths or glob patterns (e.g. `["docs/**/*.md", "/abs/path/notes.txt"]`). Each file's content is read and stored under a document ID derived from its path. Prefer this over `agent_knowledge_ingest` when the content already lives in files — no need to read them first. Use absolute paths when in doubt; relative paths resolve against the working directory.
|
|
45
47
|
|
|
46
48
|
## Updating and deleting
|
|
47
49
|
|
package/dist/tests/cli.test.js
CHANGED
|
@@ -129,6 +129,94 @@ describe("isLocalPath", () => {
|
|
|
129
129
|
expect(isLocalPath("marketing/seo")).toBe(false);
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
describe("isValidAgentName", () => {
|
|
133
|
+
// Mirrors the regex in cli.ts. --empty mode validates the first positional
|
|
134
|
+
// arg (the agent name) against this since there's no path/GitHub fetch to
|
|
135
|
+
// implicitly sanitize.
|
|
136
|
+
function isValidAgentName(name) {
|
|
137
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(name) && name.length <= 64;
|
|
138
|
+
}
|
|
139
|
+
it("accepts lowercase with hyphens", () => {
|
|
140
|
+
expect(isValidAgentName("my-agent")).toBe(true);
|
|
141
|
+
expect(isValidAgentName("marketing-seo")).toBe(true);
|
|
142
|
+
expect(isValidAgentName("agent")).toBe(true);
|
|
143
|
+
expect(isValidAgentName("a1b2c3")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
it("accepts a single character", () => {
|
|
146
|
+
expect(isValidAgentName("a")).toBe(true);
|
|
147
|
+
expect(isValidAgentName("0")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
it("rejects uppercase", () => {
|
|
150
|
+
expect(isValidAgentName("MyAgent")).toBe(false);
|
|
151
|
+
expect(isValidAgentName("AGENT")).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
it("rejects names starting with hyphen", () => {
|
|
154
|
+
expect(isValidAgentName("-agent")).toBe(false);
|
|
155
|
+
expect(isValidAgentName("--empty")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
it("rejects empty string", () => {
|
|
158
|
+
expect(isValidAgentName("")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
it("rejects whitespace", () => {
|
|
161
|
+
expect(isValidAgentName("my agent")).toBe(false);
|
|
162
|
+
expect(isValidAgentName(" my-agent")).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it("rejects underscores and other punctuation", () => {
|
|
165
|
+
expect(isValidAgentName("my_agent")).toBe(false);
|
|
166
|
+
expect(isValidAgentName("my.agent")).toBe(false);
|
|
167
|
+
expect(isValidAgentName("my/agent")).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
it("rejects names longer than 64 characters", () => {
|
|
170
|
+
expect(isValidAgentName("a".repeat(64))).toBe(true);
|
|
171
|
+
expect(isValidAgentName("a".repeat(65))).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe("--empty arg parsing", () => {
|
|
175
|
+
// Mirrors the loop in main() that walks restArgs to pick out flag values.
|
|
176
|
+
function parseRestArgs(restArgs) {
|
|
177
|
+
let harness;
|
|
178
|
+
let agentName;
|
|
179
|
+
let sandbox;
|
|
180
|
+
let isEmpty = false;
|
|
181
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
182
|
+
if (restArgs[i] === "--harness" && restArgs[i + 1])
|
|
183
|
+
harness = restArgs[++i];
|
|
184
|
+
else if (restArgs[i] === "--agent" && restArgs[i + 1])
|
|
185
|
+
agentName = restArgs[++i];
|
|
186
|
+
else if (restArgs[i] === "--sandbox" && restArgs[i + 1])
|
|
187
|
+
sandbox = restArgs[++i];
|
|
188
|
+
else if (restArgs[i] === "--empty")
|
|
189
|
+
isEmpty = true;
|
|
190
|
+
}
|
|
191
|
+
return { harness, agentName, sandbox, isEmpty };
|
|
192
|
+
}
|
|
193
|
+
it("picks up --empty as a boolean", () => {
|
|
194
|
+
expect(parseRestArgs(["--harness", "claude-code", "--empty"]).isEmpty).toBe(true);
|
|
195
|
+
expect(parseRestArgs(["--harness", "claude-code"]).isEmpty).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
it("--empty does not consume the next argument", () => {
|
|
198
|
+
const r = parseRestArgs(["--empty", "--harness", "claude-code"]);
|
|
199
|
+
expect(r.isEmpty).toBe(true);
|
|
200
|
+
expect(r.harness).toBe("claude-code");
|
|
201
|
+
});
|
|
202
|
+
it("works alongside --harness, --agent, --sandbox", () => {
|
|
203
|
+
const r = parseRestArgs([
|
|
204
|
+
"--harness",
|
|
205
|
+
"nemoclaw",
|
|
206
|
+
"--empty",
|
|
207
|
+
"--agent",
|
|
208
|
+
"my-agent",
|
|
209
|
+
"--sandbox",
|
|
210
|
+
"default",
|
|
211
|
+
]);
|
|
212
|
+
expect(r).toEqual({
|
|
213
|
+
harness: "nemoclaw",
|
|
214
|
+
agentName: "my-agent",
|
|
215
|
+
sandbox: "default",
|
|
216
|
+
isEmpty: true,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
132
220
|
describe("deriveDefaultName", () => {
|
|
133
221
|
// Mirrors the logic in resolveAgentDir:
|
|
134
222
|
// - GitHub refs: subpath with / → hyphens (marketing/seo → marketing-seo)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
// @ts-expect-error - .mjs has no type declarations; runtime import is fine
|
|
6
|
+
import { findBankTemplates, lintBankTemplate, lintAll } from "../scripts/lint-bank-templates.mjs";
|
|
7
|
+
const REPO_ROOT = resolve(__dirname, "..", "..");
|
|
8
|
+
describe("lint-bank-templates", () => {
|
|
9
|
+
let dir;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "sda-lint-"));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
it("accepts a valid template (observations_mission + >=1 mental_models, no reflect_mission)", () => {
|
|
17
|
+
const file = join(dir, "bank-template.json");
|
|
18
|
+
writeFileSync(file, JSON.stringify({
|
|
19
|
+
version: "1",
|
|
20
|
+
bank: { observations_mission: "watch X" },
|
|
21
|
+
mental_models: [{ id: "a", name: "A" }],
|
|
22
|
+
}));
|
|
23
|
+
expect(lintBankTemplate(file)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
it("rejects missing observations_mission", () => {
|
|
26
|
+
const file = join(dir, "bank-template.json");
|
|
27
|
+
writeFileSync(file, JSON.stringify({ bank: {}, mental_models: [{ id: "a", name: "A" }] }));
|
|
28
|
+
const errs = lintBankTemplate(file);
|
|
29
|
+
expect(errs.some((e) => e.includes("observations_mission"))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it("rejects empty observations_mission", () => {
|
|
32
|
+
const file = join(dir, "bank-template.json");
|
|
33
|
+
writeFileSync(file, JSON.stringify({
|
|
34
|
+
bank: { observations_mission: " " },
|
|
35
|
+
mental_models: [{ id: "a", name: "A" }],
|
|
36
|
+
}));
|
|
37
|
+
const errs = lintBankTemplate(file);
|
|
38
|
+
expect(errs.some((e) => e.includes("observations_mission"))).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it("rejects empty mental_models", () => {
|
|
41
|
+
const file = join(dir, "bank-template.json");
|
|
42
|
+
writeFileSync(file, JSON.stringify({
|
|
43
|
+
bank: { observations_mission: "watch X" },
|
|
44
|
+
mental_models: [],
|
|
45
|
+
}));
|
|
46
|
+
const errs = lintBankTemplate(file);
|
|
47
|
+
expect(errs.some((e) => e.includes("mental_models"))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("rejects missing mental_models entirely", () => {
|
|
50
|
+
const file = join(dir, "bank-template.json");
|
|
51
|
+
writeFileSync(file, JSON.stringify({ bank: { observations_mission: "watch X" } }));
|
|
52
|
+
const errs = lintBankTemplate(file);
|
|
53
|
+
expect(errs.some((e) => e.includes("mental_models"))).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("rejects deprecated reflect_mission", () => {
|
|
56
|
+
const file = join(dir, "bank-template.json");
|
|
57
|
+
writeFileSync(file, JSON.stringify({
|
|
58
|
+
bank: { observations_mission: "watch X", reflect_mission: "old" },
|
|
59
|
+
mental_models: [{ id: "a", name: "A" }],
|
|
60
|
+
}));
|
|
61
|
+
const errs = lintBankTemplate(file);
|
|
62
|
+
expect(errs.some((e) => e.includes("reflect_mission"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it("rejects invalid JSON", () => {
|
|
65
|
+
const file = join(dir, "bank-template.json");
|
|
66
|
+
writeFileSync(file, "{ not json");
|
|
67
|
+
const errs = lintBankTemplate(file);
|
|
68
|
+
expect(errs.some((e) => e.toLowerCase().includes("json"))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it("findBankTemplates walks recursively and skips node_modules", () => {
|
|
71
|
+
mkdirSync(join(dir, "a", "b"), { recursive: true });
|
|
72
|
+
mkdirSync(join(dir, "node_modules", "x"), { recursive: true });
|
|
73
|
+
writeFileSync(join(dir, "bank-template.json"), "{}");
|
|
74
|
+
writeFileSync(join(dir, "a", "bank-template.json"), "{}");
|
|
75
|
+
writeFileSync(join(dir, "a", "b", "bank-template.json"), "{}");
|
|
76
|
+
writeFileSync(join(dir, "node_modules", "x", "bank-template.json"), "{}");
|
|
77
|
+
const found = findBankTemplates(dir);
|
|
78
|
+
expect(found).toHaveLength(3);
|
|
79
|
+
expect(found.every((f) => !f.includes("node_modules"))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it("the actual repo passes lint", () => {
|
|
82
|
+
const issues = lintAll(REPO_ROOT);
|
|
83
|
+
expect(issues).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|