@arvoretech/hub 0.13.4 → 0.17.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.
package/dist/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkAndAutoRegenerate,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
colors,
|
|
5
|
+
generateCommand,
|
|
6
|
+
horizontalLine,
|
|
7
|
+
personaCommand,
|
|
8
|
+
symbols
|
|
9
|
+
} from "./chunk-Z4AZNX6V.js";
|
|
6
10
|
import {
|
|
7
11
|
loadHubConfig,
|
|
8
12
|
resolveConfigPath
|
|
9
13
|
} from "./chunk-VMN4KGAK.js";
|
|
10
14
|
|
|
11
15
|
// src/index.ts
|
|
12
|
-
import { Command as
|
|
16
|
+
import { Command as Command22 } from "commander";
|
|
13
17
|
|
|
14
18
|
// src/commands/init.ts
|
|
15
19
|
import { Command as Command2 } from "commander";
|
|
@@ -23,38 +27,6 @@ import { Box as Box10, useStdout as useStdout4 } from "ink";
|
|
|
23
27
|
|
|
24
28
|
// src/tui/components/WelcomeStep.tsx
|
|
25
29
|
import { Box, Text, useInput } from "ink";
|
|
26
|
-
|
|
27
|
-
// src/tui/theme.ts
|
|
28
|
-
var colors = {
|
|
29
|
-
brand: "#22c55e",
|
|
30
|
-
brandDim: "#16a34a",
|
|
31
|
-
blue: "#3b82f6",
|
|
32
|
-
purple: "#a78bfa",
|
|
33
|
-
muted: "#6b7280",
|
|
34
|
-
dim: "#4b5563",
|
|
35
|
-
warning: "#eab308",
|
|
36
|
-
error: "#ef4444",
|
|
37
|
-
white: "#ffffff"
|
|
38
|
-
};
|
|
39
|
-
var symbols = {
|
|
40
|
-
check: "\u2713",
|
|
41
|
-
cross: "\u2717",
|
|
42
|
-
arrow: "\u276F",
|
|
43
|
-
dot: "\u25CF",
|
|
44
|
-
circle: "\u25CB",
|
|
45
|
-
tree: "\u{1F333}",
|
|
46
|
-
line: "\u2500",
|
|
47
|
-
corner: "\u256D",
|
|
48
|
-
cornerEnd: "\u2570",
|
|
49
|
-
vertical: "\u2502",
|
|
50
|
-
cornerRight: "\u256E",
|
|
51
|
-
cornerEndRight: "\u256F"
|
|
52
|
-
};
|
|
53
|
-
function horizontalLine(width) {
|
|
54
|
-
return symbols.line.repeat(width);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// src/tui/components/WelcomeStep.tsx
|
|
58
30
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
59
31
|
var LOGO = [
|
|
60
32
|
" \u{1F33F} ",
|
|
@@ -763,7 +735,8 @@ var AVAILABLE_MCPS = [
|
|
|
763
735
|
{ name: "tempmail", description: "Temporary email for testing" },
|
|
764
736
|
{ name: "agent-teams-lead", description: "Spawn AI teammate teams that work in parallel on tasks" },
|
|
765
737
|
{ 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" }
|
|
738
|
+
{ name: "mcp-proxy", description: "Proxy gateway that reduces token usage via mcp_search/mcp_call" },
|
|
739
|
+
{ name: "kanban", description: "Kanban board for managing agent tasks across sessions" }
|
|
767
740
|
];
|
|
768
741
|
|
|
769
742
|
// src/tui/App.tsx
|
|
@@ -1279,6 +1252,7 @@ var MCP_HELPER_MAP = {
|
|
|
1279
1252
|
context7: { helper: "mcp.context7", hasNameArg: false },
|
|
1280
1253
|
"agent-teams-lead": { helper: "mcp.agentTeamsLead", hasNameArg: false },
|
|
1281
1254
|
"agent-teams-chat": { helper: "mcp.agentTeamsChat", hasNameArg: false },
|
|
1255
|
+
kanban: { helper: "mcp.kanban", hasNameArg: false },
|
|
1282
1256
|
"mcp-proxy": { helper: "mcp.proxy", hasNameArg: true }
|
|
1283
1257
|
};
|
|
1284
1258
|
function buildTypeScriptConfig(state) {
|
|
@@ -1563,12 +1537,38 @@ import chalk3 from "chalk";
|
|
|
1563
1537
|
|
|
1564
1538
|
// src/core/docker-compose.ts
|
|
1565
1539
|
import { stringify as stringify3 } from "yaml";
|
|
1566
|
-
|
|
1540
|
+
import { resolve } from "path";
|
|
1541
|
+
var SANDBOX_IMAGE = "ghcr.io/agent-infra/sandbox:latest";
|
|
1542
|
+
var SANDBOX_PORT = 8080;
|
|
1543
|
+
function buildSandboxEntry(svc, hubDir) {
|
|
1544
|
+
const port = svc.port ?? SANDBOX_PORT;
|
|
1545
|
+
const workspacePath = resolve(hubDir, svc.workspace ?? ".");
|
|
1546
|
+
return {
|
|
1547
|
+
image: SANDBOX_IMAGE,
|
|
1548
|
+
container_name: svc.name,
|
|
1549
|
+
restart: "unless-stopped",
|
|
1550
|
+
security_opt: ["seccomp:unconfined"],
|
|
1551
|
+
shm_size: "2gb",
|
|
1552
|
+
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
1553
|
+
ports: [`${port}:8080`],
|
|
1554
|
+
volumes: [`${workspacePath}:/workspace`],
|
|
1555
|
+
environment: {
|
|
1556
|
+
WORKSPACE: "/workspace",
|
|
1557
|
+
...svc.env ?? {}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function generateDockerCompose(services, hubDir = process.cwd()) {
|
|
1567
1562
|
const compose = {
|
|
1568
1563
|
services: {}
|
|
1569
1564
|
};
|
|
1570
1565
|
const svcMap = compose.services;
|
|
1566
|
+
const volumes = {};
|
|
1571
1567
|
for (const svc of services) {
|
|
1568
|
+
if (svc.type === "sandbox") {
|
|
1569
|
+
svcMap[svc.name] = buildSandboxEntry(svc, hubDir);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
1572
|
const entry = {
|
|
1573
1573
|
image: svc.image,
|
|
1574
1574
|
restart: "unless-stopped"
|
|
@@ -1581,14 +1581,14 @@ function generateDockerCompose(services) {
|
|
|
1581
1581
|
if (svc.env) {
|
|
1582
1582
|
entry.environment = svc.env;
|
|
1583
1583
|
}
|
|
1584
|
-
|
|
1584
|
+
const dataDir = guessDataDir(svc.image ?? svc.name);
|
|
1585
|
+
entry.volumes = [`${svc.name}_data:/var/lib/${dataDir}`];
|
|
1586
|
+
volumes[`${svc.name}_data`] = null;
|
|
1585
1587
|
svcMap[svc.name] = entry;
|
|
1586
1588
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
volumes[`${svc.name}_data`] = null;
|
|
1589
|
+
if (Object.keys(volumes).length > 0) {
|
|
1590
|
+
compose.volumes = volumes;
|
|
1590
1591
|
}
|
|
1591
|
-
compose.volumes = volumes;
|
|
1592
1592
|
return stringify3(compose, { lineWidth: 120 });
|
|
1593
1593
|
}
|
|
1594
1594
|
function guessDataDir(image) {
|
|
@@ -1982,7 +1982,7 @@ async function ensureCompose(hubDir) {
|
|
|
1982
1982
|
console.log(chalk5.yellow("No services defined in hub.yaml"));
|
|
1983
1983
|
process.exit(1);
|
|
1984
1984
|
}
|
|
1985
|
-
const content = generateDockerCompose(config.services);
|
|
1985
|
+
const content = generateDockerCompose(config.services, hubDir);
|
|
1986
1986
|
await writeFile6(composePath, content, "utf-8");
|
|
1987
1987
|
console.log(chalk5.green("Generated docker-compose.yml"));
|
|
1988
1988
|
}
|
|
@@ -2061,7 +2061,7 @@ var servicesCommand = new Command6("services").description("Manage Docker develo
|
|
|
2061
2061
|
import { Command as Command7 } from "commander";
|
|
2062
2062
|
import { existsSync as existsSync5, statSync } from "fs";
|
|
2063
2063
|
import { mkdir as mkdir3, readdir, readFile as readFile3, rm, cp } from "fs/promises";
|
|
2064
|
-
import { join as join8, resolve } from "path";
|
|
2064
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
2065
2065
|
import { execSync as execSync4 } from "child_process";
|
|
2066
2066
|
import chalk6 from "chalk";
|
|
2067
2067
|
var DEFAULT_REGISTRY_REPO2 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
|
|
@@ -2252,7 +2252,7 @@ async function listRemoteSkills(owner, repo) {
|
|
|
2252
2252
|
}
|
|
2253
2253
|
}
|
|
2254
2254
|
async function addFromLocalPath(localPath, hubDir, opts) {
|
|
2255
|
-
const absPath =
|
|
2255
|
+
const absPath = resolve2(localPath);
|
|
2256
2256
|
if (!existsSync5(absPath)) {
|
|
2257
2257
|
console.log(chalk6.red(` Path not found: ${absPath}`));
|
|
2258
2258
|
return;
|
|
@@ -2401,7 +2401,7 @@ Removed skill: ${name}
|
|
|
2401
2401
|
import { Command as Command8 } from "commander";
|
|
2402
2402
|
import { existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
2403
2403
|
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile4, rm as rm2, copyFile, writeFile as writeFile7 } from "fs/promises";
|
|
2404
|
-
import { join as join9, resolve as
|
|
2404
|
+
import { join as join9, resolve as resolve3 } from "path";
|
|
2405
2405
|
import { execSync as execSync5 } from "child_process";
|
|
2406
2406
|
import chalk7 from "chalk";
|
|
2407
2407
|
var DEFAULT_REGISTRY_REPO3 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
|
|
@@ -2449,7 +2449,7 @@ async function addFromRegistry2(agentName, hubDir, opts) {
|
|
|
2449
2449
|
}
|
|
2450
2450
|
}
|
|
2451
2451
|
async function addFromLocalPath2(localPath, hubDir, opts) {
|
|
2452
|
-
const absPath =
|
|
2452
|
+
const absPath = resolve3(localPath);
|
|
2453
2453
|
if (!existsSync6(absPath)) {
|
|
2454
2454
|
console.log(chalk7.red(` Path not found: ${absPath}`));
|
|
2455
2455
|
return;
|
|
@@ -2658,7 +2658,7 @@ Removed agent: ${name}
|
|
|
2658
2658
|
import { Command as Command9 } from "commander";
|
|
2659
2659
|
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
2660
2660
|
import { mkdir as mkdir5, readdir as readdir3, readFile as readFile5, rm as rm3, copyFile as copyFile2, cp as cp2 } from "fs/promises";
|
|
2661
|
-
import { join as join10, resolve as
|
|
2661
|
+
import { join as join10, resolve as resolve4 } from "path";
|
|
2662
2662
|
import { execSync as execSync6 } from "child_process";
|
|
2663
2663
|
import chalk8 from "chalk";
|
|
2664
2664
|
var DEFAULT_REGISTRY_REPO4 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
|
|
@@ -2671,10 +2671,10 @@ async function listLocalHooks(hooksDir) {
|
|
|
2671
2671
|
const entries = await readdir3(hooksDir);
|
|
2672
2672
|
for (const entry of entries) {
|
|
2673
2673
|
const entryPath = join10(hooksDir, entry);
|
|
2674
|
-
const
|
|
2675
|
-
if (
|
|
2674
|
+
const stat2 = statSync3(entryPath);
|
|
2675
|
+
if (stat2.isFile() && entry.endsWith(".sh")) {
|
|
2676
2676
|
hooks.push({ name: entry.replace(/\.sh$/, ""), description: "" });
|
|
2677
|
-
} else if (
|
|
2677
|
+
} else if (stat2.isDirectory()) {
|
|
2678
2678
|
const readmePath = join10(entryPath, "README.md");
|
|
2679
2679
|
let description = "";
|
|
2680
2680
|
if (existsSync7(readmePath)) {
|
|
@@ -2706,13 +2706,13 @@ async function addFromRegistry3(hookName, hubDir, opts) {
|
|
|
2706
2706
|
}
|
|
2707
2707
|
}
|
|
2708
2708
|
async function addFromLocalPath3(localPath, hubDir, opts) {
|
|
2709
|
-
const absPath =
|
|
2709
|
+
const absPath = resolve4(localPath);
|
|
2710
2710
|
if (!existsSync7(absPath)) {
|
|
2711
2711
|
console.log(chalk8.red(` Path not found: ${absPath}`));
|
|
2712
2712
|
return;
|
|
2713
2713
|
}
|
|
2714
|
-
const
|
|
2715
|
-
const sourceHooksDir =
|
|
2714
|
+
const stat2 = statSync3(absPath);
|
|
2715
|
+
const sourceHooksDir = stat2.isDirectory() ? existsSync7(join10(absPath, "hooks")) ? join10(absPath, "hooks") : absPath : absPath;
|
|
2716
2716
|
await installHooksFromDir(sourceHooksDir, hubDir, opts);
|
|
2717
2717
|
}
|
|
2718
2718
|
async function installHooksFromDir(sourceDir, hubDir, opts) {
|
|
@@ -2739,8 +2739,8 @@ async function installHooksFromDir(sourceDir, hubDir, opts) {
|
|
|
2739
2739
|
await mkdir5(targetBase, { recursive: true });
|
|
2740
2740
|
for (const entry of toInstall) {
|
|
2741
2741
|
const src = join10(sourceDir, entry);
|
|
2742
|
-
const
|
|
2743
|
-
if (
|
|
2742
|
+
const stat2 = statSync3(src);
|
|
2743
|
+
if (stat2.isDirectory()) {
|
|
2744
2744
|
await cp2(src, join10(targetBase, entry), { recursive: true });
|
|
2745
2745
|
} else {
|
|
2746
2746
|
await copyFile2(src, join10(targetBase, entry));
|
|
@@ -2852,7 +2852,7 @@ Hook '${name}' not found in hooks/
|
|
|
2852
2852
|
import { Command as Command10 } from "commander";
|
|
2853
2853
|
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
2854
2854
|
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile6, rm as rm4, copyFile as copyFile3, writeFile as writeFile8 } from "fs/promises";
|
|
2855
|
-
import { join as join11, resolve as
|
|
2855
|
+
import { join as join11, resolve as resolve5 } from "path";
|
|
2856
2856
|
import { execSync as execSync7 } from "child_process";
|
|
2857
2857
|
import chalk9 from "chalk";
|
|
2858
2858
|
var DEFAULT_REGISTRY_REPO5 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
|
|
@@ -2901,13 +2901,13 @@ async function addFromRegistry4(commandName, hubDir, opts) {
|
|
|
2901
2901
|
}
|
|
2902
2902
|
}
|
|
2903
2903
|
async function addFromLocalPath4(localPath, hubDir, opts) {
|
|
2904
|
-
const absPath =
|
|
2904
|
+
const absPath = resolve5(localPath);
|
|
2905
2905
|
if (!existsSync8(absPath)) {
|
|
2906
2906
|
console.log(chalk9.red(` Path not found: ${absPath}`));
|
|
2907
2907
|
return;
|
|
2908
2908
|
}
|
|
2909
|
-
const
|
|
2910
|
-
if (
|
|
2909
|
+
const stat2 = statSync4(absPath);
|
|
2910
|
+
if (stat2.isFile() && absPath.endsWith(".md")) {
|
|
2911
2911
|
const targetDir = join11(hubDir, "commands");
|
|
2912
2912
|
await mkdir6(targetDir, { recursive: true });
|
|
2913
2913
|
const fileName = absPath.split("/").pop();
|
|
@@ -2918,7 +2918,7 @@ async function addFromLocalPath4(localPath, hubDir, opts) {
|
|
|
2918
2918
|
`));
|
|
2919
2919
|
return;
|
|
2920
2920
|
}
|
|
2921
|
-
const sourceDir =
|
|
2921
|
+
const sourceDir = stat2.isDirectory() ? existsSync8(join11(absPath, "commands")) ? join11(absPath, "commands") : absPath : absPath;
|
|
2922
2922
|
await installCommandsFromDir(sourceDir, hubDir, opts);
|
|
2923
2923
|
}
|
|
2924
2924
|
async function installCommandsFromDir(sourceDir, hubDir, opts) {
|
|
@@ -3636,7 +3636,7 @@ function extractVersion(s) {
|
|
|
3636
3636
|
import { Command as Command15 } from "commander";
|
|
3637
3637
|
import { existsSync as existsSync13 } from "fs";
|
|
3638
3638
|
import { mkdir as mkdir8, readdir as readdir5, readFile as readFile7, writeFile as writeFile10, rm as rm5, appendFile as appendFile2 } from "fs/promises";
|
|
3639
|
-
import { join as join16, resolve as
|
|
3639
|
+
import { join as join16, resolve as resolve6, basename as basename2 } from "path";
|
|
3640
3640
|
import chalk14 from "chalk";
|
|
3641
3641
|
async function ensureLanceDbIgnored(memoriesDir, hubDir) {
|
|
3642
3642
|
const relative = memoriesDir.replace(hubDir + "/", "");
|
|
@@ -3663,7 +3663,7 @@ var VALID_CATEGORIES = [
|
|
|
3663
3663
|
"gotchas"
|
|
3664
3664
|
];
|
|
3665
3665
|
function getMemoriesPath(hubDir, configPath) {
|
|
3666
|
-
return
|
|
3666
|
+
return resolve6(hubDir, configPath || "memories");
|
|
3667
3667
|
}
|
|
3668
3668
|
function parseFrontmatter(raw) {
|
|
3669
3669
|
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
@@ -4400,14 +4400,683 @@ var cloneCommand = new Command19("clone").description("Clone all repositories wi
|
|
|
4400
4400
|
console.log();
|
|
4401
4401
|
});
|
|
4402
4402
|
|
|
4403
|
+
// src/commands/consolidate.ts
|
|
4404
|
+
import { Command as Command20 } from "commander";
|
|
4405
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4406
|
+
import { mkdir as mkdir10, readdir as readdir7, readFile as readFile9, writeFile as writeFile12, stat } from "fs/promises";
|
|
4407
|
+
import { join as join20 } from "path";
|
|
4408
|
+
import { homedir } from "os";
|
|
4409
|
+
import { spawn } from "child_process";
|
|
4410
|
+
import { execSync as execSync15 } from "child_process";
|
|
4411
|
+
import chalk19 from "chalk";
|
|
4412
|
+
var STATE_FILE = ".hub/consolidation-state.json";
|
|
4413
|
+
var BATCH_DIR = ".hub/consolidation";
|
|
4414
|
+
async function readState(hubDir) {
|
|
4415
|
+
const filePath = join20(hubDir, STATE_FILE);
|
|
4416
|
+
if (!existsSync16(filePath)) return { indexed: {} };
|
|
4417
|
+
try {
|
|
4418
|
+
return JSON.parse(await readFile9(filePath, "utf-8"));
|
|
4419
|
+
} catch {
|
|
4420
|
+
return { indexed: {} };
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
async function writeState(hubDir, state) {
|
|
4424
|
+
const dir = join20(hubDir, ".hub");
|
|
4425
|
+
await mkdir10(dir, { recursive: true });
|
|
4426
|
+
await writeFile12(
|
|
4427
|
+
join20(hubDir, STATE_FILE),
|
|
4428
|
+
JSON.stringify(state, null, 2) + "\n",
|
|
4429
|
+
"utf-8"
|
|
4430
|
+
);
|
|
4431
|
+
}
|
|
4432
|
+
function detectEditorCli() {
|
|
4433
|
+
const candidates = ["kiro-cli", "claude", "opencode"];
|
|
4434
|
+
for (const cli of candidates) {
|
|
4435
|
+
try {
|
|
4436
|
+
execSync15(`which ${cli}`, { stdio: "pipe" });
|
|
4437
|
+
return cli;
|
|
4438
|
+
} catch {
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
return null;
|
|
4443
|
+
}
|
|
4444
|
+
function getKiroSessionsDir() {
|
|
4445
|
+
const base = join20(
|
|
4446
|
+
homedir(),
|
|
4447
|
+
"Library",
|
|
4448
|
+
"Application Support",
|
|
4449
|
+
"Kiro",
|
|
4450
|
+
"User",
|
|
4451
|
+
"globalStorage",
|
|
4452
|
+
"kiro.kiroagent",
|
|
4453
|
+
"workspace-sessions"
|
|
4454
|
+
);
|
|
4455
|
+
if (!existsSync16(base)) return null;
|
|
4456
|
+
return base;
|
|
4457
|
+
}
|
|
4458
|
+
function getClaudeProjectsDir() {
|
|
4459
|
+
const base = join20(homedir(), ".claude", "projects");
|
|
4460
|
+
if (!existsSync16(base)) return null;
|
|
4461
|
+
return base;
|
|
4462
|
+
}
|
|
4463
|
+
function getOpenCodeStorageDir() {
|
|
4464
|
+
const base = join20(homedir(), ".local", "share", "opencode", "storage");
|
|
4465
|
+
if (!existsSync16(base)) return null;
|
|
4466
|
+
return base;
|
|
4467
|
+
}
|
|
4468
|
+
async function parseKiroSession(filePath) {
|
|
4469
|
+
try {
|
|
4470
|
+
const raw = JSON.parse(await readFile9(filePath, "utf-8"));
|
|
4471
|
+
const messages = [];
|
|
4472
|
+
if (!raw.history || !Array.isArray(raw.history)) return messages;
|
|
4473
|
+
for (const entry of raw.history) {
|
|
4474
|
+
const msg = entry.message;
|
|
4475
|
+
if (!msg || !msg.role) continue;
|
|
4476
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
4477
|
+
let content = "";
|
|
4478
|
+
if (typeof msg.content === "string") {
|
|
4479
|
+
content = msg.content;
|
|
4480
|
+
} else if (Array.isArray(msg.content)) {
|
|
4481
|
+
content = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
4482
|
+
}
|
|
4483
|
+
if (!content || content.length < 5) continue;
|
|
4484
|
+
const isToolOutput = content.startsWith("[") || content.startsWith("{") || content.includes("```\n") && content.length > 2e3;
|
|
4485
|
+
if (msg.role === "assistant" && isToolOutput && content.length > 3e3)
|
|
4486
|
+
continue;
|
|
4487
|
+
messages.push({
|
|
4488
|
+
role: msg.role,
|
|
4489
|
+
content: content.length > 800 ? content.substring(0, 800) + "..." : content
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
return messages;
|
|
4493
|
+
} catch {
|
|
4494
|
+
return [];
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
async function parseClaudeSession(filePath) {
|
|
4498
|
+
try {
|
|
4499
|
+
const raw = await readFile9(filePath, "utf-8");
|
|
4500
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
4501
|
+
const messages = [];
|
|
4502
|
+
for (const line of lines) {
|
|
4503
|
+
try {
|
|
4504
|
+
const entry = JSON.parse(line);
|
|
4505
|
+
if (!entry.message || !entry.message.role) continue;
|
|
4506
|
+
if (entry.type === "file-history-snapshot") continue;
|
|
4507
|
+
const role = entry.message.role;
|
|
4508
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
4509
|
+
let content = "";
|
|
4510
|
+
if (typeof entry.message.content === "string") {
|
|
4511
|
+
content = entry.message.content;
|
|
4512
|
+
} else if (Array.isArray(entry.message.content)) {
|
|
4513
|
+
content = entry.message.content.filter(
|
|
4514
|
+
(c) => c.type === "text" || c.type === "thinking"
|
|
4515
|
+
).map((c) => c.text || "").filter(Boolean).join("\n");
|
|
4516
|
+
}
|
|
4517
|
+
if (!content || content.length < 5) continue;
|
|
4518
|
+
if (role === "assistant" && content.length > 3e3) continue;
|
|
4519
|
+
messages.push({
|
|
4520
|
+
role,
|
|
4521
|
+
content: content.length > 800 ? content.substring(0, 800) + "..." : content
|
|
4522
|
+
});
|
|
4523
|
+
} catch {
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
return messages;
|
|
4528
|
+
} catch {
|
|
4529
|
+
return [];
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
async function parseOpenCodeSession(storageDir, sessionId) {
|
|
4533
|
+
try {
|
|
4534
|
+
const messagesDir = join20(storageDir, "message", sessionId);
|
|
4535
|
+
if (!existsSync16(messagesDir)) return [];
|
|
4536
|
+
const msgFiles = await readdir7(messagesDir);
|
|
4537
|
+
const messages = [];
|
|
4538
|
+
for (const msgFile of msgFiles.sort()) {
|
|
4539
|
+
const msgData = JSON.parse(
|
|
4540
|
+
await readFile9(join20(messagesDir, msgFile), "utf-8")
|
|
4541
|
+
);
|
|
4542
|
+
if (msgData.role !== "user" && msgData.role !== "assistant") continue;
|
|
4543
|
+
const partsDir = join20(storageDir, "part", msgData.id);
|
|
4544
|
+
if (!existsSync16(partsDir)) continue;
|
|
4545
|
+
const partFiles = await readdir7(partsDir);
|
|
4546
|
+
let content = "";
|
|
4547
|
+
for (const partFile of partFiles.sort()) {
|
|
4548
|
+
const part = JSON.parse(
|
|
4549
|
+
await readFile9(join20(partsDir, partFile), "utf-8")
|
|
4550
|
+
);
|
|
4551
|
+
if (part.type === "text" && part.text) {
|
|
4552
|
+
content += part.text;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
if (!content || content.length < 5) continue;
|
|
4556
|
+
if (msgData.role === "assistant" && content.length > 3e3) continue;
|
|
4557
|
+
messages.push({
|
|
4558
|
+
role: msgData.role,
|
|
4559
|
+
content: content.length > 800 ? content.substring(0, 800) + "..." : content
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
return messages;
|
|
4563
|
+
} catch {
|
|
4564
|
+
return [];
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
async function collectKiroSessions(hubDir, indexed, limit, since) {
|
|
4568
|
+
const sessionsBase = getKiroSessionsDir();
|
|
4569
|
+
if (!sessionsBase) return [];
|
|
4570
|
+
const workspaceDirs = await readdir7(sessionsBase);
|
|
4571
|
+
const candidates = [];
|
|
4572
|
+
for (const wsDir of workspaceDirs) {
|
|
4573
|
+
const wsPath = join20(sessionsBase, wsDir);
|
|
4574
|
+
const wsStat = await stat(wsPath);
|
|
4575
|
+
if (!wsStat.isDirectory()) continue;
|
|
4576
|
+
const files = await readdir7(wsPath);
|
|
4577
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
4578
|
+
for (const file of jsonFiles) {
|
|
4579
|
+
const id = file.replace(".json", "");
|
|
4580
|
+
if (indexed.has(`kiro:${id}`)) continue;
|
|
4581
|
+
const fileStat = await stat(join20(wsPath, file));
|
|
4582
|
+
if (since && fileStat.mtime < new Date(since)) continue;
|
|
4583
|
+
candidates.push({ file, wsPath, mtime: fileStat.mtime });
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
4587
|
+
const sessions = [];
|
|
4588
|
+
for (const { file, wsPath, mtime } of candidates) {
|
|
4589
|
+
if (sessions.length >= limit) break;
|
|
4590
|
+
const messages = await parseKiroSession(join20(wsPath, file));
|
|
4591
|
+
if (messages.length < 2) continue;
|
|
4592
|
+
sessions.push({
|
|
4593
|
+
id: file.replace(".json", ""),
|
|
4594
|
+
editor: "kiro",
|
|
4595
|
+
date: mtime.toISOString().split("T")[0],
|
|
4596
|
+
messages
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
return sessions;
|
|
4600
|
+
}
|
|
4601
|
+
async function collectClaudeSessions(hubDir, indexed, limit, since) {
|
|
4602
|
+
const projectsDir = getClaudeProjectsDir();
|
|
4603
|
+
if (!projectsDir) return [];
|
|
4604
|
+
const projects = await readdir7(projectsDir);
|
|
4605
|
+
const candidates = [];
|
|
4606
|
+
for (const project of projects) {
|
|
4607
|
+
const projectPath = join20(projectsDir, project);
|
|
4608
|
+
const projectStat = await stat(projectPath);
|
|
4609
|
+
if (!projectStat.isDirectory()) continue;
|
|
4610
|
+
const files = await readdir7(projectPath);
|
|
4611
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
4612
|
+
for (const file of jsonlFiles) {
|
|
4613
|
+
const id = file.replace(".jsonl", "");
|
|
4614
|
+
if (indexed.has(`claude:${id}`)) continue;
|
|
4615
|
+
const fileStat = await stat(join20(projectPath, file));
|
|
4616
|
+
if (since && fileStat.mtime < new Date(since)) continue;
|
|
4617
|
+
candidates.push({ file, projectPath, mtime: fileStat.mtime });
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
4621
|
+
const sessions = [];
|
|
4622
|
+
for (const { file, projectPath, mtime } of candidates) {
|
|
4623
|
+
if (sessions.length >= limit) break;
|
|
4624
|
+
const messages = await parseClaudeSession(join20(projectPath, file));
|
|
4625
|
+
if (messages.length < 2) continue;
|
|
4626
|
+
sessions.push({
|
|
4627
|
+
id: file.replace(".jsonl", ""),
|
|
4628
|
+
editor: "claude",
|
|
4629
|
+
date: mtime.toISOString().split("T")[0],
|
|
4630
|
+
messages
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
return sessions;
|
|
4634
|
+
}
|
|
4635
|
+
async function collectOpenCodeSessions(hubDir, indexed, limit, since) {
|
|
4636
|
+
const storageDir = getOpenCodeStorageDir();
|
|
4637
|
+
if (!storageDir) return [];
|
|
4638
|
+
const sessionDirs = join20(storageDir, "session");
|
|
4639
|
+
if (!existsSync16(sessionDirs)) return [];
|
|
4640
|
+
const projects = await readdir7(sessionDirs);
|
|
4641
|
+
const candidates = [];
|
|
4642
|
+
for (const project of projects) {
|
|
4643
|
+
const projectPath = join20(sessionDirs, project);
|
|
4644
|
+
const projectStat = await stat(projectPath);
|
|
4645
|
+
if (!projectStat.isDirectory()) continue;
|
|
4646
|
+
const files = await readdir7(projectPath);
|
|
4647
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
4648
|
+
for (const file of jsonFiles) {
|
|
4649
|
+
const sessionId = file.replace(".json", "");
|
|
4650
|
+
if (indexed.has(`opencode:${sessionId}`)) continue;
|
|
4651
|
+
const fileStat = await stat(join20(projectPath, file));
|
|
4652
|
+
if (since && fileStat.mtime < new Date(since)) continue;
|
|
4653
|
+
candidates.push({ sessionId, mtime: fileStat.mtime });
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
4657
|
+
const sessions = [];
|
|
4658
|
+
for (const { sessionId, mtime } of candidates) {
|
|
4659
|
+
if (sessions.length >= limit) break;
|
|
4660
|
+
const messages = await parseOpenCodeSession(storageDir, sessionId);
|
|
4661
|
+
if (messages.length < 2) continue;
|
|
4662
|
+
sessions.push({
|
|
4663
|
+
id: sessionId,
|
|
4664
|
+
editor: "opencode",
|
|
4665
|
+
date: mtime.toISOString().split("T")[0],
|
|
4666
|
+
messages
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
return sessions;
|
|
4670
|
+
}
|
|
4671
|
+
function buildBatchContent(sessions) {
|
|
4672
|
+
const parts = [];
|
|
4673
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
4674
|
+
const session = sessions[i];
|
|
4675
|
+
parts.push(`## Session ${i + 1} (${session.date}, ${session.editor})
|
|
4676
|
+
`);
|
|
4677
|
+
for (const msg of session.messages) {
|
|
4678
|
+
const label = msg.role === "user" ? "User" : "Assistant";
|
|
4679
|
+
parts.push(`${label}: ${msg.content}
|
|
4680
|
+
`);
|
|
4681
|
+
}
|
|
4682
|
+
parts.push("");
|
|
4683
|
+
}
|
|
4684
|
+
return parts.join("\n");
|
|
4685
|
+
}
|
|
4686
|
+
function buildConsolidationPrompt(batchPath, memoriesPath) {
|
|
4687
|
+
return [
|
|
4688
|
+
"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.",
|
|
4689
|
+
"",
|
|
4690
|
+
`Read the file ${batchPath}`,
|
|
4691
|
+
"",
|
|
4692
|
+
"For each useful piece of information you find, create a file in the appropriate category folder:",
|
|
4693
|
+
"",
|
|
4694
|
+
`- ${memoriesPath}/decisions/ \u2014 technical choices (e.g. "use Drizzle instead of TypeORM")`,
|
|
4695
|
+
`- ${memoriesPath}/conventions/ \u2014 patterns defined (e.g. "API errors return { code, message }")`,
|
|
4696
|
+
`- ${memoriesPath}/gotchas/ \u2014 problems to avoid (e.g. "Sentry v8 leak with NestJS")`,
|
|
4697
|
+
`- ${memoriesPath}/domain/ \u2014 business knowledge (e.g. "enrollment can stay pending for 30 days")`,
|
|
4698
|
+
"",
|
|
4699
|
+
"File format:",
|
|
4700
|
+
"---",
|
|
4701
|
+
"title: <short title>",
|
|
4702
|
+
"category: <category>",
|
|
4703
|
+
"date: <today's date>",
|
|
4704
|
+
"status: active",
|
|
4705
|
+
"tags: [tag1, tag2]",
|
|
4706
|
+
"source:",
|
|
4707
|
+
" type: consolidation",
|
|
4708
|
+
"---",
|
|
4709
|
+
"",
|
|
4710
|
+
"## Context",
|
|
4711
|
+
"<2-3 sentences of context>",
|
|
4712
|
+
"",
|
|
4713
|
+
"## Details",
|
|
4714
|
+
"<specific details>",
|
|
4715
|
+
"",
|
|
4716
|
+
"Rules:",
|
|
4717
|
+
"- Ignore specific implementation, generated code, compilation errors, tool call outputs",
|
|
4718
|
+
"- Focus on DECISIONS, PATTERNS, DISCOVERIES, and BUSINESS KNOWLEDGE",
|
|
4719
|
+
"- If a session has nothing useful, skip it",
|
|
4720
|
+
`- Before creating a file, read existing files in ${memoriesPath}/ to avoid duplicates`,
|
|
4721
|
+
"- If a similar memory already exists, do NOT create another one",
|
|
4722
|
+
"- Use kebab-case for filenames with today's date prefix (e.g. 2026-04-03-use-drizzle.md)",
|
|
4723
|
+
"- Write memory content in the same language the developers used in the chat"
|
|
4724
|
+
].join("\n");
|
|
4725
|
+
}
|
|
4726
|
+
function spawnEditorCli(cli, prompt, cwd) {
|
|
4727
|
+
return new Promise((resolve7) => {
|
|
4728
|
+
let args;
|
|
4729
|
+
switch (cli) {
|
|
4730
|
+
case "kiro-cli":
|
|
4731
|
+
args = ["chat", "--no-interactive", "--trust-all-tools", prompt];
|
|
4732
|
+
break;
|
|
4733
|
+
case "claude":
|
|
4734
|
+
args = [
|
|
4735
|
+
"-p",
|
|
4736
|
+
"--dangerously-skip-permissions",
|
|
4737
|
+
"--allowedTools",
|
|
4738
|
+
"Read,Write,Edit,Glob,Grep",
|
|
4739
|
+
prompt
|
|
4740
|
+
];
|
|
4741
|
+
break;
|
|
4742
|
+
case "opencode":
|
|
4743
|
+
args = ["--non-interactive", prompt];
|
|
4744
|
+
break;
|
|
4745
|
+
}
|
|
4746
|
+
const proc = spawn(cli, args, {
|
|
4747
|
+
cwd,
|
|
4748
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4749
|
+
env: { ...process.env }
|
|
4750
|
+
});
|
|
4751
|
+
let stdout = "";
|
|
4752
|
+
let stderr = "";
|
|
4753
|
+
proc.stdout?.on("data", (data) => {
|
|
4754
|
+
stdout += data.toString();
|
|
4755
|
+
});
|
|
4756
|
+
proc.stderr?.on("data", (data) => {
|
|
4757
|
+
stderr += data.toString();
|
|
4758
|
+
});
|
|
4759
|
+
proc.on("exit", (code) => {
|
|
4760
|
+
resolve7({ code: code ?? 1, stdout, stderr });
|
|
4761
|
+
});
|
|
4762
|
+
proc.on("error", (err) => {
|
|
4763
|
+
resolve7({ code: 1, stdout, stderr: err.message });
|
|
4764
|
+
});
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
var consolidateCommand = new Command20("consolidate").description(
|
|
4768
|
+
"Extract knowledge from chat sessions across editors into team memories"
|
|
4769
|
+
).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(
|
|
4770
|
+
"-e, --editor <editor>",
|
|
4771
|
+
"Editor to collect from (kiro, claude, opencode, all)",
|
|
4772
|
+
"all"
|
|
4773
|
+
).option(
|
|
4774
|
+
"--cli <cli>",
|
|
4775
|
+
"Editor CLI to use for extraction (kiro-cli, claude, opencode)"
|
|
4776
|
+
).option("--dry-run", "Show batch content without running extraction").option("--reset", "Reset consolidation state and reprocess all sessions").action(
|
|
4777
|
+
async (opts) => {
|
|
4778
|
+
const hubDir = process.cwd();
|
|
4779
|
+
const limit = parseInt(opts.last, 10);
|
|
4780
|
+
console.log(chalk19.blue("\nConsolidating chat sessions into memories\n"));
|
|
4781
|
+
let state = await readState(hubDir);
|
|
4782
|
+
if (opts.reset) {
|
|
4783
|
+
state = { indexed: {} };
|
|
4784
|
+
await writeState(hubDir, state);
|
|
4785
|
+
console.log(chalk19.yellow(" Reset consolidation state\n"));
|
|
4786
|
+
}
|
|
4787
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
4788
|
+
for (const [editor, data] of Object.entries(state.indexed)) {
|
|
4789
|
+
for (const sessionId of data.sessions) {
|
|
4790
|
+
indexed.add(`${editor}:${sessionId}`);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
console.log(chalk19.dim(` Already indexed: ${indexed.size} sessions`));
|
|
4794
|
+
const allSessions = [];
|
|
4795
|
+
if (opts.editor === "all" || opts.editor === "kiro") {
|
|
4796
|
+
const kiroSessions = await collectKiroSessions(
|
|
4797
|
+
hubDir,
|
|
4798
|
+
indexed,
|
|
4799
|
+
limit,
|
|
4800
|
+
opts.since
|
|
4801
|
+
);
|
|
4802
|
+
allSessions.push(...kiroSessions);
|
|
4803
|
+
if (kiroSessions.length > 0) {
|
|
4804
|
+
console.log(
|
|
4805
|
+
chalk19.green(` Found ${kiroSessions.length} new Kiro sessions`)
|
|
4806
|
+
);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
if (opts.editor === "all" || opts.editor === "claude") {
|
|
4810
|
+
const claudeSessions = await collectClaudeSessions(
|
|
4811
|
+
hubDir,
|
|
4812
|
+
indexed,
|
|
4813
|
+
limit,
|
|
4814
|
+
opts.since
|
|
4815
|
+
);
|
|
4816
|
+
allSessions.push(...claudeSessions);
|
|
4817
|
+
if (claudeSessions.length > 0) {
|
|
4818
|
+
console.log(
|
|
4819
|
+
chalk19.green(
|
|
4820
|
+
` Found ${claudeSessions.length} new Claude Code sessions`
|
|
4821
|
+
)
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
if (opts.editor === "all" || opts.editor === "opencode") {
|
|
4826
|
+
const openCodeSessions = await collectOpenCodeSessions(
|
|
4827
|
+
hubDir,
|
|
4828
|
+
indexed,
|
|
4829
|
+
limit,
|
|
4830
|
+
opts.since
|
|
4831
|
+
);
|
|
4832
|
+
allSessions.push(...openCodeSessions);
|
|
4833
|
+
if (openCodeSessions.length > 0) {
|
|
4834
|
+
console.log(
|
|
4835
|
+
chalk19.green(
|
|
4836
|
+
` Found ${openCodeSessions.length} new OpenCode sessions`
|
|
4837
|
+
)
|
|
4838
|
+
);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
if (allSessions.length === 0) {
|
|
4842
|
+
console.log(chalk19.yellow("\n No new sessions to process.\n"));
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4845
|
+
allSessions.sort(
|
|
4846
|
+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
4847
|
+
);
|
|
4848
|
+
const toProcess = allSessions.slice(0, limit);
|
|
4849
|
+
console.log(
|
|
4850
|
+
chalk19.blue(`
|
|
4851
|
+
Processing ${toProcess.length} sessions...
|
|
4852
|
+
`)
|
|
4853
|
+
);
|
|
4854
|
+
const batchContent = buildBatchContent(toProcess);
|
|
4855
|
+
const batchDir = join20(hubDir, BATCH_DIR);
|
|
4856
|
+
await mkdir10(batchDir, { recursive: true });
|
|
4857
|
+
const batchFile = `batch-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`;
|
|
4858
|
+
const batchPath = join20(batchDir, batchFile);
|
|
4859
|
+
await writeFile12(batchPath, batchContent, "utf-8");
|
|
4860
|
+
console.log(chalk19.dim(` Batch written to ${BATCH_DIR}/${batchFile}`));
|
|
4861
|
+
if (opts.dryRun) {
|
|
4862
|
+
console.log(chalk19.yellow("\n Dry run \u2014 batch content:\n"));
|
|
4863
|
+
const preview = batchContent.length > 3e3 ? batchContent.substring(0, 3e3) + "\n\n... (truncated)" : batchContent;
|
|
4864
|
+
console.log(chalk19.dim(preview));
|
|
4865
|
+
console.log(
|
|
4866
|
+
chalk19.yellow(
|
|
4867
|
+
`
|
|
4868
|
+
Would process ${toProcess.length} sessions. Run without --dry-run to extract.
|
|
4869
|
+
`
|
|
4870
|
+
)
|
|
4871
|
+
);
|
|
4872
|
+
return;
|
|
4873
|
+
}
|
|
4874
|
+
let memoriesPath = "./memories";
|
|
4875
|
+
try {
|
|
4876
|
+
const config = await loadHubConfig(hubDir);
|
|
4877
|
+
if (config.memory?.path) memoriesPath = config.memory.path;
|
|
4878
|
+
} catch {
|
|
4879
|
+
}
|
|
4880
|
+
for (const cat of ["decisions", "conventions", "gotchas", "domain"]) {
|
|
4881
|
+
await mkdir10(join20(hubDir, memoriesPath, cat), { recursive: true });
|
|
4882
|
+
}
|
|
4883
|
+
let cli = opts.cli;
|
|
4884
|
+
if (!cli) {
|
|
4885
|
+
const detected = detectEditorCli();
|
|
4886
|
+
if (!detected) {
|
|
4887
|
+
console.log(
|
|
4888
|
+
chalk19.red(
|
|
4889
|
+
"\n No editor CLI found (kiro-cli, claude, opencode)."
|
|
4890
|
+
)
|
|
4891
|
+
);
|
|
4892
|
+
console.log(
|
|
4893
|
+
chalk19.dim(
|
|
4894
|
+
" Install one or specify with --cli <kiro-cli|claude|opencode>\n"
|
|
4895
|
+
)
|
|
4896
|
+
);
|
|
4897
|
+
return;
|
|
4898
|
+
}
|
|
4899
|
+
cli = detected;
|
|
4900
|
+
}
|
|
4901
|
+
console.log(chalk19.blue(` Using ${cli} for extraction...
|
|
4902
|
+
`));
|
|
4903
|
+
const relativeBatchPath = `.hub/consolidation/${batchFile}`;
|
|
4904
|
+
const prompt = buildConsolidationPrompt(relativeBatchPath, memoriesPath);
|
|
4905
|
+
const result = await spawnEditorCli(cli, prompt, hubDir);
|
|
4906
|
+
if (result.code !== 0) {
|
|
4907
|
+
console.log(chalk19.red(`
|
|
4908
|
+
Extraction failed (exit code ${result.code})`));
|
|
4909
|
+
if (result.stderr) {
|
|
4910
|
+
console.log(chalk19.dim(` stderr: ${result.stderr.substring(0, 500)}`));
|
|
4911
|
+
}
|
|
4912
|
+
return;
|
|
4913
|
+
}
|
|
4914
|
+
console.log(chalk19.green("\n Extraction complete!"));
|
|
4915
|
+
const processedIds = toProcess.map((s) => s.id);
|
|
4916
|
+
for (const session of toProcess) {
|
|
4917
|
+
const editorKey = session.editor;
|
|
4918
|
+
if (!state.indexed[editorKey]) {
|
|
4919
|
+
state.indexed[editorKey] = { sessions: [] };
|
|
4920
|
+
}
|
|
4921
|
+
state.indexed[editorKey].sessions.push(session.id);
|
|
4922
|
+
state.indexed[editorKey].last_session_date = session.date;
|
|
4923
|
+
}
|
|
4924
|
+
state.last_run = (/* @__PURE__ */ new Date()).toISOString();
|
|
4925
|
+
await writeState(hubDir, state);
|
|
4926
|
+
console.log(
|
|
4927
|
+
chalk19.dim(` Marked ${processedIds.length} sessions as indexed`)
|
|
4928
|
+
);
|
|
4929
|
+
let newMemories = 0;
|
|
4930
|
+
for (const cat of ["decisions", "conventions", "gotchas", "domain"]) {
|
|
4931
|
+
const catDir = join20(hubDir, memoriesPath, cat);
|
|
4932
|
+
if (existsSync16(catDir)) {
|
|
4933
|
+
const files = await readdir7(catDir);
|
|
4934
|
+
newMemories += files.filter((f) => f.endsWith(".md")).length;
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
console.log(chalk19.green(` Total memories: ${newMemories}`));
|
|
4938
|
+
console.log(chalk19.green("\nDone!\n"));
|
|
4939
|
+
}
|
|
4940
|
+
);
|
|
4941
|
+
|
|
4942
|
+
// src/commands/sandbox.ts
|
|
4943
|
+
import { Command as Command21 } from "commander";
|
|
4944
|
+
import { execSync as execSync16, exec } from "child_process";
|
|
4945
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4946
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
4947
|
+
import { join as join21 } from "path";
|
|
4948
|
+
import { promisify } from "util";
|
|
4949
|
+
import chalk20 from "chalk";
|
|
4950
|
+
var execAsync = promisify(exec);
|
|
4951
|
+
function getSandboxConfig(config) {
|
|
4952
|
+
return config.services?.find((s) => s.type === "sandbox") ?? null;
|
|
4953
|
+
}
|
|
4954
|
+
async function ensureComposeFile(hubDir) {
|
|
4955
|
+
const composePath = join21(hubDir, "docker-compose.yml");
|
|
4956
|
+
if (!existsSync17(composePath)) {
|
|
4957
|
+
const config = await loadHubConfig(hubDir);
|
|
4958
|
+
const content = generateDockerCompose(config.services ?? [], hubDir);
|
|
4959
|
+
await writeFile13(composePath, content, "utf-8");
|
|
4960
|
+
}
|
|
4961
|
+
return composePath;
|
|
4962
|
+
}
|
|
4963
|
+
function isDockerRunning2() {
|
|
4964
|
+
try {
|
|
4965
|
+
execSync16("docker info", { stdio: ["pipe", "pipe", "pipe"] });
|
|
4966
|
+
return true;
|
|
4967
|
+
} catch {
|
|
4968
|
+
return false;
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
async function isSandboxRunning(name) {
|
|
4972
|
+
try {
|
|
4973
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Running}}' ${name} 2>/dev/null`);
|
|
4974
|
+
return stdout.trim() === "true";
|
|
4975
|
+
} catch {
|
|
4976
|
+
return false;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
var sandboxCommand = new Command21("sandbox").description("Manage the AIO Sandbox environment").argument("[action]", "up, down, status, open, logs", "status").action(async (action) => {
|
|
4980
|
+
if (!isDockerRunning2()) {
|
|
4981
|
+
console.log(chalk20.red("\nDocker daemon is not running."));
|
|
4982
|
+
console.log(chalk20.dim("Start Docker Desktop or the Docker daemon and try again.\n"));
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
const hubDir = process.cwd();
|
|
4986
|
+
const config = await loadHubConfig(hubDir);
|
|
4987
|
+
const svc = getSandboxConfig(config);
|
|
4988
|
+
if (!svc) {
|
|
4989
|
+
console.log(chalk20.red("\nNo sandbox service found in hub.yaml."));
|
|
4990
|
+
console.log(chalk20.dim("Add a service with type: sandbox to your hub.yaml:\n"));
|
|
4991
|
+
console.log(chalk20.dim(" services:"));
|
|
4992
|
+
console.log(chalk20.dim(" - name: sandbox"));
|
|
4993
|
+
console.log(chalk20.dim(" type: sandbox"));
|
|
4994
|
+
console.log(chalk20.dim(" port: 8080\n"));
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
const port = svc.port ?? 8080;
|
|
4998
|
+
const name = svc.name;
|
|
4999
|
+
switch (action) {
|
|
5000
|
+
case "up":
|
|
5001
|
+
case "start": {
|
|
5002
|
+
const running = await isSandboxRunning(name);
|
|
5003
|
+
if (running) {
|
|
5004
|
+
console.log(chalk20.yellow(`
|
|
5005
|
+
Sandbox is already running.`));
|
|
5006
|
+
printUrls(port);
|
|
5007
|
+
return;
|
|
5008
|
+
}
|
|
5009
|
+
console.log(chalk20.blue(`
|
|
5010
|
+
Starting sandbox...
|
|
5011
|
+
`));
|
|
5012
|
+
const composePath = await ensureComposeFile(hubDir);
|
|
5013
|
+
execSync16(`docker compose -f ${composePath} up -d ${name}`, { stdio: "inherit", cwd: hubDir });
|
|
5014
|
+
console.log(chalk20.green("\nSandbox started."));
|
|
5015
|
+
printUrls(port);
|
|
5016
|
+
break;
|
|
5017
|
+
}
|
|
5018
|
+
case "down":
|
|
5019
|
+
case "stop": {
|
|
5020
|
+
console.log(chalk20.blue(`
|
|
5021
|
+
Stopping sandbox...
|
|
5022
|
+
`));
|
|
5023
|
+
const composePath = await ensureComposeFile(hubDir);
|
|
5024
|
+
execSync16(`docker compose -f ${composePath} stop ${name}`, { stdio: "inherit", cwd: hubDir });
|
|
5025
|
+
console.log(chalk20.green("\nSandbox stopped.\n"));
|
|
5026
|
+
break;
|
|
5027
|
+
}
|
|
5028
|
+
case "logs": {
|
|
5029
|
+
const composePath = await ensureComposeFile(hubDir);
|
|
5030
|
+
execSync16(`docker compose -f ${composePath} logs -f ${name}`, { stdio: "inherit", cwd: hubDir });
|
|
5031
|
+
break;
|
|
5032
|
+
}
|
|
5033
|
+
case "open": {
|
|
5034
|
+
const running = await isSandboxRunning(name);
|
|
5035
|
+
if (!running) {
|
|
5036
|
+
console.log(chalk20.red("\nSandbox is not running. Start it first with: hub sandbox up\n"));
|
|
5037
|
+
return;
|
|
5038
|
+
}
|
|
5039
|
+
const url = `http://localhost:${port}/code-server/`;
|
|
5040
|
+
console.log(chalk20.blue(`
|
|
5041
|
+
Opening VSCode Server at ${url}
|
|
5042
|
+
`));
|
|
5043
|
+
execSync16(`open "${url}"`, { stdio: "inherit" });
|
|
5044
|
+
break;
|
|
5045
|
+
}
|
|
5046
|
+
case "status":
|
|
5047
|
+
default: {
|
|
5048
|
+
const running = await isSandboxRunning(name);
|
|
5049
|
+
if (running) {
|
|
5050
|
+
console.log(chalk20.green(`
|
|
5051
|
+
Sandbox is running.`));
|
|
5052
|
+
printUrls(port);
|
|
5053
|
+
} else {
|
|
5054
|
+
console.log(chalk20.yellow(`
|
|
5055
|
+
Sandbox is not running.`));
|
|
5056
|
+
console.log(chalk20.dim(` Start it with: hub sandbox up
|
|
5057
|
+
`));
|
|
5058
|
+
}
|
|
5059
|
+
break;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
});
|
|
5063
|
+
function printUrls(port) {
|
|
5064
|
+
console.log();
|
|
5065
|
+
console.log(chalk20.dim(` MCP: `) + chalk20.cyan(`http://localhost:${port}/mcp`));
|
|
5066
|
+
console.log(chalk20.dim(` VSCode: `) + chalk20.cyan(`http://localhost:${port}/code-server/`));
|
|
5067
|
+
console.log(chalk20.dim(` Browser: `) + chalk20.cyan(`http://localhost:${port}/vnc/index.html?autoconnect=true`));
|
|
5068
|
+
console.log(chalk20.dim(` Docs: `) + chalk20.cyan(`http://localhost:${port}/v1/docs`));
|
|
5069
|
+
console.log();
|
|
5070
|
+
}
|
|
5071
|
+
|
|
4403
5072
|
// src/index.ts
|
|
4404
5073
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4405
5074
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4406
|
-
import { dirname as dirname2, join as
|
|
5075
|
+
import { dirname as dirname2, join as join22 } from "path";
|
|
4407
5076
|
var __filename = fileURLToPath2(import.meta.url);
|
|
4408
5077
|
var __dirname = dirname2(__filename);
|
|
4409
|
-
var pkg = JSON.parse(readFileSync2(
|
|
4410
|
-
var program = new
|
|
5078
|
+
var pkg = JSON.parse(readFileSync2(join22(__dirname, "..", "package.json"), "utf-8"));
|
|
5079
|
+
var program = new Command22();
|
|
4411
5080
|
program.name("hub").description(
|
|
4412
5081
|
"Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
|
|
4413
5082
|
).version(pkg.version).enablePositionalOptions();
|
|
@@ -4433,4 +5102,7 @@ program.addCommand(updateCommand);
|
|
|
4433
5102
|
program.addCommand(directoryCommand);
|
|
4434
5103
|
program.addCommand(scanCommand);
|
|
4435
5104
|
program.addCommand(cloneCommand);
|
|
5105
|
+
program.addCommand(consolidateCommand);
|
|
5106
|
+
program.addCommand(personaCommand);
|
|
5107
|
+
program.addCommand(sandboxCommand);
|
|
4436
5108
|
program.parse();
|