@arvoretech/hub 0.3.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 +434 -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
@@ -917,6 +929,18 @@ ${prompt.sections.after_pipeline.trim()}`);
917
929
  if (prompt?.sections?.after_delivery) {
918
930
  sections.push(`
919
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\`.`);
920
944
  }
921
945
  sections.push(`
922
946
  ## Troubleshooting and Debugging
@@ -1306,6 +1330,14 @@ function buildGitignoreLines(config) {
1306
1330
  "# Task documents",
1307
1331
  "tasks/"
1308
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
+ }
1309
1341
  return lines;
1310
1342
  }
1311
1343
  var generators = {
@@ -1316,6 +1348,25 @@ var generators = {
1316
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) => {
1317
1349
  const hubDir = process.cwd();
1318
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
+ }
1319
1370
  const generator = generators[opts.editor];
1320
1371
  if (!generator) {
1321
1372
  console.log(
@@ -1904,17 +1955,32 @@ async function listLocalSkills(hubDir) {
1904
1955
  }
1905
1956
  return skills;
1906
1957
  }
1907
- async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1908
- if (!existsSync6(sourceSkillsDir)) {
1909
- console.log(chalk8.red(" No skills/ directory found in source"));
1910
- 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 };
1911
1966
  }
1912
- const available = await readdir2(sourceSkillsDir);
1913
- const skillFolders = available.filter(
1914
- (f) => existsSync6(join9(sourceSkillsDir, f, "SKILL.md"))
1915
- );
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);
1916
1982
  if (skillFolders.length === 0) {
1917
- 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)"));
1918
1984
  return;
1919
1985
  }
1920
1986
  const toInstall = opts.skill ? skillFolders.filter((s) => s === opts.skill) : skillFolders;
@@ -1925,7 +1991,7 @@ async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1925
1991
  const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1926
1992
  await mkdir4(targetBase, { recursive: true });
1927
1993
  for (const skill of toInstall) {
1928
- const src = join9(sourceSkillsDir, skill);
1994
+ const src = join9(dir, skill);
1929
1995
  const dest = join9(targetBase, skill);
1930
1996
  await cp2(src, dest, { recursive: true });
1931
1997
  console.log(chalk8.green(` Installed: ${skill}`));
@@ -1964,42 +2030,80 @@ async function addFromRegistry(skillName, hubDir, opts) {
1964
2030
  }
1965
2031
  async function addFromGitHubSkill(owner, repo, skillName, hubDir, opts) {
1966
2032
  const fullRepo = `${owner}/${repo}`;
1967
- const remotePath = `skills/${skillName}`;
1968
2033
  const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1969
2034
  const dest = join9(targetBase, skillName);
2035
+ const pathsToTry = [
2036
+ `skills/${skillName}`,
2037
+ skillName
2038
+ ];
1970
2039
  console.log(chalk8.cyan(` Downloading ${skillName} from ${fullRepo} via GitHub API...`));
1971
- try {
1972
- await downloadDirFromGitHub(fullRepo, remotePath, dest);
1973
- 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 {
1974
2053
  await rm(dest, { recursive: true }).catch(() => {
1975
2054
  });
1976
- console.log(chalk8.red(` Skill '${skillName}' not found in ${fullRepo}/skills/`));
1977
- console.log(chalk8.dim(` Check available skills: hub skills add ${fullRepo} --list`));
1978
- return;
1979
2055
  }
1980
- console.log(chalk8.green(` Installed: ${skillName} (from ${fullRepo})`));
1981
- console.log(chalk8.green(`
1982
- 1 skill(s) installed to ${opts.global ? "global" : "project"}
1983
- `));
1984
- } catch (err) {
1985
- console.log(chalk8.red(` Failed to download: ${err.message}`));
1986
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`));
1987
2059
  }
