@groundctl/cli 0.2.1 → 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/dist/index.js +264 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -274,7 +274,7 @@ function generateProjectState(db, projectName) {
|
|
|
274
274
|
md += "\n";
|
|
275
275
|
}
|
|
276
276
|
if (decisions.length > 0) {
|
|
277
|
-
md += "##
|
|
277
|
+
md += "## Architecture log\n";
|
|
278
278
|
for (const d of decisions) {
|
|
279
279
|
md += `- ${d.session_id}: ${d.description}`;
|
|
280
280
|
if (d.rationale) md += ` \u2014 ${d.rationale}`;
|
|
@@ -874,7 +874,7 @@ async function completeCommand(featureIdOrName) {
|
|
|
874
874
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
875
875
|
import { join as join4 } from "path";
|
|
876
876
|
import chalk4 from "chalk";
|
|
877
|
-
async function syncCommand() {
|
|
877
|
+
async function syncCommand(opts) {
|
|
878
878
|
const db = await openDb();
|
|
879
879
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
880
880
|
const projectState = generateProjectState(db, projectName);
|
|
@@ -883,8 +883,10 @@ async function syncCommand() {
|
|
|
883
883
|
const cwd = process.cwd();
|
|
884
884
|
writeFileSync3(join4(cwd, "PROJECT_STATE.md"), projectState);
|
|
885
885
|
writeFileSync3(join4(cwd, "AGENTS.md"), agentsMd);
|
|
886
|
-
|
|
887
|
-
|
|
886
|
+
if (!opts?.silent) {
|
|
887
|
+
console.log(chalk4.green("\n \u2713 PROJECT_STATE.md regenerated"));
|
|
888
|
+
console.log(chalk4.green(" \u2713 AGENTS.md regenerated\n"));
|
|
889
|
+
}
|
|
888
890
|
}
|
|
889
891
|
|
|
890
892
|
// src/commands/next.ts
|
|
@@ -1278,19 +1280,23 @@ function buildSummary(sessionId, fileCount, commitCount, decisionCount, lastText
|
|
|
1278
1280
|
}
|
|
1279
1281
|
|
|
1280
1282
|
// src/commands/ingest.ts
|
|
1283
|
+
function claudeEncode(p) {
|
|
1284
|
+
return p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
1285
|
+
}
|
|
1281
1286
|
function findLatestTranscript(projectPath) {
|
|
1282
1287
|
const projectsDir = join5(homedir2(), ".claude", "projects");
|
|
1283
1288
|
if (!existsSync4(projectsDir)) return null;
|
|
1284
|
-
const projectKey = projectPath.replace(/\//g, "-");
|
|
1285
1289
|
let transcriptDir = null;
|
|
1290
|
+
const projectKey = claudeEncode(projectPath);
|
|
1286
1291
|
const directMatch = join5(projectsDir, projectKey);
|
|
1287
1292
|
if (existsSync4(directMatch)) {
|
|
1288
1293
|
transcriptDir = directMatch;
|
|
1289
1294
|
} else {
|
|
1290
1295
|
const projectName = projectPath.split("/").pop() ?? "";
|
|
1296
|
+
const encodedName = claudeEncode(projectName);
|
|
1291
1297
|
const dirs = readdirSync(projectsDir);
|
|
1292
1298
|
for (const d of dirs) {
|
|
1293
|
-
if (d.endsWith(`-${
|
|
1299
|
+
if (d.endsWith(`-${encodedName}`) || d.includes(encodedName)) {
|
|
1294
1300
|
transcriptDir = join5(projectsDir, d);
|
|
1295
1301
|
break;
|
|
1296
1302
|
}
|
|
@@ -1444,7 +1450,7 @@ ${sessionRow.summary}
|
|
|
1444
1450
|
md += "\n";
|
|
1445
1451
|
}
|
|
1446
1452
|
if (decisions.length > 0) {
|
|
1447
|
-
md += "##
|
|
1453
|
+
md += "## Architecture log\n";
|
|
1448
1454
|
for (const d of decisions) {
|
|
1449
1455
|
md += `- ${d.description}`;
|
|
1450
1456
|
if (d.rationale) md += ` \u2014 ${d.rationale}`;
|
|
@@ -1569,7 +1575,7 @@ async function reportCommand(options) {
|
|
|
1569
1575
|
console.log(chalk9.green(`
|
|
1570
1576
|
\u2713 SESSION_REPORT.md written (session ${session.id})
|
|
1571
1577
|
`));
|
|
1572
|
-
console.log(chalk9.gray(` ${files.length} files \xB7 ${decisions.length}
|
|
1578
|
+
console.log(chalk9.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
|
|
1573
1579
|
console.log("");
|
|
1574
1580
|
}
|
|
1575
1581
|
|
|
@@ -1640,7 +1646,7 @@ async function healthCommand() {
|
|
|
1640
1646
|
const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1641
1647
|
const decColor = decisionCount > 0 ? chalk10.green : chalk10.yellow;
|
|
1642
1648
|
console.log(
|
|
1643
|
-
` ${decMark}
|
|
1649
|
+
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk10.gray(` +${decisionScore}pts`)
|
|
1644
1650
|
);
|
|
1645
1651
|
const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1646
1652
|
const claimColor = staleClaims === 0 ? chalk10.green : chalk10.red;
|
|
@@ -1656,7 +1662,7 @@ async function healthCommand() {
|
|
|
1656
1662
|
const recommendations = [];
|
|
1657
1663
|
if (testFiles === 0) recommendations.push("Write tests before your next feature (0 test files found).");
|
|
1658
1664
|
if (staleClaims > 0) recommendations.push(`Release ${staleClaims} stale claim(s) with groundctl complete <feature>.`);
|
|
1659
|
-
if (decisionCount === 0) recommendations.push("
|
|
1665
|
+
if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
|
|
1660
1666
|
if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
|
|
1661
1667
|
if (recommendations.length > 0) {
|
|
1662
1668
|
console.log(chalk10.bold(" Recommendations:"));
|
|
@@ -1795,7 +1801,7 @@ body{background:var(--bg);color:var(--tx);font-family:var(--mo);font-size:13px;l
|
|
|
1795
1801
|
<div class="hi">
|
|
1796
1802
|
<div><span class="${meta.done > 0 ? "ok" : "warn"}">${meta.done > 0 ? "\u2713" : "\u26A0"}</span><span>Features ${meta.done}/${meta.total}</span></div>
|
|
1797
1803
|
<div><span class="${meta.testFiles > 0 ? "ok" : "bad"}">${meta.testFiles > 0 ? "\u2713" : "\u2717"}</span><span>Tests ${meta.testFiles} files</span></div>
|
|
1798
|
-
<div><span class="${meta.decCount > 0 ? "ok" : "warn"}">${meta.decCount > 0 ? "\u2713" : "\u26A0"}</span><span>
|
|
1804
|
+
<div><span class="${meta.decCount > 0 ? "ok" : "warn"}">${meta.decCount > 0 ? "\u2713" : "\u26A0"}</span><span>Architecture log ${meta.decCount} entries</span></div>
|
|
1799
1805
|
<div><span class="${meta.stale === 0 ? "ok" : "bad"}">${meta.stale === 0 ? "\u2713" : "\u2717"}</span><span>Claims ${meta.stale > 0 ? meta.stale + " stale" : "healthy"}</span></div>
|
|
1800
1806
|
</div></div>
|
|
1801
1807
|
<div class="rn">auto-refresh 10s<br><span style="color:var(--br)">${esc(dbPath.split("/").slice(-3).join("/"))}</span></div>
|
|
@@ -1845,6 +1851,247 @@ async function dashboardCommand(options) {
|
|
|
1845
1851
|
});
|
|
1846
1852
|
}
|
|
1847
1853
|
|
|
1854
|
+
// src/commands/watch.ts
|
|
1855
|
+
import {
|
|
1856
|
+
existsSync as existsSync6,
|
|
1857
|
+
readdirSync as readdirSync2,
|
|
1858
|
+
statSync,
|
|
1859
|
+
writeFileSync as writeFileSync5,
|
|
1860
|
+
readFileSync as readFileSync6,
|
|
1861
|
+
mkdirSync as mkdirSync3,
|
|
1862
|
+
watch as fsWatch
|
|
1863
|
+
} from "fs";
|
|
1864
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
1865
|
+
import { homedir as homedir3 } from "os";
|
|
1866
|
+
import { spawn } from "child_process";
|
|
1867
|
+
import chalk12 from "chalk";
|
|
1868
|
+
var DEBOUNCE_MS = 8e3;
|
|
1869
|
+
var DIR_POLL_MS = 5e3;
|
|
1870
|
+
function claudeEncode2(p) {
|
|
1871
|
+
return p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
1872
|
+
}
|
|
1873
|
+
function findTranscriptDir(projectPath) {
|
|
1874
|
+
const projectsDir = join8(homedir3(), ".claude", "projects");
|
|
1875
|
+
if (!existsSync6(projectsDir)) return null;
|
|
1876
|
+
const projectKey = claudeEncode2(projectPath);
|
|
1877
|
+
const direct = join8(projectsDir, projectKey);
|
|
1878
|
+
if (existsSync6(direct)) return direct;
|
|
1879
|
+
const projectName = projectPath.split("/").pop() ?? "";
|
|
1880
|
+
const encodedName = claudeEncode2(projectName);
|
|
1881
|
+
const dirs = readdirSync2(projectsDir);
|
|
1882
|
+
for (const d of dirs) {
|
|
1883
|
+
if (d.endsWith(`-${encodedName}`) || d.includes(encodedName)) {
|
|
1884
|
+
return join8(projectsDir, d);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
function fileSize(p) {
|
|
1890
|
+
try {
|
|
1891
|
+
return statSync(p).size;
|
|
1892
|
+
} catch {
|
|
1893
|
+
return 0;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function writePidFile(groundctlDir, pid) {
|
|
1897
|
+
try {
|
|
1898
|
+
mkdirSync3(groundctlDir, { recursive: true });
|
|
1899
|
+
writeFileSync5(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function readPidFile(groundctlDir) {
|
|
1904
|
+
try {
|
|
1905
|
+
const raw = readFileSync6(join8(groundctlDir, "watch.pid"), "utf8").trim();
|
|
1906
|
+
return parseInt(raw) || null;
|
|
1907
|
+
} catch {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function processAlive(pid) {
|
|
1912
|
+
try {
|
|
1913
|
+
process.kill(pid, 0);
|
|
1914
|
+
return true;
|
|
1915
|
+
} catch {
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
async function runIngest(transcriptPath, projectPath) {
|
|
1920
|
+
const filename = transcriptPath.split("/").slice(-2).join("/");
|
|
1921
|
+
console.log(
|
|
1922
|
+
chalk12.gray(`
|
|
1923
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) + chalk12.cyan(`Transcript stable \u2192 ingesting ${filename}`)
|
|
1924
|
+
);
|
|
1925
|
+
try {
|
|
1926
|
+
const parsed = parseTranscript(transcriptPath, "auto", projectPath);
|
|
1927
|
+
const db = await openDb();
|
|
1928
|
+
const sessionId = parsed.sessionId;
|
|
1929
|
+
const exists = queryOne(db, "SELECT id FROM sessions WHERE id = ?", [sessionId]);
|
|
1930
|
+
if (exists) {
|
|
1931
|
+
db.run("UPDATE sessions SET ended_at = ?, summary = ? WHERE id = ?", [
|
|
1932
|
+
parsed.endedAt,
|
|
1933
|
+
parsed.summary,
|
|
1934
|
+
sessionId
|
|
1935
|
+
]);
|
|
1936
|
+
} else {
|
|
1937
|
+
db.run(
|
|
1938
|
+
"INSERT INTO sessions (id, agent, started_at, ended_at, summary) VALUES (?, ?, ?, ?, ?)",
|
|
1939
|
+
[sessionId, "claude-code", parsed.startedAt, parsed.endedAt, parsed.summary]
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
let newFiles = 0;
|
|
1943
|
+
for (const file of parsed.filesModified) {
|
|
1944
|
+
const dup = queryOne(
|
|
1945
|
+
db,
|
|
1946
|
+
"SELECT id FROM files_modified WHERE session_id = ? AND path = ?",
|
|
1947
|
+
[sessionId, file.path]
|
|
1948
|
+
);
|
|
1949
|
+
if (!dup) {
|
|
1950
|
+
db.run(
|
|
1951
|
+
"INSERT INTO files_modified (session_id, path, operation, lines_changed) VALUES (?, ?, ?, ?)",
|
|
1952
|
+
[sessionId, file.path, file.operation, file.linesChanged]
|
|
1953
|
+
);
|
|
1954
|
+
newFiles++;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
let newDecisions = 0;
|
|
1958
|
+
for (const d of parsed.decisions) {
|
|
1959
|
+
const dup = queryOne(
|
|
1960
|
+
db,
|
|
1961
|
+
"SELECT id FROM decisions WHERE session_id = ? AND description = ?",
|
|
1962
|
+
[sessionId, d.description]
|
|
1963
|
+
);
|
|
1964
|
+
if (!dup) {
|
|
1965
|
+
db.run(
|
|
1966
|
+
"INSERT INTO decisions (session_id, description, rationale) VALUES (?, ?, ?)",
|
|
1967
|
+
[sessionId, d.description, d.rationale ?? null]
|
|
1968
|
+
);
|
|
1969
|
+
newDecisions++;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
saveDb();
|
|
1973
|
+
closeDb();
|
|
1974
|
+
const parts = [];
|
|
1975
|
+
if (newFiles > 0) parts.push(`${newFiles} file${newFiles !== 1 ? "s" : ""}`);
|
|
1976
|
+
if (parsed.commits.length > 0) parts.push(`${parsed.commits.length} commit${parsed.commits.length !== 1 ? "s" : ""}`);
|
|
1977
|
+
if (newDecisions > 0) parts.push(`${newDecisions} decision${newDecisions !== 1 ? "s" : ""} captured`);
|
|
1978
|
+
const summary = parts.length > 0 ? parts.join(", ") : "no new data";
|
|
1979
|
+
console.log(chalk12.green(` \u2713 Session ingested \u2014 ${summary}`));
|
|
1980
|
+
await syncCommand({ silent: true });
|
|
1981
|
+
console.log(chalk12.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
console.log(chalk12.red(` \u2717 Ingest failed: ${err.message}`));
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function startWatcher(transcriptDir, projectPath) {
|
|
1987
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1988
|
+
const ingested = /* @__PURE__ */ new Set();
|
|
1989
|
+
const fileWatchers = /* @__PURE__ */ new Map();
|
|
1990
|
+
function schedule(filePath) {
|
|
1991
|
+
if (ingested.has(filePath)) return;
|
|
1992
|
+
const existing = pending.get(filePath);
|
|
1993
|
+
if (existing) clearTimeout(existing);
|
|
1994
|
+
const timer = setTimeout(async () => {
|
|
1995
|
+
pending.delete(filePath);
|
|
1996
|
+
if (ingested.has(filePath)) return;
|
|
1997
|
+
if (fileSize(filePath) === 0) return;
|
|
1998
|
+
ingested.add(filePath);
|
|
1999
|
+
fileWatchers.get(filePath)?.close();
|
|
2000
|
+
fileWatchers.delete(filePath);
|
|
2001
|
+
await runIngest(filePath, projectPath);
|
|
2002
|
+
}, DEBOUNCE_MS);
|
|
2003
|
+
pending.set(filePath, timer);
|
|
2004
|
+
}
|
|
2005
|
+
function watchFile(filePath) {
|
|
2006
|
+
if (fileWatchers.has(filePath) || ingested.has(filePath)) return;
|
|
2007
|
+
try {
|
|
2008
|
+
const w = fsWatch(filePath, () => {
|
|
2009
|
+
schedule(filePath);
|
|
2010
|
+
});
|
|
2011
|
+
fileWatchers.set(filePath, w);
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (existsSync6(transcriptDir)) {
|
|
2016
|
+
for (const f of readdirSync2(transcriptDir)) {
|
|
2017
|
+
if (f.endsWith(".jsonl")) {
|
|
2018
|
+
ingested.add(join8(transcriptDir, f));
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
fsWatch(transcriptDir, (_event, filename) => {
|
|
2023
|
+
if (!filename?.endsWith(".jsonl")) return;
|
|
2024
|
+
const fp = join8(transcriptDir, filename);
|
|
2025
|
+
if (!existsSync6(fp) || ingested.has(fp)) return;
|
|
2026
|
+
if (!fileWatchers.has(fp)) {
|
|
2027
|
+
watchFile(fp);
|
|
2028
|
+
schedule(fp);
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
console.log(chalk12.bold("\n groundctl watch") + chalk12.gray(" \u2014 auto-ingest on session end\n"));
|
|
2032
|
+
console.log(
|
|
2033
|
+
chalk12.gray(" Watching: ") + chalk12.blue(transcriptDir.replace(homedir3(), "~"))
|
|
2034
|
+
);
|
|
2035
|
+
console.log(chalk12.gray(" Stability threshold: ") + chalk12.white(`${DEBOUNCE_MS / 1e3}s`));
|
|
2036
|
+
console.log(chalk12.gray(" Press Ctrl+C to stop.\n"));
|
|
2037
|
+
}
|
|
2038
|
+
async function watchCommand(options) {
|
|
2039
|
+
const projectPath = options.projectPath ? resolve2(options.projectPath) : process.cwd();
|
|
2040
|
+
if (options.daemon) {
|
|
2041
|
+
const args = [process.argv[1], "watch", "--project-path", projectPath];
|
|
2042
|
+
const child = spawn(process.execPath, args, {
|
|
2043
|
+
detached: true,
|
|
2044
|
+
stdio: "ignore"
|
|
2045
|
+
});
|
|
2046
|
+
child.unref();
|
|
2047
|
+
const groundctlDir2 = join8(projectPath, ".groundctl");
|
|
2048
|
+
writePidFile(groundctlDir2, child.pid);
|
|
2049
|
+
console.log(chalk12.green(`
|
|
2050
|
+
\u2713 groundctl watch running in background (PID ${child.pid})`));
|
|
2051
|
+
console.log(chalk12.gray(` PID saved to .groundctl/watch.pid`));
|
|
2052
|
+
console.log(chalk12.gray(` To stop: kill ${child.pid}
|
|
2053
|
+
`));
|
|
2054
|
+
process.exit(0);
|
|
2055
|
+
}
|
|
2056
|
+
const groundctlDir = join8(projectPath, ".groundctl");
|
|
2057
|
+
const existingPid = readPidFile(groundctlDir);
|
|
2058
|
+
if (existingPid && processAlive(existingPid)) {
|
|
2059
|
+
console.log(chalk12.yellow(`
|
|
2060
|
+
\u26A0 A watcher is already running (PID ${existingPid}).`));
|
|
2061
|
+
console.log(chalk12.gray(` To stop it: kill ${existingPid}
|
|
2062
|
+
`));
|
|
2063
|
+
process.exit(1);
|
|
2064
|
+
}
|
|
2065
|
+
let transcriptDir = findTranscriptDir(projectPath);
|
|
2066
|
+
if (!transcriptDir) {
|
|
2067
|
+
console.log(chalk12.bold("\n groundctl watch\n"));
|
|
2068
|
+
console.log(
|
|
2069
|
+
chalk12.yellow(" No Claude Code transcript directory found for this project yet.")
|
|
2070
|
+
);
|
|
2071
|
+
console.log(chalk12.gray(" Waiting for first session to start...\n"));
|
|
2072
|
+
await new Promise((resolve3) => {
|
|
2073
|
+
const interval = setInterval(() => {
|
|
2074
|
+
const dir = findTranscriptDir(projectPath);
|
|
2075
|
+
if (dir) {
|
|
2076
|
+
clearInterval(interval);
|
|
2077
|
+
transcriptDir = dir;
|
|
2078
|
+
resolve3();
|
|
2079
|
+
}
|
|
2080
|
+
}, DIR_POLL_MS);
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
startWatcher(transcriptDir, projectPath);
|
|
2084
|
+
await new Promise(() => {
|
|
2085
|
+
process.on("SIGINT", () => {
|
|
2086
|
+
console.log(chalk12.gray("\n Watcher stopped.\n"));
|
|
2087
|
+
process.exit(0);
|
|
2088
|
+
});
|
|
2089
|
+
process.on("SIGTERM", () => {
|
|
2090
|
+
process.exit(0);
|
|
2091
|
+
});
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1848
2095
|
// src/index.ts
|
|
1849
2096
|
var require2 = createRequire(import.meta.url);
|
|
1850
2097
|
var pkg = require2("../package.json");
|
|
@@ -1870,4 +2117,10 @@ program.command("ingest").description("Parse a transcript and write session data
|
|
|
1870
2117
|
program.command("report").description("Generate SESSION_REPORT.md from SQLite").option("-s, --session <id>", "Report for a specific session").option("--all", "Generate report for all sessions").action(reportCommand);
|
|
1871
2118
|
program.command("health").description("Show product health score").action(healthCommand);
|
|
1872
2119
|
program.command("dashboard").description("Start web dashboard on port 4242").option("-p, --port <port>", "Port number", "4242").action(dashboardCommand);
|
|
2120
|
+
program.command("watch").description("Watch for session end and auto-ingest transcripts").option("--daemon", "Run in background (detached process)").option("--project-path <path>", "Project path (defaults to cwd)").action(
|
|
2121
|
+
(opts) => watchCommand({
|
|
2122
|
+
daemon: opts.daemon,
|
|
2123
|
+
projectPath: opts.projectPath
|
|
2124
|
+
})
|
|
2125
|
+
);
|
|
1873
2126
|
program.parse();
|