@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 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
- export {};
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 <harness> [--agent <name>]
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("--agent <name>")} Agent name (defaults to directory name)
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
- const { dir, source, defaultName, cleanup } = await resolveAgentDir(dirArg, spin);
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
- const contentDir = join(homedir(), ".self-driving-agents", "claude-code", agentId);
998
- mkdirSync(contentDir, { recursive: true });
999
- // Copy content files to the local dir
1000
- const contentFiles = findContentFiles(dir);
1001
- for (const relPath of contentFiles) {
1002
- const destPath = join(contentDir, relPath);
1003
- mkdirSync(join(destPath, ".."), { recursive: true });
1004
- writeFileSync(destPath, readFileSync(join(dir, relPath), "utf-8"));
1005
- }
1006
- // Copy bank-template.json if present (has mental model definitions)
1007
- const templateSrc = join(dir, "bank-template.json");
1008
- if (existsSync(templateSrc)) {
1009
- writeFileSync(join(contentDir, "bank-template.json"), readFileSync(templateSrc, "utf-8"));
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
- const prompt = `/hindsight-memory:create-agent ${agentId} from ${contentDir}`;
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
- const templatePath = join(dir, "bank-template.json");
1094
- if (existsSync(templatePath)) {
1095
- spin.start("Importing bank template...");
1096
- const template = JSON.parse(readFileSync(templatePath, "utf-8"));
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) {
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/self-driving-agents",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Install self-driving agents with portable memory on any harness",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",