@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.
- package/README.md +1 -1
- package/build/compression/semantic.js +5 -1
- package/build/database.d.ts +51 -0
- package/build/database.js +86 -0
- package/build/guardian/checklist.d.ts +2 -1
- package/build/guardian/checklist.js +20 -1
- package/build/guardian/coding-rules.d.ts +2 -1
- package/build/guardian/coding-rules.js +20 -1
- 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 +745 -742
- 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/plan.js +2 -2
- package/build/tools/session.d.ts +13 -0
- package/build/tools/session.js +59 -0
- package/package.json +3 -1
- package/skills/lucid-audit/SKILL.md +11 -0
- package/skills/lucid-context/SKILL.md +9 -0
- package/skills/lucid-plan/SKILL.md +9 -0
- package/skills/lucid-security/SKILL.md +9 -0
- package/skills/lucid-start/SKILL.md +9 -0
- 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
|
+
}
|
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.`);
|
package/build/tools/plan.js
CHANGED
|
@@ -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.
|
|
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>
|