@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.
- package/build/database.d.ts +51 -0
- package/build/database.js +86 -0
- package/build/guardian/session-tracker.d.ts +34 -0
- package/build/guardian/session-tracker.js +105 -0
- package/build/guardian/truncate-guard.d.ts +54 -0
- package/build/guardian/truncate-guard.js +136 -0
- package/build/index.js +254 -0
- package/build/local-llm/client.d.ts +20 -0
- package/build/local-llm/client.js +140 -0
- package/build/local-llm/config.d.ts +11 -0
- package/build/local-llm/config.js +50 -0
- package/build/local-llm/runtimes.d.ts +16 -0
- package/build/local-llm/runtimes.js +82 -0
- package/build/local-llm/setup-cli.d.ts +5 -0
- package/build/local-llm/setup-cli.js +298 -0
- package/build/local-llm/types.d.ts +34 -0
- package/build/local-llm/types.js +5 -0
- package/build/tools/backup.d.ts +47 -0
- package/build/tools/backup.js +107 -0
- package/build/tools/delegate-local.d.ts +23 -0
- package/build/tools/delegate-local.js +75 -0
- package/build/tools/init.js +124 -2
- package/build/tools/session.d.ts +13 -0
- package/build/tools/session.js +59 -0
- package/package.json +1 -1
package/build/tools/init.js
CHANGED
|
@@ -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(`
|
|
154
|
-
lines.push(`
|
|
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.
|
|
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": {
|