@vectorize-io/self-driving-agents 0.0.22 → 0.0.24
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 +8 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +81 -28
- package/dist/tests/cli.test.js +99 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,14 @@ npx @vectorize-io/self-driving-agents install ./my-agent --harness claude
|
|
|
37
37
|
npx @vectorize-io/self-driving-agents install my-org/my-repo/my-agent --harness openclaw
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Or start with a blank agent — no template, no seed content — using `--empty`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx @vectorize-io/self-driving-agents install my-agent --harness claude-code --empty
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The first positional becomes the agent name. The CLI provisions the bank, sets up the harness, and lets you build everything up from the conversation.
|
|
47
|
+
|
|
40
48
|
## How it works
|
|
41
49
|
|
|
42
50
|
1. You chat with the agent
|
package/dist/cli.d.ts
CHANGED
|
@@ -14,4 +14,12 @@
|
|
|
14
14
|
* bank-template.json — optional: bank config at this level
|
|
15
15
|
* *.md, *.txt, ... — content files (found recursively, excluding bank-template.json)
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Whether a string is a valid agent name. Used by --empty mode where the
|
|
19
|
+
* first positional arg becomes the agent name (no path, no GitHub fetch).
|
|
20
|
+
*
|
|
21
|
+
* Rules: starts with [a-z0-9], then lowercase alphanumerics or hyphens, max
|
|
22
|
+
* 64 chars. Matches the lowercase-with-hyphens convention the create-agent
|
|
23
|
+
* skill expects.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isValidAgentName(name: string): boolean;
|
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
|
*
|
|
@@ -713,16 +724,19 @@ async function main() {
|
|
|
713
724
|
${color.bold("self-driving-agents")} — install a self-driving agent
|
|
714
725
|
|
|
715
726
|
${color.dim("Usage:")}
|
|
716
|
-
npx @vectorize-io/self-driving-agents install <agent> --harness <
|
|
727
|
+
npx @vectorize-io/self-driving-agents install <agent> --harness <h> ${color.dim("# from path or GitHub")}
|
|
728
|
+
npx @vectorize-io/self-driving-agents install <name> --harness <h> --empty ${color.dim("# blank agent")}
|
|
717
729
|
|
|
718
|
-
${color.dim("Agent sources:")}
|
|
730
|
+
${color.dim("Agent sources (without --empty):")}
|
|
719
731
|
${color.cyan("marketing-agent")} → ${DEFAULT_REPO}/marketing-agent
|
|
720
732
|
${color.cyan("org/repo/my-agent")} → org/repo/my-agent on GitHub
|
|
721
733
|
${color.cyan("./local-dir")} → local directory
|
|
722
734
|
|
|
723
735
|
${color.dim("Options:")}
|
|
724
736
|
${color.cyan("--harness <h>")} Required. openclaw | nemoclaw | hermes | claude | claude-code
|
|
725
|
-
${color.cyan("--
|
|
737
|
+
${color.cyan("--empty")} Create a blank agent. First positional becomes the agent name;
|
|
738
|
+
no content fetched, no bank-template imported from disk.
|
|
739
|
+
${color.cyan("--agent <name>")} Override the agent name (defaults to directory name)
|
|
726
740
|
${color.cyan("--sandbox <name>")} NemoClaw sandbox (auto-detected if only one exists)
|
|
727
741
|
`);
|
|
728
742
|
process.exit(0);
|
|
@@ -736,6 +750,7 @@ async function main() {
|
|
|
736
750
|
let harness;
|
|
737
751
|
let agentName;
|
|
738
752
|
let sandbox;
|
|
753
|
+
let isEmpty = false;
|
|
739
754
|
for (let i = 0; i < restArgs.length; i++) {
|
|
740
755
|
if (restArgs[i] === "--harness" && restArgs[i + 1])
|
|
741
756
|
harness = restArgs[++i];
|
|
@@ -743,6 +758,8 @@ async function main() {
|
|
|
743
758
|
agentName = restArgs[++i];
|
|
744
759
|
else if (restArgs[i] === "--sandbox" && restArgs[i + 1])
|
|
745
760
|
sandbox = restArgs[++i];
|
|
761
|
+
else if (restArgs[i] === "--empty")
|
|
762
|
+
isEmpty = true;
|
|
746
763
|
}
|
|
747
764
|
if (!harness) {
|
|
748
765
|
p.cancel("--harness required (openclaw | nemoclaw | hermes | claude | claude-code)");
|
|
@@ -752,9 +769,30 @@ async function main() {
|
|
|
752
769
|
sandbox = await detectNemoClawSandbox();
|
|
753
770
|
}
|
|
754
771
|
p.intro(color.bgCyan(color.black(` self-driving-agents `)));
|
|
755
|
-
// Step 0: Resolve agent directory (local or GitHub)
|
|
772
|
+
// Step 0: Resolve agent directory (local or GitHub) — skipped when --empty
|
|
773
|
+
// is set; in that case the first positional becomes the agent name and
|
|
774
|
+
// there's no source content to ingest.
|
|
756
775
|
const spin = p.spinner();
|
|
757
|
-
|
|
776
|
+
let dir = "";
|
|
777
|
+
let source;
|
|
778
|
+
let defaultName;
|
|
779
|
+
let cleanup;
|
|
780
|
+
if (isEmpty) {
|
|
781
|
+
if (!dirArg || dirArg.startsWith("-")) {
|
|
782
|
+
p.cancel("--empty needs an agent name as the first positional argument.\n" +
|
|
783
|
+
" e.g. install my-agent --harness claude-code --empty");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
if (!isValidAgentName(dirArg)) {
|
|
787
|
+
p.cancel(`Invalid agent name '${dirArg}'. Use lowercase letters, digits, and hyphens (max 64 chars), e.g. my-agent.`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
source = "<empty>";
|
|
791
|
+
defaultName = dirArg;
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
({ dir, source, defaultName, cleanup } = await resolveAgentDir(dirArg, spin));
|
|
795
|
+
}
|
|
758
796
|
try {
|
|
759
797
|
let agentId;
|
|
760
798
|
if (agentName) {
|
|
@@ -985,25 +1023,32 @@ async function main() {
|
|
|
985
1023
|
ccConfig.dynamicBankId = true;
|
|
986
1024
|
ccConfig.dynamicBankGranularity = expectedGranularity;
|
|
987
1025
|
ccConfig.enableKnowledgeTools = true;
|
|
1026
|
+
// Coalesce all worktrees of a repo into one bank by resolving the
|
|
1027
|
+
// `project` field to the main repo basename instead of the cwd basename.
|
|
1028
|
+
// Plugin default is already true, but pin it explicitly so future plugin
|
|
1029
|
+
// version flips don't fragment a user's memory across worktrees.
|
|
1030
|
+
ccConfig.resolveWorktrees = true;
|
|
988
1031
|
mkdirSync(ccConfigDir, { recursive: true });
|
|
989
1032
|
writeFileSync(ccConfigPath, JSON.stringify(ccConfig, null, 2) + "\n");
|
|
990
1033
|
p.log.success(`Plugin config: ${color.dim(ccConfigPath)}`);
|
|
991
|
-
// Step 4: Save content locally for the agent
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1034
|
+
// Step 4: Save content locally for the agent (skipped when --empty —
|
|
1035
|
+
// the create-agent skill goes interactive without a `from <path>`).
|
|
1036
|
+
let contentDir = null;
|
|
1037
|
+
if (!isEmpty) {
|
|
1038
|
+
contentDir = join(homedir(), ".self-driving-agents", "claude-code", agentId);
|
|
1039
|
+
mkdirSync(contentDir, { recursive: true });
|
|
1040
|
+
const contentFiles = findContentFiles(dir);
|
|
1041
|
+
for (const relPath of contentFiles) {
|
|
1042
|
+
const destPath = join(contentDir, relPath);
|
|
1043
|
+
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
1044
|
+
writeFileSync(destPath, readFileSync(join(dir, relPath), "utf-8"));
|
|
1045
|
+
}
|
|
1046
|
+
const templateSrc = join(dir, "bank-template.json");
|
|
1047
|
+
if (existsSync(templateSrc)) {
|
|
1048
|
+
writeFileSync(join(contentDir, "bank-template.json"), readFileSync(templateSrc, "utf-8"));
|
|
1049
|
+
}
|
|
1050
|
+
p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
|
|
1005
1051
|
}
|
|
1006
|
-
p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
|
|
1007
1052
|
// Auto-approve hindsight MCP tools and skills in user settings
|
|
1008
1053
|
const userSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
1009
1054
|
let userSettings = {};
|
|
@@ -1036,7 +1081,11 @@ async function main() {
|
|
|
1036
1081
|
writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2) + "\n");
|
|
1037
1082
|
p.log.success("Auto-approved hindsight tools in Claude Code");
|
|
1038
1083
|
}
|
|
1039
|
-
|
|
1084
|
+
// With --empty the skill runs interactively (Mode B); otherwise it
|
|
1085
|
+
// ingests from the staged content directory (Mode A).
|
|
1086
|
+
const prompt = contentDir
|
|
1087
|
+
? `/hindsight-memory:create-agent ${agentId} from ${contentDir}`
|
|
1088
|
+
: `/hindsight-memory:create-agent ${agentId}`;
|
|
1040
1089
|
p.note([
|
|
1041
1090
|
`${color.yellow("⚠")} ${color.bold(`Important:`)} the agent's memory is scoped to the directory where you start ${color.cyan("claude")}.`,
|
|
1042
1091
|
` Always start your Claude Code sessions from the same project directory.`,
|
|
@@ -1084,20 +1133,24 @@ async function main() {
|
|
|
1084
1133
|
p.cancel(`Cannot reach Hindsight at ${apiUrl}\nStart the server or reconfigure the plugin.`);
|
|
1085
1134
|
process.exit(1);
|
|
1086
1135
|
}
|
|
1087
|
-
// Step 4: Import bank template
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1136
|
+
// Step 4: Import bank template — or, with --empty, provision a blank
|
|
1137
|
+
// bank so later writes from the harness have somewhere to land.
|
|
1138
|
+
const templatePath = isEmpty ? "" : join(dir, "bank-template.json");
|
|
1139
|
+
const hasTemplate = !isEmpty && existsSync(templatePath);
|
|
1140
|
+
if (isEmpty || hasTemplate) {
|
|
1141
|
+
spin.start(isEmpty ? "Provisioning bank..." : "Importing bank template...");
|
|
1142
|
+
const template = hasTemplate
|
|
1143
|
+
? JSON.parse(readFileSync(templatePath, "utf-8"))
|
|
1144
|
+
: { version: "1" };
|
|
1092
1145
|
await sdk.importBankTemplate({
|
|
1093
1146
|
client: lowLevel,
|
|
1094
1147
|
path: { bank_id: bankId },
|
|
1095
1148
|
body: template,
|
|
1096
1149
|
});
|
|
1097
|
-
spin.stop("Bank template imported");
|
|
1150
|
+
spin.stop(isEmpty ? "Bank provisioned" : "Bank template imported");
|
|
1098
1151
|
}
|
|
1099
1152
|
// Step 5: Ingest content (recursive — all text files except bank-template.json)
|
|
1100
|
-
const contentFiles = findContentFiles(dir);
|
|
1153
|
+
const contentFiles = isEmpty ? [] : findContentFiles(dir);
|
|
1101
1154
|
if (contentFiles.length > 0) {
|
|
1102
1155
|
spin.start(`Ingesting ${contentFiles.length} file(s)...`);
|
|
1103
1156
|
for (const relPath of contentFiles) {
|
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)
|
|
@@ -695,6 +783,9 @@ describe("claude-code Hindsight config persistence", () => {
|
|
|
695
783
|
hindsightApiUrl: prompted.apiUrl,
|
|
696
784
|
hindsightApiToken: prompted.apiToken,
|
|
697
785
|
enableKnowledgeTools: true,
|
|
786
|
+
// Pin worktree resolution so all worktrees of a repo land in the same
|
|
787
|
+
// bank, matching what cli.ts writes during the claude-code install flow.
|
|
788
|
+
resolveWorktrees: true,
|
|
698
789
|
};
|
|
699
790
|
}
|
|
700
791
|
it("prompts on first install (empty config)", () => {
|
|
@@ -726,4 +817,12 @@ describe("claude-code Hindsight config persistence", () => {
|
|
|
726
817
|
const result = applyClaudeConfig({ enableKnowledgeTools: false }, { apiUrl: "https://x.com", apiToken: "t" });
|
|
727
818
|
expect(result.enableKnowledgeTools).toBe(true);
|
|
728
819
|
});
|
|
820
|
+
it("always pins resolveWorktrees=true so worktrees share one bank", () => {
|
|
821
|
+
// Plugin already defaults to true, but SDA pins it explicitly so a future
|
|
822
|
+
// plugin default flip can't fragment a user's memory across worktrees.
|
|
823
|
+
const fresh = applyClaudeConfig({}, { apiUrl: "https://x.com", apiToken: "t" });
|
|
824
|
+
expect(fresh.resolveWorktrees).toBe(true);
|
|
825
|
+
const overridden = applyClaudeConfig({ resolveWorktrees: false }, { apiUrl: "https://x.com", apiToken: "t" });
|
|
826
|
+
expect(overridden.resolveWorktrees).toBe(true);
|
|
827
|
+
});
|
|
729
828
|
});
|