@cubedot/cli 0.1.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/LICENSE +16 -0
- package/README.md +37 -0
- package/cli-assets/CLAUDE.md +169 -0
- package/cli-assets/CubedotVerifier.md +94 -0
- package/cli-assets/fia-operating-manual.md +203 -0
- package/dist/cli.js +1171 -0
- package/package.json +38 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
7
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/commands/init.ts
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
12
|
+
import { join as join3 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/mcp.ts
|
|
15
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
17
|
+
var REQUIRED_TOOLS = [
|
|
18
|
+
"orient",
|
|
19
|
+
"get",
|
|
20
|
+
"tree",
|
|
21
|
+
"recall",
|
|
22
|
+
"fetch",
|
|
23
|
+
"get_document",
|
|
24
|
+
"list_work"
|
|
25
|
+
];
|
|
26
|
+
function buildConnectUrl(mcpUrl, projectId) {
|
|
27
|
+
const u = new URL(mcpUrl);
|
|
28
|
+
const pathname = u.pathname.replace(/\/+$/, "");
|
|
29
|
+
return `${u.origin}${pathname}?project=${encodeURIComponent(projectId)}`;
|
|
30
|
+
}
|
|
31
|
+
function categorizeError(err) {
|
|
32
|
+
if (err === null || err === void 0) return "Unknown error";
|
|
33
|
+
const e = err;
|
|
34
|
+
const status = e["response"]?.status ?? e["status"] ?? e["statusCode"];
|
|
35
|
+
const code = e["code"] ?? e["cause"]?.["code"];
|
|
36
|
+
const name = err.name;
|
|
37
|
+
if (status === 400) return "Bad request (400): invalid project ID or API key format.";
|
|
38
|
+
if (status === 401) return "Authentication failed (401): check your API key.";
|
|
39
|
+
if (status === 404)
|
|
40
|
+
return "Project not found (404): check your project ID and MCP URL.";
|
|
41
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND")
|
|
42
|
+
return `Connection refused: check the MCP URL (${err.message ?? ""}).`;
|
|
43
|
+
if (name === "TimeoutError" || name === "AbortError")
|
|
44
|
+
return "Connection timed out: check the URL and network.";
|
|
45
|
+
return `Connection failed: ${err.message ?? String(err)}`;
|
|
46
|
+
}
|
|
47
|
+
async function createMcpClient(connectUrl, key, timeoutMs = 3e4) {
|
|
48
|
+
const client = new Client({ name: "cubedot-cli", version: "0.1.0" });
|
|
49
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
50
|
+
const transport = new StreamableHTTPClientTransport(new URL(connectUrl), {
|
|
51
|
+
requestInit: {
|
|
52
|
+
headers: { "x-api-key": key },
|
|
53
|
+
signal
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
await client.connect(transport);
|
|
57
|
+
return client;
|
|
58
|
+
}
|
|
59
|
+
async function validateTools(client) {
|
|
60
|
+
const result = await client.listTools();
|
|
61
|
+
if (!Array.isArray(result.tools)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Invalid response from MCP server: tools list is not an array."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const names = result.tools.map((t) => t.name);
|
|
67
|
+
const missing = REQUIRED_TOOLS.filter((n) => !names.includes(n));
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`MCP server is missing required tools: ${missing.join(", ")}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return names;
|
|
74
|
+
}
|
|
75
|
+
async function callTool(client, name, args = {}) {
|
|
76
|
+
const result = await client.callTool({ name, arguments: args });
|
|
77
|
+
if (!result?.content || !Array.isArray(result.content)) return "";
|
|
78
|
+
return result.content.filter((c) => c.type === "text").map((c) => String(c.text ?? "")).join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/scaffold.ts
|
|
82
|
+
import {
|
|
83
|
+
copyFileSync,
|
|
84
|
+
existsSync,
|
|
85
|
+
mkdirSync,
|
|
86
|
+
readFileSync,
|
|
87
|
+
writeFileSync
|
|
88
|
+
} from "fs";
|
|
89
|
+
import { dirname, join } from "path";
|
|
90
|
+
import { fileURLToPath } from "url";
|
|
91
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
function getAssetPath(name) {
|
|
93
|
+
return fileURLToPath(new URL(`../cli-assets/${name}`, import.meta.url));
|
|
94
|
+
}
|
|
95
|
+
function writeMcpJson(cwd, mcpUrl, projectId, key) {
|
|
96
|
+
const path = join(cwd, ".mcp.json");
|
|
97
|
+
let existing = {};
|
|
98
|
+
if (existsSync(path)) {
|
|
99
|
+
try {
|
|
100
|
+
existing = JSON.parse(readFileSync(path, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const config = {
|
|
105
|
+
...existing,
|
|
106
|
+
mcpServers: {
|
|
107
|
+
...existing.mcpServers ?? {},
|
|
108
|
+
cubedot: {
|
|
109
|
+
type: "http",
|
|
110
|
+
url: `${mcpUrl}?project=${encodeURIComponent(projectId)}`,
|
|
111
|
+
headers: { "x-api-key": key }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
116
|
+
}
|
|
117
|
+
function readCubedotConfig(cwd) {
|
|
118
|
+
const path = join(cwd, ".cubedot", "config.json");
|
|
119
|
+
if (!existsSync(path)) return null;
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function writeCubedotConfig(cwd, config) {
|
|
127
|
+
const dir = join(cwd, ".cubedot");
|
|
128
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
129
|
+
writeFileSync(
|
|
130
|
+
join(dir, "config.json"),
|
|
131
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
132
|
+
"utf8"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function installAgentAssets(cwd) {
|
|
136
|
+
const agentsDir = join(cwd, ".claude", "agents");
|
|
137
|
+
if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
|
|
138
|
+
copyFileSync(
|
|
139
|
+
getAssetPath("CubedotVerifier.md"),
|
|
140
|
+
join(agentsDir, "CubedotVerifier.md")
|
|
141
|
+
);
|
|
142
|
+
const cubedotDir = join(cwd, ".cubedot");
|
|
143
|
+
if (!existsSync(cubedotDir)) mkdirSync(cubedotDir, { recursive: true });
|
|
144
|
+
copyFileSync(
|
|
145
|
+
getAssetPath("fia-operating-manual.md"),
|
|
146
|
+
join(cubedotDir, "fia-operating-manual.md")
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function scaffoldCubedotDirs(cwd) {
|
|
150
|
+
const progress = join(cwd, ".cubedot", "progress");
|
|
151
|
+
const reports = join(cwd, ".cubedot", "reports");
|
|
152
|
+
const docs = join(cwd, ".cubedot", "docs");
|
|
153
|
+
const sync = join(cwd, ".cubedot", "sync");
|
|
154
|
+
for (const dir of [progress, reports, docs, sync]) {
|
|
155
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
const fieldNotes = join(cwd, ".cubedot", "field-notes.md");
|
|
158
|
+
if (!existsSync(fieldNotes)) {
|
|
159
|
+
writeFileSync(
|
|
160
|
+
fieldNotes,
|
|
161
|
+
"# Field Notes\n\nAppend-only log of spec gaps, inferred assumptions, and approved overrides.\n",
|
|
162
|
+
"utf8"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const linksJson = join(cwd, ".cubedot", "links.json");
|
|
166
|
+
if (!existsSync(linksJson)) {
|
|
167
|
+
writeFileSync(linksJson, "{}\n", "utf8");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
var GITIGNORE_ENTRIES = [
|
|
171
|
+
".mcp.json",
|
|
172
|
+
".cubedot/config.json",
|
|
173
|
+
".cubedot/docs/",
|
|
174
|
+
".cubedot/sync/"
|
|
175
|
+
];
|
|
176
|
+
function updateGitignore(cwd) {
|
|
177
|
+
const path = join(cwd, ".gitignore");
|
|
178
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
179
|
+
const lines = content.split("\n");
|
|
180
|
+
let changed = false;
|
|
181
|
+
for (const entry of GITIGNORE_ENTRIES) {
|
|
182
|
+
if (!lines.some((l) => l.trim() === entry)) {
|
|
183
|
+
content = content.endsWith("\n") ? content + entry + "\n" : content + "\n" + entry + "\n";
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (changed) {
|
|
188
|
+
writeFileSync(path, content, "utf8");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readHashes(cwd) {
|
|
192
|
+
const path = join(cwd, ".cubedot", "sync", "hashes.json");
|
|
193
|
+
if (!existsSync(path)) return {};
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
196
|
+
} catch {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function writeHashes(cwd, hashes) {
|
|
201
|
+
const dir = join(cwd, ".cubedot", "sync");
|
|
202
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
203
|
+
writeFileSync(join(dir, "hashes.json"), JSON.stringify(hashes, null, 2) + "\n", "utf8");
|
|
204
|
+
}
|
|
205
|
+
function readLinks(cwd) {
|
|
206
|
+
const path = join(cwd, ".cubedot", "links.json");
|
|
207
|
+
if (!existsSync(path)) return {};
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
210
|
+
} catch {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/hotmemory.ts
|
|
216
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
217
|
+
import { join as join2 } from "path";
|
|
218
|
+
var START = (name) => `<!-- CUBEDOT:START:${name} -->`;
|
|
219
|
+
var END = (name) => `<!-- CUBEDOT:END:${name} -->`;
|
|
220
|
+
function hasAllMarkers(content) {
|
|
221
|
+
for (const name of ["IDENTITY", "CONVENTIONS", "REGISTRY"]) {
|
|
222
|
+
const s = content.indexOf(START(name));
|
|
223
|
+
const e = content.indexOf(END(name));
|
|
224
|
+
if (s === -1 || e === -1 || e <= s) return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
function replaceBlock(text, name, newContent) {
|
|
229
|
+
const s = START(name);
|
|
230
|
+
const e = END(name);
|
|
231
|
+
const si = text.indexOf(s);
|
|
232
|
+
const ei = text.indexOf(e);
|
|
233
|
+
if (si === -1 || ei === -1 || ei <= si) return text;
|
|
234
|
+
return text.slice(0, si + s.length) + "\n" + newContent.trimEnd() + "\n" + text.slice(ei);
|
|
235
|
+
}
|
|
236
|
+
function updateMarkers(content, identity, conventions, registry) {
|
|
237
|
+
let out = content;
|
|
238
|
+
out = replaceBlock(out, "IDENTITY", identity);
|
|
239
|
+
out = replaceBlock(out, "CONVENTIONS", conventions);
|
|
240
|
+
out = replaceBlock(out, "REGISTRY", registry);
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
function parseIdentity(orientText) {
|
|
244
|
+
const lines = orientText.split("\n").filter((l) => l.trim() !== "");
|
|
245
|
+
if (lines.length === 0) return "Project: (unknown \u2014 run `orient` to populate)";
|
|
246
|
+
const head = lines[0].replace(/^#+\s*/, "");
|
|
247
|
+
const rest = lines.slice(1, 8);
|
|
248
|
+
return [head, ...rest].join("\n");
|
|
249
|
+
}
|
|
250
|
+
function parseRegistry(treeText) {
|
|
251
|
+
if (!treeText.trim() || treeText.startsWith("No features")) {
|
|
252
|
+
return "No features with codes yet \u2014 commit M02, then `cubedot sync` to populate the registry.";
|
|
253
|
+
}
|
|
254
|
+
const lines = treeText.split("\n");
|
|
255
|
+
const kept = [];
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
if (!line.trim()) continue;
|
|
258
|
+
const indent = line.length - line.trimStart().length;
|
|
259
|
+
if (indent === 0 || indent === 2) {
|
|
260
|
+
kept.push(line);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return kept.length > 0 ? kept.join("\n") : "No features with codes yet \u2014 commit M02, then `cubedot sync` to populate the registry.";
|
|
264
|
+
}
|
|
265
|
+
function extractFnCodes(treeText) {
|
|
266
|
+
const codes = /* @__PURE__ */ new Set();
|
|
267
|
+
const fnPattern = /^\s{2}(FN-\d+-\d+)/;
|
|
268
|
+
for (const line of treeText.split("\n")) {
|
|
269
|
+
const m = fnPattern.exec(line);
|
|
270
|
+
if (m) codes.add(m[1]);
|
|
271
|
+
}
|
|
272
|
+
return codes;
|
|
273
|
+
}
|
|
274
|
+
function parseRegistryBlock(content) {
|
|
275
|
+
const map = /* @__PURE__ */ new Map();
|
|
276
|
+
const s = START("REGISTRY");
|
|
277
|
+
const e = END("REGISTRY");
|
|
278
|
+
const si = content.indexOf(s);
|
|
279
|
+
const ei = content.indexOf(e);
|
|
280
|
+
if (si === -1 || ei === -1) return map;
|
|
281
|
+
const block = content.slice(si + s.length, ei);
|
|
282
|
+
const fnLine = /^\s{2}(FN-\d+-\d+):\s*(.+)$/;
|
|
283
|
+
for (const line of block.split("\n")) {
|
|
284
|
+
const m = fnLine.exec(line);
|
|
285
|
+
if (m) map.set(m[1], m[2].trim());
|
|
286
|
+
}
|
|
287
|
+
return map;
|
|
288
|
+
}
|
|
289
|
+
function parseConventions(techStackText) {
|
|
290
|
+
if (!techStackText || !techStackText.trim()) {
|
|
291
|
+
return "Tech stack not yet defined. Run `cubedot sync` after populating tech-stack in CubeDot.";
|
|
292
|
+
}
|
|
293
|
+
const stripped = techStackText.replace(/^#\s+Tech Stack\s*\n?/, "").trim();
|
|
294
|
+
if (!stripped) return "Tech stack defined but empty \u2014 check CubeDot.";
|
|
295
|
+
return stripped;
|
|
296
|
+
}
|
|
297
|
+
function generateMemoryFiles(cwd, identity, conventions, registry) {
|
|
298
|
+
const templatePath = getAssetPath("CLAUDE.md");
|
|
299
|
+
const template = readFileSync2(templatePath, "utf8");
|
|
300
|
+
const filled = updateMarkers(template, identity, conventions, registry);
|
|
301
|
+
writeFileSync2(join2(cwd, "AGENTS.md"), filled, "utf8");
|
|
302
|
+
const claudePath = join2(cwd, "CLAUDE.md");
|
|
303
|
+
if (!existsSync2(claudePath)) {
|
|
304
|
+
writeFileSync2(claudePath, filled, "utf8");
|
|
305
|
+
} else {
|
|
306
|
+
const existing = readFileSync2(claudePath, "utf8");
|
|
307
|
+
if (hasAllMarkers(existing)) {
|
|
308
|
+
writeFileSync2(
|
|
309
|
+
claudePath,
|
|
310
|
+
updateMarkers(existing, identity, conventions, registry),
|
|
311
|
+
"utf8"
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
console.warn(
|
|
315
|
+
"[cubedot] CLAUDE.md exists but has no CUBEDOT markers \u2014 inserting generated blocks."
|
|
316
|
+
);
|
|
317
|
+
const h1Match = /^(#\s+[^\n]+\n)/m.exec(existing);
|
|
318
|
+
const insertAfter = h1Match ? h1Match.index + h1Match[0].length : 0;
|
|
319
|
+
const markerBlocks = extractMarkerBlocks(filled);
|
|
320
|
+
const updated = existing.slice(0, insertAfter) + "\n" + markerBlocks + "\n" + existing.slice(insertAfter);
|
|
321
|
+
writeFileSync2(claudePath, updated, "utf8");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function extractMarkerBlocks(filled) {
|
|
326
|
+
const blocks = [];
|
|
327
|
+
for (const name of ["IDENTITY", "CONVENTIONS", "REGISTRY"]) {
|
|
328
|
+
const s = filled.indexOf(START(name));
|
|
329
|
+
const e = filled.indexOf(END(name));
|
|
330
|
+
if (s !== -1 && e !== -1 && e > s) {
|
|
331
|
+
blocks.push(filled.slice(s, e + END(name).length));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return blocks.join("\n\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/commands/init.ts
|
|
338
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
339
|
+
import { dirname as dirname2 } from "path";
|
|
340
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
341
|
+
var __dirname2 = dirname2(__filename);
|
|
342
|
+
var pkg = JSON.parse(
|
|
343
|
+
readFileSync3(join3(__dirname2, "../package.json"), "utf8")
|
|
344
|
+
);
|
|
345
|
+
function decodeToken(token) {
|
|
346
|
+
let json;
|
|
347
|
+
try {
|
|
348
|
+
const b64 = token.replace(/-/g, "+").replace(/_/g, "/");
|
|
349
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
350
|
+
json = Buffer.from(padded, "base64").toString("utf8");
|
|
351
|
+
} catch {
|
|
352
|
+
throw new Error("Invalid connect token: cannot decode base64url.");
|
|
353
|
+
}
|
|
354
|
+
let parsed;
|
|
355
|
+
try {
|
|
356
|
+
parsed = JSON.parse(json);
|
|
357
|
+
} catch {
|
|
358
|
+
throw new Error("Invalid connect token: decoded value is not valid JSON.");
|
|
359
|
+
}
|
|
360
|
+
const t = parsed;
|
|
361
|
+
if (t["v"] !== 1) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Unsupported connect token version: ${t["v"]}. Expected v:1. The web page that generated this token must emit {v:1, projectId, key, mcpUrl}.`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (!t["projectId"] || !t["key"] || !t["mcpUrl"]) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
"Invalid connect token: missing required fields (projectId, key, mcpUrl)."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
v: 1,
|
|
373
|
+
projectId: String(t["projectId"]),
|
|
374
|
+
key: String(t["key"]),
|
|
375
|
+
mcpUrl: String(t["mcpUrl"])
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async function initCommand(opts) {
|
|
379
|
+
const cwd = process.cwd();
|
|
380
|
+
let projectId;
|
|
381
|
+
let key;
|
|
382
|
+
let mcpUrl;
|
|
383
|
+
if (opts.token) {
|
|
384
|
+
const t = decodeToken(opts.token);
|
|
385
|
+
projectId = t.projectId;
|
|
386
|
+
key = t.key;
|
|
387
|
+
mcpUrl = t.mcpUrl;
|
|
388
|
+
} else if (opts.project && opts.key && opts.url) {
|
|
389
|
+
projectId = opts.project;
|
|
390
|
+
key = opts.key;
|
|
391
|
+
mcpUrl = opts.url;
|
|
392
|
+
} else if (opts.project || opts.key || opts.url) {
|
|
393
|
+
console.error(
|
|
394
|
+
pc.red(
|
|
395
|
+
"Error: --project, --key, and --url must all be provided together."
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
} else {
|
|
400
|
+
if (!process.stdin.isTTY) {
|
|
401
|
+
console.error(
|
|
402
|
+
pc.red(
|
|
403
|
+
"Non-interactive shell: pass --token or --project/--key/--url."
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
console.log(
|
|
409
|
+
pc.yellow(
|
|
410
|
+
"\u26A0 The connect token contains your API key \u2014 treat it like a password."
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
const { default: input } = await import("@inquirer/input");
|
|
414
|
+
const raw = await input({
|
|
415
|
+
message: "Paste your connect token (from the CubeDot web UI):"
|
|
416
|
+
});
|
|
417
|
+
const t = decodeToken(raw.trim());
|
|
418
|
+
projectId = t.projectId;
|
|
419
|
+
key = t.key;
|
|
420
|
+
mcpUrl = t.mcpUrl;
|
|
421
|
+
}
|
|
422
|
+
if (opts.token || !opts.project) {
|
|
423
|
+
if (!process.stdin.isTTY && opts.token) {
|
|
424
|
+
console.warn(
|
|
425
|
+
pc.yellow(
|
|
426
|
+
"\u26A0 --token contains your API key \u2014 store it in a secret, not in shell history."
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const connectUrl = buildConnectUrl(mcpUrl, projectId);
|
|
432
|
+
console.log(pc.dim(`Connecting to ${connectUrl} \u2026`));
|
|
433
|
+
let client;
|
|
434
|
+
try {
|
|
435
|
+
client = await createMcpClient(connectUrl, key);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(pc.red(`Error: ${categorizeError(err)}`));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
await validateTools(client);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
await client.close();
|
|
444
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
let orientText = "";
|
|
448
|
+
let treeText = "";
|
|
449
|
+
let techStackText = null;
|
|
450
|
+
try {
|
|
451
|
+
orientText = await callTool(client, "orient");
|
|
452
|
+
treeText = await callTool(client, "tree");
|
|
453
|
+
try {
|
|
454
|
+
techStackText = await callTool(client, "get_document", {
|
|
455
|
+
name: "tech-stack"
|
|
456
|
+
});
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
await client.close();
|
|
461
|
+
console.error(
|
|
462
|
+
pc.red(`Error fetching project data: ${err.message}`)
|
|
463
|
+
);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
} finally {
|
|
466
|
+
await client.close();
|
|
467
|
+
}
|
|
468
|
+
writeMcpJson(cwd, mcpUrl, projectId, key);
|
|
469
|
+
writeCubedotConfig(cwd, {
|
|
470
|
+
projectId,
|
|
471
|
+
mcpUrl,
|
|
472
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
473
|
+
cliVersion: pkg.version
|
|
474
|
+
});
|
|
475
|
+
const identity = parseIdentity(orientText);
|
|
476
|
+
const conventions = parseConventions(techStackText);
|
|
477
|
+
const registry = parseRegistry(treeText);
|
|
478
|
+
generateMemoryFiles(cwd, identity, conventions, registry);
|
|
479
|
+
installAgentAssets(cwd);
|
|
480
|
+
scaffoldCubedotDirs(cwd);
|
|
481
|
+
updateGitignore(cwd);
|
|
482
|
+
const projectName = orientText.split("\n").find((l) => l.trim())?.replace(/^#+\s*/, "") ?? projectId;
|
|
483
|
+
console.log("\n" + pc.green("\u2713") + " Cubedot connected: " + pc.bold(projectName));
|
|
484
|
+
console.log(pc.dim(` Project ID : ${projectId}`));
|
|
485
|
+
console.log(pc.dim(` MCP URL : ${mcpUrl}`));
|
|
486
|
+
console.log(
|
|
487
|
+
pc.dim(` Tools : ${REQUIRED_TOOLS.join(", ")}`)
|
|
488
|
+
);
|
|
489
|
+
console.log("\nFiles written:");
|
|
490
|
+
const written = [
|
|
491
|
+
".mcp.json (gitignored \u2014 holds your key)",
|
|
492
|
+
".cubedot/config.json (gitignored)",
|
|
493
|
+
"AGENTS.md",
|
|
494
|
+
"CLAUDE.md",
|
|
495
|
+
".claude/agents/CubedotVerifier.md",
|
|
496
|
+
".cubedot/fia-operating-manual.md",
|
|
497
|
+
".cubedot/progress/ (empty, commit-tracked)",
|
|
498
|
+
".cubedot/reports/ (commit-tracked)",
|
|
499
|
+
".cubedot/field-notes.md (commit-tracked)",
|
|
500
|
+
".gitignore (updated)"
|
|
501
|
+
];
|
|
502
|
+
for (const f of written) console.log(" " + pc.dim(f));
|
|
503
|
+
console.log("\nOpen Claude Code in this directory to start building. \u2728");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/commands/sync.ts
|
|
507
|
+
import pc2 from "picocolors";
|
|
508
|
+
import { createHash } from "crypto";
|
|
509
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
510
|
+
import { join as join5 } from "path";
|
|
511
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
512
|
+
import { dirname as dirname3 } from "path";
|
|
513
|
+
|
|
514
|
+
// src/ledger.ts
|
|
515
|
+
import {
|
|
516
|
+
existsSync as existsSync4,
|
|
517
|
+
mkdirSync as mkdirSync2,
|
|
518
|
+
readFileSync as readFileSync4,
|
|
519
|
+
readdirSync,
|
|
520
|
+
renameSync,
|
|
521
|
+
writeFileSync as writeFileSync3
|
|
522
|
+
} from "fs";
|
|
523
|
+
import { join as join4 } from "path";
|
|
524
|
+
import { randomUUID } from "crypto";
|
|
525
|
+
var FN_CODE_REGEX = /^FN-\d{3}-\d{2,}$/;
|
|
526
|
+
function validateFnCode(code) {
|
|
527
|
+
if (!FN_CODE_REGEX.test(code)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Invalid FN code: "${code}". Expected format: FN-NNN-NN (e.g. FN-001-02).`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function getLedgerPath(cwd, code) {
|
|
534
|
+
return join4(cwd, ".cubedot", "progress", `${code}.json`);
|
|
535
|
+
}
|
|
536
|
+
function readLedger(cwd, code) {
|
|
537
|
+
const path = getLedgerPath(cwd, code);
|
|
538
|
+
if (!existsSync4(path)) return null;
|
|
539
|
+
try {
|
|
540
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
541
|
+
} catch {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Failed to read ledger for ${code}: file exists but contains malformed JSON.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function readAllLedgers(cwd) {
|
|
548
|
+
const dir = join4(cwd, ".cubedot", "progress");
|
|
549
|
+
if (!existsSync4(dir)) return [];
|
|
550
|
+
return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
551
|
+
try {
|
|
552
|
+
return JSON.parse(readFileSync4(join4(dir, f), "utf8"));
|
|
553
|
+
} catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}).filter((r) => r !== null);
|
|
557
|
+
}
|
|
558
|
+
function writeLedger(cwd, record) {
|
|
559
|
+
const path = getLedgerPath(cwd, record.code);
|
|
560
|
+
const dir = join4(cwd, ".cubedot", "progress");
|
|
561
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
562
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
563
|
+
writeFileSync3(tmp, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
564
|
+
renameSync(tmp, path);
|
|
565
|
+
}
|
|
566
|
+
function writeLedgerFlags(cwd, code, flags) {
|
|
567
|
+
const record = readLedger(cwd, code);
|
|
568
|
+
if (!record) return;
|
|
569
|
+
writeLedger(cwd, { ...record, flags, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
570
|
+
}
|
|
571
|
+
function startFn(cwd, code, actor = "agent") {
|
|
572
|
+
validateFnCode(code);
|
|
573
|
+
const existing = readLedger(cwd, code);
|
|
574
|
+
if (existing) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`Cannot start ${code}: already in state "${existing.state}". Use 'cubedot complete ${code}' or 'cubedot done ${code}' to advance.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
580
|
+
const record = {
|
|
581
|
+
code,
|
|
582
|
+
state: "in_progress",
|
|
583
|
+
flags: [],
|
|
584
|
+
actor,
|
|
585
|
+
history: [{ state: "in_progress", actor, at: now }],
|
|
586
|
+
overrides: [],
|
|
587
|
+
discrepancies: [],
|
|
588
|
+
reportPath: `.cubedot/reports/${code}.md`,
|
|
589
|
+
updatedAt: now
|
|
590
|
+
};
|
|
591
|
+
writeLedger(cwd, record);
|
|
592
|
+
return record;
|
|
593
|
+
}
|
|
594
|
+
function completeFn(cwd, code, actor = "agent") {
|
|
595
|
+
validateFnCode(code);
|
|
596
|
+
const existing = readLedger(cwd, code);
|
|
597
|
+
if (!existing) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Cannot complete ${code}: no ledger entry found. Run 'cubedot start ${code}' first.`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (existing.state !== "in_progress") {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Cannot complete ${code}: current state is "${existing.state}", expected "in_progress".`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
608
|
+
const record = {
|
|
609
|
+
...existing,
|
|
610
|
+
state: "code-complete",
|
|
611
|
+
actor,
|
|
612
|
+
history: [...existing.history, { state: "code-complete", actor, at: now }],
|
|
613
|
+
updatedAt: now
|
|
614
|
+
};
|
|
615
|
+
writeLedger(cwd, record);
|
|
616
|
+
return record;
|
|
617
|
+
}
|
|
618
|
+
function doneFn(cwd, code, actor = "human", force = false, discrepancies = []) {
|
|
619
|
+
validateFnCode(code);
|
|
620
|
+
const existing = readLedger(cwd, code);
|
|
621
|
+
if (!existing) {
|
|
622
|
+
throw new Error(`Cannot mark ${code} done: no ledger entry found.`);
|
|
623
|
+
}
|
|
624
|
+
if (existing.state === "done") {
|
|
625
|
+
throw new Error(`${code} is already done.`);
|
|
626
|
+
}
|
|
627
|
+
if (existing.state === "in_progress" && !force) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Cannot mark ${code} done: current state is "in_progress", not "code-complete". Use --force to override (a discrepancy will be recorded).`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
633
|
+
const forcedOverride = force && existing.state === "in_progress" ? [
|
|
634
|
+
...existing.overrides,
|
|
635
|
+
{ condition: "done from in_progress", approvedBy: actor, at: now }
|
|
636
|
+
] : existing.overrides;
|
|
637
|
+
const record = {
|
|
638
|
+
...existing,
|
|
639
|
+
state: "done",
|
|
640
|
+
actor,
|
|
641
|
+
history: [...existing.history, { state: "done", actor, at: now }],
|
|
642
|
+
overrides: forcedOverride,
|
|
643
|
+
discrepancies: [...existing.discrepancies, ...discrepancies],
|
|
644
|
+
updatedAt: now
|
|
645
|
+
};
|
|
646
|
+
writeLedger(cwd, record);
|
|
647
|
+
return record;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/commands/sync.ts
|
|
651
|
+
var __filename2 = fileURLToPath3(import.meta.url);
|
|
652
|
+
var __dirname3 = dirname3(__filename2);
|
|
653
|
+
var pkg2 = JSON.parse(
|
|
654
|
+
readFileSync5(join5(__dirname3, "../package.json"), "utf8")
|
|
655
|
+
);
|
|
656
|
+
function fileHash(filePath) {
|
|
657
|
+
if (!existsSync5(filePath)) return "";
|
|
658
|
+
try {
|
|
659
|
+
return createHash("sha256").update(readFileSync5(filePath)).digest("hex");
|
|
660
|
+
} catch {
|
|
661
|
+
return "";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function syncCommand() {
|
|
665
|
+
const cwd = process.cwd();
|
|
666
|
+
const config = readCubedotConfig(cwd);
|
|
667
|
+
if (!config) {
|
|
668
|
+
console.error(
|
|
669
|
+
pc2.red(
|
|
670
|
+
"Error: .cubedot/config.json not found. Run `cubedot init` first."
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const STALE_DAYS = 3;
|
|
676
|
+
if (config.lastSynced) {
|
|
677
|
+
const ageDays = (Date.now() - new Date(config.lastSynced).getTime()) / 864e5;
|
|
678
|
+
if (ageDays >= STALE_DAYS) {
|
|
679
|
+
console.log(
|
|
680
|
+
pc2.yellow(
|
|
681
|
+
`\u26A0 Last synced ${Math.floor(ageDays)} day(s) ago \u2014 refreshing now.`
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const connectUrl = buildConnectUrl(config.mcpUrl, config.projectId);
|
|
687
|
+
console.log(pc2.dim(`Connecting to ${connectUrl} \u2026`));
|
|
688
|
+
let client;
|
|
689
|
+
try {
|
|
690
|
+
client = await createMcpClient(connectUrl, await readKeyFromMcpJson(cwd));
|
|
691
|
+
} catch (err) {
|
|
692
|
+
console.error(pc2.red(`Error: ${categorizeError(err)}`));
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
await validateTools(client);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
await client.close();
|
|
699
|
+
console.error(pc2.red(`Error: ${err.message}`));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
let orientText = "";
|
|
703
|
+
let treeText = "";
|
|
704
|
+
let techStackText = null;
|
|
705
|
+
try {
|
|
706
|
+
orientText = await callTool(client, "orient");
|
|
707
|
+
treeText = await callTool(client, "tree");
|
|
708
|
+
try {
|
|
709
|
+
techStackText = await callTool(client, "get_document", {
|
|
710
|
+
name: "tech-stack"
|
|
711
|
+
});
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
await client.close();
|
|
716
|
+
console.error(
|
|
717
|
+
pc2.red(`Error fetching project data: ${err.message}`)
|
|
718
|
+
);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
} finally {
|
|
721
|
+
await client.close();
|
|
722
|
+
}
|
|
723
|
+
generateMemoryFiles(
|
|
724
|
+
cwd,
|
|
725
|
+
parseIdentity(orientText),
|
|
726
|
+
parseConventions(techStackText),
|
|
727
|
+
parseRegistry(treeText)
|
|
728
|
+
);
|
|
729
|
+
writeCubedotConfig(cwd, {
|
|
730
|
+
...config,
|
|
731
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
732
|
+
cliVersion: pkg2.version
|
|
733
|
+
});
|
|
734
|
+
const specFnCodes = extractFnCodes(treeText);
|
|
735
|
+
const ledgers = readAllLedgers(cwd);
|
|
736
|
+
const prevHashes = readHashes(cwd);
|
|
737
|
+
const nextHashes = { ...prevHashes };
|
|
738
|
+
const links = readLinks(cwd);
|
|
739
|
+
const orphaned = [];
|
|
740
|
+
const needsReverification = [];
|
|
741
|
+
const skipOrphanDetection = specFnCodes.size === 0 && ledgers.length > 0;
|
|
742
|
+
if (skipOrphanDetection) {
|
|
743
|
+
console.log(
|
|
744
|
+
pc2.yellow(
|
|
745
|
+
"\u26A0 Spec tree returned no functionalities \u2014 skipping orphan detection this sync."
|
|
746
|
+
)
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
for (const record of ledgers) {
|
|
750
|
+
if (record.flags.includes("orphaned")) continue;
|
|
751
|
+
if (!skipOrphanDetection && !specFnCodes.has(record.code)) {
|
|
752
|
+
const newFlags = [.../* @__PURE__ */ new Set([...record.flags, "orphaned"])];
|
|
753
|
+
writeLedgerFlags(cwd, record.code, newFlags);
|
|
754
|
+
orphaned.push(record.code);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (record.state === "done") {
|
|
758
|
+
const files = links[record.code] ?? [];
|
|
759
|
+
for (const filePath of files) {
|
|
760
|
+
const absPath = join5(cwd, filePath);
|
|
761
|
+
const newHash = fileHash(absPath);
|
|
762
|
+
const oldHash = prevHashes[filePath] ?? "";
|
|
763
|
+
nextHashes[filePath] = newHash;
|
|
764
|
+
if (oldHash && newHash !== oldHash) {
|
|
765
|
+
if (!record.flags.includes("needs-re-verification")) {
|
|
766
|
+
const newFlags = [
|
|
767
|
+
.../* @__PURE__ */ new Set([...record.flags, "needs-re-verification"])
|
|
768
|
+
];
|
|
769
|
+
writeLedgerFlags(cwd, record.code, newFlags);
|
|
770
|
+
needsReverification.push(record.code);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
for (const [code, files] of Object.entries(links)) {
|
|
777
|
+
for (const filePath of files) {
|
|
778
|
+
const absPath = join5(cwd, filePath);
|
|
779
|
+
nextHashes[filePath] = fileHash(absPath);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
writeHashes(cwd, nextHashes);
|
|
783
|
+
console.log(pc2.green("\u2713") + " Sync complete.");
|
|
784
|
+
if (orphaned.length > 0) {
|
|
785
|
+
console.log(
|
|
786
|
+
pc2.yellow(` Orphaned (removed from spec): ${orphaned.join(", ")}`)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
if (needsReverification.length > 0) {
|
|
790
|
+
console.log(
|
|
791
|
+
pc2.yellow(
|
|
792
|
+
` Needs re-verification (spec changed after done): ${needsReverification.join(", ")}`
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
if (orphaned.length === 0 && needsReverification.length === 0) {
|
|
797
|
+
console.log(pc2.dim(" No drift detected."));
|
|
798
|
+
}
|
|
799
|
+
console.log(pc2.dim(" AGENTS.md and CLAUDE.md updated."));
|
|
800
|
+
}
|
|
801
|
+
async function readKeyFromMcpJson(cwd) {
|
|
802
|
+
const mcpPath = join5(cwd, ".mcp.json");
|
|
803
|
+
if (!existsSync5(mcpPath)) {
|
|
804
|
+
throw new Error(".mcp.json not found. Run `cubedot init` first.");
|
|
805
|
+
}
|
|
806
|
+
const mcp = JSON.parse(readFileSync5(mcpPath, "utf8"));
|
|
807
|
+
const key = mcp.mcpServers?.["cubedot"]?.headers?.["x-api-key"];
|
|
808
|
+
if (!key) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
".mcp.json is missing the cubedot x-api-key. Re-run `cubedot init`."
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return key;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/commands/status.ts
|
|
817
|
+
import pc3 from "picocolors";
|
|
818
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
819
|
+
import { join as join6 } from "path";
|
|
820
|
+
async function statusCommand() {
|
|
821
|
+
const cwd = process.cwd();
|
|
822
|
+
let ok = true;
|
|
823
|
+
const config = readCubedotConfig(cwd);
|
|
824
|
+
if (!config) {
|
|
825
|
+
console.log(pc3.red("\u2717") + " .cubedot/config.json \u2014 missing (run `cubedot init`)");
|
|
826
|
+
ok = false;
|
|
827
|
+
} else {
|
|
828
|
+
console.log(pc3.green("\u2713") + " .cubedot/config.json");
|
|
829
|
+
console.log(pc3.dim(` projectId: ${config.projectId}`));
|
|
830
|
+
if (config.lastSynced) {
|
|
831
|
+
const ageDays = (Date.now() - new Date(config.lastSynced).getTime()) / 864e5;
|
|
832
|
+
const ageStr = ageDays < 1 ? "today" : `${Math.floor(ageDays)} day(s) ago`;
|
|
833
|
+
const stale = ageDays >= 3;
|
|
834
|
+
console.log(
|
|
835
|
+
(stale ? pc3.yellow("\u26A0 ") : pc3.dim(" ")) + ` Last synced: ${ageStr}${stale ? " \u2014 consider running `cubedot sync`" : ""}`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const mcpPath = join6(cwd, ".mcp.json");
|
|
840
|
+
if (!existsSync6(mcpPath)) {
|
|
841
|
+
console.log(pc3.red("\u2717") + " .mcp.json \u2014 missing");
|
|
842
|
+
ok = false;
|
|
843
|
+
} else {
|
|
844
|
+
try {
|
|
845
|
+
const mcp = JSON.parse(readFileSync6(mcpPath, "utf8"));
|
|
846
|
+
const keyRaw = mcp.mcpServers?.["cubedot"]?.headers?.["x-api-key"] ?? "";
|
|
847
|
+
const keyTail = keyRaw ? keyRaw.slice(-8) : "(empty)";
|
|
848
|
+
console.log(pc3.green("\u2713") + ` .mcp.json \u2014 key tail: \u2026${keyTail}`);
|
|
849
|
+
} catch {
|
|
850
|
+
console.log(pc3.red("\u2717") + " .mcp.json \u2014 malformed JSON");
|
|
851
|
+
ok = false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
for (const file of ["AGENTS.md", "CLAUDE.md"]) {
|
|
855
|
+
const path = join6(cwd, file);
|
|
856
|
+
if (!existsSync6(path)) {
|
|
857
|
+
console.log(pc3.red("\u2717") + ` ${file} \u2014 missing`);
|
|
858
|
+
ok = false;
|
|
859
|
+
} else {
|
|
860
|
+
const content = readFileSync6(path, "utf8");
|
|
861
|
+
const markers = hasAllMarkers(content);
|
|
862
|
+
console.log(
|
|
863
|
+
(markers ? pc3.green("\u2713") : pc3.yellow("\u26A0")) + ` ${file}${markers ? "" : " \u2014 markers missing or malformed (run `cubedot sync`)"}`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const verifier = join6(cwd, ".claude", "agents", "CubedotVerifier.md");
|
|
868
|
+
const manual = join6(cwd, ".cubedot", "fia-operating-manual.md");
|
|
869
|
+
console.log(
|
|
870
|
+
(existsSync6(verifier) ? pc3.green("\u2713") : pc3.red("\u2717")) + " .claude/agents/CubedotVerifier.md"
|
|
871
|
+
);
|
|
872
|
+
console.log(
|
|
873
|
+
(existsSync6(manual) ? pc3.green("\u2713") : pc3.red("\u2717")) + " .cubedot/fia-operating-manual.md"
|
|
874
|
+
);
|
|
875
|
+
const ledgers = readAllLedgers(cwd);
|
|
876
|
+
if (ledgers.length === 0) {
|
|
877
|
+
console.log(pc3.dim(" Ledger: no entries yet"));
|
|
878
|
+
} else {
|
|
879
|
+
const counts = {};
|
|
880
|
+
for (const l of ledgers) counts[l.state] = (counts[l.state] ?? 0) + 1;
|
|
881
|
+
const summary = Object.entries(counts).map(([s, n]) => `${n} ${s}`).join(", ");
|
|
882
|
+
console.log(pc3.dim(` Ledger: ${ledgers.length} entries (${summary})`));
|
|
883
|
+
}
|
|
884
|
+
if (config) {
|
|
885
|
+
console.log(pc3.dim("\nChecking MCP connectivity \u2026"));
|
|
886
|
+
try {
|
|
887
|
+
const key = await readKeyFromMcpJson(cwd);
|
|
888
|
+
const connectUrl = buildConnectUrl(config.mcpUrl, config.projectId);
|
|
889
|
+
const client = await createMcpClient(connectUrl, key);
|
|
890
|
+
try {
|
|
891
|
+
await validateTools(client);
|
|
892
|
+
console.log(pc3.green("\u2713") + " MCP server reachable \u2014 all 7 tools present");
|
|
893
|
+
} finally {
|
|
894
|
+
await client.close();
|
|
895
|
+
}
|
|
896
|
+
} catch (err) {
|
|
897
|
+
console.log(
|
|
898
|
+
pc3.red("\u2717") + " MCP server: " + categorizeError(err)
|
|
899
|
+
);
|
|
900
|
+
ok = false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
process.exit(ok ? 0 : 1);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/commands/check.ts
|
|
907
|
+
import pc4 from "picocolors";
|
|
908
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
909
|
+
import { join as join7 } from "path";
|
|
910
|
+
async function checkCommand() {
|
|
911
|
+
const cwd = process.cwd();
|
|
912
|
+
const issues = [];
|
|
913
|
+
const config = readCubedotConfig(cwd);
|
|
914
|
+
if (!config) {
|
|
915
|
+
issues.push(".cubedot/config.json missing \u2014 run `cubedot init`");
|
|
916
|
+
}
|
|
917
|
+
const mcpPath = join7(cwd, ".mcp.json");
|
|
918
|
+
if (!existsSync7(mcpPath)) {
|
|
919
|
+
issues.push(".mcp.json missing \u2014 run `cubedot init`");
|
|
920
|
+
} else {
|
|
921
|
+
try {
|
|
922
|
+
const mcp = JSON.parse(readFileSync7(mcpPath, "utf8"));
|
|
923
|
+
if (!mcp.mcpServers?.["cubedot"]) {
|
|
924
|
+
issues.push(
|
|
925
|
+
".mcp.json missing mcpServers.cubedot \u2014 run `cubedot init`"
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
} catch {
|
|
929
|
+
issues.push(".mcp.json contains malformed JSON");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const agentsPath = join7(cwd, "AGENTS.md");
|
|
933
|
+
if (!existsSync7(agentsPath)) {
|
|
934
|
+
issues.push("AGENTS.md missing \u2014 run `cubedot init`");
|
|
935
|
+
} else {
|
|
936
|
+
const content = readFileSync7(agentsPath, "utf8");
|
|
937
|
+
if (!hasAllMarkers(content)) {
|
|
938
|
+
issues.push(
|
|
939
|
+
"AGENTS.md is missing or has malformed CUBEDOT markers \u2014 run `cubedot sync`"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const claudePath = join7(cwd, "CLAUDE.md");
|
|
944
|
+
if (!existsSync7(claudePath)) {
|
|
945
|
+
issues.push("CLAUDE.md missing \u2014 run `cubedot init`");
|
|
946
|
+
} else {
|
|
947
|
+
const content = readFileSync7(claudePath, "utf8");
|
|
948
|
+
if (!hasAllMarkers(content)) {
|
|
949
|
+
issues.push(
|
|
950
|
+
"CLAUDE.md is missing or has malformed CUBEDOT markers \u2014 run `cubedot sync`"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const verifier = join7(cwd, ".claude", "agents", "CubedotVerifier.md");
|
|
955
|
+
if (!existsSync7(verifier)) {
|
|
956
|
+
issues.push(
|
|
957
|
+
".claude/agents/CubedotVerifier.md missing \u2014 run `cubedot init`"
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const manual = join7(cwd, ".cubedot", "fia-operating-manual.md");
|
|
961
|
+
if (!existsSync7(manual)) {
|
|
962
|
+
issues.push(
|
|
963
|
+
".cubedot/fia-operating-manual.md missing \u2014 run `cubedot init`"
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
for (const dir of [".cubedot/progress", ".cubedot/reports"]) {
|
|
967
|
+
if (!existsSync7(join7(cwd, dir))) {
|
|
968
|
+
issues.push(`${dir}/ missing \u2014 run 'cubedot init'`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (existsSync7(claudePath)) {
|
|
972
|
+
const content = readFileSync7(claudePath, "utf8");
|
|
973
|
+
if (content.includes("fia-operating-manual") && !existsSync7(join7(cwd, ".cubedot", "fia-operating-manual.md"))) {
|
|
974
|
+
issues.push(
|
|
975
|
+
"CLAUDE.md references .cubedot/fia-operating-manual.md but it is missing"
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (issues.length === 0) {
|
|
980
|
+
console.log(pc4.green("\u2713") + " All checks passed.");
|
|
981
|
+
process.exit(0);
|
|
982
|
+
} else {
|
|
983
|
+
console.log(pc4.red(`\u2717 ${issues.length} issue(s) found:
|
|
984
|
+
`));
|
|
985
|
+
for (const issue of issues) {
|
|
986
|
+
console.log(" " + pc4.red("\u2022") + " " + issue);
|
|
987
|
+
}
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/commands/start.ts
|
|
993
|
+
import pc5 from "picocolors";
|
|
994
|
+
function startCommand(fn, opts) {
|
|
995
|
+
const cwd = process.cwd();
|
|
996
|
+
const actor = opts.actor === "human" ? "human" : "agent";
|
|
997
|
+
try {
|
|
998
|
+
const record = startFn(cwd, fn, actor);
|
|
999
|
+
console.log(
|
|
1000
|
+
pc5.green("\u2713") + ` ${fn} \u2192 in_progress (actor: ${record.actor}, at: ${record.updatedAt})`
|
|
1001
|
+
);
|
|
1002
|
+
console.log(pc5.dim(` Ledger: .cubedot/progress/${fn}.json`));
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
console.error(pc5.red(`Error: ${err.message}`));
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/commands/complete.ts
|
|
1010
|
+
import pc6 from "picocolors";
|
|
1011
|
+
function completeCommand(fn, opts) {
|
|
1012
|
+
const cwd = process.cwd();
|
|
1013
|
+
const actor = opts.actor === "human" ? "human" : "agent";
|
|
1014
|
+
try {
|
|
1015
|
+
const record = completeFn(cwd, fn, actor);
|
|
1016
|
+
console.log(
|
|
1017
|
+
pc6.green("\u2713") + ` ${fn} \u2192 code-complete (actor: ${record.actor}, at: ${record.updatedAt})`
|
|
1018
|
+
);
|
|
1019
|
+
console.log(
|
|
1020
|
+
pc6.dim(
|
|
1021
|
+
` Next: spawn CubedotVerifier, then ask the developer to run \`cubedot done ${fn}\`.`
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
console.error(pc6.red(`Error: ${err.message}`));
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/commands/done.ts
|
|
1031
|
+
import pc7 from "picocolors";
|
|
1032
|
+
function doneCommand(fn, opts) {
|
|
1033
|
+
const cwd = process.cwd();
|
|
1034
|
+
const actor = opts.actor === "agent" ? "agent" : "human";
|
|
1035
|
+
const force = opts.force === true;
|
|
1036
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1037
|
+
const discrepancies = (opts.discrepancy ?? []).map(
|
|
1038
|
+
(criterion) => ({ criterion, at: now })
|
|
1039
|
+
);
|
|
1040
|
+
try {
|
|
1041
|
+
const record = doneFn(cwd, fn, actor, force, discrepancies);
|
|
1042
|
+
console.log(
|
|
1043
|
+
pc7.green("\u2713") + ` ${fn} \u2192 done (actor: ${record.actor}, at: ${record.updatedAt})`
|
|
1044
|
+
);
|
|
1045
|
+
if (record.overrides.length > 0) {
|
|
1046
|
+
console.log(
|
|
1047
|
+
pc7.yellow(
|
|
1048
|
+
` \u26A0 Override recorded: ${record.overrides[record.overrides.length - 1].condition}`
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (discrepancies.length > 0) {
|
|
1053
|
+
console.log(
|
|
1054
|
+
pc7.yellow(
|
|
1055
|
+
` \u26A0 ${discrepancies.length} discrepancy(ies) recorded (criteria accepted unmet):`
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
for (const d of discrepancies) {
|
|
1059
|
+
console.log(pc7.yellow(` - ${d.criterion}`));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
console.error(pc7.red(`Error: ${err.message}`));
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/commands/progress.ts
|
|
1069
|
+
import pc8 from "picocolors";
|
|
1070
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
1071
|
+
import { join as join8 } from "path";
|
|
1072
|
+
function progressCommand() {
|
|
1073
|
+
const cwd = process.cwd();
|
|
1074
|
+
const ledgers = readAllLedgers(cwd);
|
|
1075
|
+
let fnNames = /* @__PURE__ */ new Map();
|
|
1076
|
+
for (const file of ["AGENTS.md", "CLAUDE.md"]) {
|
|
1077
|
+
const path = join8(cwd, file);
|
|
1078
|
+
if (existsSync8(path)) {
|
|
1079
|
+
fnNames = parseRegistryBlock(readFileSync8(path, "utf8"));
|
|
1080
|
+
if (fnNames.size > 0) break;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const byState = {
|
|
1084
|
+
done: [],
|
|
1085
|
+
"code-complete": [],
|
|
1086
|
+
in_progress: []
|
|
1087
|
+
};
|
|
1088
|
+
const flagged = [];
|
|
1089
|
+
for (const l of ledgers) {
|
|
1090
|
+
(byState[l.state] ?? []).push(l);
|
|
1091
|
+
if (l.flags.length > 0) flagged.push(l);
|
|
1092
|
+
}
|
|
1093
|
+
const total = fnNames.size || ledgers.length;
|
|
1094
|
+
const doneCount = byState["done"].length;
|
|
1095
|
+
const ccCount = byState["code-complete"].length;
|
|
1096
|
+
const ipCount = byState["in_progress"].length;
|
|
1097
|
+
const todoCount = Math.max(0, total - doneCount - ccCount - ipCount);
|
|
1098
|
+
console.log("\n" + pc8.bold("Progress Summary"));
|
|
1099
|
+
console.log("\u2501".repeat(40));
|
|
1100
|
+
console.log(` Done: ${pc8.green(String(doneCount))}/${total}`);
|
|
1101
|
+
if (ccCount > 0)
|
|
1102
|
+
console.log(` Code-complete: ${pc8.cyan(String(ccCount))}/${total}`);
|
|
1103
|
+
if (ipCount > 0)
|
|
1104
|
+
console.log(` In progress: ${pc8.yellow(String(ipCount))}/${total}`);
|
|
1105
|
+
if (todoCount > 0)
|
|
1106
|
+
console.log(` Todo: ${pc8.dim(String(todoCount))}/${total}`);
|
|
1107
|
+
const active = [
|
|
1108
|
+
...byState["in_progress"],
|
|
1109
|
+
...byState["code-complete"]
|
|
1110
|
+
];
|
|
1111
|
+
if (active.length > 0) {
|
|
1112
|
+
console.log("\n" + pc8.bold("Active:"));
|
|
1113
|
+
for (const l of active) {
|
|
1114
|
+
const name = fnNames.get(l.code) ? ` \u2014 ${fnNames.get(l.code)}` : "";
|
|
1115
|
+
const stateLabel = l.state === "in_progress" ? pc8.yellow("in_progress") : pc8.cyan("code-complete");
|
|
1116
|
+
console.log(` ${l.code}${name} [${stateLabel}]`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (fnNames.size > 0) {
|
|
1120
|
+
const ledgerCodes = new Set(ledgers.map((l) => l.code));
|
|
1121
|
+
const registryOrder = Array.from(fnNames.keys());
|
|
1122
|
+
const nextCode = registryOrder.find((code) => !ledgerCodes.has(code));
|
|
1123
|
+
if (nextCode) {
|
|
1124
|
+
console.log("\n" + pc8.bold("Next unblocked:"));
|
|
1125
|
+
console.log(` ${nextCode} \u2014 ${fnNames.get(nextCode) ?? ""}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (flagged.length > 0) {
|
|
1129
|
+
console.log("\n" + pc8.bold(pc8.yellow("Flagged:")));
|
|
1130
|
+
for (const l of flagged) {
|
|
1131
|
+
console.log(` ${l.code} [${l.flags.join(", ")}]`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (byState["done"].length > 0) {
|
|
1135
|
+
console.log("\n" + pc8.bold("Done:"));
|
|
1136
|
+
for (const l of byState["done"]) {
|
|
1137
|
+
const name = fnNames.get(l.code) ? ` \u2014 ${fnNames.get(l.code)}` : "";
|
|
1138
|
+
console.log(` ${pc8.green("\u2713")} ${l.code}${name}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
console.log("");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/cli.ts
|
|
1145
|
+
var __filename3 = fileURLToPath4(import.meta.url);
|
|
1146
|
+
var __dirname4 = dirname4(__filename3);
|
|
1147
|
+
var pkg3 = JSON.parse(
|
|
1148
|
+
readFileSync9(join9(__dirname4, "../package.json"), "utf8")
|
|
1149
|
+
);
|
|
1150
|
+
var program = new Command();
|
|
1151
|
+
var collect = (value, prev) => [...prev, value];
|
|
1152
|
+
program.name("cubedot").description(
|
|
1153
|
+
"Connect your repo to the Cubedot MCP and drive the spec-first build loop."
|
|
1154
|
+
).version(pkg3.version);
|
|
1155
|
+
program.command("init").description("Connect and scaffold the repo (validates MCP, writes config + hot-memory files).").option("--token <token>", "Base64url connect token {v:1, projectId, key, mcpUrl}").option("--project <projectId>", "Project ID (alternative to --token)").option("--key <key>", "MCP API key (alternative to --token)").option("--url <url>", "MCP URL (alternative to --token)").action(initCommand);
|
|
1156
|
+
program.command("sync").description("Refresh local projection (AGENTS.md/CLAUDE.md markers) and detect drift.").action(syncCommand);
|
|
1157
|
+
program.command("status").description("Connectivity ping + file-health check (read-only).").action(statusCommand);
|
|
1158
|
+
program.command("check").description("Validate markers, config, and server name (read-only; CI-friendly).").action(checkCommand);
|
|
1159
|
+
program.command("start <fn>").description("Set a functionality to in_progress.").option("--actor <actor>", "Actor performing the action", "agent").action(startCommand);
|
|
1160
|
+
program.command("complete <fn>").description("Set a functionality to code-complete (requires in_progress state).").option("--actor <actor>", "Actor performing the action", "agent").action(completeCommand);
|
|
1161
|
+
program.command("done <fn>").description("Mark a functionality done (human gate; requires code-complete state).").option("--actor <actor>", "Actor performing the action", "human").option("--force", "Override the code-complete gate (records a discrepancy).").option(
|
|
1162
|
+
"--discrepancy <text>",
|
|
1163
|
+
"Record an acceptance criterion accepted as unmet at done (repeatable).",
|
|
1164
|
+
collect,
|
|
1165
|
+
[]
|
|
1166
|
+
).action(doneCommand);
|
|
1167
|
+
program.command("progress").description("Print completion summary across all tracked functionalities (read-only).").action(progressCommand);
|
|
1168
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1169
|
+
console.error(err.message);
|
|
1170
|
+
process.exit(1);
|
|
1171
|
+
});
|