@a13xu/lucid 1.10.0 → 1.11.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 +15 -1
- package/build/index.js +199 -1
- package/build/instance.d.ts +9 -0
- package/build/instance.js +78 -0
- package/build/tools/e2e.d.ts +13 -0
- package/build/tools/e2e.js +109 -0
- package/build/tools/webdev/accessibility-audit.d.ts +23 -0
- package/build/tools/webdev/accessibility-audit.js +214 -0
- package/build/tools/webdev/api-client.d.ts +24 -0
- package/build/tools/webdev/api-client.js +167 -0
- package/build/tools/webdev/design-tokens.d.ts +18 -0
- package/build/tools/webdev/design-tokens.js +375 -0
- package/build/tools/webdev/generate-component.d.ts +18 -0
- package/build/tools/webdev/generate-component.js +123 -0
- package/build/tools/webdev/index.d.ts +10 -0
- package/build/tools/webdev/index.js +10 -0
- package/build/tools/webdev/perf-hints.d.ts +24 -0
- package/build/tools/webdev/perf-hints.js +247 -0
- package/build/tools/webdev/responsive-layout.d.ts +18 -0
- package/build/tools/webdev/responsive-layout.js +229 -0
- package/build/tools/webdev/scaffold-page.d.ts +18 -0
- package/build/tools/webdev/scaffold-page.js +137 -0
- package/build/tools/webdev/security-scan.d.ts +23 -0
- package/build/tools/webdev/security-scan.js +247 -0
- package/build/tools/webdev/seo-meta.d.ts +24 -0
- package/build/tools/webdev/seo-meta.js +114 -0
- package/build/tools/webdev/test-generator.d.ts +18 -0
- package/build/tools/webdev/test-generator.js +269 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Default DB path: `~/.claude/memory.db`
|
|
|
53
53
|
6. "What do we know?" → recall("query") → knowledge graph search
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## Tools (
|
|
56
|
+
## Tools (30)
|
|
57
57
|
|
|
58
58
|
### Memory
|
|
59
59
|
| Tool | Description |
|
|
@@ -99,6 +99,20 @@ Default DB path: `~/.claude/memory.db`
|
|
|
99
99
|
| `coding_rules` | Get the 25 Golden Rules checklist — naming, single responsibility, file/function size, error handling, frontend component rules, architecture separation. |
|
|
100
100
|
| `check_code_quality` | Analyze a file or snippet against the 25 Golden Rules. Detects file/function bloat, vague naming, deep nesting, dead code, and for React/Vue files: prop explosion, inline styles, fetch-in-component, direct DOM access. Complements `validate_file`. |
|
|
101
101
|
|
|
102
|
+
### Web Dev Skills
|
|
103
|
+
| Tool | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `generate_component` | Generate a complete component scaffold from a natural language description. Supports React (TSX/JSX) and Vue/Nuxt (`<script setup>` Composition API). Styling: Tailwind, CSS Modules, or none. |
|
|
106
|
+
| `scaffold_page` | Generate a full page with layout, SEO head, and placeholder sections. Supports Nuxt (`useHead`), Next.js (`Metadata` API), and plain Vue. |
|
|
107
|
+
| `seo_meta` | Generate complete SEO metadata: HTML meta tags, Open Graph, Twitter Card, and JSON-LD structured data (Article, Product, WebSite, WebPage). |
|
|
108
|
+
| `accessibility_audit` | Audit HTML/JSX/Vue snippets for WCAG A/AA/AAA violations. Checks missing alt text, unlabeled inputs, empty buttons/links, positive tabindex, non-interactive click handlers, and more. Returns severity + corrected code. |
|
|
109
|
+
| `api_client` | Generate a typed TypeScript async fetch function for a REST endpoint. Includes error handling (throws on non-2xx), full type aliases, and a usage example. Auth: Bearer, cookie, API key, or none. |
|
|
110
|
+
| `test_generator` | Generate a complete test file covering happy path, edge cases, error path, and mock setup. Frameworks: Vitest, Jest, Playwright. Component testing: Vue Test Utils or React Testing Library. |
|
|
111
|
+
| `responsive_layout` | Generate a mobile-first responsive layout from a wireframe description. Output: Tailwind utility classes, CSS Grid with named areas, or Flexbox + media queries. Container types: full, centered, sidebar. |
|
|
112
|
+
| `security_scan` | Scan JS/TS/HTML/Vue for web security vulnerabilities: XSS, eval/injection, SQL injection, hardcoded secrets, open redirects, prototype pollution, path traversal, insecure CORS. Context-aware (frontend/backend/api). |
|
|
113
|
+
| `design_tokens` | Generate a complete design token set from a brand color and mood. Produces 11-step color scales (50–950), neutral scale, semantic aliases, typography, spacing, radius, and shadows. Output: CSS variables, Tailwind config, or JSON. |
|
|
114
|
+
| `perf_hints` | Analyze a component or page for Core Web Vitals issues (LCP, CLS, INP) and perf anti-patterns: missing image dimensions, render-blocking scripts, fetch-in-render, heavy click handlers, missing useMemo/computed, whole-library imports. |
|
|
115
|
+
|
|
102
116
|
## Token optimization in depth
|
|
103
117
|
|
|
104
118
|
### How `get_context` works
|
package/build/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { handleGetContext, GetContextSchema, handleGetRecent, GetRecentSchema, }
|
|
|
21
21
|
import { handleReward, RewardSchema, handlePenalize, PenalizeSchema, handleShowRewards, ShowRewardsSchema, } from "./tools/reward.js";
|
|
22
22
|
import { handleGetCodingRules, handleCheckCodeQuality, CheckCodeQualitySchema, } from "./tools/coding-guard.js";
|
|
23
23
|
import { handlePlanCreate, PlanCreateSchema, handlePlanList, PlanListSchema, handlePlanGet, PlanGetSchema, handlePlanUpdateTask, PlanUpdateTaskSchema, } from "./tools/plan.js";
|
|
24
|
+
import { GenerateComponentSchema, handleGenerateComponent, ScaffoldPageSchema, handleScaffoldPage, SeoMetaSchema, handleSeoMeta, AccessibilityAuditSchema, handleAccessibilityAudit, ApiClientSchema, handleApiClient, TestGeneratorSchema, handleTestGenerator, ResponsiveLayoutSchema, handleResponsiveLayout, SecurityScanSchema, handleSecurityScan, DesignTokensSchema, handleDesignTokens, PerfHintsSchema, handlePerfHints, } from "./tools/webdev/index.js";
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Init DB
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -53,7 +54,7 @@ else {
|
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
// MCP Server
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
56
|
-
const server = new Server({ name: "lucid", version: "1.
|
|
57
|
+
const server = new Server({ name: "lucid", version: "1.11.0" }, { capabilities: { tools: {} } });
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
58
59
|
// Tool definitions
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
@@ -392,6 +393,172 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
392
393
|
required: ["task_id", "status"],
|
|
393
394
|
},
|
|
394
395
|
},
|
|
396
|
+
// ── Web Dev Skills ───────────────────────────────────────────────────────
|
|
397
|
+
{
|
|
398
|
+
name: "generate_component",
|
|
399
|
+
description: "Generate a complete component scaffold from a natural language description. " +
|
|
400
|
+
"Supports React (TSX/JSX) and Vue/Nuxt (Composition API + <script setup>). " +
|
|
401
|
+
"Styling options: Tailwind CSS, CSS Modules, or none.",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
description: { type: "string", description: "Natural language description of the component" },
|
|
406
|
+
framework: { type: "string", enum: ["react", "vue", "nuxt"], description: "Target framework" },
|
|
407
|
+
styling: { type: "string", enum: ["tailwind", "css-modules", "none"], description: "Styling approach" },
|
|
408
|
+
typescript: { type: "boolean", description: "Whether to use TypeScript" },
|
|
409
|
+
},
|
|
410
|
+
required: ["description", "framework", "styling", "typescript"],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "scaffold_page",
|
|
415
|
+
description: "Generate a full page scaffold with layout, SEO head meta, and placeholder sections. " +
|
|
416
|
+
"Supports Nuxt (useHead), Next.js (Metadata API), and plain Vue.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
page_name: { type: "string", description: "Page name (e.g. About, Dashboard)" },
|
|
421
|
+
framework: { type: "string", enum: ["nuxt", "next", "vue"], description: "Target framework" },
|
|
422
|
+
sections: { type: "array", items: { type: "string" }, description: "Section names (e.g. hero, features, footer)" },
|
|
423
|
+
seo_title: { type: "string", description: "Optional SEO title" },
|
|
424
|
+
},
|
|
425
|
+
required: ["page_name", "framework", "sections"],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "seo_meta",
|
|
430
|
+
description: "Generate complete SEO metadata for a page: HTML meta tags, Open Graph, Twitter Card, " +
|
|
431
|
+
"and JSON-LD structured data (Article, Product, WebSite, or WebPage).",
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
title: { type: "string", description: "Page title" },
|
|
436
|
+
description: { type: "string", description: "Meta description (≤160 chars recommended)" },
|
|
437
|
+
keywords: { type: "array", items: { type: "string" }, description: "SEO keywords" },
|
|
438
|
+
page_type: { type: "string", enum: ["article", "product", "landing", "home"], description: "Page type for JSON-LD" },
|
|
439
|
+
url: { type: "string", description: "Canonical page URL" },
|
|
440
|
+
image_url: { type: "string", description: "OG/Twitter image URL" },
|
|
441
|
+
},
|
|
442
|
+
required: ["title", "description", "keywords", "page_type"],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "accessibility_audit",
|
|
447
|
+
description: "Audit HTML, JSX, or Vue template snippets for WCAG accessibility violations. " +
|
|
448
|
+
"Checks: missing alt text, unlabeled inputs, empty buttons/links, positive tabindex, " +
|
|
449
|
+
"non-interactive click handlers, open-in-new-tab links, and more. " +
|
|
450
|
+
"Returns severity (critical/warning/info), WCAG criterion, and corrected code.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
code: { type: "string", description: "HTML, JSX, or Vue snippet to audit" },
|
|
455
|
+
wcag_level: { type: "string", enum: ["A", "AA", "AAA"], description: "WCAG conformance level" },
|
|
456
|
+
framework: { type: "string", enum: ["html", "jsx", "vue"], description: "Code framework" },
|
|
457
|
+
},
|
|
458
|
+
required: ["code", "wcag_level", "framework"],
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "api_client",
|
|
463
|
+
description: "Generate a typed TypeScript async function for a REST API endpoint. " +
|
|
464
|
+
"Includes full types, error handling (throws on non-2xx), and a usage example. " +
|
|
465
|
+
"Auth strategies: Bearer token, cookie, API key, or none.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
endpoint: { type: "string", description: "API endpoint path (e.g. /users/:id)" },
|
|
470
|
+
method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
471
|
+
request_schema: { type: "string", description: "TypeScript type for request body" },
|
|
472
|
+
response_schema: { type: "string", description: "TypeScript type for response" },
|
|
473
|
+
auth: { type: "string", enum: ["bearer", "cookie", "apikey", "none"] },
|
|
474
|
+
base_url_var: { type: "string", description: "Env var name for base URL (e.g. NEXT_PUBLIC_API_URL)" },
|
|
475
|
+
},
|
|
476
|
+
required: ["endpoint", "method", "auth"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "test_generator",
|
|
481
|
+
description: "Generate a complete test file for a function, component, or API handler. " +
|
|
482
|
+
"Covers: happy path, edge cases (empty/null/boundary), error path, and mock setup. " +
|
|
483
|
+
"Frameworks: Vitest, Jest, or Playwright (e2e). " +
|
|
484
|
+
"Component testing: Vue Test Utils or React Testing Library.",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
type: "object",
|
|
487
|
+
properties: {
|
|
488
|
+
code: { type: "string", description: "Source code to generate tests for" },
|
|
489
|
+
test_framework: { type: "string", enum: ["vitest", "jest", "playwright"] },
|
|
490
|
+
test_type: { type: "string", enum: ["unit", "integration", "e2e"] },
|
|
491
|
+
component_framework: { type: "string", enum: ["vue", "react", "none"] },
|
|
492
|
+
},
|
|
493
|
+
required: ["code", "test_framework", "test_type"],
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "responsive_layout",
|
|
498
|
+
description: "Generate a responsive mobile-first layout from a wireframe description. " +
|
|
499
|
+
"Outputs: Tailwind CSS utility classes, CSS Grid with named areas, or Flexbox with media queries. " +
|
|
500
|
+
"Container types: full-width, centered max-width, or sidebar layout.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {
|
|
504
|
+
description: { type: "string", description: "Wireframe description (e.g. 'sidebar + main + right panel')" },
|
|
505
|
+
framework: { type: "string", enum: ["tailwind", "css-grid", "flexbox"] },
|
|
506
|
+
breakpoints: { type: "array", items: { type: "string" }, description: "Breakpoint names (e.g. ['mobile', 'tablet', 'desktop'])" },
|
|
507
|
+
container: { type: "string", enum: ["full", "centered", "sidebar"] },
|
|
508
|
+
},
|
|
509
|
+
required: ["description", "framework", "breakpoints"],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "security_scan",
|
|
514
|
+
description: "Scan JavaScript/TypeScript/HTML/Vue code for common web security vulnerabilities. " +
|
|
515
|
+
"Detects: XSS (innerHTML, v-html, dangerouslySetInnerHTML), code injection (eval, new Function), " +
|
|
516
|
+
"SQL injection, hardcoded secrets, open redirects, prototype pollution, path traversal, " +
|
|
517
|
+
"render-blocking scripts, and insecure CORS. Context-aware: frontend vs backend vs API rules. " +
|
|
518
|
+
"Complements validate_file (logic drift) — this focuses on web security patterns.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
code: { type: "string", description: "Code snippet to scan" },
|
|
523
|
+
language: { type: "string", enum: ["javascript", "typescript", "html", "vue"] },
|
|
524
|
+
context: { type: "string", enum: ["frontend", "backend", "api"] },
|
|
525
|
+
},
|
|
526
|
+
required: ["code", "language", "context"],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "design_tokens",
|
|
531
|
+
description: "Generate a complete design system token set from a brand color and mood. " +
|
|
532
|
+
"Produces: 11-step color scales (50–950), neutral scale, semantic color aliases, " +
|
|
533
|
+
"typography scale, spacing, border-radius, and shadow tokens. " +
|
|
534
|
+
"Output formats: CSS custom properties, Tailwind config, or JSON.",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {
|
|
538
|
+
brand_name: { type: "string", description: "Brand or project name" },
|
|
539
|
+
primary_color: { type: "string", description: "Primary color as hex (#3B82F6) or name (blue)" },
|
|
540
|
+
mood: { type: "string", enum: ["minimal", "bold", "playful", "corporate"] },
|
|
541
|
+
output_format: { type: "string", enum: ["css-variables", "tailwind-config", "json"] },
|
|
542
|
+
},
|
|
543
|
+
required: ["brand_name", "primary_color", "mood", "output_format"],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "perf_hints",
|
|
548
|
+
description: "Analyze a component or page file for Core Web Vitals (CWV) and web performance issues. " +
|
|
549
|
+
"Detects: missing LCP image priority, images without dimensions (CLS), render-blocking scripts, " +
|
|
550
|
+
"fetch-in-render (TTFB), heavy click handlers (INP), missing useMemo/computed, " +
|
|
551
|
+
"whole-library imports, and inline style objects. Issues ranked by CWV metric impact.",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
code: { type: "string", description: "Component or page source code to analyze" },
|
|
556
|
+
framework: { type: "string", enum: ["react", "vue", "nuxt", "vanilla"] },
|
|
557
|
+
context: { type: "string", enum: ["component", "page", "layout"] },
|
|
558
|
+
},
|
|
559
|
+
required: ["code", "framework", "context"],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
395
562
|
],
|
|
396
563
|
}));
|
|
397
564
|
// ---------------------------------------------------------------------------
|
|
@@ -487,6 +654,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
487
654
|
case "plan_update_task":
|
|
488
655
|
text = handlePlanUpdateTask(stmts, PlanUpdateTaskSchema.parse(args));
|
|
489
656
|
break;
|
|
657
|
+
// Web Dev Skills
|
|
658
|
+
case "generate_component":
|
|
659
|
+
text = handleGenerateComponent(GenerateComponentSchema.parse(args));
|
|
660
|
+
break;
|
|
661
|
+
case "scaffold_page":
|
|
662
|
+
text = handleScaffoldPage(ScaffoldPageSchema.parse(args));
|
|
663
|
+
break;
|
|
664
|
+
case "seo_meta":
|
|
665
|
+
text = handleSeoMeta(SeoMetaSchema.parse(args));
|
|
666
|
+
break;
|
|
667
|
+
case "accessibility_audit":
|
|
668
|
+
text = handleAccessibilityAudit(AccessibilityAuditSchema.parse(args));
|
|
669
|
+
break;
|
|
670
|
+
case "api_client":
|
|
671
|
+
text = handleApiClient(ApiClientSchema.parse(args));
|
|
672
|
+
break;
|
|
673
|
+
case "test_generator":
|
|
674
|
+
text = handleTestGenerator(TestGeneratorSchema.parse(args));
|
|
675
|
+
break;
|
|
676
|
+
case "responsive_layout":
|
|
677
|
+
text = handleResponsiveLayout(ResponsiveLayoutSchema.parse(args));
|
|
678
|
+
break;
|
|
679
|
+
case "security_scan":
|
|
680
|
+
text = handleSecurityScan(SecurityScanSchema.parse(args));
|
|
681
|
+
break;
|
|
682
|
+
case "design_tokens":
|
|
683
|
+
text = handleDesignTokens(DesignTokensSchema.parse(args));
|
|
684
|
+
break;
|
|
685
|
+
case "perf_hints":
|
|
686
|
+
text = handlePerfHints(PerfHintsSchema.parse(args));
|
|
687
|
+
break;
|
|
490
688
|
default:
|
|
491
689
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
492
690
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface InstanceInfo {
|
|
3
|
+
instance_id: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function registerInstance(db: Database.Database): InstanceInfo;
|
|
8
|
+
export declare function getInstanceInfo(): InstanceInfo | null;
|
|
9
|
+
export declare function logAction(db: Database.Database, tool_name: string, args: unknown, result_ok: boolean, duration_ms: number): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
const MAX_ACTIONS_KEPT = 200;
|
|
3
|
+
// Fields whose values may be large blobs — truncate in stored args
|
|
4
|
+
const BLOB_FIELDS = new Set(["code", "content", "observations", "body", "data", "text"]);
|
|
5
|
+
let _instanceInfo = null;
|
|
6
|
+
function sanitizeArgs(args) {
|
|
7
|
+
if (!args || typeof args !== "object")
|
|
8
|
+
return JSON.stringify(args ?? {});
|
|
9
|
+
const copy = {};
|
|
10
|
+
for (const [k, v] of Object.entries(args)) {
|
|
11
|
+
if (typeof v === "string" && BLOB_FIELDS.has(k) && v.length > 200) {
|
|
12
|
+
copy[k] = `[${v.length} chars]`;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
copy[k] = v;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(copy);
|
|
19
|
+
}
|
|
20
|
+
export function registerInstance(db) {
|
|
21
|
+
if (_instanceInfo)
|
|
22
|
+
return _instanceInfo;
|
|
23
|
+
const instance_id = randomUUID();
|
|
24
|
+
const pid = process.pid;
|
|
25
|
+
const label = process.env["LUCID_INSTANCE_LABEL"] ?? "";
|
|
26
|
+
db.prepare("INSERT OR REPLACE INTO instances (instance_id, pid, label, started_at, last_heartbeat, status) VALUES (?, ?, ?, unixepoch(), unixepoch(), 'active')").run(instance_id, pid, label);
|
|
27
|
+
_instanceInfo = { instance_id, pid, label };
|
|
28
|
+
// Prepared statements for heartbeat (compiled once)
|
|
29
|
+
const hbStmt = db.prepare("UPDATE instances SET last_heartbeat = unixepoch(), status = 'active' WHERE instance_id = ?");
|
|
30
|
+
const staleStmt = db.prepare("UPDATE instances SET status = 'stale' WHERE status = 'active' AND last_heartbeat < unixepoch() - 45 AND instance_id != ?");
|
|
31
|
+
const deadStmt = db.prepare("UPDATE instances SET status = 'dead' WHERE status != 'dead' AND last_heartbeat < unixepoch() - 120 AND instance_id != ?");
|
|
32
|
+
const timer = setInterval(() => {
|
|
33
|
+
try {
|
|
34
|
+
hbStmt.run(instance_id);
|
|
35
|
+
staleStmt.run(instance_id);
|
|
36
|
+
deadStmt.run(instance_id);
|
|
37
|
+
}
|
|
38
|
+
catch { /* suppress — never break the tool handler */ }
|
|
39
|
+
}, 15_000);
|
|
40
|
+
timer.unref();
|
|
41
|
+
const markDead = () => {
|
|
42
|
+
try {
|
|
43
|
+
db.prepare("UPDATE instances SET status = 'dead' WHERE instance_id = ?").run(instance_id);
|
|
44
|
+
}
|
|
45
|
+
catch { /* suppress */ }
|
|
46
|
+
};
|
|
47
|
+
// "exit" fires during an already-in-progress exit — no process.exit() needed
|
|
48
|
+
process.once("exit", markDead);
|
|
49
|
+
// SIGTERM/SIGINT: mark dead then let the process exit (must call process.exit()
|
|
50
|
+
// explicitly, otherwise the signal listener swallows the default exit behaviour)
|
|
51
|
+
process.once("SIGTERM", () => { markDead(); process.exit(0); });
|
|
52
|
+
process.once("SIGINT", () => { markDead(); process.exit(0); });
|
|
53
|
+
console.error(`[lucid] Instance registered: ${instance_id} (PID ${pid})`);
|
|
54
|
+
return _instanceInfo;
|
|
55
|
+
}
|
|
56
|
+
export function getInstanceInfo() {
|
|
57
|
+
return _instanceInfo;
|
|
58
|
+
}
|
|
59
|
+
export function logAction(db, tool_name, args, result_ok, duration_ms) {
|
|
60
|
+
if (!_instanceInfo)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
const args_json = sanitizeArgs(args);
|
|
64
|
+
db.prepare("INSERT INTO instance_actions (instance_id, tool_name, args_json, result_ok, duration_ms) VALUES (?, ?, ?, ?, ?)").run(_instanceInfo.instance_id, tool_name, args_json, result_ok ? 1 : 0, duration_ms);
|
|
65
|
+
// Keep only the last MAX_ACTIONS_KEPT rows per instance
|
|
66
|
+
db.prepare(`
|
|
67
|
+
DELETE FROM instance_actions
|
|
68
|
+
WHERE instance_id = ?
|
|
69
|
+
AND id NOT IN (
|
|
70
|
+
SELECT id FROM instance_actions
|
|
71
|
+
WHERE instance_id = ?
|
|
72
|
+
ORDER BY id DESC
|
|
73
|
+
LIMIT ${MAX_ACTIONS_KEPT}
|
|
74
|
+
)
|
|
75
|
+
`).run(_instanceInfo.instance_id, _instanceInfo.instance_id);
|
|
76
|
+
}
|
|
77
|
+
catch { /* suppress — never propagate to caller */ }
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type Database from "better-sqlite3";
|
|
3
|
+
import type { Statements } from "../database.js";
|
|
4
|
+
export declare const RunE2eTestSchema: z.ZodObject<{
|
|
5
|
+
task_id: z.ZodNumber;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
task_id: number;
|
|
8
|
+
}, {
|
|
9
|
+
task_id: number;
|
|
10
|
+
}>;
|
|
11
|
+
type RunE2eTestArgs = z.infer<typeof RunE2eTestSchema>;
|
|
12
|
+
export declare function handleRunE2eTest(db: Database.Database, stmts: Statements, args: RunE2eTestArgs): string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Schema
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export const RunE2eTestSchema = z.object({
|
|
7
|
+
task_id: z.number().int().positive(),
|
|
8
|
+
});
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Handler
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function handleRunE2eTest(db, stmts, args) {
|
|
13
|
+
const { task_id } = args;
|
|
14
|
+
const task = stmts.getTaskById.get(task_id);
|
|
15
|
+
if (!task)
|
|
16
|
+
return `Error: Task #${task_id} not found.`;
|
|
17
|
+
if (!task.is_e2e)
|
|
18
|
+
return `Error: Task #${task_id} is not an E2E task.`;
|
|
19
|
+
const criteria = task.test_criteria.trim();
|
|
20
|
+
if (!criteria)
|
|
21
|
+
return `Error: Task #${task_id} has empty test_criteria.`;
|
|
22
|
+
const newRetryCount = task.retry_count + 1;
|
|
23
|
+
// Execute test_criteria as a shell command
|
|
24
|
+
const result = spawnSync(criteria, {
|
|
25
|
+
shell: true,
|
|
26
|
+
timeout: 60_000,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
maxBuffer: 1024 * 1024,
|
|
29
|
+
});
|
|
30
|
+
const exitCode = result.status ?? 1;
|
|
31
|
+
const stdout = (result.stdout ?? "").trim();
|
|
32
|
+
const stderr = (result.stderr ?? "").trim();
|
|
33
|
+
const timedOut = result.error?.message?.includes("ETIMEDOUT") ||
|
|
34
|
+
result.signal === "SIGTERM";
|
|
35
|
+
const passed = exitCode === 0 && !timedOut;
|
|
36
|
+
const e2eResult = passed ? "pass" : "fail";
|
|
37
|
+
let e2eError = null;
|
|
38
|
+
if (!passed) {
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (timedOut)
|
|
41
|
+
parts.push("Timed out after 60s");
|
|
42
|
+
if (stderr)
|
|
43
|
+
parts.push(stderr);
|
|
44
|
+
if (!timedOut && !stderr && stdout)
|
|
45
|
+
parts.push(stdout);
|
|
46
|
+
if (result.error && !timedOut)
|
|
47
|
+
parts.push(result.error.message);
|
|
48
|
+
e2eError = parts.filter(Boolean).join("\n") || `Exit code: ${exitCode}`;
|
|
49
|
+
}
|
|
50
|
+
// Update e2e_result, e2e_error, retry_count in DB
|
|
51
|
+
stmts.updateTaskE2eResult.run(e2eResult, e2eError, newRetryCount, task_id);
|
|
52
|
+
// If passed, also mark the task as done
|
|
53
|
+
if (passed) {
|
|
54
|
+
const plan = stmts.getPlanById.get(task.plan_id);
|
|
55
|
+
const maxRetries = plan?.max_retries ?? 3;
|
|
56
|
+
let notes = [];
|
|
57
|
+
try {
|
|
58
|
+
notes = JSON.parse(task.notes);
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
notes.push({ text: `E2E passed on attempt ${newRetryCount}`, ts: Math.floor(Date.now() / 1000) });
|
|
62
|
+
stmts.updateTaskStatus.run("done", JSON.stringify(notes), task_id);
|
|
63
|
+
const remaining = stmts.countRemainingTasks.get(task.plan_id);
|
|
64
|
+
if (remaining && remaining.count === 0) {
|
|
65
|
+
stmts.updatePlanStatus.run("completed", task.plan_id);
|
|
66
|
+
}
|
|
67
|
+
const lines = [
|
|
68
|
+
`[E2E PASS] Task #${task_id} — ${task.title}`,
|
|
69
|
+
`Attempt: ${newRetryCount}`,
|
|
70
|
+
];
|
|
71
|
+
if (stdout)
|
|
72
|
+
lines.push(`Output: ${stdout}`);
|
|
73
|
+
lines.push(`Task status updated to done.`);
|
|
74
|
+
if (remaining && remaining.count === 0) {
|
|
75
|
+
lines.push(`Plan #${task.plan_id} completed.`);
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
// Failed — check retry budget
|
|
80
|
+
const plan = stmts.getPlanById.get(task.plan_id);
|
|
81
|
+
const maxRetries = plan?.max_retries ?? 3;
|
|
82
|
+
const retriesLeft = maxRetries - newRetryCount;
|
|
83
|
+
const lines = [
|
|
84
|
+
`[E2E FAIL] Task #${task_id} — ${task.title}`,
|
|
85
|
+
`Attempt: ${newRetryCount}/${maxRetries}`,
|
|
86
|
+
`Error: ${e2eError ?? "unknown"}`,
|
|
87
|
+
];
|
|
88
|
+
if (stdout)
|
|
89
|
+
lines.push(`Stdout: ${stdout}`);
|
|
90
|
+
if (retriesLeft > 0) {
|
|
91
|
+
// Create a [FIX] remediation task for this iteration
|
|
92
|
+
const errorSummary = (e2eError ?? "unknown error").slice(0, 100).replace(/\n/g, " ");
|
|
93
|
+
const fixTitle = `[FIX] Iteration ${newRetryCount}: ${errorSummary}`;
|
|
94
|
+
const fixDescription = `Fix the E2E failure from iteration ${newRetryCount}.\nError: ${e2eError ?? "unknown"}\nAfter fixing, mark this task done to trigger automatic E2E re-run.`;
|
|
95
|
+
const maxSeqRow = stmts.getMaxSeqForPlan.get(task.plan_id);
|
|
96
|
+
const nextSeq = (maxSeqRow?.max_seq ?? 0) + 1;
|
|
97
|
+
const fixResult = stmts.insertFixTask.run(task.plan_id, nextSeq, fixTitle, fixDescription, task.test_criteria, task_id);
|
|
98
|
+
const fixTaskId = fixResult.lastInsertRowid;
|
|
99
|
+
lines.push(`Retries left: ${retriesLeft}.`);
|
|
100
|
+
lines.push(`Created [FIX] task #${fixTaskId} "${fixTitle}".`);
|
|
101
|
+
lines.push(`Fix the issue and mark task #${fixTaskId} done — this will automatically re-run the E2E test.`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Exhausted all retries — mark plan as e2e_failed
|
|
105
|
+
stmts.updatePlanStatus.run("e2e_failed", task.plan_id);
|
|
106
|
+
lines.push(`No retries left. Plan #${task.plan_id} marked as e2e_failed.`);
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AccessibilityAuditSchema: z.ZodObject<{
|
|
3
|
+
code: z.ZodString;
|
|
4
|
+
wcag_level: z.ZodEnum<["A", "AA", "AAA"]>;
|
|
5
|
+
framework: z.ZodEnum<["html", "jsx", "vue"]>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
code: string;
|
|
8
|
+
framework: "vue" | "jsx" | "html";
|
|
9
|
+
wcag_level: "A" | "AA" | "AAA";
|
|
10
|
+
}, {
|
|
11
|
+
code: string;
|
|
12
|
+
framework: "vue" | "jsx" | "html";
|
|
13
|
+
wcag_level: "A" | "AA" | "AAA";
|
|
14
|
+
}>;
|
|
15
|
+
export type A11ySeverity = "critical" | "warning" | "info";
|
|
16
|
+
export interface A11yIssue {
|
|
17
|
+
line: number;
|
|
18
|
+
severity: A11ySeverity;
|
|
19
|
+
criterion: string;
|
|
20
|
+
message: string;
|
|
21
|
+
fix: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function handleAccessibilityAudit(args: z.infer<typeof AccessibilityAuditSchema>): string;
|