@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 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) {
@@ -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
- const contentDir = join(homedir(), ".self-driving-agents", "claude-code", agentId);
993
- mkdirSync(contentDir, { recursive: true });
994
- // Copy content files to the local dir
995
- const contentFiles = findContentFiles(dir);
996
- for (const relPath of contentFiles) {
997
- const destPath = join(contentDir, relPath);
998
- mkdirSync(join(destPath, ".."), { recursive: true });
999
- writeFileSync(destPath, readFileSync(join(dir, relPath), "utf-8"));
1000
- }
1001
- // Copy bank-template.json if present (has mental model definitions)
1002
- const templateSrc = join(dir, "bank-template.json");
1003
- if (existsSync(templateSrc)) {
1004
- 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)`);
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
- 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}`;
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
- const templatePath = join(dir, "bank-template.json");
1089
- if (existsSync(templatePath)) {
1090
- spin.start("Importing bank template...");
1091
- 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" };
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) {
@@ -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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/self-driving-agents",
3
- "version": "0.0.22",
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",