@vectorize-io/self-driving-agents 0.0.23 → 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 +76 -28
- package/dist/tests/cli.test.js +88 -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) {
|
|
@@ -993,22 +1031,24 @@ async function main() {
|
|
|
993
1031
|
mkdirSync(ccConfigDir, { recursive: true });
|
|
994
1032
|
writeFileSync(ccConfigPath, JSON.stringify(ccConfig, null, 2) + "\n");
|
|
995
1033
|
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
|
-
|
|
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)`);
|
|
1010
1051
|
}
|
|
1011
|
-
p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
|
|
1012
1052
|
// Auto-approve hindsight MCP tools and skills in user settings
|
|
1013
1053
|
const userSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
1014
1054
|
let userSettings = {};
|
|
@@ -1041,7 +1081,11 @@ async function main() {
|
|
|
1041
1081
|
writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2) + "\n");
|
|
1042
1082
|
p.log.success("Auto-approved hindsight tools in Claude Code");
|
|
1043
1083
|
}
|
|
1044
|
-
|
|
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}`;
|
|
1045
1089
|
p.note([
|
|
1046
1090
|
`${color.yellow("⚠")} ${color.bold(`Important:`)} the agent's memory is scoped to the directory where you start ${color.cyan("claude")}.`,
|
|
1047
1091
|
` Always start your Claude Code sessions from the same project directory.`,
|
|
@@ -1089,20 +1133,24 @@ async function main() {
|
|
|
1089
1133
|
p.cancel(`Cannot reach Hindsight at ${apiUrl}\nStart the server or reconfigure the plugin.`);
|
|
1090
1134
|
process.exit(1);
|
|
1091
1135
|
}
|
|
1092
|
-
// Step 4: Import bank template
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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" };
|
|
1097
1145
|
await sdk.importBankTemplate({
|
|
1098
1146
|
client: lowLevel,
|
|
1099
1147
|
path: { bank_id: bankId },
|
|
1100
1148
|
body: template,
|
|
1101
1149
|
});
|
|
1102
|
-
spin.stop("Bank template imported");
|
|
1150
|
+
spin.stop(isEmpty ? "Bank provisioned" : "Bank template imported");
|
|
1103
1151
|
}
|
|
1104
1152
|
// Step 5: Ingest content (recursive — all text files except bank-template.json)
|
|
1105
|
-
const contentFiles = findContentFiles(dir);
|
|
1153
|
+
const contentFiles = isEmpty ? [] : findContentFiles(dir);
|
|
1106
1154
|
if (contentFiles.length > 0) {
|
|
1107
1155
|
spin.start(`Ingesting ${contentFiles.length} file(s)...`);
|
|
1108
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)
|