1988
2060
  async function listRemoteSkills(owner, repo) {
1989
2061
  const fullRepo = `${owner}/${repo}`;
1990
2062
  console.log(chalk8.cyan(` Fetching skills from ${fullRepo}...
1991
2063
  `));
2064
+ const headers = { Accept: "application/vnd.github.v3+json" };
1992
2065
  try {
1993
- const apiUrl = `https://api.github.com/repos/${fullRepo}/contents/skills`;
1994
- const res = await fetch(apiUrl, {
1995
- headers: { Accept: "application/vnd.github.v3+json" }
1996
- });
1997
- if (!res.ok) {
1998
- console.log(chalk8.red(` Could not list skills from ${fullRepo}`));
1999
- 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
+ }
2000
2106
  }
2001
- const items = await res.json();
2002
- const dirs = items.filter((i) => i.type === "dir");
2003
2107
  if (dirs.length === 0) {
2004
2108
  console.log(chalk8.dim(" No skills found."));
2005
2109
  return;
@@ -2027,8 +2131,7 @@ async function addFromLocalPath(localPath, hubDir, opts) {
2027
2131
  console.log(chalk8.red(` Path is not a directory: ${absPath}`));
2028
2132
  return;
2029
2133
  }
2030
- const sourceSkillsDir = join9(absPath, "skills");
2031
- await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
2134
+ await installSkillsFromDir(absPath, hubDir, opts);
2032
2135
  }
2033
2136
  async function addFromGitRepo(source, hubDir, opts) {
2034
2137
  const tmp = tmpDir();
@@ -2043,8 +2146,7 @@ async function addFromGitRepo(source, hubDir, opts) {
2043
2146
  console.log(chalk8.dim(" Make sure the URL is correct and you have access to the repository."));
2044
2147
  return;
2045
2148
  }
2046
- const sourceSkillsDir = join9(tmp, "skills");
2047
- await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
2149
+ await installSkillsFromDir(tmp, hubDir, opts);
2048
2150
  } finally {
2049
2151
  if (existsSync6(tmp)) {
2050
2152
  await rm(tmp, { recursive: true });
@@ -2061,7 +2163,7 @@ function parseGitHubSource(source) {
2061
2163
  return null;
2062
2164
  }
2063
2165
  var skillsCommand = new Command8("skills").description("Manage agent skills").addCommand(
2064
- 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) => {
2065
2167
  const hubDir = process.cwd();
2066
2168
  if (isLocalPath(source)) {
2067
2169
  console.log(chalk8.blue(`
@@ -2106,9 +2208,10 @@ Installing skill ${source} from registry
2106
2208
  await addFromRegistry(source, hubDir, opts);
2107
2209
  })
2108
2210
  ).addCommand(
2109
- new Command8("find").description("Browse community skills on skills.sh").argument("[query]", "Search term (opens skills.sh)").action(async (query) => {
2110
- const url = query ? `https://skills.sh/?q=${encodeURIComponent(query)}` : "https://skills.sh";
2111
- 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"));
2112
2215
  console.log(chalk8.cyan(` ${url}
2113
2216
  `));
2114
2217
  console.log(chalk8.dim(" Install with: hub skills add <owner>/<repo>/<skill-name>"));
@@ -2215,18 +2318,33 @@ async function addFromLocalPath2(localPath, hubDir, opts) {
2215
2318
  console.log(chalk9.red(` Path not found: ${absPath}`));
2216
2319
  return;
2217
2320
  }
2218
- const sourceAgentsDir = statSync2(absPath).isDirectory() ? join10(absPath, "agents") : absPath;
2219
- await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2220
- }
2221
- async function installAgentsFromDir(sourceAgentsDir, hubDir, opts) {
2222
- if (!existsSync7(sourceAgentsDir)) {
2223
- 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}`));
2224
2323
  return;
2225
2324
  }
2226
- const files = await readdir3(sourceAgentsDir);
2227
- 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);
2228
2346
  if (mdFiles.length === 0) {
2229
- 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)"));
2230
2348
  return;
2231
2349
  }
2232
2350
  const toInstall = opts.agent ? mdFiles.filter((f) => f === `${opts.agent}.md` || f === opts.agent) : mdFiles;
@@ -2238,7 +2356,7 @@ async function installAgentsFromDir(sourceAgentsDir, hubDir, opts) {
2238
2356
  const targetBase = opts.global ? join10(process.env.HOME || "~", ".cursor", "agents") : join10(hubDir, "agents");
2239
2357
  await mkdir5(targetBase, { recursive: true });
2240
2358
  for (const file of toInstall) {
2241
- await copyFile2(join10(sourceAgentsDir, file), join10(targetBase, file));
2359
+ await copyFile2(join10(dir, file), join10(targetBase, file));
2242
2360
  console.log(chalk9.green(` Installed: ${file.replace(/\.md$/, "")}`));
2243
2361
  }
2244
2362
  console.log(
@@ -2261,8 +2379,7 @@ async function addFromGitRepo2(source, hubDir, opts) {
2261
2379
  console.log(chalk9.red(` Repository not found or not accessible: ${source}`));
2262
2380
  return;
2263
2381
  }
2264
- const sourceAgentsDir = join10(tmp, "agents");
2265
- await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2382
+ await installAgentsFromDir(tmp, hubDir, opts);
2266
2383
  } finally {
2267
2384
  if (existsSync7(tmp)) {
2268
2385
  await rm2(tmp, { recursive: true });
@@ -2337,6 +2454,16 @@ Agent '${name}' not found in ${opts.global ? "global" : "project"}
2337
2454
  Removed agent: ${name}
2338
2455
  `));
2339
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
+ })
2340
2467
  ).addCommand(
2341
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) => {
2342
2469
  const hubDir = process.cwd();
@@ -3341,17 +3468,253 @@ function extractVersion(s) {
3341
3468
  return match?.[1] || s;
3342
3469
  }
3343
3470
 
3344
- // src/commands/update.ts
3471
+ // src/commands/memory.ts
3345
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";
3346
3709
  import { execSync as execSync12 } from "child_process";
3347
3710
  import { readFileSync } from "fs";
3348
- import { join as join17, dirname } from "path";
3711
+ import { join as join18, dirname } from "path";
3349
3712
  import { fileURLToPath } from "url";
3350
- import chalk16 from "chalk";
3713
+ import chalk17 from "chalk";
3351
3714
  var PACKAGE_NAME = "@arvoretech/hub";
3352
3715
  function getCurrentVersion() {
3353
3716
  const __dirname = dirname(fileURLToPath(import.meta.url));
3354
- const pkgPath = join17(__dirname, "..", "package.json");
3717
+ const pkgPath = join18(__dirname, "..", "package.json");
3355
3718
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3356
3719
  return pkg.version;
3357
3720
  }
@@ -3374,54 +3737,54 @@ function detectPackageManager() {
3374
3737
  }
3375
3738
  }
3376
3739
  }
3377
- 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) => {
3378
3741
  const currentVersion = getCurrentVersion();
3379
- console.log(chalk16.blue(`
3742
+ console.log(chalk17.blue(`
3380
3743
  Current version: ${currentVersion}`));
3381
3744
  let latestVersion;
3382
3745
  try {
3383
3746
  latestVersion = await getLatestVersion();
3384
3747
  } catch (err) {
3385
- console.log(chalk16.red(` Failed to check for updates: ${err.message}
3748
+ console.log(chalk17.red(` Failed to check for updates: ${err.message}
3386
3749
  `));
3387
3750
  return;
3388
3751
  }
3389
- console.log(chalk16.blue(` Latest version: ${latestVersion}`));
3752
+ console.log(chalk17.blue(` Latest version: ${latestVersion}`));
3390
3753
  if (currentVersion === latestVersion) {
3391
- 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"));
3392
3755
  return;
3393
3756
  }
3394
- console.log(chalk16.yellow(`
3757
+ console.log(chalk17.yellow(`
3395
3758
  Update available: ${currentVersion} \u2192 ${latestVersion}`));
3396
3759
  if (opts.check) {
3397
3760
  const pm2 = detectPackageManager();
3398
- console.log(chalk16.dim(`
3761
+ console.log(chalk17.dim(`
3399
3762
  Run 'hub update' or '${pm2} install -g ${PACKAGE_NAME}@latest' to update.
3400
3763
  `));
3401
3764
  return;
3402
3765
  }
3403
3766
  const pm = detectPackageManager();
3404
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`;
3405
- console.log(chalk16.cyan(`
3768
+ console.log(chalk17.cyan(`
3406
3769
  Updating with ${pm}...
3407
3770
  `));
3408
- console.log(chalk16.dim(` $ ${installCmd}
3771
+ console.log(chalk17.dim(` $ ${installCmd}
3409
3772
  `));
3410
3773
  try {
3411
3774
  execSync12(installCmd, { stdio: "inherit" });
3412
- console.log(chalk16.green(`
3775
+ console.log(chalk17.green(`
3413
3776
  Updated to ${latestVersion} successfully.
3414
3777
  `));
3415
3778
  } catch {
3416
- console.log(chalk16.red(`
3779
+ console.log(chalk17.red(`
3417
3780
  Update failed. Try running manually:`));
3418
- console.log(chalk16.dim(` $ ${installCmd}
3781
+ console.log(chalk17.dim(` $ ${installCmd}
3419
3782
  `));
3420
3783
  }
3421
3784
  });
3422
3785
 
3423
3786
  // src/index.ts
3424
- var program = new Command17();
3787
+ var program = new Command18();
3425
3788
  program.name("hub").description(
3426
3789
  "Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
3427
3790
  ).version("0.2.0").enablePositionalOptions();
@@ -3442,5 +3805,6 @@ program.addCommand(execCommand);
3442
3805
  program.addCommand(worktreeCommand);
3443
3806
  program.addCommand(doctorCommand);
3444
3807
  program.addCommand(toolsCommand);
3808
+ program.addCommand(memoryCommand);
3445
3809
  program.addCommand(updateCommand);
3446
3810
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.3.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",