@cruxhive/cli 0.3.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/bin/cruxhive.js +45 -0
- package/lib/health.js +130 -0
- package/lib/index.js +20 -0
- package/lib/init.js +179 -0
- package/lib/propose.js +75 -0
- package/lib/review.js +80 -0
- package/lib/sync.js +68 -0
- package/lib/ui.js +75 -0
- package/package.json +23 -0
package/bin/cruxhive.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { init } = require("../lib/init");
|
|
5
|
+
const { sync } = require("../lib/sync");
|
|
6
|
+
const { health } = require("../lib/health");
|
|
7
|
+
const { ui } = require("../lib/ui");
|
|
8
|
+
const { index } = require("../lib/index");
|
|
9
|
+
const { propose } = require("../lib/propose");
|
|
10
|
+
const { review } = require("../lib/review");
|
|
11
|
+
|
|
12
|
+
const [, , cmd, ...args] = process.argv;
|
|
13
|
+
|
|
14
|
+
const commands = { init, sync, health, ui, index, propose, review };
|
|
15
|
+
|
|
16
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
17
|
+
console.log(`cruxhive v${require("../package.json").version}
|
|
18
|
+
|
|
19
|
+
Usage: cruxhive <command>
|
|
20
|
+
|
|
21
|
+
Commands:
|
|
22
|
+
init Bootstrap CruxHive in the current project
|
|
23
|
+
index Index .llm/ markdown files into the local knowledge base
|
|
24
|
+
propose Propose a new knowledge entry for human review
|
|
25
|
+
review Interactively approve or reject pending proposals
|
|
26
|
+
sync Sync org-layer context from the configured remote
|
|
27
|
+
health Show knowledge base health summary
|
|
28
|
+
ui Open the approval queue dashboard (localhost:3847)
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--help Show this help message
|
|
32
|
+
`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fn = commands[cmd];
|
|
37
|
+
if (!fn) {
|
|
38
|
+
console.error(`Unknown command: ${cmd}\nRun cruxhive --help for usage.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn(args).catch((err) => {
|
|
43
|
+
console.error(`\nError: ${err.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
package/lib/health.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { existsSync, readdirSync, readFileSync, statSync } = require("fs");
|
|
4
|
+
const { join } = require("path");
|
|
5
|
+
|
|
6
|
+
function countFiles(dir) {
|
|
7
|
+
if (!existsSync(dir)) return 0;
|
|
8
|
+
try {
|
|
9
|
+
return readdirSync(dir).filter((f) => f.endsWith(".md")).length;
|
|
10
|
+
} catch {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function staleCheck(filePath, maxDays = 90) {
|
|
16
|
+
if (!existsSync(filePath)) return null;
|
|
17
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
18
|
+
const ageDays = (Date.now() - mtime) / 86400000;
|
|
19
|
+
return ageDays > maxDays ? Math.floor(ageDays) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseFrontmatter(content) {
|
|
23
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
24
|
+
if (!m) return {};
|
|
25
|
+
const obj = {};
|
|
26
|
+
for (const line of m[1].split("\n")) {
|
|
27
|
+
const [k, ...rest] = line.split(":");
|
|
28
|
+
if (k && rest.length) obj[k.trim()] = rest.join(":").trim();
|
|
29
|
+
}
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function badge(ok) {
|
|
34
|
+
return ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmt(label, value, note = "") {
|
|
38
|
+
const pad = " ".repeat(Math.max(0, 14 - label.length));
|
|
39
|
+
const n = note ? ` \x1b[90m${note}\x1b[0m` : "";
|
|
40
|
+
console.log(` ${label}${pad}${value}${n}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function health(_args) {
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const date = new Date().toISOString().split("T")[0];
|
|
46
|
+
const project = cwd.split("/").pop();
|
|
47
|
+
|
|
48
|
+
console.log(`\n\x1b[1mcruxhive health\x1b[0m — ${date} · ${project}`);
|
|
49
|
+
console.log(" " + "─".repeat(44));
|
|
50
|
+
|
|
51
|
+
const contextExists = existsSync(join(cwd, ".llm", "CONTEXT.md"));
|
|
52
|
+
const mcpConfigured = (() => {
|
|
53
|
+
const p = join(cwd, ".mcp.json");
|
|
54
|
+
if (!existsSync(p)) return false;
|
|
55
|
+
try {
|
|
56
|
+
const cfg = JSON.parse(readFileSync(p, "utf8"));
|
|
57
|
+
return !!(cfg.mcpServers?.cruxhive);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
const plansDir = join(cwd, ".llm", "plans");
|
|
64
|
+
const memoryDir = join(cwd, ".llm", "memory");
|
|
65
|
+
const contextDir = join(cwd, ".llm", "context");
|
|
66
|
+
|
|
67
|
+
const planCount = countFiles(plansDir) - (existsSync(join(plansDir, "active.md")) ? 1 : 0);
|
|
68
|
+
const memCount = countFiles(memoryDir);
|
|
69
|
+
const contextCount = countFiles(contextDir);
|
|
70
|
+
|
|
71
|
+
// Count all entries across .llm/ tree
|
|
72
|
+
let totalEntries = 0;
|
|
73
|
+
let constraintCount = 0;
|
|
74
|
+
let pendingCount = 0;
|
|
75
|
+
let stalePaths = [];
|
|
76
|
+
|
|
77
|
+
const scanDir = (dir) => {
|
|
78
|
+
if (!existsSync(dir)) return;
|
|
79
|
+
for (const f of readdirSync(dir)) {
|
|
80
|
+
if (!f.endsWith(".md")) continue;
|
|
81
|
+
const p = join(dir, f);
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(p, "utf8");
|
|
84
|
+
const fm = parseFrontmatter(content);
|
|
85
|
+
if (fm.type) {
|
|
86
|
+
totalEntries++;
|
|
87
|
+
if (fm.type === "constraint") constraintCount++;
|
|
88
|
+
if (!fm.approved_by || fm.approved_by === "~") pendingCount++;
|
|
89
|
+
}
|
|
90
|
+
const stale = staleCheck(p, 90);
|
|
91
|
+
if (stale) stalePaths.push({ path: f, days: stale });
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
scanDir(memoryDir);
|
|
97
|
+
scanDir(contextDir);
|
|
98
|
+
scanDir(plansDir);
|
|
99
|
+
|
|
100
|
+
// Output
|
|
101
|
+
fmt("CONTEXT.md", `${badge(contextExists)} ${contextExists ? "present" : "missing"}`,
|
|
102
|
+
contextExists ? "" : "run: cruxhive init");
|
|
103
|
+
fmt("MCP server", `${badge(mcpConfigured)} ${mcpConfigured ? "configured" : "not wired"}`,
|
|
104
|
+
mcpConfigured ? "" : "run: cruxhive init");
|
|
105
|
+
|
|
106
|
+
console.log(" " + "─".repeat(44));
|
|
107
|
+
|
|
108
|
+
fmt("Plans", planCount.toString());
|
|
109
|
+
fmt("Memory files", memCount.toString());
|
|
110
|
+
fmt("Context files", contextCount.toString());
|
|
111
|
+
if (totalEntries > 0) {
|
|
112
|
+
fmt("Typed entries", totalEntries.toString(), `${constraintCount} constraints`);
|
|
113
|
+
}
|
|
114
|
+
if (pendingCount > 0) {
|
|
115
|
+
fmt("Pending approval", pendingCount.toString(), "\x1b[33mapproval needed\x1b[0m");
|
|
116
|
+
}
|
|
117
|
+
if (stalePaths.length > 0) {
|
|
118
|
+
fmt("Stale (>90d)", stalePaths.length.toString(), stalePaths.map((s) => s.path).join(", "));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(" " + "─".repeat(44));
|
|
122
|
+
|
|
123
|
+
if (!contextExists || !mcpConfigured) {
|
|
124
|
+
console.log("\n Run \x1b[36mcruxhive init\x1b[0m to finish setup.\n");
|
|
125
|
+
} else {
|
|
126
|
+
console.log("\n \x1b[32m✓ Ready.\x1b[0m MCP tools: context_radar, context_next_slice, context_write_plan\n");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { health };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function ok(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
6
|
+
function err(msg) { console.log(` \x1b[31m✗\x1b[0m ${msg}`); }
|
|
7
|
+
|
|
8
|
+
async function index(_args) {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
console.log(`\n\x1b[1mcruxhive index\x1b[0m`);
|
|
11
|
+
|
|
12
|
+
const r = spawnSync("cruxhive-index", [], { cwd, stdio: "inherit" });
|
|
13
|
+
if (r.error) {
|
|
14
|
+
err("cruxhive-index not found — run: pip install cruxhive-mcp");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (r.status !== 0) process.exit(r.status);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { index };
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { mkdirSync, writeFileSync, existsSync, readFileSync } = require("fs");
|
|
5
|
+
const { join } = require("path");
|
|
6
|
+
const { createInterface } = require("readline");
|
|
7
|
+
|
|
8
|
+
const CONTEXT_TEMPLATE = (projectName, date) => `---
|
|
9
|
+
type: fact
|
|
10
|
+
scope: project
|
|
11
|
+
topic: project-context
|
|
12
|
+
valid_at: ${date}
|
|
13
|
+
confidence: high
|
|
14
|
+
source: human
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# ${projectName} — Context
|
|
18
|
+
|
|
19
|
+
> One sentence describing what this project is and does.
|
|
20
|
+
|
|
21
|
+
**Stack**: [your stack here]
|
|
22
|
+
|
|
23
|
+
## Always read first
|
|
24
|
+
|
|
25
|
+
\`.llm/plans/active.md\` — current sprint focus.
|
|
26
|
+
|
|
27
|
+
## Repository layout
|
|
28
|
+
|
|
29
|
+
| Path | Contents |
|
|
30
|
+
|---|---|
|
|
31
|
+
| \`src/\` | Source code |
|
|
32
|
+
|
|
33
|
+
## Conventions
|
|
34
|
+
|
|
35
|
+
- Add your team's key conventions here
|
|
36
|
+
- Each rule should explain WHY, not just WHAT
|
|
37
|
+
|
|
38
|
+
## Three-tier context model (CruxHive)
|
|
39
|
+
|
|
40
|
+
| Tier | Location | Contents |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **Org** | Shared remote | Cross-project facts, guardrails, architecture decisions |
|
|
43
|
+
| **Project** | \`.llm/\` (this repo) | Plans, audits, context — project-specific |
|
|
44
|
+
| **Personal** | \`~/.cruxhive/personal/\` | Developer preferences — never shared |
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const MCP_ENTRY = {
|
|
48
|
+
command: "uvx",
|
|
49
|
+
args: ["cruxhive-mcp"],
|
|
50
|
+
type: "stdio",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function ok(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
54
|
+
function info(msg) { console.log(` \x1b[36m·\x1b[0m ${msg}`); }
|
|
55
|
+
function warn(msg) { console.log(` \x1b[33m!\x1b[0m ${msg}`); }
|
|
56
|
+
function step(msg) { console.log(`\n\x1b[1m${msg}\x1b[0m`); }
|
|
57
|
+
|
|
58
|
+
function hasBin(name) {
|
|
59
|
+
return spawnSync(name, ["--version"], { stdio: "pipe" }).status === 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function installMcp() {
|
|
63
|
+
if (hasBin("uv")) {
|
|
64
|
+
const r = spawnSync("uv", ["pip", "install", "cruxhive-mcp"], { stdio: "inherit" });
|
|
65
|
+
if (r.status !== 0) throw new Error("uv pip install cruxhive-mcp failed");
|
|
66
|
+
return "uv";
|
|
67
|
+
}
|
|
68
|
+
if (hasBin("pip3") || hasBin("pip")) {
|
|
69
|
+
const pip = hasBin("pip3") ? "pip3" : "pip";
|
|
70
|
+
const r = spawnSync(pip, ["install", "cruxhive-mcp"], { stdio: "inherit" });
|
|
71
|
+
if (r.status !== 0) throw new Error(`${pip} install cruxhive-mcp failed`);
|
|
72
|
+
return pip;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Neither uv nor pip found. Install uv (https://docs.astral.sh/uv/) or pip first."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wireMcp(cwd) {
|
|
80
|
+
const mcpPath = join(cwd, ".mcp.json");
|
|
81
|
+
let cfg = existsSync(mcpPath)
|
|
82
|
+
? JSON.parse(readFileSync(mcpPath, "utf8"))
|
|
83
|
+
: { mcpServers: {} };
|
|
84
|
+
|
|
85
|
+
if (!cfg.mcpServers) cfg.mcpServers = {};
|
|
86
|
+
|
|
87
|
+
if (cfg.mcpServers.cruxhive) {
|
|
88
|
+
info(".mcp.json already has cruxhive entry");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cfg.mcpServers.cruxhive = MCP_ENTRY;
|
|
93
|
+
writeFileSync(mcpPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
94
|
+
ok("cruxhive-mcp registered in .mcp.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function wireClaudeMd(cwd) {
|
|
98
|
+
const claudePath = join(cwd, "CLAUDE.md");
|
|
99
|
+
const contextPath = ".llm/CONTEXT.md";
|
|
100
|
+
|
|
101
|
+
if (existsSync(claudePath)) {
|
|
102
|
+
const content = readFileSync(claudePath, "utf8");
|
|
103
|
+
if (content.includes("CONTEXT.md")) {
|
|
104
|
+
info("CLAUDE.md already references CONTEXT.md");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
writeFileSync(claudePath, content.trimEnd() + "\n\n<!-- CruxHive canonical context: .llm/CONTEXT.md -->\n");
|
|
108
|
+
ok("CLAUDE.md patched with CONTEXT.md reference");
|
|
109
|
+
} else {
|
|
110
|
+
const target = contextPath;
|
|
111
|
+
// Create a thin symlink — requires filesystem support
|
|
112
|
+
try {
|
|
113
|
+
require("fs").symlinkSync(target, claudePath);
|
|
114
|
+
ok("CLAUDE.md → .llm/CONTEXT.md (symlink)");
|
|
115
|
+
} catch {
|
|
116
|
+
warn("Could not create CLAUDE.md symlink — copy .llm/CONTEXT.md to CLAUDE.md manually");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function init(args) {
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
const date = new Date().toISOString().split("T")[0];
|
|
124
|
+
const projectName = cwd.split("/").pop();
|
|
125
|
+
|
|
126
|
+
console.log(`\n\x1b[1mCruxHive init\x1b[0m — ${projectName}`);
|
|
127
|
+
|
|
128
|
+
// ─── .llm/ structure ─────────────────────────────────────────────────────
|
|
129
|
+
step("1/4 Creating .llm/ structure");
|
|
130
|
+
|
|
131
|
+
const dirs = [".llm", ".llm/plans", ".llm/context", ".llm/memory"];
|
|
132
|
+
for (const dir of dirs) {
|
|
133
|
+
mkdirSync(join(cwd, dir), { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
ok("directories: .llm/ .llm/plans/ .llm/context/ .llm/memory/");
|
|
136
|
+
|
|
137
|
+
const contextPath = join(cwd, ".llm", "CONTEXT.md");
|
|
138
|
+
if (!existsSync(contextPath)) {
|
|
139
|
+
writeFileSync(contextPath, CONTEXT_TEMPLATE(projectName, date));
|
|
140
|
+
ok(".llm/CONTEXT.md created");
|
|
141
|
+
} else {
|
|
142
|
+
info(".llm/CONTEXT.md already exists — skipped");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const activePath = join(cwd, ".llm", "plans", "active.md");
|
|
146
|
+
if (!existsSync(activePath)) {
|
|
147
|
+
writeFileSync(activePath, `# Active Plans\n\n_No active plans yet._\n`);
|
|
148
|
+
ok(".llm/plans/active.md created");
|
|
149
|
+
} else {
|
|
150
|
+
info(".llm/plans/active.md already exists — skipped");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── install cruxhive-mcp ─────────────────────────────────────────────────
|
|
154
|
+
step("2/4 Installing cruxhive-mcp");
|
|
155
|
+
const installer = installMcp();
|
|
156
|
+
ok(`cruxhive-mcp installed via ${installer}`);
|
|
157
|
+
|
|
158
|
+
// ─── wire .mcp.json ───────────────────────────────────────────────────────
|
|
159
|
+
step("3/4 Wiring .mcp.json");
|
|
160
|
+
wireMcp(cwd);
|
|
161
|
+
|
|
162
|
+
// ─── wire AI tool context files ───────────────────────────────────────────
|
|
163
|
+
step("4/4 Wiring AI tools");
|
|
164
|
+
wireClaudeMd(cwd);
|
|
165
|
+
|
|
166
|
+
// ─── done ─────────────────────────────────────────────────────────────────
|
|
167
|
+
console.log(`
|
|
168
|
+
\x1b[32m✓ CruxHive initialized in ${projectName}\x1b[0m
|
|
169
|
+
|
|
170
|
+
Next steps:
|
|
171
|
+
1. Edit \x1b[36m.llm/CONTEXT.md\x1b[0m — describe your project, stack, and conventions
|
|
172
|
+
2. Reload your AI tool — the cruxhive-mcp server is now available
|
|
173
|
+
3. Run \x1b[36mcruxhive health\x1b[0m to see knowledge base status
|
|
174
|
+
|
|
175
|
+
MCP tools now available: context_radar, context_next_slice, context_write_plan, context_sync_memory
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { init };
|
package/lib/propose.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { createInterface } = require("readline");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
const { writeFileSync, unlinkSync, readFileSync } = require("fs");
|
|
7
|
+
const { tmpdir } = require("os");
|
|
8
|
+
const { join } = require("path");
|
|
9
|
+
|
|
10
|
+
const TYPES = ["fact", "decision", "plan", "pattern", "constraint", "research", "outcome"];
|
|
11
|
+
|
|
12
|
+
function prompt(rl, question) {
|
|
13
|
+
return new Promise((res) => rl.question(question, res));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function selectType(rl) {
|
|
17
|
+
console.log("\n Type:");
|
|
18
|
+
TYPES.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
19
|
+
const ans = await prompt(rl, " Choose [1-7]: ");
|
|
20
|
+
const idx = parseInt(ans, 10) - 1;
|
|
21
|
+
return TYPES[idx] || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function openEditor(initial) {
|
|
25
|
+
const tmp = join(tmpdir(), `cruxhive-propose-${Date.now()}.md`);
|
|
26
|
+
writeFileSync(tmp, initial, "utf8");
|
|
27
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "nano";
|
|
28
|
+
spawnSync(editor, [tmp], { stdio: "inherit" });
|
|
29
|
+
const content = readFileSync(tmp, "utf8").trim();
|
|
30
|
+
unlinkSync(tmp);
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function propose(_args) {
|
|
35
|
+
const cwd = process.cwd();
|
|
36
|
+
console.log(`\n\x1b[1mcruxhive propose\x1b[0m — add a knowledge entry`);
|
|
37
|
+
|
|
38
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
39
|
+
|
|
40
|
+
const type = await selectType(rl);
|
|
41
|
+
if (!type) {
|
|
42
|
+
console.log(" Invalid selection.");
|
|
43
|
+
rl.close();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const topic = (await prompt(rl, `\n Topic (1-3 words): `)).trim();
|
|
48
|
+
if (!topic) { rl.close(); return; }
|
|
49
|
+
|
|
50
|
+
const scope = (await prompt(rl, ` Scope [project]: `)).trim() || "project";
|
|
51
|
+
rl.close();
|
|
52
|
+
|
|
53
|
+
console.log(`\n Opening editor for content…`);
|
|
54
|
+
const placeholder = `<!-- Describe the ${type}: what, why, and any relevant context -->\n`;
|
|
55
|
+
const content = openEditor(placeholder);
|
|
56
|
+
|
|
57
|
+
if (!content || content === placeholder.trim()) {
|
|
58
|
+
console.log(" \x1b[33m!\x1b[0m Empty content — proposal cancelled.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const r = spawnSync(
|
|
63
|
+
"cruxhive-propose",
|
|
64
|
+
[type, topic, scope],
|
|
65
|
+
{ cwd, stdio: ["pipe", "inherit", "inherit"], input: content }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (r.error) {
|
|
69
|
+
console.log(" \x1b[31m✗\x1b[0m cruxhive-propose not found — run: pip install cruxhive-mcp");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (r.status !== 0) process.exit(r.status);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { propose };
|
package/lib/review.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { createInterface } = require("readline");
|
|
5
|
+
|
|
6
|
+
function prompt(rl, question) {
|
|
7
|
+
return new Promise((res) => rl.question(question, res));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function badge(type) {
|
|
11
|
+
const colors = {
|
|
12
|
+
fact: "\x1b[34m", constraint: "\x1b[31m", decision: "\x1b[32m",
|
|
13
|
+
pattern: "\x1b[33m", plan: "\x1b[35m", research: "\x1b[36m", outcome: "\x1b[32m",
|
|
14
|
+
};
|
|
15
|
+
const c = colors[type] || "\x1b[90m";
|
|
16
|
+
return `${c}[${type || "?"}]\x1b[0m`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function review(_args) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
console.log(`\n\x1b[1mcruxhive review\x1b[0m — pending proposals\n`);
|
|
22
|
+
|
|
23
|
+
const r = spawnSync("cruxhive-review", [], { cwd, stdio: ["inherit", "pipe", "inherit"] });
|
|
24
|
+
if (r.error) {
|
|
25
|
+
console.log(" \x1b[31m✗\x1b[0m cruxhive-review not found — run: pip install cruxhive-mcp");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let pending;
|
|
30
|
+
try {
|
|
31
|
+
pending = JSON.parse(r.stdout.toString());
|
|
32
|
+
} catch {
|
|
33
|
+
console.log(" No pending proposals or index not built. Run: cruxhive index");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (pending.error) {
|
|
38
|
+
console.log(` \x1b[31m✗\x1b[0m ${pending.error}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!pending.length) {
|
|
43
|
+
console.log(" \x1b[32m✓\x1b[0m No pending proposals — knowledge base is fully reviewed.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(` ${pending.length} pending proposal(s)\n`);
|
|
48
|
+
|
|
49
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
|
|
51
|
+
let approved = 0, rejected = 0, skipped = 0;
|
|
52
|
+
|
|
53
|
+
for (const p of pending) {
|
|
54
|
+
const preview = (p.preview || "").trim().slice(0, 120);
|
|
55
|
+
console.log(` ${badge(p.type)} \x1b[1m${p.path}\x1b[0m`);
|
|
56
|
+
console.log(` topic: ${p.topic || "—"} · proposed: ${p.valid_at || "?"}`);
|
|
57
|
+
if (preview) console.log(` \x1b[90m${preview}…\x1b[0m`);
|
|
58
|
+
|
|
59
|
+
const ans = (await prompt(rl, ` [a]pprove / [r]eject / [s]kip: `)).trim().toLowerCase();
|
|
60
|
+
|
|
61
|
+
if (ans === "a" || ans === "approve") {
|
|
62
|
+
const approver = (await prompt(rl, ` Your name: `)).trim();
|
|
63
|
+
if (!approver) { console.log(" Skipped (no name given).\n"); skipped++; continue; }
|
|
64
|
+
const ra = spawnSync("cruxhive-approve", [p.path, approver], { cwd, stdio: "inherit" });
|
|
65
|
+
if (ra.status === 0) approved++;
|
|
66
|
+
} else if (ans === "r" || ans === "reject") {
|
|
67
|
+
const rr = spawnSync("cruxhive-reject", [p.path], { cwd, stdio: "inherit" });
|
|
68
|
+
if (rr.status === 0) rejected++;
|
|
69
|
+
} else {
|
|
70
|
+
console.log(" Skipped.");
|
|
71
|
+
skipped++;
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
rl.close();
|
|
77
|
+
console.log(` Done: ${approved} approved · ${rejected} rejected · ${skipped} skipped\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { review };
|
package/lib/sync.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const { existsSync, readFileSync } = require("fs");
|
|
5
|
+
const { join } = require("path");
|
|
6
|
+
|
|
7
|
+
function ok(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
8
|
+
function warn(msg) { console.log(` \x1b[33m!\x1b[0m ${msg}`); }
|
|
9
|
+
function err(msg) { console.log(` \x1b[31m✗\x1b[0m ${msg}`); }
|
|
10
|
+
|
|
11
|
+
function findSyncScript(cwd) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
join(cwd, "..", "scripts", "sync-platform-memory.sh"),
|
|
14
|
+
join(cwd, "scripts", "sync-platform-memory.sh"),
|
|
15
|
+
];
|
|
16
|
+
return candidates.find(existsSync) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getOrgRemote(cwd) {
|
|
20
|
+
const configPath = join(cwd, "cruxhive.config.yaml");
|
|
21
|
+
if (!existsSync(configPath)) return null;
|
|
22
|
+
const content = readFileSync(configPath, "utf8");
|
|
23
|
+
const m = content.match(/org_remote\s*:\s*(.+)/);
|
|
24
|
+
return m ? m[1].trim() : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function sync(_args) {
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
console.log(`\n\x1b[1mcruxhive sync\x1b[0m`);
|
|
30
|
+
|
|
31
|
+
// Try workspace-level sync script first (mozbridge pattern)
|
|
32
|
+
const script = findSyncScript(cwd);
|
|
33
|
+
if (script) {
|
|
34
|
+
const r = spawnSync(script, [], { cwd, stdio: "inherit" });
|
|
35
|
+
if (r.status === 0) {
|
|
36
|
+
ok("Org context synced via sync-platform-memory.sh");
|
|
37
|
+
} else {
|
|
38
|
+
err("Sync script exited with non-zero status");
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try git-based org remote (Phase 4 pattern)
|
|
44
|
+
const remote = getOrgRemote(cwd);
|
|
45
|
+
if (remote) {
|
|
46
|
+
console.log(` Pulling org context from: ${remote}`);
|
|
47
|
+
const r = spawnSync("git", ["pull", "--rebase", remote, "main"], {
|
|
48
|
+
cwd: join(cwd, ".llm"),
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
});
|
|
51
|
+
if (r.status === 0) {
|
|
52
|
+
ok("Org context synced from remote");
|
|
53
|
+
} else {
|
|
54
|
+
err("git pull failed — check your org_remote in cruxhive.config.yaml");
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
warn("No sync source configured.");
|
|
60
|
+
console.log(`
|
|
61
|
+
Options:
|
|
62
|
+
1. Workspace sync script at ../scripts/sync-platform-memory.sh
|
|
63
|
+
2. Set org_remote in cruxhive.config.yaml for git-based org sync
|
|
64
|
+
3. Use Mozbridge for managed cloud sync (Phase 6)
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { sync };
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync, spawn } = require("child_process");
|
|
4
|
+
const { existsSync } = require("fs");
|
|
5
|
+
|
|
6
|
+
const PORT = 3847;
|
|
7
|
+
|
|
8
|
+
function ok(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
9
|
+
function info(msg) { console.log(` \x1b[36m·\x1b[0m ${msg}`); }
|
|
10
|
+
function err(msg) { console.log(` \x1b[31m✗\x1b[0m ${msg}`); }
|
|
11
|
+
|
|
12
|
+
function openBrowser(url) {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
15
|
+
spawnSync(cmd, [url], { stdio: "ignore" });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function checkUvicorn() {
|
|
19
|
+
const r = spawnSync("uvicorn", ["--version"], { stdio: "pipe" });
|
|
20
|
+
return r.status === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function ui(args) {
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
const serve = args.includes("--serve");
|
|
26
|
+
|
|
27
|
+
console.log(`\n\x1b[1mcruxhive ui\x1b[0m — approval dashboard`);
|
|
28
|
+
|
|
29
|
+
if (!existsSync(`${cwd}/.llm/cruxhive.db`)) {
|
|
30
|
+
console.log(`\n \x1b[33m!\x1b[0m Knowledge base not indexed yet.`);
|
|
31
|
+
console.log(` Run: \x1b[36mcruxhive index\x1b[0m (or context_index MCP tool)\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!checkUvicorn()) {
|
|
35
|
+
err("uvicorn not found — install the [ui] extra:");
|
|
36
|
+
console.log(`\n \x1b[36mpip install "cruxhive-mcp[ui]"\x1b[0m\n`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = `http://localhost:${PORT}`;
|
|
41
|
+
info(`Starting approval queue at ${url}`);
|
|
42
|
+
|
|
43
|
+
const proc = spawn(
|
|
44
|
+
"uvicorn",
|
|
45
|
+
[
|
|
46
|
+
"cruxhive_mcp.ui:app",
|
|
47
|
+
"--host", "0.0.0.0",
|
|
48
|
+
"--port", String(PORT),
|
|
49
|
+
"--factory",
|
|
50
|
+
],
|
|
51
|
+
{
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: "inherit",
|
|
54
|
+
env: { ...process.env, CRUXHIVE_ROOT: cwd },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
proc.on("error", (e) => {
|
|
59
|
+
err(`Failed to start server: ${e.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Give the server a moment to bind, then open browser
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
ok(`Server running at ${url}`);
|
|
66
|
+
openBrowser(url);
|
|
67
|
+
}, 1200);
|
|
68
|
+
|
|
69
|
+
process.on("SIGINT", () => {
|
|
70
|
+
proc.kill();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { ui };
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cruxhive/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CruxHive — team AI knowledge governance layer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cruxhive/cruxhive"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://cruxhive.com",
|
|
11
|
+
"keywords": ["ai", "mcp", "context", "knowledge", "claude", "cursor", "llm"],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"cruxhive": "bin/cruxhive.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"lib/"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {}
|
|
23
|
+
}
|