@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.
Files changed (2) hide show
  1. package/dist/index.js +258 -5
  2. 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
- console.log(chalk4.green("\n \u2713 PROJECT_STATE.md regenerated"));
887
- console.log(chalk4.green(" \u2713 AGENTS.md regenerated\n"));
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(`-${projectName}`) || d.includes(projectName.replace(/\//g, "-"))) {
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundctl/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Product memory for AI agent builders",
5
5
  "license": "MIT",
6
6
  "bin": {