@arvoretech/hub 0.2.0 → 0.4.0

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.
Files changed (2) hide show
  1. package/dist/index.js +446 -70
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command17 } from "commander";
4
+ import { Command as Command18 } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import { Command } from "commander";
@@ -747,6 +747,18 @@ ${prompt.sections.after_pipeline.trim()}`);
747
747
  if (prompt?.sections?.after_delivery) {
748
748
  sections.push(`
749
749
  ${prompt.sections.after_delivery.trim()}`);
750
+ }
751
+ if (config.memory) {
752
+ sections.push(`
753
+ ## Team Memory
754
+
755
+ This workspace has a team memory knowledge base available via the \`team-memory\` MCP.
756
+
757
+ **Before starting any task**, use \`search_memories\` to find relevant context \u2014 past decisions, conventions, known issues, and domain knowledge. This avoids repeating mistakes and ensures consistency with previous choices.
758
+
759
+ **After completing a task**, if you discovered something valuable (a decision, a gotcha, a convention, domain insight), use \`add_memory\` to capture it for the team.
760
+
761
+ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
750
762
  }
751
763
  sections.push(`
752
764
  ## Troubleshooting and Debugging
@@ -803,6 +815,10 @@ Follow each step sequentially, applying the role-specific instructions from the
803
815
  }
804
816
  const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
805
817
  parts.push(`### ${stepTitle}`);
818
+ if (step.mode === "plan") {
819
+ parts.push(`**This step is a planning phase.** Do NOT make any code changes. Focus on reading, analyzing, and collaborating with the user to define requirements before proceeding.`);
820
+ parts.push(``);
821
+ }
806
822
  if (step.agent) {
807
823
  parts.push(`Follow the instructions from the \`agent-${step.agent}.md\` steering file.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
808
824
  if (step.step === "refinement") {
@@ -913,6 +929,18 @@ ${prompt.sections.after_pipeline.trim()}`);
913
929
  if (prompt?.sections?.after_delivery) {
914
930
  sections.push(`
915
931
  ${prompt.sections.after_delivery.trim()}`);
932
+ }
933
+ if (config.memory) {
934
+ sections.push(`
935
+ ## Team Memory
936
+
937
+ This workspace has a team memory knowledge base available via the \`team-memory\` MCP.
938
+
939
+ **Before starting any task**, use \`search_memories\` to find relevant context \u2014 past decisions, conventions, known issues, and domain knowledge. This avoids repeating mistakes and ensures consistency with previous choices.
940
+
941
+ **After completing a task**, if you discovered something valuable (a decision, a gotcha, a convention, domain insight), use \`add_memory\` to capture it for the team.
942
+
943
+ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
916
944
  }
917
945
  sections.push(`
918
946
  ## Troubleshooting and Debugging
@@ -997,6 +1025,10 @@ function buildPipelineSection(steps) {
997
1025
  }
998
1026
  const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
999
1027
  parts.push(`### ${stepTitle}`);
1028
+ if (step.mode === "plan") {
1029
+ parts.push(`**Before starting this step, switch to Plan Mode** by calling \`SwitchMode\` with \`target_mode_id: "plan"\`. This ensures collaborative planning with the user in a read-only context before any implementation begins.`);
1030
+ parts.push(``);
1031
+ }
1000
1032
  if (step.agent) {
1001
1033
  parts.push(`Call the \`${step.agent}\` agent.${step.output ? ` It writes to \`${step.output}\`.` : ""}`);
1002
1034
  if (step.step === "refinement") {
@@ -1032,6 +1064,10 @@ If any coding agent has doubts, they will write questions in their document. App
1032
1064
  If any validation agent leaves comments requiring fixes, call the relevant coding agents again to address them.`);
1033
1065
  }
1034
1066
  }
1067
+ if (step.mode === "plan") {
1068
+ parts.push(`
1069
+ **After this step is complete and approved**, switch back to Agent Mode to proceed with the next step.`);
1070
+ }
1035
1071
  parts.push("");
1036
1072
  }
1037
1073
  return parts.join("\n");
@@ -1294,6 +1330,14 @@ function buildGitignoreLines(config) {
1294
1330
  "# Task documents",
1295
1331
  "tasks/"
1296
1332
  );
1333
+ if (config.memory) {
1334
+ const memPath = config.memory.path || "memories";
1335
+ lines.push(
1336
+ "",
1337
+ "# Memory vector store (generated from markdown files)",
1338
+ `${memPath}/.lancedb/`
1339
+ );
1340
+ }
1297
1341
  return lines;
1298
1342
  }
