@a13xu/lucid 1.16.1 → 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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/build/compression/semantic.js +5 -1
  3. package/build/database.d.ts +51 -0
  4. package/build/database.js +86 -0
  5. package/build/guardian/checklist.d.ts +2 -1
  6. package/build/guardian/checklist.js +20 -1
  7. package/build/guardian/coding-rules.d.ts +2 -1
  8. package/build/guardian/coding-rules.js +20 -1
  9. package/build/guardian/session-tracker.d.ts +34 -0
  10. package/build/guardian/session-tracker.js +105 -0
  11. package/build/guardian/truncate-guard.d.ts +54 -0
  12. package/build/guardian/truncate-guard.js +136 -0
  13. package/build/index.js +745 -742
  14. package/build/local-llm/client.d.ts +20 -0
  15. package/build/local-llm/client.js +140 -0
  16. package/build/local-llm/config.d.ts +11 -0
  17. package/build/local-llm/config.js +50 -0
  18. package/build/local-llm/runtimes.d.ts +16 -0
  19. package/build/local-llm/runtimes.js +82 -0
  20. package/build/local-llm/setup-cli.d.ts +5 -0
  21. package/build/local-llm/setup-cli.js +298 -0
  22. package/build/local-llm/types.d.ts +34 -0
  23. package/build/local-llm/types.js +5 -0
  24. package/build/tools/backup.d.ts +47 -0
  25. package/build/tools/backup.js +107 -0
  26. package/build/tools/delegate-local.d.ts +23 -0
  27. package/build/tools/delegate-local.js +75 -0
  28. package/build/tools/init.js +124 -2
  29. package/build/tools/plan.js +2 -2
  30. package/build/tools/session.d.ts +13 -0
  31. package/build/tools/session.js +59 -0
  32. package/package.json +3 -1
  33. package/skills/lucid-audit/SKILL.md +11 -0
  34. package/skills/lucid-context/SKILL.md +9 -0
  35. package/skills/lucid-plan/SKILL.md +9 -0
  36. package/skills/lucid-security/SKILL.md +9 -0
  37. package/skills/lucid-start/SKILL.md +9 -0
  38. package/skills/lucid-webdev/SKILL.md +14 -0
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import type { Statements } from "../database.js";
3
+ export declare const BackupFileSchema: z.ZodObject<{
4
+ path: z.ZodString;
5
+ reason: z.ZodOptional<z.ZodString>;
6
+ }, "strip", z.ZodTypeAny, {
7
+ path: string;
8
+ reason?: string | undefined;
9
+ }, {
10
+ path: string;
11
+ reason?: string | undefined;
12
+ }>;
13
+ export declare function handleBackupFile(stmts: Statements, args: z.infer<typeof BackupFileSchema>): string;
14
+ export declare const RestoreFileSchema: z.ZodObject<{
15
+ path: z.ZodString;
16
+ version: z.ZodOptional<z.ZodNumber>;
17
+ backup_id: z.ZodOptional<z.ZodNumber>;
18
+ dry_run: z.ZodOptional<z.ZodBoolean>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ path: string;
21
+ version?: number | undefined;
22
+ backup_id?: number | undefined;
23
+ dry_run?: boolean | undefined;
24
+ }, {
25
+ path: string;
26
+ version?: number | undefined;
27
+ backup_id?: number | undefined;
28
+ dry_run?: boolean | undefined;
29
+ }>;
30
+ export declare function handleRestoreFile(stmts: Statements, args: z.infer<typeof RestoreFileSchema>): string;
31
+ export declare const CheckTruncateRiskSchema: z.ZodObject<{
32
+ path: z.ZodString;
33
+ new_content: z.ZodOptional<z.ZodString>;
34
+ new_size: z.ZodOptional<z.ZodNumber>;
35
+ record: z.ZodOptional<z.ZodBoolean>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ path: string;
38
+ new_content?: string | undefined;
39
+ new_size?: number | undefined;
40
+ record?: boolean | undefined;
41
+ }, {
42
+ path: string;
43
+ new_content?: string | undefined;
44
+ new_size?: number | undefined;
45
+ record?: boolean | undefined;
46
+ }>;
47
+ export declare function handleCheckTruncateRisk(stmts: Statements, args: z.infer<typeof CheckTruncateRiskSchema>): string;
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { resolve } from "path";
3
+ import { writeFileSync } from "fs";
4
+ import { decompress } from "../store/content.js";
5
+ import { assessTruncate, backupFile, recordTruncateEvent, TUNABLES, } from "../guardian/truncate-guard.js";
6
+ // ---------------------------------------------------------------------------
7
+ // backup_file
8
+ // ---------------------------------------------------------------------------
9
+ export const BackupFileSchema = z.object({
10
+ path: z.string().min(1).describe("File to snapshot"),
11
+ reason: z.string().optional().describe("Why this snapshot was taken (logged)"),
12
+ });
13
+ export function handleBackupFile(stmts, args) {
14
+ const result = backupFile(stmts, args.path, args.reason ?? "manual");
15
+ const absPath = resolve(args.path);
16
+ const total = stmts.countBackups.get(absPath)?.count ?? 0;
17
+ if (!result.saved)
18
+ return `⏭️ ${result.reason} (${absPath})`;
19
+ return [
20
+ `📸 Backup created: ${absPath}`,
21
+ ` size: ${result.size}B hash: ${result.hash?.slice(0, 12)}…`,
22
+ ` versions retained: ${Math.min(total, TUNABLES.BACKUP_RETENTION)}/${TUNABLES.BACKUP_RETENTION}`,
23
+ ].join("\n");
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // restore_file
27
+ // ---------------------------------------------------------------------------
28
+ export const RestoreFileSchema = z.object({
29
+ path: z.string().min(1).describe("File to restore"),
30
+ version: z.number().int().positive().optional()
31
+ .describe("1 = latest backup, 2 = previous, etc. Default: 1"),
32
+ backup_id: z.number().int().positive().optional()
33
+ .describe("Specific backup row id (overrides version)"),
34
+ dry_run: z.boolean().optional().describe("Show what would be restored without writing"),
35
+ });
36
+ export function handleRestoreFile(stmts, args) {
37
+ const absPath = resolve(args.path);
38
+ if (args.backup_id !== undefined) {
39
+ const row = stmts.getBackupById.get(args.backup_id);
40
+ if (!row)
41
+ return `❌ Backup id=${args.backup_id} not found`;
42
+ if (row.filepath !== absPath) {
43
+ return `❌ Backup id=${args.backup_id} belongs to ${row.filepath}, not ${absPath}`;
44
+ }
45
+ return doRestore(absPath, row.content, row.created_at, row.original_size, args.dry_run === true);
46
+ }
47
+ const all = stmts.getBackupsByPath.all(absPath);
48
+ if (all.length === 0)
49
+ return `❌ No backups found for: ${absPath}`;
50
+ const idx = (args.version ?? 1) - 1;
51
+ if (idx < 0 || idx >= all.length) {
52
+ return `❌ Version ${args.version} out of range (have ${all.length} backups for this file)`;
53
+ }
54
+ const row = all[idx];
55
+ return doRestore(absPath, row.content, row.created_at, row.original_size, args.dry_run === true);
56
+ }
57
+ function doRestore(absPath, blob, createdAt, originalSize, dryRun) {
58
+ const content = decompress(blob);
59
+ const ts = new Date(createdAt * 1000).toISOString();
60
+ if (dryRun) {
61
+ return [
62
+ `🔍 DRY RUN — would restore ${absPath}`,
63
+ ` from snapshot at ${ts}`,
64
+ ` size: ${originalSize}B (${content.split("\n").length} lines)`,
65
+ ].join("\n");
66
+ }
67
+ writeFileSync(absPath, content, "utf-8");
68
+ return [
69
+ `♻️ Restored: ${absPath}`,
70
+ ` from snapshot at ${ts}`,
71
+ ` size: ${originalSize}B`,
72
+ ].join("\n");
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // check_truncate_risk
76
+ // ---------------------------------------------------------------------------
77
+ export const CheckTruncateRiskSchema = z.object({
78
+ path: z.string().min(1).describe("File path the write would target"),
79
+ new_content: z.string().optional()
80
+ .describe("Proposed new content. Omit to query cascade-lock status only."),
81
+ new_size: z.number().int().nonnegative().optional()
82
+ .describe("Proposed new size in bytes (alternative to new_content)"),
83
+ record: z.boolean().optional()
84
+ .describe("If true, log this as a truncate event (used by hook). Default: false"),
85
+ });
86
+ export function handleCheckTruncateRisk(stmts, args) {
87
+ const probeContent = args.new_content
88
+ ?? (args.new_size !== undefined ? " ".repeat(args.new_size) : null);
89
+ const verdict = assessTruncate(args.path, probeContent, stmts);
90
+ if (args.record === true && verdict.blocked) {
91
+ recordTruncateEvent(stmts, args.path, verdict.prevSize, verdict.newSize, true);
92
+ }
93
+ if (!verdict.blocked) {
94
+ return [
95
+ `✅ Safe write: ${resolve(args.path)}`,
96
+ ` prev: ${verdict.prevSize}B → new: ${verdict.newSize >= 0 ? verdict.newSize + "B" : "?"} ` +
97
+ `(keeps ${Math.round(verdict.shrinkRatio * 100)}%)`,
98
+ ].join("\n");
99
+ }
100
+ return [
101
+ `🛑 BLOCK [${verdict.rule}]: ${resolve(args.path)}`,
102
+ ` ${verdict.reason}`,
103
+ verdict.cascade
104
+ ? ` cascade_count=${verdict.cascadeCount} within ${TUNABLES.CASCADE_WINDOW_SECONDS}s`
105
+ : ` prev=${verdict.prevSize}B new=${verdict.newSize}B ratio=${verdict.shrinkRatio.toFixed(2)}`,
106
+ ].join("\n");
107
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ export declare const DelegateLocalSchema: z.ZodObject<{
3
+ prompt: z.ZodString;
4
+ system: z.ZodOptional<z.ZodString>;
5
+ max_tokens: z.ZodOptional<z.ZodNumber>;
6
+ temperature: z.ZodOptional<z.ZodNumber>;
7
+ model: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ prompt: string;
10
+ model?: string | undefined;
11
+ system?: string | undefined;
12
+ temperature?: number | undefined;
13
+ max_tokens?: number | undefined;
14
+ }, {
15
+ prompt: string;
16
+ model?: string | undefined;
17
+ system?: string | undefined;
18
+ temperature?: number | undefined;
19
+ max_tokens?: number | undefined;
20
+ }>;
21
+ export declare function handleDelegateLocal(args: z.infer<typeof DelegateLocalSchema>): Promise<string>;
22
+ export declare const LocalLlmStatusSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
23
+ export declare function handleLocalLlmStatus(): Promise<string>;
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { loadLocalConfig } from "../local-llm/config.js";
3
+ import { generate, ping, LocalLlmError } from "../local-llm/client.js";
4
+ import { describeRuntime } from "../local-llm/runtimes.js";
5
+ // ---------------------------------------------------------------------------
6
+ // delegate_local — direct passthrough to the configured local LLM
7
+ // ---------------------------------------------------------------------------
8
+ export const DelegateLocalSchema = z.object({
9
+ prompt: z.string().min(1).describe("User prompt for the local model."),
10
+ system: z.string().optional().describe("Optional system prompt (Python coding role, conventions, …)."),
11
+ max_tokens: z.number().int().positive().max(8192).optional().describe("Cap on completion tokens. Default 2048."),
12
+ temperature: z.number().min(0).max(2).optional().describe("Sampling temperature. Default 0.2 (deterministic)."),
13
+ model: z.string().optional().describe("Override the configured default model."),
14
+ });
15
+ export async function handleDelegateLocal(args) {
16
+ const cfg = loadLocalConfig();
17
+ if (!cfg) {
18
+ return [
19
+ `❌ Local LLM not configured.`,
20
+ ` Run in your terminal: lucid local init`,
21
+ ` Then restart Claude Code so the new config is picked up.`,
22
+ ].join("\n");
23
+ }
24
+ if (!cfg.enabled) {
25
+ return `❌ Local LLM is disabled in ${cfg.endpoint} config. Run \`lucid local init\` to re-enable.`;
26
+ }
27
+ const effective = args.model ? { ...cfg, model: args.model } : cfg;
28
+ try {
29
+ const res = await generate(effective, {
30
+ prompt: args.prompt,
31
+ system: args.system,
32
+ max_tokens: args.max_tokens,
33
+ temperature: args.temperature,
34
+ });
35
+ const tokens = res.prompt_tokens !== undefined && res.completion_tokens !== undefined
36
+ ? `prompt=${res.prompt_tokens}, completion=${res.completion_tokens}`
37
+ : "tokens=?";
38
+ return [
39
+ `🤖 ${effective.model} via ${describeRuntime(effective.runtime)} (${res.latency_ms}ms, ${tokens})`,
40
+ ``,
41
+ res.text.trim(),
42
+ ].join("\n");
43
+ }
44
+ catch (e) {
45
+ if (e instanceof LocalLlmError) {
46
+ return `❌ ${e.message}`;
47
+ }
48
+ return `❌ Unexpected error: ${e instanceof Error ? e.message : String(e)}`;
49
+ }
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // local_llm_status — informational
53
+ // ---------------------------------------------------------------------------
54
+ export const LocalLlmStatusSchema = z.object({});
55
+ export async function handleLocalLlmStatus() {
56
+ const cfg = loadLocalConfig();
57
+ if (!cfg) {
58
+ return [
59
+ `Local LLM: not configured.`,
60
+ ``,
61
+ `To set it up, run in your terminal: lucid local init`,
62
+ `It walks you through runtime detection (Ollama / LM Studio / llama.cpp /`,
63
+ `remote endpoint), model selection, and a reachability test.`,
64
+ ].join("\n");
65
+ }
66
+ const reach = await ping(cfg);
67
+ return [
68
+ `Local LLM: ${cfg.enabled ? "enabled" : "disabled"}`,
69
+ ` runtime: ${describeRuntime(cfg.runtime)}`,
70
+ ` endpoint: ${cfg.endpoint}`,
71
+ ` model: ${cfg.model}`,
72
+ ` reachable: ${reach.ok ? `✓ ${reach.latency_ms}ms` : `✗ ${reach.detail ?? "?"}`}`,
73
+ ` saved at: ${cfg.configured_at}`,
74
+ ].join("\n");
75
+ }
@@ -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.`);
@@ -16,10 +16,10 @@ export const PlanListSchema = z.object({
16
16
  status: z.enum(["active", "completed", "abandoned", "all"]).optional().default("active"),
17
17
  });
18
18
  export const PlanGetSchema = z.object({
19
- plan_id: z.number().int().positive(),
19
+ plan_id: z.coerce.number().int().positive(),
20
20
  });
21
21
  export const PlanUpdateTaskSchema = z.object({
22
- task_id: z.number().int().positive(),
22
+ task_id: z.coerce.number().int().positive(),
23
23
  status: z.enum(["pending", "in_progress", "done", "blocked"]),
24
24
  note: z.string().optional(),
25
25
  });
@@ -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.1",
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": {
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
+ "compress-prompts": "node scripts/compress-prompts.mjs",
18
19
  "prepublishOnly": "npm run build"
19
20
  },
20
21
  "keywords": [
@@ -53,6 +54,7 @@
53
54
  "better-sqlite3": "^12.0.0",
54
55
  "chokidar": "^4.0.3",
55
56
  "express": "^5.2.1",
57
+ "sharp": "^0.34.5",
56
58
  "zod": "^3.23.8"
57
59
  },
58
60
  "devDependencies": {
@@ -2,6 +2,17 @@
2
2
  name: lucid-audit
3
3
  description: MANDATORY before marking any task done — runs Logic Guardian + Code Quality checks. HARD-GATE blocks completion without validation.
4
4
  argument-hint: "[file path or 'all changed files']"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__validate_file
8
+ - mcp__lucid__check_drift
9
+ - mcp__lucid__get_checklist
10
+ - mcp__lucid__check_code_quality
11
+ - mcp__lucid__coding_rules
12
+ - mcp__lucid__get_recent
13
+ - mcp__lucid__sync_file
14
+ - Read
15
+ - Glob
5
16
  ---
6
17
 
7
18
  <HARD-GATE>
@@ -2,6 +2,15 @@
2
2
  name: lucid-context
3
3
  description: Use BEFORE starting any coding task — retrieves relevant context via smart_context (code + knowledge graph). HARD-GATE: do not read files manually before calling smart_context.
4
4
  argument-hint: "[what you are working on]"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__smart_context
8
+ - mcp__lucid__get_context
9
+ - mcp__lucid__get_recent
10
+ - mcp__lucid__recall
11
+ - mcp__lucid__grep_code
12
+ - mcp__lucid__reward
13
+ - mcp__lucid__penalize
5
14
  ---
6
15
 
7
16
  <HARD-GATE>
@@ -2,6 +2,15 @@
2
2
  name: lucid-plan
3
3
  description: MANDATORY before writing code for any non-trivial feature — creates a persisted plan with tasks. HARD-GATE: no coding without a plan.
4
4
  argument-hint: "[feature or task description]"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__plan_create
8
+ - mcp__lucid__plan_list
9
+ - mcp__lucid__plan_get
10
+ - mcp__lucid__plan_update_task
11
+ - mcp__lucid__smart_context
12
+ - mcp__lucid__recall
13
+ - mcp__lucid__remember
5
14
  ---
6
15
 
7
16
  <HARD-GATE>
@@ -2,6 +2,15 @@
2
2
  name: lucid-security
3
3
  description: Run before merging any code that handles user input, auth, or external data — security scan + drift check for injection, XSS, and credential exposure.
4
4
  argument-hint: "[file path or directory]"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__security_scan
8
+ - mcp__lucid__check_drift
9
+ - mcp__lucid__validate_file
10
+ - mcp__lucid__get_recent
11
+ - mcp__lucid__grep_code
12
+ - Read
13
+ - Glob
5
14
  ---
6
15
 
7
16
  <HARD-GATE>
@@ -2,6 +2,15 @@
2
2
  name: lucid-start
3
3
  description: MANDATORY at every session start and before any coding task — loads project context via Lucid before Claude reads any file or writes any code
4
4
  argument-hint: "[optional: what you are about to work on]"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__init_project
8
+ - mcp__lucid__sync_file
9
+ - mcp__lucid__sync_project
10
+ - mcp__lucid__memory_stats
11
+ - mcp__lucid__recall
12
+ - mcp__lucid__get_recent
13
+ - mcp__lucid__smart_context
5
14
  ---
6
15
 
7
16
  <HARD-GATE>
@@ -2,6 +2,20 @@
2
2
  name: lucid-webdev
3
3
  description: Use for web development tasks — generates components, pages, audits, API clients, and performance hints via Lucid's 10 web dev tools.
4
4
  argument-hint: "[what you are building: component/page/api/audit]"
5
+ allowed-tools:
6
+ - mcp__lucid__suggest_model
7
+ - mcp__lucid__generate_component
8
+ - mcp__lucid__scaffold_page
9
+ - mcp__lucid__seo_meta
10
+ - mcp__lucid__accessibility_audit
11
+ - mcp__lucid__api_client
12
+ - mcp__lucid__test_generator
13
+ - mcp__lucid__responsive_layout
14
+ - mcp__lucid__security_scan
15
+ - mcp__lucid__design_tokens
16
+ - mcp__lucid__perf_hints
17
+ - Write
18
+ - Edit
5
19
  ---
6
20
 
7
21
  <HARD-GATE>