@a13xu/lucid 1.16.2 → 1.19.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.
@@ -5,6 +5,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
5
5
  import { fileURLToPath } from "url";
6
6
  import { indexProject } from "../indexer/project.js";
7
7
  import { saveAdminConfig, loadAdminConfig, isAdminConfigured, sendTestAlert, } from "../security/alerts.js";
8
+ import { isConfigured as isLocalLlmConfigured, loadLocalConfig } from "../local-llm/config.js";
9
+ import { describeRuntime } from "../local-llm/runtimes.js";
8
10
  export const InitProjectSchema = z.object({
9
11
  directory: z.string().optional(),
10
12
  // ── Admin alert configuration (asked once at project init) ──────────────
@@ -31,6 +33,9 @@ export const InitProjectSchema = z.object({
31
33
  });
32
34
  const LUCID_MARKER = "Lucid: call sync_file";
33
35
  const LUCID_UPDATE_MARKER = "lucid-update-check";
36
+ const LUCID_GUARD_MARKER = "lucid-guard-pre-edit";
37
+ const LUCID_SESSION_TICK_MARKER = "lucid-session-tick";
38
+ const LUCID_SESSION_COMPACT_MARKER = "lucid-session-compact";
34
39
  const LUCID_HOOK = {
35
40
  matcher: "Write|Edit|NotebookEdit",
36
41
  hooks: [
@@ -40,6 +45,38 @@ const LUCID_HOOK = {
40
45
  },
41
46
  ],
42
47
  };
48
+ // PreToolUse hook: snapshot file + block destructive truncates BEFORE write.
49
+ // Reads Claude Code's PreToolUse JSON from stdin; exit 2 hard-blocks the tool.
50
+ // Marker token included so we can idempotently detect prior installs.
51
+ const LUCID_PRE_EDIT_HOOK = {
52
+ matcher: "Write|Edit|MultiEdit|NotebookEdit",
53
+ hooks: [
54
+ {
55
+ type: "command",
56
+ command: `lucid guard pre-edit 2>&1 # ${LUCID_GUARD_MARKER}`,
57
+ },
58
+ ],
59
+ };
60
+ // UserPromptSubmit hook: track session length → emit /compact and /clear hints.
61
+ // stdout from this hook is injected into Claude's context for the upcoming turn.
62
+ const LUCID_SESSION_TICK_HOOK = {
63
+ hooks: [
64
+ {
65
+ type: "command",
66
+ command: `lucid session tick # ${LUCID_SESSION_TICK_MARKER}`,
67
+ },
68
+ ],
69
+ };
70
+ // PreCompact hook: reset per-session counters when /compact runs so the next
71
+ // hint cycle starts from a clean baseline.
72
+ const LUCID_PRE_COMPACT_HOOK = {
73
+ hooks: [
74
+ {
75
+ type: "command",
76
+ command: `lucid session compact # ${LUCID_SESSION_COMPACT_MARKER}`,
77
+ },
78
+ ],
79
+ };
43
80
  // SessionStart hook: checks npm registry and notifies if update is available.
44
81
  // Uses only Node.js built-in https module — no external dependencies required.
45
82
  const LUCID_UPDATE_HOOK = {
@@ -84,6 +121,36 @@ function installHooks(dir) {
84
121
  hooks["PostToolUse"] = [...postToolUse, LUCID_HOOK];
85
122
  changed = true;
86
123
  }
124
+ // ── PreToolUse: backup + truncate guard ──────────────────────────────────
125
+ const preToolUse = hooks["PreToolUse"] ?? [];
126
+ const guardAlreadyInstalled = preToolUse.some((h) => {
127
+ const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
128
+ return cmd.includes(LUCID_GUARD_MARKER);
129
+ });
130
+ if (!guardAlreadyInstalled) {
131
+ hooks["PreToolUse"] = [...preToolUse, LUCID_PRE_EDIT_HOOK];
132
+ changed = true;
133
+ }
134
+ // ── UserPromptSubmit: session-cost hints (/compact, /clear) ──────────────
135
+ const userPromptSubmit = hooks["UserPromptSubmit"] ?? [];
136
+ const sessionTickInstalled = userPromptSubmit.some((h) => {
137
+ const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
138
+ return cmd.includes(LUCID_SESSION_TICK_MARKER);
139
+ });
140
+ if (!sessionTickInstalled) {
141
+ hooks["UserPromptSubmit"] = [...userPromptSubmit, LUCID_SESSION_TICK_HOOK];
142
+ changed = true;
143
+ }
144
+ // ── PreCompact: reset counters when /compact fires ────────────────────────
145
+ const preCompact = hooks["PreCompact"] ?? [];
146
+ const preCompactInstalled = preCompact.some((h) => {
147
+ const cmd = h.command ?? h.hooks?.[0]?.command ?? "";
148
+ return cmd.includes(LUCID_SESSION_COMPACT_MARKER);
149
+ });
150
+ if (!preCompactInstalled) {
151
+ hooks["PreCompact"] = [...preCompact, LUCID_PRE_COMPACT_HOOK];
152
+ changed = true;
153
+ }
87
154
  // ── SessionStart: version check ───────────────────────────────────────────
88
155
  const sessionStart = hooks["SessionStart"] ?? [];
89
156
  const updateAlreadyInstalled = sessionStart.some((h) => {
@@ -116,6 +183,42 @@ sync_file(path="<path of the file you just wrote/edited>")
116
183
 
117
184
  This keeps the Lucid knowledge graph up to date with the latest code.
118
185
  If multiple files changed (refactor, git pull), call sync_project() instead.
186
+
187
+ ## 🛡️ Lucid — Backup & Truncate Guard (automatic)
188
+
189
+ A PreToolUse hook (\`lucid guard pre-edit\`) runs before every Write/Edit/MultiEdit:
190
+ 1. **Snapshot** the file into the versioned backup store (last 10 versions kept).
191
+ 2. **Block** destructive truncates: empty/whitespace overwrite, >70% shrink, or
192
+ ≥2 truncate attempts within 60s (cascade lock — common in autonomous loops).
193
+
194
+ Tools available when you need them:
195
+ - \`backup_file(path)\` — manual snapshot before risky refactors.
196
+ - \`restore_file(path, version=1, dry_run=true)\` — preview/restore from backup.
197
+ - \`check_truncate_risk(path, new_content)\` — assess a planned write.
198
+
199
+ If a guard blocks legitimately (e.g. you really do want to empty a file), set
200
+ \`LUCID_TRUNCATE_OVERRIDE=1\` for that one invocation, or run
201
+ \`lucid guard clear\` to release a cascade lock. Do **not** disable the hook.
202
+
203
+ ## 💸 Lucid — Session-cost hints (/compact, /clear)
204
+
205
+ Two hooks track session length to keep per-turn cost predictable:
206
+
207
+ - **UserPromptSubmit** runs \`lucid session tick\` and may inject one or more of:
208
+ - \`/compact\` hint at **15 prompts**, re-emitted every **10** afterwards.
209
+ - \`/clear\` hint at **30 prompts** (one-shot — preferable when switching tasks).
210
+ - Cache-cold hint when idle gap exceeds **5 min** (prompt cache TTL).
211
+ - **PreCompact** runs \`lucid session compact\` to reset the per-session counter
212
+ so the next hint cycle starts from a clean baseline.
213
+
214
+ When you see a hint, surface it to the user and act on it — the cost model is
215
+ per-turn linear with transcript length even when the cache is warm. Tools:
216
+ - \`session_status\` — inspect prompt counts and pending hints.
217
+ - \`lucid session reset\` — manually reset a single session counter (CLI).
218
+
219
+ Tunable via env: \`LUCID_COMPACT_HINT_AT\`, \`LUCID_COMPACT_HINT_EVERY\`,
220
+ \`LUCID_CLEAR_HINT_AT\`, \`LUCID_CACHE_STALE_SECONDS\`,
221
+ \`LUCID_SESSION_HINTS_DISABLED=1\` to silence completely.
119
222
  <!-- /LUCID_SYNC -->
120
223
  `;
121
224
  function injectClaudeMdInstruction(dir) {
@@ -150,8 +253,11 @@ export async function handleInitProject(stmts, input) {
150
253
  const hookResult = installHooks(dir);
151
254
  if (hookResult.installed) {
152
255
  lines.push(`🔗 Claude Code hooks installed (.claude/settings.json)`);
153
- lines.push(` PostToolUse: reminder to call sync_file() after every Write/Edit`);
154
- lines.push(` SessionStart: auto-check for Lucid updates on session start`);
256
+ lines.push(` PreToolUse: backup + truncate guard before every Write/Edit/MultiEdit`);
257
+ lines.push(` PostToolUse: reminder to call sync_file() after every Write/Edit`);
258
+ lines.push(` UserPromptSubmit: session-cost hints (/compact at 15, /clear at 30, cache-cold)`);
259
+ lines.push(` PreCompact: reset session counters when /compact fires`);
260
+ lines.push(` SessionStart: auto-check for Lucid updates on session start`);
155
261
  }
156
262
  else {
157
263
  lines.push(`🔗 Hooks: ${hookResult.reason}`);
@@ -256,6 +362,22 @@ export async function handleInitProject(stmts, input) {
256
362
  lines.push(` Webhook HMAC signing: set LUCID_WEBHOOK_SECRET env var.`);
257
363
  }
258
364
  }
365
+ // ── Local LLM nudge ───────────────────────────────────────────────────────
366
+ lines.push(``);
367
+ lines.push(`🤖 Local LLM (delegate_local)`);
368
+ if (isLocalLlmConfigured()) {
369
+ const cfg = loadLocalConfig();
370
+ lines.push(` Configured: ${cfg.model} via ${describeRuntime(cfg.runtime)} @ ${cfg.endpoint}`);
371
+ }
372
+ else {
373
+ lines.push(` Not configured. To enable delegation of small specialized tasks to a local`);
374
+ lines.push(` coder LLM (Ollama / LM Studio / llama.cpp / remote endpoint), run in your terminal:`);
375
+ lines.push(``);
376
+ lines.push(` lucid local init`);
377
+ lines.push(``);
378
+ lines.push(` It walks you through runtime detection, model selection, and a reachability test.`);
379
+ lines.push(` Config is saved globally to ~/.lucid/local.json (one setup → all projects).`);
380
+ }
259
381
  lines.push(``);
260
382
  lines.push(`From now on, call sync_file(path) after every file you write or edit.`);
261
383
  lines.push(`Use recall() to query accumulated project knowledge.`);
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import type { Statements } from "../database.js";
3
+ export declare const SessionStatusSchema: z.ZodObject<{
4
+ session_id: z.ZodOptional<z.ZodString>;
5
+ limit: z.ZodOptional<z.ZodNumber>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ session_id?: string | undefined;
8
+ limit?: number | undefined;
9
+ }, {
10
+ session_id?: string | undefined;
11
+ limit?: number | undefined;
12
+ }>;
13
+ export declare function handleSessionStatus(stmts: Statements, args: z.infer<typeof SessionStatusSchema>): string;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { SESSION_TUNABLES } from "../guardian/session-tracker.js";
3
+ // ---------------------------------------------------------------------------
4
+ // session_status — show current session-cost state and hint thresholds
5
+ // ---------------------------------------------------------------------------
6
+ export const SessionStatusSchema = z.object({
7
+ session_id: z.string().optional()
8
+ .describe("Specific session id. Defaults to the most recently active session."),
9
+ limit: z.number().int().positive().max(50).optional()
10
+ .describe("How many recent sessions to list when session_id is omitted (default 5)."),
11
+ });
12
+ export function handleSessionStatus(stmts, args) {
13
+ const limit = args.limit ?? 5;
14
+ if (args.session_id) {
15
+ const row = stmts.getCliSession.get(args.session_id);
16
+ if (!row)
17
+ return `No session found with id=${args.session_id}`;
18
+ return formatSession(row, true);
19
+ }
20
+ const recent = stmts.recentCliSessions.all(limit);
21
+ if (recent.length === 0) {
22
+ return [
23
+ `No Claude Code sessions tracked yet.`,
24
+ ``,
25
+ `Hints fire at:`,
26
+ ` /compact at ${SESSION_TUNABLES.COMPACT_HINT_AT} prompts (re-emitted every ${SESSION_TUNABLES.COMPACT_HINT_EVERY})`,
27
+ ` /clear at ${SESSION_TUNABLES.CLEAR_HINT_AT} prompts (one-shot)`,
28
+ ` cache-cold after ${SESSION_TUNABLES.CACHE_STALE_SECONDS}s idle`,
29
+ ].join("\n");
30
+ }
31
+ const lines = [
32
+ `📊 Recent Claude Code sessions (top ${recent.length}):`,
33
+ ``,
34
+ ];
35
+ for (const r of recent)
36
+ lines.push(formatSession(r, false));
37
+ lines.push(``);
38
+ lines.push(`Thresholds: /compact@${SESSION_TUNABLES.COMPACT_HINT_AT} (+${SESSION_TUNABLES.COMPACT_HINT_EVERY}), ` +
39
+ `/clear@${SESSION_TUNABLES.CLEAR_HINT_AT}, cache-cold>${SESSION_TUNABLES.CACHE_STALE_SECONDS}s`);
40
+ return lines.join("\n");
41
+ }
42
+ function formatSession(r, full) {
43
+ const idle = Math.floor(Date.now() / 1000) - r.last_activity_at;
44
+ const idleStr = idle < 60 ? `${idle}s` : idle < 3600 ? `${Math.round(idle / 60)}m` : `${Math.round(idle / 3600)}h`;
45
+ const status = r.prompt_count >= SESSION_TUNABLES.CLEAR_HINT_AT ? "🔴" :
46
+ r.prompt_count >= SESSION_TUNABLES.COMPACT_HINT_AT ? "🟠" : "🟢";
47
+ const head = `${status} ${r.session_id.slice(0, 8)}… ${r.prompt_count} prompts idle=${idleStr} compacts=${r.compact_count}`;
48
+ if (!full)
49
+ return " " + head;
50
+ return [
51
+ head,
52
+ ` started: ${new Date(r.started_at * 1000).toISOString()}`,
53
+ ` last activ: ${new Date(r.last_activity_at * 1000).toISOString()}`,
54
+ r.last_compact_event_at
55
+ ? ` last compact: ${new Date(r.last_compact_event_at * 1000).toISOString()}`
56
+ : ` last compact: —`,
57
+ r.cwd ? ` cwd: ${r.cwd}` : ``,
58
+ ].filter(Boolean).join("\n");
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a13xu/lucid",
3
- "version": "1.16.2",
3
+ "version": "1.19.0",
4
4
  "description": "Token-efficient memory, code indexing, and validation for Claude Code agents — SQLite + FTS5, TF-IDF + Qdrant retrieval, AST skeleton pruning, diff-aware context, Logic Guardian drift detection",
5
5
  "type": "module",
6
6
  "bin": {