@groundctl/cli 0.2.2 → 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 +258 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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();
|