1299
1343
  var generators = {
@@ -1304,6 +1348,25 @@ var generators = {
1304
1348
  var generateCommand = new Command4("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro)", "cursor").action(async (opts) => {
1305
1349
  const hubDir = process.cwd();
1306
1350
  const config = await loadHubConfig(hubDir);
1351
+ if (config.memory) {
1352
+ const hasMemoryMcp = config.mcps?.some(
1353
+ (m) => m.name === "team-memory" || m.package === "@arvoretech/memory-mcp"
1354
+ );
1355
+ if (!hasMemoryMcp) {
1356
+ console.log(chalk4.red(`
1357
+ Error: 'memory' is configured but no memory MCP is declared in 'mcps'.
1358
+ `));
1359
+ console.log(chalk4.yellow(` Add this to your hub.yaml:
1360
+ `));
1361
+ console.log(chalk4.dim(` mcps:`));
1362
+ console.log(chalk4.dim(` - name: team-memory`));
1363
+ console.log(chalk4.dim(` package: "@arvoretech/memory-mcp"`));
1364
+ console.log(chalk4.dim(` env:`));
1365
+ console.log(chalk4.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
1366
+ `));
1367
+ process.exit(1);
1368
+ }
1369
+ }
1307
1370
  const generator = generators[opts.editor];
1308
1371
  if (!generator) {
1309
1372
  console.log(
@@ -1892,17 +1955,32 @@ async function listLocalSkills(hubDir) {
1892
1955
  }
1893
1956
  return skills;
1894
1957
  }
1895
- async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1896
- if (!existsSync6(sourceSkillsDir)) {
1897
- console.log(chalk8.red(" No skills/ directory found in source"));
1898
- return;
1958
+ async function findSkillFolders(rootDir) {
1959
+ const skillsDir = join9(rootDir, "skills");
1960
+ if (existsSync6(skillsDir)) {
1961
+ const entries = await readdir2(skillsDir);
1962
+ const folders = entries.filter(
1963
+ (f) => existsSync6(join9(skillsDir, f, "SKILL.md"))
1964
+ );
1965
+ if (folders.length > 0) return { dir: skillsDir, folders };
1899
1966
  }
1900
- const available = await readdir2(sourceSkillsDir);
1901
- const skillFolders = available.filter(
1902
- (f) => existsSync6(join9(sourceSkillsDir, f, "SKILL.md"))
1903
- );
1967
+ if (existsSync6(rootDir)) {
1968
+ const entries = await readdir2(rootDir);
1969
+ const folders = entries.filter(
1970
+ (f) => !f.startsWith(".") && f !== "node_modules" && existsSync6(join9(rootDir, f, "SKILL.md"))
1971
+ );
1972
+ if (folders.length > 0) return { dir: rootDir, folders };
1973
+ }
1974
+ if (existsSync6(join9(rootDir, "SKILL.md"))) {
1975
+ return { dir: join9(rootDir, ".."), folders: [rootDir.split("/").pop()] };
1976
+ }
1977
+ return { dir: skillsDir, folders: [] };
1978
+ }
1979
+ async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1980
+ const rootDir = sourceSkillsDir.endsWith("/skills") ? sourceSkillsDir.replace(/\/skills$/, "") : sourceSkillsDir;
1981
+ const { dir, folders: skillFolders } = await findSkillFolders(rootDir);
1904
1982
  if (skillFolders.length === 0) {
1905
- console.log(chalk8.red(" No skills found (looking for skills/*/SKILL.md)"));
1983
+ console.log(chalk8.red(" No skills found (looked in skills/, root dirs, and SKILL.md)"));
1906
1984
  return;
1907
1985
  }
1908
1986
  const toInstall = opts.skill ? skillFolders.filter((s) => s === opts.skill) : skillFolders;
@@ -1913,7 +1991,7 @@ async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1913
1991
  const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1914
1992
  await mkdir4(targetBase, { recursive: true });
1915
1993
  for (const skill of toInstall) {
1916
- const src = join9(sourceSkillsDir, skill);
1994
+ const src = join9(dir, skill);
1917
1995
  const dest = join9(targetBase, skill);
1918
1996
  await cp2(src, dest, { recursive: true });
1919
1997
  console.log(chalk8.green(` Installed: ${skill}`));
@@ -1952,42 +2030,80 @@ async function addFromRegistry(skillName, hubDir, opts) {
1952
2030
  }
1953
2031
  async function addFromGitHubSkill(owner, repo, skillName, hubDir, opts) {
1954
2032
  const fullRepo = `${owner}/${repo}`;
1955
- const remotePath = `skills/${skillName}`;
1956
2033
  const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1957
2034
  const dest = join9(targetBase, skillName);
2035
+ const pathsToTry = [
2036
+ `skills/${skillName}`,
2037
+ skillName
2038
+ ];
1958
2039
  console.log(chalk8.cyan(` Downloading ${skillName} from ${fullRepo} via GitHub API...`));
1959
- try {
1960
- await downloadDirFromGitHub(fullRepo, remotePath, dest);
1961
- if (!existsSync6(join9(dest, "SKILL.md"))) {
2040
+ for (const remotePath of pathsToTry) {
2041
+ try {
2042
+ await downloadDirFromGitHub(fullRepo, remotePath, dest);
2043
+ if (existsSync6(join9(dest, "SKILL.md"))) {
2044
+ console.log(chalk8.green(` Installed: ${skillName} (from ${fullRepo})`));
2045
+ console.log(chalk8.green(`
2046
+ 1 skill(s) installed to ${opts.global ? "global" : "project"}
2047
+ `));
2048
+ return;
2049
+ }
2050
+ await rm(dest, { recursive: true }).catch(() => {
2051
+ });
2052
+ } catch {
1962
2053
  await rm(dest, { recursive: true }).catch(() => {
1963
2054
  });
1964
- console.log(chalk8.red(` Skill '${skillName}' not found in ${fullRepo}/skills/`));
1965
- console.log(chalk8.dim(` Check available skills: hub skills add ${fullRepo} --list`));
1966
- return;
1967
2055
  }
1968
- console.log(chalk8.green(` Installed: ${skillName} (from ${fullRepo})`));
1969
- console.log(chalk8.green(`
1970
- 1 skill(s) installed to ${opts.global ? "global" : "project"}
1971
- `));
1972
- } catch (err) {
1973
- console.log(chalk8.red(` Failed to download: ${err.message}`));
1974
2056
  }
2057
+ console.log(chalk8.red(` Skill '${skillName}' not found in ${fullRepo}`));
2058
+ console.log(chalk8.dim(` Check available skills: hub skills add ${fullRepo} --list`));
1975
2059
  }
1976
2060
  async function listRemoteSkills(owner, repo) {
1977
2061
  const fullRepo = `${owner}/${repo}`;
1978
2062
  console.log(chalk8.cyan(` Fetching skills from ${fullRepo}...
1979
2063
  `));
2064
+ const headers = { Accept: "application/vnd.github.v3+json" };
1980
2065
  try {
1981
- const apiUrl = `https://api.github.com/repos/${fullRepo}/contents/skills`;
1982
- const res = await fetch(apiUrl, {
1983
- headers: { Accept: "application/vnd.github.v3+json" }
1984
- });
1985
- if (!res.ok) {
1986
- console.log(chalk8.red(` Could not list skills from ${fullRepo}`));
1987
- return;
2066
+ let dirs = [];
2067
+ const skillsRes = await fetch(
2068
+ `https://api.github.com/repos/${fullRepo}/contents/skills`,
2069
+ { headers }
2070
+ );
2071
+ if (skillsRes.ok) {
2072
+ const items = await skillsRes.json();
2073
+ dirs = items.filter((i) => i.type === "dir");
2074
+ }
2075
+ if (dirs.length === 0) {
2076
+ const rootRes = await fetch(
2077
+ `https://api.github.com/repos/${fullRepo}/contents`,
2078
+ { headers }
2079
+ );
2080
+ if (rootRes.ok) {
2081
+ const items = await rootRes.json();
2082
+ const candidates = items.filter(
2083
+ (i) => i.type === "dir" && !i.name.startsWith(".")
2084
+ );
2085
+ for (const c of candidates) {
2086
+ const checkRes = await fetch(
2087
+ `https://api.github.com/repos/${fullRepo}/contents/${c.name}/SKILL.md`,
2088
+ { headers }
2089
+ );
2090
+ if (checkRes.ok) dirs.push({ name: c.name });
2091
+ }
2092
+ }
2093
+ }
2094
+ if (dirs.length === 0) {
2095
+ const rootSkill = await fetch(
2096
+ `https://api.github.com/repos/${fullRepo}/contents/SKILL.md`,
2097
+ { headers }
2098
+ );
2099
+ if (rootSkill.ok) {
2100
+ console.log(chalk8.green(` This repo is a single skill.
2101
+ `));
2102
+ console.log(chalk8.dim(` Install with: hub skills add ${fullRepo}
2103
+ `));
2104
+ return;
2105
+ }
1988
2106
  }
1989
- const items = await res.json();
1990
- const dirs = items.filter((i) => i.type === "dir");
1991
2107
  if (dirs.length === 0) {
1992
2108
  console.log(chalk8.dim(" No skills found."));
1993
2109
  return;
@@ -2015,8 +2131,7 @@ async function addFromLocalPath(localPath, hubDir, opts) {
2015
2131
  console.log(chalk8.red(` Path is not a directory: ${absPath}`));
2016
2132
  return;
2017
2133
  }
2018
- const sourceSkillsDir = join9(absPath, "skills");
2019
- await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
2134
+ await installSkillsFromDir(absPath, hubDir, opts);
2020
2135
  }
2021
2136
  async function addFromGitRepo(source, hubDir, opts) {
2022
2137
  const tmp = tmpDir();
@@ -2031,8 +2146,7 @@ async function addFromGitRepo(source, hubDir, opts) {
2031
2146
  console.log(chalk8.dim(" Make sure the URL is correct and you have access to the repository."));
2032
2147
  return;
2033
2148
  }
2034
- const sourceSkillsDir = join9(tmp, "skills");
2035
- await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
2149
+ await installSkillsFromDir(tmp, hubDir, opts);
2036
2150
  } finally {
2037
2151
  if (existsSync6(tmp)) {
2038
2152
  await rm(tmp, { recursive: true });
@@ -2049,7 +2163,7 @@ function parseGitHubSource(source) {
2049
2163
  return null;
2050
2164
  }
2051
2165
  var skillsCommand = new Command8("skills").description("Manage agent skills").addCommand(
2052
- new Command8("add").description("Install skills from registry, GitHub (skills.sh compatible), git URL, or local path").argument("<source>", "Skill name, owner/repo, owner/repo/skill, git URL, or local path").option("-s, --skill <name>", "Install a specific skill only (for repo sources)").option("-g, --global", "Install to global ~/.cursor/skills/").option("-r, --repo <repo>", "Registry repository (owner/repo)").option("-l, --list", "List available skills without installing").action(async (source, opts) => {
2166
+ new Command8("add").description("Install skills from registry, GitHub, git URL, or local path").argument("<source>", "Skill name, owner/repo, owner/repo/skill, git URL, or local path").option("-s, --skill <name>", "Install a specific skill only (for repo sources)").option("-g, --global", "Install to global ~/.cursor/skills/").option("-r, --repo <repo>", "Registry repository (owner/repo)").option("-l, --list", "List available skills without installing").action(async (source, opts) => {
2053
2167
  const hubDir = process.cwd();
2054
2168
  if (isLocalPath(source)) {
2055
2169
  console.log(chalk8.blue(`
@@ -2094,9 +2208,10 @@ Installing skill ${source} from registry
2094
2208
  await addFromRegistry(source, hubDir, opts);
2095
2209
  })
2096
2210
  ).addCommand(
2097
- new Command8("find").description("Browse community skills on skills.sh").argument("[query]", "Search term (opens skills.sh)").action(async (query) => {
2098
- const url = query ? `https://skills.sh/?q=${encodeURIComponent(query)}` : "https://skills.sh";
2099
- console.log(chalk8.blue("\n Browse community skills at:\n"));
2211
+ new Command8("find").description("Browse curated skills in the Repo Hub directory").argument("[query]", "Search term").action(async (query) => {
2212
+ const base = "https://rhm-website.vercel.app/directory?type=skill";
2213
+ const url = query ? `${base}&q=${encodeURIComponent(query)}` : base;
2214
+ console.log(chalk8.blue("\n Browse curated skills at:\n"));
2100
2215
  console.log(chalk8.cyan(` ${url}
2101
2216
  `));
2102
2217
  console.log(chalk8.dim(" Install with: hub skills add <owner>/<repo>/<skill-name>"));
@@ -2203,18 +2318,33 @@ async function addFromLocalPath2(localPath, hubDir, opts) {
2203
2318
  console.log(chalk9.red(` Path not found: ${absPath}`));
2204
2319
  return;
2205
2320
  }
2206
- const sourceAgentsDir = statSync2(absPath).isDirectory() ? join10(absPath, "agents") : absPath;
2207
- await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2208
- }
2209
- async function installAgentsFromDir(sourceAgentsDir, hubDir, opts) {
2210
- if (!existsSync7(sourceAgentsDir)) {
2211
- console.log(chalk9.red(" No agents/ directory found in source"));
2321
+ if (!statSync2(absPath).isDirectory()) {
2322
+ console.log(chalk9.red(` Path is not a directory: ${absPath}`));
2212
2323
  return;
2213
2324
  }
2214
- const files = await readdir3(sourceAgentsDir);
2215
- const mdFiles = files.filter((f) => f.endsWith(".md"));
2325
+ await installAgentsFromDir(absPath, hubDir, opts);
2326
+ }
2327
+ async function findAgentFiles(rootDir) {
2328
+ const agentsDir = join10(rootDir, "agents");
2329
+ if (existsSync7(agentsDir)) {
2330
+ const entries = await readdir3(agentsDir);
2331
+ const mdFiles = entries.filter((f) => f.endsWith(".md"));
2332
+ if (mdFiles.length > 0) return { dir: agentsDir, files: mdFiles };
2333
+ }
2334
+ if (existsSync7(rootDir)) {
2335
+ const entries = await readdir3(rootDir);
2336
+ const mdFiles = entries.filter(
2337
+ (f) => f.endsWith(".md") && !f.startsWith(".") && f !== "README.md" && f !== "CHANGELOG.md"
2338
+ );
2339
+ if (mdFiles.length > 0) return { dir: rootDir, files: mdFiles };
2340
+ }
2341
+ return { dir: agentsDir, files: [] };
2342
+ }
2343
+ async function installAgentsFromDir(sourceDir, hubDir, opts) {
2344
+ const rootDir = sourceDir.endsWith("/agents") ? sourceDir.replace(/\/agents$/, "") : sourceDir;
2345
+ const { dir, files: mdFiles } = await findAgentFiles(rootDir);
2216
2346
  if (mdFiles.length === 0) {
2217
- console.log(chalk9.red(" No agent files found (looking for agents/*.md)"));
2347
+ console.log(chalk9.red(" No agent files found (looked in agents/ and root .md files)"));
2218
2348
  return;
2219
2349
  }
2220
2350
  const toInstall = opts.agent ? mdFiles.filter((f) => f === `${opts.agent}.md` || f === opts.agent) : mdFiles;
@@ -2226,7 +2356,7 @@ async function installAgentsFromDir(sourceAgentsDir, hubDir, opts) {
2226
2356
  const targetBase = opts.global ? join10(process.env.HOME || "~", ".cursor", "agents") : join10(hubDir, "agents");
2227
2357
  await mkdir5(targetBase, { recursive: true });
2228
2358
  for (const file of toInstall) {
2229
- await copyFile2(join10(sourceAgentsDir, file), join10(targetBase, file));
2359
+ await copyFile2(join10(dir, file), join10(targetBase, file));
2230
2360
  console.log(chalk9.green(` Installed: ${file.replace(/\.md$/, "")}`));
2231
2361
  }
2232
2362
  console.log(
@@ -2249,8 +2379,7 @@ async function addFromGitRepo2(source, hubDir, opts) {
2249
2379
  console.log(chalk9.red(` Repository not found or not accessible: ${source}`));
2250
2380
  return;
2251
2381
  }
2252
- const sourceAgentsDir = join10(tmp, "agents");
2253
- await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2382
+ await installAgentsFromDir(tmp, hubDir, opts);
2254
2383
  } finally {
2255
2384
  if (existsSync7(tmp)) {
2256
2385
  await rm2(tmp, { recursive: true });
@@ -2325,6 +2454,16 @@ Agent '${name}' not found in ${opts.global ? "global" : "project"}
2325
2454
  Removed agent: ${name}
2326
2455
  `));
2327
2456
  })
2457
+ ).addCommand(
2458
+ new Command9("find").description("Browse curated agents in the Repo Hub directory").argument("[query]", "Search term").action(async (query) => {
2459
+ const base = "https://rhm-website.vercel.app/directory?type=agent";
2460
+ const url = query ? `${base}&q=${encodeURIComponent(query)}` : base;
2461
+ console.log(chalk9.blue("\n Browse curated agents at:\n"));
2462
+ console.log(chalk9.cyan(` ${url}
2463
+ `));
2464
+ console.log(chalk9.dim(" Install with: hub agents add <owner>/<repo>"));
2465
+ console.log(chalk9.dim(" Example: hub agents add my-org/my-agents\n"));
2466
+ })
2328
2467
  ).addCommand(
2329
2468
  new Command9("sync").description("Install all agents referenced in hub.yaml from the registry").option("-g, --global", "Install to global ~/.cursor/agents/").option("-r, --repo <repo>", "Registry repository (owner/repo)").option("-f, --force", "Re-install even if the agent already exists locally").action(async (opts) => {
2330
2469
  const hubDir = process.cwd();
@@ -3329,17 +3468,253 @@ function extractVersion(s) {
3329
3468
  return match?.[1] || s;
3330
3469
  }
3331
3470
 
3332
- // src/commands/update.ts
3471
+ // src/commands/memory.ts
3333
3472
  import { Command as Command16 } from "commander";
3473
+ import { existsSync as existsSync14 } from "fs";
3474
+ import { mkdir as mkdir9, readdir as readdir6, readFile as readFile9, writeFile as writeFile11, rm as rm5, appendFile as appendFile2 } from "fs/promises";
3475
+ import { join as join17, resolve as resolve6, basename as basename2 } from "path";
3476
+ import chalk16 from "chalk";
3477
+ async function ensureLanceDbIgnored(memoriesDir, hubDir) {
3478
+ const relative = memoriesDir.replace(hubDir + "/", "");
3479
+ const pattern = `${relative}/.lancedb/`;
3480
+ const gitignorePath = join17(hubDir, ".gitignore");
3481
+ if (existsSync14(gitignorePath)) {
3482
+ const content = await readFile9(gitignorePath, "utf-8");
3483
+ if (content.includes(".lancedb")) return;
3484
+ await appendFile2(gitignorePath, `
3485
+ # Memory vector store (generated)
3486
+ ${pattern}
3487
+ `);
3488
+ } else {
3489
+ await writeFile11(gitignorePath, `# Memory vector store (generated)
3490
+ ${pattern}
3491
+ `, "utf-8");
3492
+ }
3493
+ }
3494
+ var VALID_CATEGORIES = [
3495
+ "decisions",
3496
+ "conventions",
3497
+ "incidents",
3498
+ "domain",
3499
+ "gotchas"
3500
+ ];
3501
+ function getMemoriesPath(hubDir, configPath) {
3502
+ return resolve6(hubDir, configPath || "memories");
3503
+ }
3504
+ function parseFrontmatter(raw) {
3505
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
3506
+ if (!match) return { data: {}, content: raw };
3507
+ const data = {};
3508
+ for (const line of match[1].split("\n")) {
3509
+ const idx = line.indexOf(":");
3510
+ if (idx === -1) continue;
3511
+ const key = line.slice(0, idx).trim();
3512
+ let value = line.slice(idx + 1).trim();
3513
+ if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
3514
+ value = value.slice(1, -1).split(",").map((s) => s.trim());
3515
+ }
3516
+ data[key] = value;
3517
+ }
3518
+ return { data, content: match[2] };
3519
+ }
3520
+ function buildFrontmatter(data) {
3521
+ const lines = ["---"];
3522
+ for (const [key, value] of Object.entries(data)) {
3523
+ if (value === void 0) continue;
3524
+ if (Array.isArray(value)) {
3525
+ lines.push(`${key}: [${value.join(", ")}]`);
3526
+ } else {
3527
+ lines.push(`${key}: ${value}`);
3528
+ }
3529
+ }
3530
+ lines.push("---");
3531
+ return lines.join("\n");
3532
+ }
3533
+ async function listMemories(memoriesDir, opts) {
3534
+ if (!existsSync14(memoriesDir)) {
3535
+ console.log(chalk16.dim(" No memories directory found."));
3536
+ return;
3537
+ }
3538
+ let total = 0;
3539
+ for (const cat of VALID_CATEGORIES) {
3540
+ if (opts.category && opts.category !== cat) continue;
3541
+ const catDir = join17(memoriesDir, cat);
3542
+ if (!existsSync14(catDir)) continue;
3543
+ const files = (await readdir6(catDir)).filter((f) => f.endsWith(".md"));
3544
+ if (files.length === 0) continue;
3545
+ const entries = [];
3546
+ for (const file of files) {
3547
+ const raw = await readFile9(join17(catDir, file), "utf-8");
3548
+ const { data } = parseFrontmatter(raw);
3549
+ const status = data.status || "active";
3550
+ if (opts.status && status !== opts.status) continue;
3551
+ entries.push({
3552
+ id: basename2(file, ".md"),
3553
+ title: data.title || basename2(file, ".md"),
3554
+ date: data.date || "unknown",
3555
+ status,
3556
+ tags: data.tags || []
3557
+ });
3558
+ }
3559
+ if (entries.length === 0) continue;
3560
+ console.log(chalk16.cyan(`
3561
+ ${cat} (${entries.length})`));
3562
+ for (const e of entries) {
3563
+ const statusIcon = e.status === "active" ? chalk16.green("\u25CF") : chalk16.dim("\u25CB");
3564
+ const tags = e.tags.length > 0 ? chalk16.dim(` [${e.tags.join(", ")}]`) : "";
3565
+ console.log(` ${statusIcon} ${chalk16.yellow(e.id)} \u2014 ${e.title} ${chalk16.dim(`(${e.date})`)}${tags}`);
3566
+ }
3567
+ total += entries.length;
3568
+ }
3569
+ if (total === 0) {
3570
+ console.log(chalk16.dim(" No memories found."));
3571
+ } else {
3572
+ console.log(chalk16.green(`
3573
+ Total: ${total} memories
3574
+ `));
3575
+ }
3576
+ }
3577
+ var memoryCommand = new Command16("memory").description("Manage team memories \u2014 persistent knowledge base for AI context").addCommand(
3578
+ new Command16("add").description("Create a new memory entry").argument("<category>", `Category: ${VALID_CATEGORIES.join(", ")}`).argument("<title>", "Memory title").option("-c, --content <content>", "Memory content (or provide via stdin)").option("-t, --tags <tags>", "Comma-separated tags").option("-a, --author <author>", "Author name").action(
3579
+ async (category, title, opts) => {
3580
+ if (!VALID_CATEGORIES.includes(category)) {
3581
+ console.log(
3582
+ chalk16.red(`
3583
+ Invalid category: ${category}. Valid: ${VALID_CATEGORIES.join(", ")}
3584
+ `)
3585
+ );
3586
+ return;
3587
+ }
3588
+ const hubDir = process.cwd();
3589
+ let memoriesDir;
3590
+ try {
3591
+ const config = await loadHubConfig(hubDir);
3592
+ memoriesDir = getMemoriesPath(hubDir, config.memory?.path);
3593
+ } catch {
3594
+ memoriesDir = getMemoriesPath(hubDir);
3595
+ }
3596
+ const catDir = join17(memoriesDir, category);
3597
+ await mkdir9(catDir, { recursive: true });
3598
+ await ensureLanceDbIgnored(memoriesDir, hubDir);
3599
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3600
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3601
+ const id = `${date}-${slug}`;
3602
+ const filePath = join17(catDir, `${id}.md`);
3603
+ const fm = {
3604
+ title,
3605
+ category,
3606
+ date,
3607
+ status: "active"
3608
+ };
3609
+ if (opts.author) fm.author = opts.author;
3610
+ if (opts.tags) fm.tags = opts.tags.split(",").map((t) => t.trim());
3611
+ const content = opts.content || `## Context
3612
+
3613
+
3614
+
3615
+ ## Details
3616
+
3617
+ `;
3618
+ const fileContent = `${buildFrontmatter(fm)}
3619
+
3620
+ ${content}
3621
+ `;
3622
+ await writeFile11(filePath, fileContent, "utf-8");
3623
+ console.log(chalk16.green(`
3624
+ Created: ${category}/${id}.md`));
3625
+ console.log(chalk16.dim(` Path: ${filePath}`));
3626
+ if (!opts.content) {
3627
+ console.log(chalk16.yellow(" Edit the file to add content.\n"));
3628
+ } else {
3629
+ console.log();
3630
+ }
3631
+ }
3632
+ )
3633
+ ).addCommand(
3634
+ new Command16("list").description("List all memories").option("-c, --category <category>", "Filter by category").option("-s, --status <status>", "Filter by status (active, archived, superseded)").action(async (opts) => {
3635
+ const hubDir = process.cwd();
3636
+ let memoriesDir;
3637
+ try {
3638
+ const config = await loadHubConfig(hubDir);
3639
+ memoriesDir = getMemoriesPath(hubDir, config.memory?.path);
3640
+ } catch {
3641
+ memoriesDir = getMemoriesPath(hubDir);
3642
+ }
3643
+ console.log(chalk16.blue("\nTeam Memories"));
3644
+ await listMemories(memoriesDir, opts);
3645
+ })
3646
+ ).addCommand(
3647
+ new Command16("archive").description("Archive a memory (soft-delete)").argument("<id>", "Memory ID (filename without .md)").action(async (id) => {
3648
+ const hubDir = process.cwd();
3649
+ let memoriesDir;
3650
+ try {
3651
+ const config = await loadHubConfig(hubDir);
3652
+ memoriesDir = getMemoriesPath(hubDir, config.memory?.path);
3653
+ } catch {
3654
+ memoriesDir = getMemoriesPath(hubDir);
3655
+ }
3656
+ let found = false;
3657
+ for (const cat of VALID_CATEGORIES) {
3658
+ const filePath = join17(memoriesDir, cat, `${id}.md`);
3659
+ if (!existsSync14(filePath)) continue;
3660
+ const raw = await readFile9(filePath, "utf-8");
3661
+ const { data, content } = parseFrontmatter(raw);
3662
+ data.status = "archived";
3663
+ const updated = `${buildFrontmatter(data)}
3664
+ ${content}`;
3665
+ await writeFile11(filePath, updated, "utf-8");
3666
+ console.log(chalk16.green(`
3667
+ Archived: ${cat}/${id}.md
3668
+ `));
3669
+ found = true;
3670
+ break;
3671
+ }
3672
+ if (!found) {
3673
+ console.log(chalk16.red(`
3674
+ Memory "${id}" not found.
3675
+ `));
3676
+ }
3677
+ })
3678
+ ).addCommand(
3679
+ new Command16("remove").description("Permanently delete a memory").argument("<id>", "Memory ID (filename without .md)").action(async (id) => {
3680
+ const hubDir = process.cwd();
3681
+ let memoriesDir;
3682
+ try {
3683
+ const config = await loadHubConfig(hubDir);
3684
+ memoriesDir = getMemoriesPath(hubDir, config.memory?.path);
3685
+ } catch {
3686
+ memoriesDir = getMemoriesPath(hubDir);
3687
+ }
3688
+ let found = false;
3689
+ for (const cat of VALID_CATEGORIES) {
3690
+ const filePath = join17(memoriesDir, cat, `${id}.md`);
3691
+ if (!existsSync14(filePath)) continue;
3692
+ await rm5(filePath);
3693
+ console.log(chalk16.green(`
3694
+ Removed: ${cat}/${id}.md
3695
+ `));
3696
+ found = true;
3697
+ break;
3698
+ }
3699
+ if (!found) {
3700
+ console.log(chalk16.red(`
3701
+ Memory "${id}" not found.
3702
+ `));
3703
+ }
3704
+ })
3705
+ );
3706
+
3707
+ // src/commands/update.ts
3708
+ import { Command as Command17 } from "commander";
3334
3709
  import { execSync as execSync12 } from "child_process";
3335
3710
  import { readFileSync } from "fs";
3336
- import { join as join17, dirname } from "path";
3711
+ import { join as join18, dirname } from "path";
3337
3712
  import { fileURLToPath } from "url";
3338
- import chalk16 from "chalk";
3713
+ import chalk17 from "chalk";
3339
3714
  var PACKAGE_NAME = "@arvoretech/hub";
3340
3715
  function getCurrentVersion() {
3341
3716
  const __dirname = dirname(fileURLToPath(import.meta.url));
3342
- const pkgPath = join17(__dirname, "..", "package.json");
3717
+ const pkgPath = join18(__dirname, "..", "package.json");
3343
3718
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3344
3719
  return pkg.version;
3345
3720
  }
@@ -3362,54 +3737,54 @@ function detectPackageManager() {
3362
3737
  }
3363
3738
  }
3364
3739
  }
3365
- var updateCommand = new Command16("update").description("Update hub CLI to the latest version").option("--check", "Only check for updates without installing").action(async (opts) => {
3740
+ var updateCommand = new Command17("update").description("Update hub CLI to the latest version").option("--check", "Only check for updates without installing").action(async (opts) => {
3366
3741
  const currentVersion = getCurrentVersion();
3367
- console.log(chalk16.blue(`
3742
+ console.log(chalk17.blue(`
3368
3743
  Current version: ${currentVersion}`));
3369
3744
  let latestVersion;
3370
3745
  try {
3371
3746
  latestVersion = await getLatestVersion();
3372
3747
  } catch (err) {
3373
- console.log(chalk16.red(` Failed to check for updates: ${err.message}
3748
+ console.log(chalk17.red(` Failed to check for updates: ${err.message}
3374
3749
  `));
3375
3750
  return;
3376
3751
  }
3377
- console.log(chalk16.blue(` Latest version: ${latestVersion}`));
3752
+ console.log(chalk17.blue(` Latest version: ${latestVersion}`));
3378
3753
  if (currentVersion === latestVersion) {
3379
- console.log(chalk16.green("\n You're already on the latest version.\n"));
3754
+ console.log(chalk17.green("\n You're already on the latest version.\n"));
3380
3755
  return;
3381
3756
  }
3382
- console.log(chalk16.yellow(`
3757
+ console.log(chalk17.yellow(`
3383
3758
  Update available: ${currentVersion} \u2192 ${latestVersion}`));
3384
3759
  if (opts.check) {
3385
3760
  const pm2 = detectPackageManager();
3386
- console.log(chalk16.dim(`
3761
+ console.log(chalk17.dim(`
3387
3762
  Run 'hub update' or '${pm2} install -g ${PACKAGE_NAME}@latest' to update.
3388
3763
  `));
3389
3764
  return;
3390
3765
  }
3391
3766
  const pm = detectPackageManager();
3392
3767
  const installCmd = pm === "pnpm" ? `pnpm install -g ${PACKAGE_NAME}@latest` : pm === "yarn" ? `yarn global add ${PACKAGE_NAME}@latest` : `npm install -g ${PACKAGE_NAME}@latest`;
3393
- console.log(chalk16.cyan(`
3768
+ console.log(chalk17.cyan(`
3394
3769
  Updating with ${pm}...
3395
3770
  `));
3396
- console.log(chalk16.dim(` $ ${installCmd}
3771
+ console.log(chalk17.dim(` $ ${installCmd}
3397
3772
  `));
3398
3773
  try {
3399
3774
  execSync12(installCmd, { stdio: "inherit" });
3400
- console.log(chalk16.green(`
3775
+ console.log(chalk17.green(`
3401
3776
  Updated to ${latestVersion} successfully.
3402
3777
  `));
3403
3778
  } catch {
3404
- console.log(chalk16.red(`
3779
+ console.log(chalk17.red(`
3405
3780
  Update failed. Try running manually:`));
3406
- console.log(chalk16.dim(` $ ${installCmd}
3781
+ console.log(chalk17.dim(` $ ${installCmd}
3407
3782
  `));
3408
3783
  }
3409
3784
  });
3410
3785
 
3411
3786
  // src/index.ts
3412
- var program = new Command17();
3787
+ var program = new Command18();
3413
3788
  program.name("hub").description(
3414
3789
  "Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
3415
3790
  ).version("0.2.0").enablePositionalOptions();
@@ -3430,5 +3805,6 @@ program.addCommand(execCommand);
3430
3805
  program.addCommand(worktreeCommand);
3431
3806
  program.addCommand(doctorCommand);
3432
3807
  program.addCommand(toolsCommand);
3808
+ program.addCommand(memoryCommand);
3433
3809
  program.addCommand(updateCommand);
3434
3810
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for managing AI-aware multi-repository workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",