@arvoretech/hub 0.13.3 → 0.16.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.
@@ -5,7 +5,7 @@ import {
5
5
  // src/commands/generate.ts
6
6
  import { Command } from "commander";
7
7
  import { existsSync as existsSync2 } from "fs";
8
- import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir2, copyFile, readFile as readFile3, cp } from "fs/promises";
8
+ import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir2, copyFile, readFile as readFile3, cp, rm } from "fs/promises";
9
9
  import { join as join3, resolve as resolve2 } from "path";
10
10
  import chalk3 from "chalk";
11
11
  import inquirer from "inquirer";
@@ -117,7 +117,7 @@ async function checkAndAutoRegenerate(hubDir) {
117
117
  return;
118
118
  }
119
119
  console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
120
- const { generators: generators2 } = await import("./generate-YSMMXJNC.js");
120
+ const { generators: generators2 } = await import("./generate-YJEPLTSQ.js");
121
121
  const generator = generators2[result.editor];
122
122
  if (!generator) {
123
123
  console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
@@ -666,6 +666,11 @@ async function generateCursor(config, hubDir) {
666
666
  const orchestratorRule = buildOrchestratorRule(config);
667
667
  await writeFile3(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
668
668
  console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
669
+ const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
670
+ const skillsSectionCursor = await buildSkillsSection(hubDir, config);
671
+ const agentsMdCursor = skillsSectionCursor ? cleanedOrchestratorForAgents + "\n" + skillsSectionCursor : cleanedOrchestratorForAgents;
672
+ await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdCursor + "\n", "utf-8");
673
+ console.log(chalk3.green(" Generated AGENTS.md"));
669
674
  const hubSteeringDirCursor = resolve2(hubDir, "steering");
670
675
  try {
671
676
  const steeringFiles = await readdir2(hubSteeringDirCursor);
@@ -903,24 +908,32 @@ ${rawContent}`;
903
908
  return `${lines.join("\n")}
904
909
  ${body}`;
905
910
  }
911
+ function stripDollarPrefix(env) {
912
+ const result = {};
913
+ for (const [key, value] of Object.entries(env)) {
914
+ result[key] = value.replace(/\$\{env:(\w+)\}/g, "{env:$1}").replace(/\$\{(\w+)\}/g, "{env:$1}");
915
+ }
916
+ return result;
917
+ }
906
918
  function buildOpenCodeMcpEntry(mcp) {
919
+ const env = mcp.env ? stripDollarPrefix(mcp.env) : void 0;
907
920
  if (mcp.url) {
908
921
  return { type: "remote", url: mcp.url };
909
922
  }
910
923
  if (mcp.image) {
911
924
  const cmd = ["docker", "run", "-i", "--rm"];
912
- if (mcp.env) {
913
- for (const [key, value] of Object.entries(mcp.env)) {
925
+ if (env) {
926
+ for (const [key, value] of Object.entries(env)) {
914
927
  cmd.push("-e", `${key}=${value}`);
915
928
  }
916
929
  }
917
930
  cmd.push(mcp.image);
918
- return { type: "local", command: cmd, ...mcp.env && { environment: mcp.env } };
931
+ return { type: "local", command: cmd, ...env && { environment: env } };
919
932
  }
920
933
  return {
921
934
  type: "local",
922
935
  command: ["npx", "-y", mcp.package],
923
- ...mcp.env && { environment: mcp.env }
936
+ ...env && { environment: env }
924
937
  };
925
938
  }
926
939
  function buildOpenCodeHooksPlugin(hooks) {
@@ -965,6 +978,22 @@ tools:
965
978
  bash: true
966
979
  ---
967
980
 
981
+ ${body.trim()}
982
+ `;
983
+ }
984
+ function buildOpenCodePrimaryAgentMarkdown(description, body) {
985
+ return `---
986
+ description: "${description}"
987
+ mode: primary
988
+ tools:
989
+ write: true
990
+ edit: true
991
+ bash: true
992
+ permission:
993
+ task:
994
+ "*": allow
995
+ ---
996
+
968
997
  ${body.trim()}
969
998
  `;
970
999
  }
@@ -1050,6 +1079,49 @@ You can communicate with agents from other developers on the team via the \`agen
1050
1079
  - Read the thread before replying to avoid repeating what others said
1051
1080
  - When starting a task that touches shared code, check recent threads for relevant context`;
1052
1081
  }
1082
+ function hasKanbanMcp(mcps) {
1083
+ if (!mcps) return false;
1084
+ const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
1085
+ const directMatch = mcps.some((m) => m.name === "kanban" || m.package === "@arvoretech/kanban-mcp");
1086
+ const upstreamMatch = proxyMcp?.upstreams?.includes("kanban") ?? false;
1087
+ return directMatch || upstreamMatch;
1088
+ }
1089
+ function buildKanbanSection(mcps) {
1090
+ if (!hasKanbanMcp(mcps)) return "";
1091
+ return `
1092
+ ## Kanban Board
1093
+
1094
+ This workspace has a persistent kanban board via the \`kanban\` MCP. Use it to organize work, track progress across sessions, and coordinate with other chats.
1095
+
1096
+ **When to use the kanban:**
1097
+ - At the start of a task, check the board for existing cards and active sessions
1098
+ - Break complex features into cards before starting implementation
1099
+ - Claim cards you're working on so other sessions can see
1100
+ - Release cards when done (default status: review)
1101
+ - Search for related cards before creating duplicates
1102
+
1103
+ **Workflow:**
1104
+ 1. \`list_boards\` / \`get_board\` \u2014 See what's on the board and who's working on what
1105
+ 2. \`create_card\` \u2014 Add new tasks to the appropriate column
1106
+ 3. \`claim_card\` \u2014 Mark a card as being worked on by this session
1107
+ 4. \`move_card\` \u2014 Move cards between columns as work progresses
1108
+ 5. \`release_card\` \u2014 Release when done, with status and detail (e.g. "PR #123 created")
1109
+ 6. \`search_cards\` \u2014 Find cards by meaning (semantic search)
1110
+
1111
+ **Available tools:** \`list_boards\`, \`create_board\`, \`get_board\`, \`get_card\`, \`create_card\`, \`update_card\`, \`move_card\`, \`claim_card\`, \`release_card\`, \`search_cards\`, \`archive_card\`, \`delete_card\`.
1112
+
1113
+ **Multi-session coordination:**
1114
+ - Always \`claim_card\` before starting work \u2014 other sessions will see it's taken
1115
+ - If a card is already claimed, pick another or use \`force: true\` to override stale sessions
1116
+ - Use \`get_board\` to see active sessions with duration (helps identify abandoned claims)
1117
+ - When finishing, \`release_card\` with a meaningful detail so the next session has context
1118
+
1119
+ **Best practices:**
1120
+ - Use subtasks (\`parent_card_id\`) to break down large cards
1121
+ - Tag cards consistently for easy filtering
1122
+ - Set priority to help triage (urgent > high > medium > low)
1123
+ - Check the board at the start of every session \u2014 don't start from zero`;
1124
+ }
1053
1125
  function buildMcpToolsSection(mcps) {
1054
1126
  if (!mcps || mcps.length === 0) return "";
1055
1127
  const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
@@ -1101,6 +1173,141 @@ ${mcp.instructions.trim()}`);
1101
1173
  }
1102
1174
  return lines.join("\n");
1103
1175
  }
1176
+ async function buildSkillsSection(hubDir, config) {
1177
+ const skillsDir = resolve2(hubDir, "skills");
1178
+ const skillEntries = [];
1179
+ try {
1180
+ const folders = await readdir2(skillsDir);
1181
+ for (const folder of folders) {
1182
+ const skillPath = join3(skillsDir, folder, "SKILL.md");
1183
+ try {
1184
+ const content = await readFile3(skillPath, "utf-8");
1185
+ const fm = parseFrontMatter(content);
1186
+ if (fm?.name) {
1187
+ skillEntries.push({
1188
+ name: fm.name,
1189
+ description: fm.description || ""
1190
+ });
1191
+ }
1192
+ } catch {
1193
+ }
1194
+ }
1195
+ } catch {
1196
+ return null;
1197
+ }
1198
+ if (skillEntries.length === 0) return null;
1199
+ const repoSkillMap = /* @__PURE__ */ new Map();
1200
+ for (const repo of config.repos) {
1201
+ if (repo.skills?.length) {
1202
+ for (const skill of repo.skills) {
1203
+ const repos = repoSkillMap.get(skill) || [];
1204
+ repos.push(repo.path);
1205
+ repoSkillMap.set(skill, repos);
1206
+ }
1207
+ }
1208
+ }
1209
+ const parts = [];
1210
+ parts.push(`
1211
+ ## Skills
1212
+
1213
+ This workspace has skills that provide specialized knowledge for specific domains and repositories.
1214
+ Consult the relevant skill before working in an unfamiliar area \u2014 they contain patterns, conventions, and project-specific guidance.
1215
+
1216
+ | Skill | Description | Repositories |
1217
+ |-------|-------------|--------------|`);
1218
+ for (const entry of skillEntries) {
1219
+ const repos = repoSkillMap.get(entry.name);
1220
+ const repoCol = repos ? repos.map((r) => `\`${r}\``).join(", ") : "\u2014";
1221
+ const desc = entry.description.replace(/\|/g, "\\|").split(".")[0].trim();
1222
+ parts.push(`| \`${entry.name}\` | ${desc} | ${repoCol} |`);
1223
+ }
1224
+ parts.push(`
1225
+ When to consult a skill:
1226
+ - Before writing code in a repository that has an associated skill
1227
+ - When making architecture or pattern decisions in a specific domain
1228
+ - When unsure about project conventions, libraries, or testing approaches
1229
+ - When the user's request touches a domain covered by an available skill
1230
+
1231
+ Additional context sources:
1232
+ - Use documentation MCPs to check library and framework docs before implementing
1233
+ - Use database MCPs to understand schema, query data, and verify state
1234
+ - Use package registry MCPs to verify security and versions before installing dependencies
1235
+ - Use the repository CLI commands (build, test, lint) to validate changes after implementation
1236
+ - Use monitoring MCPs for production debugging and log analysis when available`);
1237
+ return parts.join("\n");
1238
+ }
1239
+ function buildCoreBehaviorSections() {
1240
+ const sections = [];
1241
+ sections.push(`
1242
+ ## Core Behavior
1243
+
1244
+ Be concise, clear, direct, and useful.
1245
+ Prefer technical accuracy over reassurance.
1246
+ Do not use hype, flattery, or exaggerated validation.
1247
+ Do not repeatedly apologize when something unexpected happens \u2014 explain what happened and continue.
1248
+ Do not claim actions were performed unless they were actually performed.
1249
+ Never invent facts, code behavior, file contents, tool capabilities, or execution outcomes.
1250
+ Focus on completing the user's task, not on narrating unnecessary process.`);
1251
+ sections.push(`
1252
+ ## Working Style
1253
+
1254
+ Prefer the simplest solution that fully satisfies the request.
1255
+ Avoid over-engineering, speculative abstractions, premature generalization, and cleanup outside the requested scope.
1256
+ Prefer editing existing files over creating new files.
1257
+ Prefer minimal, reversible changes over broad rewrites unless the task explicitly requires a rewrite.
1258
+ Ask the user questions only when a real ambiguity materially affects the solution.
1259
+ Bias toward finding the answer yourself when the available context and tools are sufficient.`);
1260
+ sections.push(`
1261
+ ## Search, Reading, and Investigation
1262
+
1263
+ If you are unsure how to satisfy the user's request, gather more information before answering.
1264
+ Prefer discovering answers yourself over asking the user for information that is likely available in the workspace, files, memories, or tools.
1265
+
1266
+ When reading code or documents:
1267
+ - Read enough surrounding context to avoid missing critical behavior
1268
+ - Do not propose modifications to code you have not inspected
1269
+ - If partial views may hide important logic, continue reading before deciding
1270
+
1271
+ For broader exploration:
1272
+ - Use lightweight search first
1273
+ - Escalate to deeper exploration or subagents only when the task is broad, ambiguous, or likely to require several search passes`);
1274
+ sections.push(`
1275
+ ## Code Changes
1276
+
1277
+ When making code changes:
1278
+ - Ensure the produced code is runnable and internally consistent
1279
+ - Add required imports, wiring, dependencies, and integration points
1280
+ - Preserve the project's existing patterns unless there is a strong reason to change them
1281
+ - Read the relevant files or sections before modifying existing code
1282
+ - Understand the surrounding code paths and conventions
1283
+ - Prefer small, precise edits
1284
+
1285
+ If you introduce errors:
1286
+ - Try to fix them
1287
+ - Do not get stuck in unbounded retry loops (max 3 attempts on the same issue)
1288
+ - If repeated fixes fail, explain the remaining problem clearly
1289
+
1290
+ Never assume a library is available \u2014 check the dependency file or neighboring code first.
1291
+ When creating a new component, look at existing components to understand conventions.`);
1292
+ sections.push(`
1293
+ ## Security and Safety
1294
+
1295
+ Never hardcode secrets, credentials, tokens, or API keys.
1296
+ Flag security risks when noticed.
1297
+ Avoid introducing vulnerabilities such as command injection, SQL injection, XSS, insecure secret handling, broken auth flows, unsafe deserialization, SSRF, or privilege escalation.
1298
+ Do not expose secrets in code, tests, examples, or logs.`);
1299
+ sections.push(`
1300
+ ## Git and Operational Discipline
1301
+
1302
+ Do not commit, push, open pull requests, or notify external systems unless the user asked for it or the workspace flow explicitly requires it.
1303
+
1304
+ When handling git work:
1305
+ - Inspect status and diff before committing
1306
+ - Follow existing repository commit conventions
1307
+ - Prefer specific file staging over indiscriminate staging
1308
+ - Do not use destructive git commands without explicit user authorization`);
1309
+ return sections;
1310
+ }
1104
1311
  function buildOpenCodeOrchestratorRule(config) {
1105
1312
  const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
1106
1313
  const steps = config.workflow?.pipeline || [];
@@ -1194,6 +1401,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1194
1401
  if (agentTeamsSectionOpenCode) sections.push(agentTeamsSectionOpenCode);
1195
1402
  const agentTeamsChatSectionOpenCode = buildAgentTeamsChatSection(config.mcps);
1196
1403
  if (agentTeamsChatSectionOpenCode) sections.push(agentTeamsChatSectionOpenCode);
1404
+ const kanbanSectionOpenCode = buildKanbanSection(config.mcps);
1405
+ if (kanbanSectionOpenCode) sections.push(kanbanSectionOpenCode);
1197
1406
  sections.push(`
1198
1407
  ## Troubleshooting and Debugging
1199
1408
 
@@ -1204,6 +1413,7 @@ It will:
1204
1413
  3. Form and test hypotheses systematically
1205
1414
  4. Identify the root cause
1206
1415
  5. Propose a solution or call coding agents to implement the fix`);
1416
+ sections.push(...buildCoreBehaviorSections());
1207
1417
  if (prompt?.sections) {
1208
1418
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1209
1419
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1303,9 +1513,24 @@ async function generateOpenCode(config, hubDir) {
1303
1513
  const gitignoreLines = buildGitignoreLines(config);
1304
1514
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1305
1515
  console.log(chalk3.green(" Generated .gitignore"));
1306
- const orchestratorRule = buildOpenCodeOrchestratorRule(config);
1307
- await writeFile3(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
1308
- console.log(chalk3.green(" Generated .opencode/rules/orchestrator.md"));
1516
+ if (config.repos.length > 0) {
1517
+ const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
1518
+ await writeFile3(join3(hubDir, ".ignore"), ignoreContent, "utf-8");
1519
+ console.log(chalk3.green(" Generated .ignore"));
1520
+ }
1521
+ const orchestratorContent = buildOpenCodeOrchestratorRule(config);
1522
+ const orchestratorAgent = buildOpenCodePrimaryAgentMarkdown(
1523
+ "Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
1524
+ orchestratorContent
1525
+ );
1526
+ await writeFile3(join3(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
1527
+ console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
1528
+ await rm(join3(opencodeDir, "rules", "orchestrator.md")).catch(() => {
1529
+ });
1530
+ const skillsSectionOC = await buildSkillsSection(hubDir, config);
1531
+ const agentsMdOC = skillsSectionOC ? orchestratorContent + "\n" + skillsSectionOC : orchestratorContent;
1532
+ await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdOC + "\n", "utf-8");
1533
+ console.log(chalk3.green(" Generated AGENTS.md"));
1309
1534
  const hubSteeringDirOC = resolve2(hubDir, "steering");
1310
1535
  try {
1311
1536
  const steeringFiles = await readdir2(hubSteeringDirOC);
@@ -1321,7 +1546,8 @@ async function generateOpenCode(config, hubDir) {
1321
1546
  } catch {
1322
1547
  }
1323
1548
  const opencodeConfig = {
1324
- $schema: "https://opencode.ai/config.json"
1549
+ $schema: "https://opencode.ai/config.json",
1550
+ default_agent: "orchestrator"
1325
1551
  };
1326
1552
  if (config.mcps?.length) {
1327
1553
  const mcpConfig = {};
@@ -1348,6 +1574,7 @@ async function generateOpenCode(config, hubDir) {
1348
1574
  const agentFiles = await readdir2(agentsDir);
1349
1575
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1350
1576
  for (const file of mdFiles) {
1577
+ if (file === "orchestrator.md") continue;
1351
1578
  const content = await readFile3(join3(agentsDir, file), "utf-8");
1352
1579
  const agentName = file.replace(/\.md$/, "");
1353
1580
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
@@ -1491,6 +1718,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1491
1718
  if (agentTeamsSectionKiro) sections.push(agentTeamsSectionKiro);
1492
1719
  const agentTeamsChatSectionKiro = buildAgentTeamsChatSection(config.mcps);
1493
1720
  if (agentTeamsChatSectionKiro) sections.push(agentTeamsChatSectionKiro);
1721
+ const kanbanSectionKiro = buildKanbanSection(config.mcps);
1722
+ if (kanbanSectionKiro) sections.push(kanbanSectionKiro);
1494
1723
  sections.push(`
1495
1724
  ## Troubleshooting and Debugging
1496
1725
 
@@ -1500,6 +1729,7 @@ For bug reports or unexpected behavior, follow the debugging process from the \`
1500
1729
  3. Form and test hypotheses systematically
1501
1730
  4. Identify the root cause
1502
1731
  5. Propose and implement the fix`);
1732
+ sections.push(...buildCoreBehaviorSections());
1503
1733
  if (prompt?.sections) {
1504
1734
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1505
1735
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1683,6 +1913,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
1683
1913
  if (agentTeamsSectionCursor) sections.push(agentTeamsSectionCursor);
1684
1914
  const agentTeamsChatSectionCursor = buildAgentTeamsChatSection(config.mcps);
1685
1915
  if (agentTeamsChatSectionCursor) sections.push(agentTeamsChatSectionCursor);
1916
+ const kanbanSectionCursor = buildKanbanSection(config.mcps);
1917
+ if (kanbanSectionCursor) sections.push(kanbanSectionCursor);
1686
1918
  sections.push(`
1687
1919
  ## Troubleshooting and Debugging
1688
1920
 
@@ -1693,6 +1925,7 @@ It will:
1693
1925
  3. Form and test hypotheses systematically
1694
1926
  4. Identify the root cause
1695
1927
  5. Propose a solution or call coding agents to implement the fix`);
1928
+ sections.push(...buildCoreBehaviorSections());
1696
1929
  if (prompt?.sections) {
1697
1930
  const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
1698
1931
  for (const [name, content] of Object.entries(prompt.sections)) {
@@ -1866,6 +2099,10 @@ async function generateClaudeCode(config, hubDir) {
1866
2099
  await mkdir3(join3(claudeDir, "agents"), { recursive: true });
1867
2100
  const orchestratorRule = buildOrchestratorRule(config);
1868
2101
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
2102
+ const skillsSectionClaude = await buildSkillsSection(hubDir, config);
2103
+ const agentsMdClaude = skillsSectionClaude ? cleanedOrchestrator + "\n" + skillsSectionClaude : cleanedOrchestrator;
2104
+ await writeFile3(join3(hubDir, "AGENTS.md"), agentsMdClaude + "\n", "utf-8");
2105
+ console.log(chalk3.green(" Generated AGENTS.md"));
1869
2106
  const claudeMdSections = [];
1870
2107
  claudeMdSections.push(cleanedOrchestrator);
1871
2108
  const agentsDir = resolve2(hubDir, "agents");
@@ -2016,10 +2253,9 @@ async function generateKiro(config, hubDir) {
2016
2253
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
2017
2254
  console.log(chalk3.green(" Generated .gitignore"));
2018
2255
  const kiroRule = buildKiroOrchestratorRule(config);
2019
- const kiroOrchestrator = buildKiroSteeringContent(kiroRule, "always", { name: "orchestrator" });
2020
- await writeFile3(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
2021
- console.log(chalk3.green(" Generated .kiro/steering/orchestrator.md"));
2022
- await writeFile3(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
2256
+ const skillsSection = await buildSkillsSection(hubDir, config);
2257
+ const kiroRuleWithSkills = skillsSection ? kiroRule + "\n" + skillsSection : kiroRule;
2258
+ await writeFile3(join3(hubDir, "AGENTS.md"), kiroRuleWithSkills + "\n", "utf-8");
2023
2259
  console.log(chalk3.green(" Generated AGENTS.md"));
2024
2260
  const hubSteeringDir = resolve2(hubDir, "steering");
2025
2261
  try {
@@ -196,6 +196,7 @@ declare const mcp: {
196
196
  context7(overrides?: MCPOverrides): MCPConfig;
197
197
  agentTeamsLead(overrides?: MCPOverrides): MCPConfig;
198
198
  agentTeamsChat(overrides?: MCPOverrides): MCPConfig;
199
+ kanban(overrides?: MCPOverrides): MCPConfig;
199
200
  proxy(name: string, overrides: MCPOverrides & {
200
201
  upstreams: string[];
201
202
  }): MCPConfig;
@@ -210,6 +210,14 @@ var mcp = {
210
210
  env: { ...overrides?.env }
211
211
  };
212
212
  },
213
+ kanban(overrides) {
214
+ return {
215
+ name: "kanban",
216
+ package: "@arvoretech/kanban-mcp",
217
+ ...overrides,
218
+ env: { ...overrides?.env }
219
+ };
220
+ },
213
221
  proxy(name, overrides) {
214
222
  return {
215
223
  name,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateCommand,
3
3
  generators
4
- } from "./chunk-H6BAEWCZ.js";
4
+ } from "./chunk-ALJAZQ33.js";
5
5
  import "./chunk-VMN4KGAK.js";
6
6
  export {
7
7
  generateCommand,
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  checkAndAutoRegenerate,
4
4
  generateCommand
5
- } from "./chunk-H6BAEWCZ.js";
5
+ } from "./chunk-ALJAZQ33.js";
6
6
  import {
7
7
  loadHubConfig,
8
8
  resolveConfigPath
9
9
  } from "./chunk-VMN4KGAK.js";
10
10
 
11
11
  // src/index.ts
12
- import { Command as Command20 } from "commander";
12
+ import { Command as Command21 } from "commander";
13
13
 
14
14
  // src/commands/init.ts
15
15
  import { Command as Command2 } from "commander";
@@ -763,7 +763,8 @@ var AVAILABLE_MCPS = [
763
763
  { name: "tempmail", description: "Temporary email for testing" },
764
764
  { name: "agent-teams-lead", description: "Spawn AI teammate teams that work in parallel on tasks" },
765
765
  { name: "agent-teams-chat", description: "Cross-developer agent communication via Slack threads" },
766
- { name: "mcp-proxy", description: "Proxy gateway that reduces token usage via mcp_search/mcp_call" }
766
+ { name: "mcp-proxy", description: "Proxy gateway that reduces token usage via mcp_search/mcp_call" },
767
+ { name: "kanban", description: "Kanban board for managing agent tasks across sessions" }
767
768
  ];
768
769
 
769
770
  // src/tui/App.tsx
@@ -1279,6 +1280,7 @@ var MCP_HELPER_MAP = {
1279
1280
  context7: { helper: "mcp.context7", hasNameArg: false },
1280
1281
  "agent-teams-lead": { helper: "mcp.agentTeamsLead", hasNameArg: false },
1281
1282
  "agent-teams-chat": { helper: "mcp.agentTeamsChat", hasNameArg: false },
1283
+ kanban: { helper: "mcp.kanban", hasNameArg: false },
1282
1284
  "mcp-proxy": { helper: "mcp.proxy", hasNameArg: true }
1283
1285
  };
1284
1286
  function buildTypeScriptConfig(state) {
@@ -2671,10 +2673,10 @@ async function listLocalHooks(hooksDir) {
2671
2673
  const entries = await readdir3(hooksDir);
2672
2674
  for (const entry of entries) {
2673
2675
  const entryPath = join10(hooksDir, entry);
2674
- const stat = statSync3(entryPath);
2675
- if (stat.isFile() && entry.endsWith(".sh")) {
2676
+ const stat2 = statSync3(entryPath);
2677
+ if (stat2.isFile() && entry.endsWith(".sh")) {
2676
2678
  hooks.push({ name: entry.replace(/\.sh$/, ""), description: "" });
2677
- } else if (stat.isDirectory()) {
2679
+ } else if (stat2.isDirectory()) {
2678
2680
  const readmePath = join10(entryPath, "README.md");
2679
2681
  let description = "";
2680
2682
  if (existsSync7(readmePath)) {
@@ -2711,8 +2713,8 @@ async function addFromLocalPath3(localPath, hubDir, opts) {
2711
2713
  console.log(chalk8.red(` Path not found: ${absPath}`));
2712
2714
  return;
2713
2715
  }
2714
- const stat = statSync3(absPath);
2715
- const sourceHooksDir = stat.isDirectory() ? existsSync7(join10(absPath, "hooks")) ? join10(absPath, "hooks") : absPath : absPath;
2716
+ const stat2 = statSync3(absPath);
2717
+ const sourceHooksDir = stat2.isDirectory() ? existsSync7(join10(absPath, "hooks")) ? join10(absPath, "hooks") : absPath : absPath;
2716
2718
  await installHooksFromDir(sourceHooksDir, hubDir, opts);
2717
2719
  }
2718
2720
  async function installHooksFromDir(sourceDir, hubDir, opts) {
@@ -2739,8 +2741,8 @@ async function installHooksFromDir(sourceDir, hubDir, opts) {
2739
2741
  await mkdir5(targetBase, { recursive: true });
2740
2742
  for (const entry of toInstall) {
2741
2743
  const src = join10(sourceDir, entry);
2742
- const stat = statSync3(src);
2743
- if (stat.isDirectory()) {
2744
+ const stat2 = statSync3(src);
2745
+ if (stat2.isDirectory()) {
2744
2746
  await cp2(src, join10(targetBase, entry), { recursive: true });
2745
2747
  } else {
2746
2748
  await copyFile2(src, join10(targetBase, entry));
@@ -2906,8 +2908,8 @@ async function addFromLocalPath4(localPath, hubDir, opts) {
2906
2908
  console.log(chalk9.red(` Path not found: ${absPath}`));
2907
2909
  return;
2908
2910
  }
2909
- const stat = statSync4(absPath);
2910
- if (stat.isFile() && absPath.endsWith(".md")) {
2911
+ const stat2 = statSync4(absPath);
2912
+ if (stat2.isFile() && absPath.endsWith(".md")) {
2911
2913
  const targetDir = join11(hubDir, "commands");
2912
2914
  await mkdir6(targetDir, { recursive: true });
2913
2915
  const fileName = absPath.split("/").pop();
@@ -2918,7 +2920,7 @@ async function addFromLocalPath4(localPath, hubDir, opts) {
2918
2920
  `));
2919
2921
  return;
2920
2922
  }
2921
- const sourceDir = stat.isDirectory() ? existsSync8(join11(absPath, "commands")) ? join11(absPath, "commands") : absPath : absPath;
2923
+ const sourceDir = stat2.isDirectory() ? existsSync8(join11(absPath, "commands")) ? join11(absPath, "commands") : absPath : absPath;
2922
2924
  await installCommandsFromDir(sourceDir, hubDir, opts);
2923
2925
  }
2924
2926
  async function installCommandsFromDir(sourceDir, hubDir, opts) {
@@ -4400,14 +4402,553 @@ var cloneCommand = new Command19("clone").description("Clone all repositories wi
4400
4402
  console.log();
4401
4403
  });
4402
4404
 
4405
+ // src/commands/consolidate.ts
4406
+ import { Command as Command20 } from "commander";
4407
+ import { existsSync as existsSync16 } from "fs";
4408
+ import { mkdir as mkdir10, readdir as readdir7, readFile as readFile9, writeFile as writeFile12, stat } from "fs/promises";
4409
+ import { join as join20 } from "path";
4410
+ import { homedir } from "os";
4411
+ import { spawn } from "child_process";
4412
+ import { execSync as execSync15 } from "child_process";
4413
+ import chalk19 from "chalk";
4414
+ var STATE_FILE = ".hub/consolidation-state.json";
4415
+ var BATCH_DIR = ".hub/consolidation";
4416
+ async function readState(hubDir) {
4417
+ const filePath = join20(hubDir, STATE_FILE);
4418
+ if (!existsSync16(filePath)) return { indexed: {} };
4419
+ try {
4420
+ return JSON.parse(await readFile9(filePath, "utf-8"));
4421
+ } catch {
4422
+ return { indexed: {} };
4423
+ }
4424
+ }
4425
+ async function writeState(hubDir, state) {
4426
+ const dir = join20(hubDir, ".hub");
4427
+ await mkdir10(dir, { recursive: true });
4428
+ await writeFile12(
4429
+ join20(hubDir, STATE_FILE),
4430
+ JSON.stringify(state, null, 2) + "\n",
4431
+ "utf-8"
4432
+ );
4433
+ }
4434
+ function detectEditorCli() {
4435
+ const candidates = ["kiro-cli", "claude", "opencode"];
4436
+ for (const cli of candidates) {
4437
+ try {
4438
+ execSync15(`which ${cli}`, { stdio: "pipe" });
4439
+ return cli;
4440
+ } catch {
4441
+ continue;
4442
+ }
4443
+ }
4444
+ return null;
4445
+ }
4446
+ function getKiroSessionsDir() {
4447
+ const base = join20(
4448
+ homedir(),
4449
+ "Library",
4450
+ "Application Support",
4451
+ "Kiro",
4452
+ "User",
4453
+ "globalStorage",
4454
+ "kiro.kiroagent",
4455
+ "workspace-sessions"
4456
+ );
4457
+ if (!existsSync16(base)) return null;
4458
+ return base;
4459
+ }
4460
+ function getClaudeProjectsDir() {
4461
+ const base = join20(homedir(), ".claude", "projects");
4462
+ if (!existsSync16(base)) return null;
4463
+ return base;
4464
+ }
4465
+ function getOpenCodeStorageDir() {
4466
+ const base = join20(homedir(), ".local", "share", "opencode", "storage");
4467
+ if (!existsSync16(base)) return null;
4468
+ return base;
4469
+ }
4470
+ async function parseKiroSession(filePath) {
4471
+ try {
4472
+ const raw = JSON.parse(await readFile9(filePath, "utf-8"));
4473
+ const messages = [];
4474
+ if (!raw.history || !Array.isArray(raw.history)) return messages;
4475
+ for (const entry of raw.history) {
4476
+ const msg = entry.message;
4477
+ if (!msg || !msg.role) continue;
4478
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
4479
+ let content = "";
4480
+ if (typeof msg.content === "string") {
4481
+ content = msg.content;
4482
+ } else if (Array.isArray(msg.content)) {
4483
+ content = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
4484
+ }
4485
+ if (!content || content.length < 5) continue;
4486
+ const isToolOutput = content.startsWith("[") || content.startsWith("{") || content.includes("```\n") && content.length > 2e3;
4487
+ if (msg.role === "assistant" && isToolOutput && content.length > 3e3)
4488
+ continue;
4489
+ messages.push({
4490
+ role: msg.role,
4491
+ content: content.length > 800 ? content.substring(0, 800) + "..." : content
4492
+ });
4493
+ }
4494
+ return messages;
4495
+ } catch {
4496
+ return [];
4497
+ }
4498
+ }
4499
+ async function parseClaudeSession(filePath) {
4500
+ try {
4501
+ const raw = await readFile9(filePath, "utf-8");
4502
+ const lines = raw.split("\n").filter((l) => l.trim());
4503
+ const messages = [];
4504
+ for (const line of lines) {
4505
+ try {
4506
+ const entry = JSON.parse(line);
4507
+ if (!entry.message || !entry.message.role) continue;
4508
+ if (entry.type === "file-history-snapshot") continue;
4509
+ const role = entry.message.role;
4510
+ if (role !== "user" && role !== "assistant") continue;
4511
+ let content = "";
4512
+ if (typeof entry.message.content === "string") {
4513
+ content = entry.message.content;
4514
+ } else if (Array.isArray(entry.message.content)) {
4515
+ content = entry.message.content.filter(
4516
+ (c) => c.type === "text" || c.type === "thinking"
4517
+ ).map((c) => c.text || "").filter(Boolean).join("\n");
4518
+ }
4519
+ if (!content || content.length < 5) continue;
4520
+ if (role === "assistant" && content.length > 3e3) continue;
4521
+ messages.push({
4522
+ role,
4523
+ content: content.length > 800 ? content.substring(0, 800) + "..." : content
4524
+ });
4525
+ } catch {
4526
+ continue;
4527
+ }
4528
+ }
4529
+ return messages;
4530
+ } catch {
4531
+ return [];
4532
+ }
4533
+ }
4534
+ async function parseOpenCodeSession(storageDir, sessionId) {
4535
+ try {
4536
+ const messagesDir = join20(storageDir, "message", sessionId);
4537
+ if (!existsSync16(messagesDir)) return [];
4538
+ const msgFiles = await readdir7(messagesDir);
4539
+ const messages = [];
4540
+ for (const msgFile of msgFiles.sort()) {
4541
+ const msgData = JSON.parse(
4542
+ await readFile9(join20(messagesDir, msgFile), "utf-8")
4543
+ );
4544
+ if (msgData.role !== "user" && msgData.role !== "assistant") continue;
4545
+ const partsDir = join20(storageDir, "part", msgData.id);
4546
+ if (!existsSync16(partsDir)) continue;
4547
+ const partFiles = await readdir7(partsDir);
4548
+ let content = "";
4549
+ for (const partFile of partFiles.sort()) {
4550
+ const part = JSON.parse(
4551
+ await readFile9(join20(partsDir, partFile), "utf-8")
4552
+ );
4553
+ if (part.type === "text" && part.text) {
4554
+ content += part.text;
4555
+ }
4556
+ }
4557
+ if (!content || content.length < 5) continue;
4558
+ if (msgData.role === "assistant" && content.length > 3e3) continue;
4559
+ messages.push({
4560
+ role: msgData.role,
4561
+ content: content.length > 800 ? content.substring(0, 800) + "..." : content
4562
+ });
4563
+ }
4564
+ return messages;
4565
+ } catch {
4566
+ return [];
4567
+ }
4568
+ }
4569
+ async function collectKiroSessions(hubDir, indexed, limit, since) {
4570
+ const sessionsBase = getKiroSessionsDir();
4571
+ if (!sessionsBase) return [];
4572
+ const workspaceDirs = await readdir7(sessionsBase);
4573
+ const candidates = [];
4574
+ for (const wsDir of workspaceDirs) {
4575
+ const wsPath = join20(sessionsBase, wsDir);
4576
+ const wsStat = await stat(wsPath);
4577
+ if (!wsStat.isDirectory()) continue;
4578
+ const files = await readdir7(wsPath);
4579
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
4580
+ for (const file of jsonFiles) {
4581
+ const id = file.replace(".json", "");
4582
+ if (indexed.has(`kiro:${id}`)) continue;
4583
+ const fileStat = await stat(join20(wsPath, file));
4584
+ if (since && fileStat.mtime < new Date(since)) continue;
4585
+ candidates.push({ file, wsPath, mtime: fileStat.mtime });
4586
+ }
4587
+ }
4588
+ candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
4589
+ const sessions = [];
4590
+ for (const { file, wsPath, mtime } of candidates) {
4591
+ if (sessions.length >= limit) break;
4592
+ const messages = await parseKiroSession(join20(wsPath, file));
4593
+ if (messages.length < 2) continue;
4594
+ sessions.push({
4595
+ id: file.replace(".json", ""),
4596
+ editor: "kiro",
4597
+ date: mtime.toISOString().split("T")[0],
4598
+ messages
4599
+ });
4600
+ }
4601
+ return sessions;
4602
+ }
4603
+ async function collectClaudeSessions(hubDir, indexed, limit, since) {
4604
+ const projectsDir = getClaudeProjectsDir();
4605
+ if (!projectsDir) return [];
4606
+ const projects = await readdir7(projectsDir);
4607
+ const candidates = [];
4608
+ for (const project of projects) {
4609
+ const projectPath = join20(projectsDir, project);
4610
+ const projectStat = await stat(projectPath);
4611
+ if (!projectStat.isDirectory()) continue;
4612
+ const files = await readdir7(projectPath);
4613
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
4614
+ for (const file of jsonlFiles) {
4615
+ const id = file.replace(".jsonl", "");
4616
+ if (indexed.has(`claude:${id}`)) continue;
4617
+ const fileStat = await stat(join20(projectPath, file));
4618
+ if (since && fileStat.mtime < new Date(since)) continue;
4619
+ candidates.push({ file, projectPath, mtime: fileStat.mtime });
4620
+ }
4621
+ }
4622
+ candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
4623
+ const sessions = [];
4624
+ for (const { file, projectPath, mtime } of candidates) {
4625
+ if (sessions.length >= limit) break;
4626
+ const messages = await parseClaudeSession(join20(projectPath, file));
4627
+ if (messages.length < 2) continue;
4628
+ sessions.push({
4629
+ id: file.replace(".jsonl", ""),
4630
+ editor: "claude",
4631
+ date: mtime.toISOString().split("T")[0],
4632
+ messages
4633
+ });
4634
+ }
4635
+ return sessions;
4636
+ }
4637
+ async function collectOpenCodeSessions(hubDir, indexed, limit, since) {
4638
+ const storageDir = getOpenCodeStorageDir();
4639
+ if (!storageDir) return [];
4640
+ const sessionDirs = join20(storageDir, "session");
4641
+ if (!existsSync16(sessionDirs)) return [];
4642
+ const projects = await readdir7(sessionDirs);
4643
+ const candidates = [];
4644
+ for (const project of projects) {
4645
+ const projectPath = join20(sessionDirs, project);
4646
+ const projectStat = await stat(projectPath);
4647
+ if (!projectStat.isDirectory()) continue;
4648
+ const files = await readdir7(projectPath);
4649
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
4650
+ for (const file of jsonFiles) {
4651
+ const sessionId = file.replace(".json", "");
4652
+ if (indexed.has(`opencode:${sessionId}`)) continue;
4653
+ const fileStat = await stat(join20(projectPath, file));
4654
+ if (since && fileStat.mtime < new Date(since)) continue;
4655
+ candidates.push({ sessionId, mtime: fileStat.mtime });
4656
+ }
4657
+ }
4658
+ candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
4659
+ const sessions = [];
4660
+ for (const { sessionId, mtime } of candidates) {
4661
+ if (sessions.length >= limit) break;
4662
+ const messages = await parseOpenCodeSession(storageDir, sessionId);
4663
+ if (messages.length < 2) continue;
4664
+ sessions.push({
4665
+ id: sessionId,
4666
+ editor: "opencode",
4667
+ date: mtime.toISOString().split("T")[0],
4668
+ messages
4669
+ });
4670
+ }
4671
+ return sessions;
4672
+ }
4673
+ function buildBatchContent(sessions) {
4674
+ const parts = [];
4675
+ for (let i = 0; i < sessions.length; i++) {
4676
+ const session = sessions[i];
4677
+ parts.push(`## Session ${i + 1} (${session.date}, ${session.editor})
4678
+ `);
4679
+ for (const msg of session.messages) {
4680
+ const label = msg.role === "user" ? "User" : "Assistant";
4681
+ parts.push(`${label}: ${msg.content}
4682
+ `);
4683
+ }
4684
+ parts.push("");
4685
+ }
4686
+ return parts.join("\n");
4687
+ }
4688
+ function buildConsolidationPrompt(batchPath, memoriesPath) {
4689
+ return [
4690
+ "You are a knowledge extractor. Your task is to read transcripts of chat sessions between developers and AI agents, and extract information that would be useful for future sessions.",
4691
+ "",
4692
+ `Read the file ${batchPath}`,
4693
+ "",
4694
+ "For each useful piece of information you find, create a file in the appropriate category folder:",
4695
+ "",
4696
+ `- ${memoriesPath}/decisions/ \u2014 technical choices (e.g. "use Drizzle instead of TypeORM")`,
4697
+ `- ${memoriesPath}/conventions/ \u2014 patterns defined (e.g. "API errors return { code, message }")`,
4698
+ `- ${memoriesPath}/gotchas/ \u2014 problems to avoid (e.g. "Sentry v8 leak with NestJS")`,
4699
+ `- ${memoriesPath}/domain/ \u2014 business knowledge (e.g. "enrollment can stay pending for 30 days")`,
4700
+ "",
4701
+ "File format:",
4702
+ "---",
4703
+ "title: <short title>",
4704
+ "category: <category>",
4705
+ "date: <today's date>",
4706
+ "status: active",
4707
+ "tags: [tag1, tag2]",
4708
+ "source:",
4709
+ " type: consolidation",
4710
+ "---",
4711
+ "",
4712
+ "## Context",
4713
+ "<2-3 sentences of context>",
4714
+ "",
4715
+ "## Details",
4716
+ "<specific details>",
4717
+ "",
4718
+ "Rules:",
4719
+ "- Ignore specific implementation, generated code, compilation errors, tool call outputs",
4720
+ "- Focus on DECISIONS, PATTERNS, DISCOVERIES, and BUSINESS KNOWLEDGE",
4721
+ "- If a session has nothing useful, skip it",
4722
+ `- Before creating a file, read existing files in ${memoriesPath}/ to avoid duplicates`,
4723
+ "- If a similar memory already exists, do NOT create another one",
4724
+ "- Use kebab-case for filenames with today's date prefix (e.g. 2026-04-03-use-drizzle.md)",
4725
+ "- Write memory content in the same language the developers used in the chat"
4726
+ ].join("\n");
4727
+ }
4728
+ function spawnEditorCli(cli, prompt, cwd) {
4729
+ return new Promise((resolve6) => {
4730
+ let args;
4731
+ switch (cli) {
4732
+ case "kiro-cli":
4733
+ args = ["chat", "--no-interactive", "--trust-all-tools", prompt];
4734
+ break;
4735
+ case "claude":
4736
+ args = [
4737
+ "-p",
4738
+ "--dangerously-skip-permissions",
4739
+ "--allowedTools",
4740
+ "Read,Write,Edit,Glob,Grep",
4741
+ prompt
4742
+ ];
4743
+ break;
4744
+ case "opencode":
4745
+ args = ["--non-interactive", prompt];
4746
+ break;
4747
+ }
4748
+ const proc = spawn(cli, args, {
4749
+ cwd,
4750
+ stdio: ["pipe", "pipe", "pipe"],
4751
+ env: { ...process.env }
4752
+ });
4753
+ let stdout = "";
4754
+ let stderr = "";
4755
+ proc.stdout?.on("data", (data) => {
4756
+ stdout += data.toString();
4757
+ });
4758
+ proc.stderr?.on("data", (data) => {
4759
+ stderr += data.toString();
4760
+ });
4761
+ proc.on("exit", (code) => {
4762
+ resolve6({ code: code ?? 1, stdout, stderr });
4763
+ });
4764
+ proc.on("error", (err) => {
4765
+ resolve6({ code: 1, stdout, stderr: err.message });
4766
+ });
4767
+ });
4768
+ }
4769
+ var consolidateCommand = new Command20("consolidate").description(
4770
+ "Extract knowledge from chat sessions across editors into team memories"
4771
+ ).option("-n, --last <count>", "Number of recent sessions to process", "20").option("-s, --since <date>", "Only process sessions after this date (YYYY-MM-DD)").option(
4772
+ "-e, --editor <editor>",
4773
+ "Editor to collect from (kiro, claude, opencode, all)",
4774
+ "all"
4775
+ ).option(
4776
+ "--cli <cli>",
4777
+ "Editor CLI to use for extraction (kiro-cli, claude, opencode)"
4778
+ ).option("--dry-run", "Show batch content without running extraction").option("--reset", "Reset consolidation state and reprocess all sessions").action(
4779
+ async (opts) => {
4780
+ const hubDir = process.cwd();
4781
+ const limit = parseInt(opts.last, 10);
4782
+ console.log(chalk19.blue("\nConsolidating chat sessions into memories\n"));
4783
+ let state = await readState(hubDir);
4784
+ if (opts.reset) {
4785
+ state = { indexed: {} };
4786
+ await writeState(hubDir, state);
4787
+ console.log(chalk19.yellow(" Reset consolidation state\n"));
4788
+ }
4789
+ const indexed = /* @__PURE__ */ new Set();
4790
+ for (const [editor, data] of Object.entries(state.indexed)) {
4791
+ for (const sessionId of data.sessions) {
4792
+ indexed.add(`${editor}:${sessionId}`);
4793
+ }
4794
+ }
4795
+ console.log(chalk19.dim(` Already indexed: ${indexed.size} sessions`));
4796
+ const allSessions = [];
4797
+ if (opts.editor === "all" || opts.editor === "kiro") {
4798
+ const kiroSessions = await collectKiroSessions(
4799
+ hubDir,
4800
+ indexed,
4801
+ limit,
4802
+ opts.since
4803
+ );
4804
+ allSessions.push(...kiroSessions);
4805
+ if (kiroSessions.length > 0) {
4806
+ console.log(
4807
+ chalk19.green(` Found ${kiroSessions.length} new Kiro sessions`)
4808
+ );
4809
+ }
4810
+ }
4811
+ if (opts.editor === "all" || opts.editor === "claude") {
4812
+ const claudeSessions = await collectClaudeSessions(
4813
+ hubDir,
4814
+ indexed,
4815
+ limit,
4816
+ opts.since
4817
+ );
4818
+ allSessions.push(...claudeSessions);
4819
+ if (claudeSessions.length > 0) {
4820
+ console.log(
4821
+ chalk19.green(
4822
+ ` Found ${claudeSessions.length} new Claude Code sessions`
4823
+ )
4824
+ );
4825
+ }
4826
+ }
4827
+ if (opts.editor === "all" || opts.editor === "opencode") {
4828
+ const openCodeSessions = await collectOpenCodeSessions(
4829
+ hubDir,
4830
+ indexed,
4831
+ limit,
4832
+ opts.since
4833
+ );
4834
+ allSessions.push(...openCodeSessions);
4835
+ if (openCodeSessions.length > 0) {
4836
+ console.log(
4837
+ chalk19.green(
4838
+ ` Found ${openCodeSessions.length} new OpenCode sessions`
4839
+ )
4840
+ );
4841
+ }
4842
+ }
4843
+ if (allSessions.length === 0) {
4844
+ console.log(chalk19.yellow("\n No new sessions to process.\n"));
4845
+ return;
4846
+ }
4847
+ allSessions.sort(
4848
+ (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
4849
+ );
4850
+ const toProcess = allSessions.slice(0, limit);
4851
+ console.log(
4852
+ chalk19.blue(`
4853
+ Processing ${toProcess.length} sessions...
4854
+ `)
4855
+ );
4856
+ const batchContent = buildBatchContent(toProcess);
4857
+ const batchDir = join20(hubDir, BATCH_DIR);
4858
+ await mkdir10(batchDir, { recursive: true });
4859
+ const batchFile = `batch-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`;
4860
+ const batchPath = join20(batchDir, batchFile);
4861
+ await writeFile12(batchPath, batchContent, "utf-8");
4862
+ console.log(chalk19.dim(` Batch written to ${BATCH_DIR}/${batchFile}`));
4863
+ if (opts.dryRun) {
4864
+ console.log(chalk19.yellow("\n Dry run \u2014 batch content:\n"));
4865
+ const preview = batchContent.length > 3e3 ? batchContent.substring(0, 3e3) + "\n\n... (truncated)" : batchContent;
4866
+ console.log(chalk19.dim(preview));
4867
+ console.log(
4868
+ chalk19.yellow(
4869
+ `
4870
+ Would process ${toProcess.length} sessions. Run without --dry-run to extract.
4871
+ `
4872
+ )
4873
+ );
4874
+ return;
4875
+ }
4876
+ let memoriesPath = "./memories";
4877
+ try {
4878
+ const config = await loadHubConfig(hubDir);
4879
+ if (config.memory?.path) memoriesPath = config.memory.path;
4880
+ } catch {
4881
+ }
4882
+ for (const cat of ["decisions", "conventions", "gotchas", "domain"]) {
4883
+ await mkdir10(join20(hubDir, memoriesPath, cat), { recursive: true });
4884
+ }
4885
+ let cli = opts.cli;
4886
+ if (!cli) {
4887
+ const detected = detectEditorCli();
4888
+ if (!detected) {
4889
+ console.log(
4890
+ chalk19.red(
4891
+ "\n No editor CLI found (kiro-cli, claude, opencode)."
4892
+ )
4893
+ );
4894
+ console.log(
4895
+ chalk19.dim(
4896
+ " Install one or specify with --cli <kiro-cli|claude|opencode>\n"
4897
+ )
4898
+ );
4899
+ return;
4900
+ }
4901
+ cli = detected;
4902
+ }
4903
+ console.log(chalk19.blue(` Using ${cli} for extraction...
4904
+ `));
4905
+ const relativeBatchPath = `.hub/consolidation/${batchFile}`;
4906
+ const prompt = buildConsolidationPrompt(relativeBatchPath, memoriesPath);
4907
+ const result = await spawnEditorCli(cli, prompt, hubDir);
4908
+ if (result.code !== 0) {
4909
+ console.log(chalk19.red(`
4910
+ Extraction failed (exit code ${result.code})`));
4911
+ if (result.stderr) {
4912
+ console.log(chalk19.dim(` stderr: ${result.stderr.substring(0, 500)}`));
4913
+ }
4914
+ return;
4915
+ }
4916
+ console.log(chalk19.green("\n Extraction complete!"));
4917
+ const processedIds = toProcess.map((s) => s.id);
4918
+ for (const session of toProcess) {
4919
+ const editorKey = session.editor;
4920
+ if (!state.indexed[editorKey]) {
4921
+ state.indexed[editorKey] = { sessions: [] };
4922
+ }
4923
+ state.indexed[editorKey].sessions.push(session.id);
4924
+ state.indexed[editorKey].last_session_date = session.date;
4925
+ }
4926
+ state.last_run = (/* @__PURE__ */ new Date()).toISOString();
4927
+ await writeState(hubDir, state);
4928
+ console.log(
4929
+ chalk19.dim(` Marked ${processedIds.length} sessions as indexed`)
4930
+ );
4931
+ let newMemories = 0;
4932
+ for (const cat of ["decisions", "conventions", "gotchas", "domain"]) {
4933
+ const catDir = join20(hubDir, memoriesPath, cat);
4934
+ if (existsSync16(catDir)) {
4935
+ const files = await readdir7(catDir);
4936
+ newMemories += files.filter((f) => f.endsWith(".md")).length;
4937
+ }
4938
+ }
4939
+ console.log(chalk19.green(` Total memories: ${newMemories}`));
4940
+ console.log(chalk19.green("\nDone!\n"));
4941
+ }
4942
+ );
4943
+
4403
4944
  // src/index.ts
4404
4945
  import { readFileSync as readFileSync2 } from "fs";
4405
4946
  import { fileURLToPath as fileURLToPath2 } from "url";
4406
- import { dirname as dirname2, join as join20 } from "path";
4947
+ import { dirname as dirname2, join as join21 } from "path";
4407
4948
  var __filename = fileURLToPath2(import.meta.url);
4408
4949
  var __dirname = dirname2(__filename);
4409
- var pkg = JSON.parse(readFileSync2(join20(__dirname, "..", "package.json"), "utf-8"));
4410
- var program = new Command20();
4950
+ var pkg = JSON.parse(readFileSync2(join21(__dirname, "..", "package.json"), "utf-8"));
4951
+ var program = new Command21();
4411
4952
  program.name("hub").description(
4412
4953
  "Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
4413
4954
  ).version(pkg.version).enablePositionalOptions();
@@ -4433,4 +4974,5 @@ program.addCommand(updateCommand);
4433
4974
  program.addCommand(directoryCommand);
4434
4975
  program.addCommand(scanCommand);
4435
4976
  program.addCommand(cloneCommand);
4977
+ program.addCommand(consolidateCommand);
4436
4978
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.13.3",
3
+ "version": "0.16.0",
4
4
  "description": "CLI for managing AI-aware multi-repository workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",