@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-
|
|
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 (
|
|
913
|
-
for (const [key, value] of Object.entries(
|
|
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, ...
|
|
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
|
-
...
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
|
2020
|
-
|
|
2021
|
-
|
|
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 {
|
package/dist/config/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/config/index.js
CHANGED
|
@@ -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,
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
checkAndAutoRegenerate,
|
|
4
4
|
generateCommand
|
|
5
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
2675
|
-
if (
|
|
2676
|
+
const stat2 = statSync3(entryPath);
|
|
2677
|
+
if (stat2.isFile() && entry.endsWith(".sh")) {
|
|
2676
2678
|
hooks.push({ name: entry.replace(/\.sh$/, ""), description: "" });
|
|
2677
|
-
} else if (
|
|
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
|
|
2715
|
-
const sourceHooksDir =
|
|
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
|
|
2743
|
-
if (
|
|
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
|
|
2910
|
-
if (
|
|
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 =
|
|
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
|
|
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(
|
|
4410
|
-
var program = new
|
|
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();
|