mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-27 12:10:00 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/colombo-v3
# Conflicts: # CHANGELOG.md # VERSION # package.json
This commit is contained in:
@@ -192,7 +192,10 @@ function resolveSkillFile(args: CliArgs): string | null {
|
||||
|
||||
function gbrainAvailable(): boolean {
|
||||
try {
|
||||
execFileSync("command", ["-v", "gbrain"], { stdio: "ignore" });
|
||||
execFileSync("gbrain", ["--version"], {
|
||||
stdio: "ignore",
|
||||
timeout: MCP_TIMEOUT_MS,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -287,13 +287,20 @@ function gbrainSupportsSourcesRename(env?: NodeJS.ProcessEnv): boolean {
|
||||
* `env` is the environment passed to the spawned `gbrain` process; defaults
|
||||
* to `process.env`. Tests inject a PATH that points at a gbrain shim so the
|
||||
* helper can be exercised without a real gbrain CLI.
|
||||
*
|
||||
* Shape note: `gbrain sources list --json` returns `{sources: [...]}` (v0.20+);
|
||||
* older versions returned a flat array. Accept both for forward/backward compat
|
||||
* (mirrors `probeSource`/`sourcePageCount` in lib/gbrain-sources.ts).
|
||||
*/
|
||||
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
|
||||
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
|
||||
const raw = execGbrainJson<unknown>(
|
||||
["sources", "list", "--json"],
|
||||
{ baseEnv: env },
|
||||
);
|
||||
if (!list) return null;
|
||||
if (!raw) return null;
|
||||
const list: Array<{ id?: string; local_path?: string }> = Array.isArray(raw)
|
||||
? (raw as Array<{ id?: string; local_path?: string }>)
|
||||
: ((raw as { sources?: Array<{ id?: string; local_path?: string }> }).sources ?? []);
|
||||
const found = list.find((s) => s.id === sourceId);
|
||||
return found?.local_path ?? null;
|
||||
}
|
||||
|
||||
@@ -273,16 +273,23 @@ function resolveClaudeCodeCwd(
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractCwdFromJsonl(filePath: string): string | null {
|
||||
export function extractCwdFromJsonl(filePath: string): string | null {
|
||||
// Read a capped prefix so huge JSONL files don't blow up memory. 64KB
|
||||
// comfortably fits the largest observed session headers; the old 8KB cap
|
||||
// would sometimes fall inside a single long line and silently drop the
|
||||
// project (JSON.parse failure on the truncated tail).
|
||||
const MAX_BYTES = 64 * 1024;
|
||||
const MAX_LINES = 30;
|
||||
try {
|
||||
// Read only the first 8KB to avoid loading huge JSONL files into memory
|
||||
const fd = openSync(filePath, "r");
|
||||
const buf = Buffer.alloc(8192);
|
||||
const bytesRead = readSync(fd, buf, 0, 8192, 0);
|
||||
const buf = Buffer.alloc(MAX_BYTES);
|
||||
const bytesRead = readSync(fd, buf, 0, MAX_BYTES, 0);
|
||||
closeSync(fd);
|
||||
const text = buf.toString("utf-8", 0, bytesRead);
|
||||
const lines = text.split("\n").slice(0, 15);
|
||||
for (const line of lines) {
|
||||
// Drop the final segment — it may be an incomplete line at the cap boundary.
|
||||
const parts = text.split("\n");
|
||||
const completeLines = parts.length > 1 ? parts.slice(0, -1) : parts;
|
||||
for (const line of completeLines.slice(0, MAX_LINES)) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
|
||||
@@ -194,7 +194,7 @@ Options:
|
||||
--all-history Walk transcripts older than 90 days too.
|
||||
--sources <list> Comma-separated subset: ${ALL_TYPES.join(",")}
|
||||
--limit <N> Stop after N pages written (smoke testing).
|
||||
--no-write Skip gbrain put_page calls (still updates state file).
|
||||
--no-write Skip gbrain put calls (still updates state file).
|
||||
Used by tests + dry runs without actual ingest.
|
||||
--scan-secrets Opt-in per-file gitleaks scan during prepare. Off by
|
||||
default; gstack-brain-sync already gates the git-push
|
||||
@@ -1061,7 +1061,7 @@ async function probeMode(args: CliArgs): Promise<ProbeReport> {
|
||||
}
|
||||
|
||||
// Per ED2: ~25-35 min for ~11.7K transcripts = ~150ms/page synchronous
|
||||
// (gitleaks + render + put_page + embedding). Scale linearly.
|
||||
// (gitleaks + render + put + embedding). Scale linearly.
|
||||
const estimateMinutes = Math.max(1, Math.round((newCount + updatedCount) * 0.15 / 60));
|
||||
|
||||
return {
|
||||
@@ -1374,7 +1374,7 @@ async function ingestPass(args: CliArgs): Promise<BulkResult> {
|
||||
if (args.noWrite) {
|
||||
// --no-write: skip the gbrain import call but still record state for
|
||||
// prepared pages (treat them as ingested for dedup purposes). Matches
|
||||
// the prior contract from --help: "Skip gbrain put_page calls (still
|
||||
// the prior contract from --help: "Skip gbrain put calls (still
|
||||
// updates state file)".
|
||||
const nowIso = new Date().toISOString();
|
||||
for (const p of prep.prepared) {
|
||||
|
||||
+6
-2
@@ -9,7 +9,7 @@
|
||||
# CI / container env where HOME may be unset.
|
||||
#
|
||||
# Chains:
|
||||
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA -> $HOME/.gstack -> .gstack
|
||||
# GSTACK_STATE_ROOT: GSTACK_HOME -> CLAUDE_PLUGIN_DATA (only when CLAUDE_PLUGIN_ROOT=*gstack*) -> $HOME/.gstack -> .gstack
|
||||
# PLAN_ROOT: GSTACK_PLAN_DIR -> CLAUDE_PLANS_DIR -> $HOME/.claude/plans -> .claude/plans
|
||||
# TMP_ROOT: TMPDIR -> TMP -> .gstack/tmp (and mkdir -p, best-effort)
|
||||
#
|
||||
@@ -21,7 +21,11 @@ set -u
|
||||
# State root: where gstack writes projects/, sessions/, analytics/.
|
||||
if [ -n "${GSTACK_HOME:-}" ]; then
|
||||
_state_root="$GSTACK_HOME"
|
||||
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then
|
||||
elif [ -n "${CLAUDE_PLUGIN_DATA:-}" ] && echo "${CLAUDE_PLUGIN_ROOT:-}" | grep -qi "gstack"; then
|
||||
# Guard: only trust CLAUDE_PLUGIN_DATA when CLAUDE_PLUGIN_ROOT confirms we are
|
||||
# running as the gstack plugin. Without this, a CLAUDE_PLUGIN_DATA from another
|
||||
# plugin (e.g. codex) that leaked into the session env via CLAUDE_ENV_FILE would
|
||||
# be picked up, writing all gstack state into the wrong directory.
|
||||
_state_root="$CLAUDE_PLUGIN_DATA"
|
||||
elif [ -n "${HOME:-}" ]; then
|
||||
_state_root="$HOME/.gstack"
|
||||
|
||||
@@ -107,7 +107,13 @@ BATCH="$BATCH]"
|
||||
[ "$COUNT" -eq 0 ] && exit 0
|
||||
|
||||
# ─── POST to edge function ───────────────────────────────────
|
||||
RESP_FILE="$(mktemp /tmp/gstack-sync-XXXXXX 2>/dev/null || echo "/tmp/gstack-sync-$$")"
|
||||
# Create response file atomically. If mktemp fails, refuse to continue rather
|
||||
# than fall back to a predictable $$-based path (race + overwrite footgun).
|
||||
RESP_FILE="$(mktemp "${TMPDIR:-/tmp}/gstack-sync-XXXXXX")" || {
|
||||
echo "gstack-telemetry-sync: mktemp failed — skipping this run" >&2
|
||||
exit 0
|
||||
}
|
||||
trap 'rm -f "$RESP_FILE"' EXIT
|
||||
HTTP_CODE="$(curl -s -w '%{http_code}' --max-time 10 \
|
||||
-X POST "${SUPABASE_URL}/functions/v1/telemetry-ingest" \
|
||||
-H "Content-Type: application/json" \
|
||||
|
||||
Reference in New Issue
Block a user