@shahmilsaari/memory-core 0.2.8 → 0.2.11
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 +221 -58
- package/dist/chunk-73SRPNAL.js +196 -0
- package/dist/chunk-HAGRPKR3.js +30 -0
- package/dist/chunk-KSLFLWB4.js +32 -0
- package/dist/cli.js +935 -386
- package/dist/db-KU4EEG4Y.js +28 -0
- package/dist/embedding-PAYD2JYW.js +8 -0
- package/package.json +4 -2
- package/profiles/go-api.yml +43 -0
- package/profiles/nextjs.yml +0 -32
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
embed
|
|
4
|
+
} from "./chunk-HAGRPKR3.js";
|
|
5
|
+
import {
|
|
6
|
+
closePool,
|
|
7
|
+
deleteMemory,
|
|
8
|
+
getMemory,
|
|
9
|
+
listMemories,
|
|
10
|
+
runMigrations,
|
|
11
|
+
saveMemory,
|
|
12
|
+
searchMemories,
|
|
13
|
+
updateMemory,
|
|
14
|
+
upsertMemory
|
|
15
|
+
} from "./chunk-73SRPNAL.js";
|
|
16
|
+
import "./chunk-KSLFLWB4.js";
|
|
2
17
|
|
|
3
18
|
// src/cli.ts
|
|
4
19
|
import { Command } from "commander";
|
|
5
20
|
import { input, select, confirm } from "@inquirer/prompts";
|
|
6
21
|
import chalk3 from "chalk";
|
|
7
22
|
import ora from "ora";
|
|
8
|
-
import { readFileSync as
|
|
23
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync2, appendFileSync, rmSync, unlinkSync as unlinkSync2 } from "fs";
|
|
9
24
|
import { join as join6, dirname as dirname2 } from "path";
|
|
10
25
|
import { homedir } from "os";
|
|
11
|
-
import { execSync as
|
|
26
|
+
import { execSync as execSync2 } from "child_process";
|
|
12
27
|
|
|
13
28
|
// src/generator.ts
|
|
14
29
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
@@ -317,30 +332,34 @@ var seeds = [
|
|
|
317
332
|
{ type: "rule", scope: "global", architecture: "hexagonal", title: "Port naming convention", content: "Name ports as I + [Action] + [Resource]: ICreateUser, IFindOrderById, ISendEmail. Adapters use the technology name: PostgresCreateUser, StripeCharge.", tags: ["naming", "ports", "hexagonal"] },
|
|
318
333
|
{ type: "decision", scope: "global", architecture: "hexagonal", title: "Test core without infrastructure", content: "Core unit tests run in milliseconds with no DB or network. Integration tests add adapters one at a time. E2E tests use real infrastructure.", tags: ["testing", "hexagonal"] },
|
|
319
334
|
// ══════════════════════════════════════════════════════════════════════════
|
|
320
|
-
//
|
|
335
|
+
// GO REST API
|
|
321
336
|
// ══════════════════════════════════════════════════════════════════════════
|
|
322
|
-
// ──
|
|
323
|
-
{ type: "rule", scope: "global", architecture: "
|
|
324
|
-
{ type: "rule", scope: "global", architecture: "
|
|
325
|
-
{ type: "rule", scope: "global", architecture: "
|
|
326
|
-
{ type: "rule", scope: "global", architecture: "
|
|
327
|
-
|
|
328
|
-
{ type: "
|
|
329
|
-
|
|
330
|
-
{ type: "rule", scope: "global", architecture: "
|
|
331
|
-
{ type: "
|
|
332
|
-
|
|
333
|
-
{ type: "rule", scope: "global", architecture: "
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
{ type: "rule", scope: "global", architecture: "
|
|
337
|
-
{ type: "rule", scope: "global", architecture: "
|
|
338
|
-
// ──
|
|
339
|
-
{ type: "rule", scope: "global", architecture: "
|
|
340
|
-
{ type: "rule", scope: "global", architecture: "
|
|
341
|
-
|
|
342
|
-
{ type: "
|
|
343
|
-
{ type: "
|
|
337
|
+
// ── Package Structure ────────────────────────────────────────────────────
|
|
338
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "cmd/internal/pkg layout", content: "Organize code into cmd/ (main packages), internal/ (private app code), pkg/ (reusable public packages). cmd/api/main.go is the only entry point. Never put business logic in main.go.", tags: ["structure", "packages", "go"] },
|
|
339
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Thin HTTP handlers", content: "Handlers parse the request, call a service method, and write the response. No business logic, no DB calls, no conditional branching beyond input validation in handlers.", tags: ["handler", "structure", "go"] },
|
|
340
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Service layer owns business logic", content: "Services accept and return domain types, not http.Request or http.ResponseWriter. Services are fully testable without starting an HTTP server.", tags: ["service", "business-logic", "go"] },
|
|
341
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Repository layer for data access", content: "All database calls live in a repository layer. Services depend on repository interfaces, never on database drivers (database/sql, pgx, gorm) directly.", tags: ["repository", "database", "go"] },
|
|
342
|
+
// ── Error Handling ───────────────────────────────────────────────────────
|
|
343
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Always return errors explicitly", content: "Return errors as the last return value. Never use panic in library, service, or handler code. Reserve panic only for unrecoverable failures at startup (e.g., missing config).", tags: ["error-handling", "go"] },
|
|
344
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Wrap errors with context", content: 'Wrap errors at each layer boundary: fmt.Errorf("createUser: %w", err). This preserves the original error for errors.Is/errors.As and adds context to stack traces.', tags: ["error-handling", "wrapping", "go"] },
|
|
345
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Never ignore returned errors", content: "Every returned error must be handled or explicitly discarded with a comment. Silently assigning to _ hides bugs. Lint with errcheck to enforce this.", tags: ["error-handling", "go"] },
|
|
346
|
+
{ type: "pattern", scope: "global", architecture: "go-api", title: "Sentinel errors for known cases", content: 'Define sentinel errors (var ErrNotFound = errors.New("not found")) for expected failure cases. Callers use errors.Is() to check. Never compare error strings.', tags: ["error-handling", "sentinel", "go"] },
|
|
347
|
+
// ── Interfaces & Types ───────────────────────────────────────────────────
|
|
348
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Small interfaces at the point of use", content: "Define interfaces where they are consumed, not where implementations live. Prefer single-method interfaces. A 10-method interface is a sign the consumer needs too much.", tags: ["interfaces", "design", "go"] },
|
|
349
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Request/response structs for all handlers", content: "Define explicit request and response structs for every handler. Never bind directly to domain models. This decouples API shape from internal representation.", tags: ["dto", "handler", "go"] },
|
|
350
|
+
// ── Context & Concurrency ────────────────────────────────────────────────
|
|
351
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "context.Context as first parameter", content: "Every function that may block, make a network call, or query a database accepts context.Context as its first parameter. Never store Context in a struct.", tags: ["context", "concurrency", "go"] },
|
|
352
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Graceful shutdown", content: "Catch SIGINT and SIGTERM via signal.NotifyContext. Call server.Shutdown(ctx) to drain in-flight requests before exiting. Never call os.Exit(1) directly in the server loop.", tags: ["shutdown", "reliability", "go"] },
|
|
353
|
+
// ── Configuration & Logging ──────────────────────────────────────────────
|
|
354
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Config struct loaded at startup", content: "Read all configuration from environment variables into a validated Config struct in main.go before starting the server. Fail fast on missing required values. Never read env vars inside handlers or services.", tags: ["config", "go"] },
|
|
355
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Structured logging only", content: "Use slog (stdlib) or zerolog for all logging. Log with key-value fields, not format strings. Never use fmt.Println or log.Printf in production paths.", tags: ["logging", "observability", "go"] },
|
|
356
|
+
// ── Testing ──────────────────────────────────────────────────────────────
|
|
357
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Table-driven tests", content: "Write unit tests as table-driven tests using t.Run(). Each test case has a name, input, and expected output. This keeps tests readable and easy to extend.", tags: ["testing", "go"] },
|
|
358
|
+
{ type: "pattern", scope: "global", architecture: "go-api", title: "Test handlers with httptest", content: "Test HTTP handlers using httptest.NewRecorder() and httptest.NewRequest(). Never spin up a real server in unit tests.", tags: ["testing", "handler", "go"] },
|
|
359
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Mock with interfaces, not libraries", content: "Inject mock implementations via interfaces rather than using reflection-based mock libraries. Write mocks by hand or use mockery \u2014 keep them simple.", tags: ["testing", "mocking", "go"] },
|
|
360
|
+
// ── Middleware & Security ────────────────────────────────────────────────
|
|
361
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Middleware registered at router level", content: "Register all middleware (auth, logging, CORS, recovery) at the router, not inside individual handlers. Middleware wraps the handler chain \u2014 it should never be called manually.", tags: ["middleware", "structure", "go"] },
|
|
362
|
+
{ type: "rule", scope: "global", architecture: "go-api", title: "Validate all incoming data", content: "Validate and sanitize all request inputs before passing to the service layer. Return 400 Bad Request with a structured error body for invalid input. Never trust client data.", tags: ["validation", "security", "go"] },
|
|
344
363
|
// ══════════════════════════════════════════════════════════════════════════
|
|
345
364
|
// LARAVEL SERVICE REPOSITORY
|
|
346
365
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -535,109 +554,6 @@ var seeds = [
|
|
|
535
554
|
{ type: "rule", scope: "global", architecture: "svelte", title: "Avoid options API style \u2014 runes only", content: "Do not use the Svelte 4 options-style patterns (export let, $: reactive statements, $store subscriptions) in new Svelte 5 components. Use runes throughout.", reason: "Mixing the two reactivity systems in the same codebase creates two mental models, confuses new developers, and makes future migrations harder. Svelte 5 runes supersede every Svelte 4 pattern.", tags: ["svelte", "runes", "anti-pattern"] }
|
|
536
555
|
];
|
|
537
556
|
|
|
538
|
-
// src/config.ts
|
|
539
|
-
import { config } from "dotenv";
|
|
540
|
-
import { existsSync as existsSync2 } from "fs";
|
|
541
|
-
import { join as join2 } from "path";
|
|
542
|
-
var localEnv = join2(process.cwd(), ".memory-core.env");
|
|
543
|
-
config({ path: existsSync2(localEnv) ? localEnv : join2(process.cwd(), ".env") });
|
|
544
|
-
var Config = {
|
|
545
|
-
get databaseUrl() {
|
|
546
|
-
return process.env.DATABASE_URL ?? "";
|
|
547
|
-
},
|
|
548
|
-
get ollamaUrl() {
|
|
549
|
-
return process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
550
|
-
},
|
|
551
|
-
get ollamaModel() {
|
|
552
|
-
return process.env.OLLAMA_MODEL ?? "nomic-embed-text";
|
|
553
|
-
},
|
|
554
|
-
get chatModel() {
|
|
555
|
-
return process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
// src/embedding.ts
|
|
560
|
-
async function embed(text) {
|
|
561
|
-
let response;
|
|
562
|
-
try {
|
|
563
|
-
response = await fetch(`${Config.ollamaUrl}/api/embeddings`, {
|
|
564
|
-
method: "POST",
|
|
565
|
-
headers: { "Content-Type": "application/json" },
|
|
566
|
-
body: JSON.stringify({ model: Config.ollamaModel, prompt: text })
|
|
567
|
-
});
|
|
568
|
-
} catch {
|
|
569
|
-
throw new Error(
|
|
570
|
-
`Cannot reach Ollama at ${Config.ollamaUrl}. Run: ollama serve`
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
if (!response.ok) {
|
|
574
|
-
const body = await response.text();
|
|
575
|
-
throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
|
|
576
|
-
}
|
|
577
|
-
const data = await response.json();
|
|
578
|
-
return data.embedding;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/db.ts
|
|
582
|
-
import pg from "pg";
|
|
583
|
-
var { Pool } = pg;
|
|
584
|
-
var pool = null;
|
|
585
|
-
function getPool() {
|
|
586
|
-
if (!pool) {
|
|
587
|
-
if (!Config.databaseUrl) {
|
|
588
|
-
throw new Error("DATABASE_URL is not set. Add it to your .env or .memory-core.env file.");
|
|
589
|
-
}
|
|
590
|
-
pool = new Pool({ connectionString: Config.databaseUrl });
|
|
591
|
-
}
|
|
592
|
-
return pool;
|
|
593
|
-
}
|
|
594
|
-
async function runMigrations() {
|
|
595
|
-
await getPool().query(
|
|
596
|
-
`ALTER TABLE memories ADD COLUMN IF NOT EXISTS reason TEXT`
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
async function saveMemory(memory) {
|
|
600
|
-
const { type, scope, architecture, projectName, title, content, reason, tags, embedding } = memory;
|
|
601
|
-
await getPool().query(
|
|
602
|
-
`INSERT INTO memories (type, scope, architecture, project_name, title, content, reason, tags, embedding)
|
|
603
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
604
|
-
[type, scope, architecture ?? null, projectName ?? null, title ?? null, content, reason ?? null, tags ?? [], `[${embedding.join(",")}]`]
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
async function searchMemories(embedding, architecture, limit = 10) {
|
|
608
|
-
const vector = `[${embedding.join(",")}]`;
|
|
609
|
-
const params = [vector];
|
|
610
|
-
let whereClause = "";
|
|
611
|
-
if (architecture) {
|
|
612
|
-
whereClause = `WHERE (architecture = $2 OR scope = 'global')`;
|
|
613
|
-
params.push(architecture);
|
|
614
|
-
}
|
|
615
|
-
const client = await getPool().connect();
|
|
616
|
-
try {
|
|
617
|
-
await client.query("BEGIN");
|
|
618
|
-
await client.query("SET LOCAL ivfflat.probes = 10");
|
|
619
|
-
const result = await client.query(
|
|
620
|
-
`SELECT id, type, scope, architecture, project_name, title, content, reason, tags,
|
|
621
|
-
1 - (embedding <=> $1) AS similarity
|
|
622
|
-
FROM memories
|
|
623
|
-
${whereClause}
|
|
624
|
-
ORDER BY embedding <=> $1
|
|
625
|
-
LIMIT $${params.length + 1}`,
|
|
626
|
-
[...params, limit]
|
|
627
|
-
);
|
|
628
|
-
await client.query("COMMIT");
|
|
629
|
-
return result.rows;
|
|
630
|
-
} finally {
|
|
631
|
-
client.release();
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
async function closePool() {
|
|
635
|
-
if (pool) {
|
|
636
|
-
await pool.end();
|
|
637
|
-
pool = null;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
557
|
// src/retriever.ts
|
|
642
558
|
async function retrieve(query, architecture, limit = 10) {
|
|
643
559
|
const embedding = await embed(query);
|
|
@@ -645,20 +561,17 @@ async function retrieve(query, architecture, limit = 10) {
|
|
|
645
561
|
}
|
|
646
562
|
|
|
647
563
|
// src/project-detector.ts
|
|
648
|
-
import { existsSync as
|
|
649
|
-
import { join as
|
|
564
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
565
|
+
import { join as join2 } from "path";
|
|
650
566
|
function detectProject(cwd = process.cwd()) {
|
|
651
|
-
const has = (file) =>
|
|
567
|
+
const has = (file) => existsSync2(join2(cwd, file));
|
|
652
568
|
const readJson = (file) => {
|
|
653
569
|
try {
|
|
654
|
-
return JSON.parse(readFileSync2(
|
|
570
|
+
return JSON.parse(readFileSync2(join2(cwd, file), "utf-8"));
|
|
655
571
|
} catch {
|
|
656
572
|
return {};
|
|
657
573
|
}
|
|
658
574
|
};
|
|
659
|
-
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) {
|
|
660
|
-
return { language: "TypeScript", framework: "Next.js" };
|
|
661
|
-
}
|
|
662
575
|
if (has("artisan") && has("composer.json")) {
|
|
663
576
|
return { language: "PHP", framework: "Laravel" };
|
|
664
577
|
}
|
|
@@ -667,7 +580,7 @@ function detectProject(cwd = process.cwd()) {
|
|
|
667
580
|
}
|
|
668
581
|
if (has("manage.py")) {
|
|
669
582
|
if (has("requirements.txt")) {
|
|
670
|
-
const req = readFileSync2(
|
|
583
|
+
const req = readFileSync2(join2(cwd, "requirements.txt"), "utf-8");
|
|
671
584
|
if (req.includes("djangorestframework")) {
|
|
672
585
|
return { language: "Python", framework: "Django REST Framework" };
|
|
673
586
|
}
|
|
@@ -707,24 +620,172 @@ function detectProject(cwd = process.cwd()) {
|
|
|
707
620
|
}
|
|
708
621
|
|
|
709
622
|
// src/hook.ts
|
|
710
|
-
import { execSync } from "child_process";
|
|
711
|
-
import { writeFileSync as
|
|
623
|
+
import { execSync, spawnSync } from "child_process";
|
|
624
|
+
import { writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync, readFileSync as readFileSync4, chmodSync } from "fs";
|
|
712
625
|
import { join as join4 } from "path";
|
|
713
626
|
import chalk from "chalk";
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
627
|
+
|
|
628
|
+
// src/chat.ts
|
|
629
|
+
function getChatConfig() {
|
|
630
|
+
const provider = process.env.CHAT_PROVIDER ?? "ollama";
|
|
631
|
+
const model = process.env.CHAT_MODEL ?? process.env.OLLAMA_CHAT_MODEL ?? "llama3.2";
|
|
632
|
+
return {
|
|
633
|
+
provider,
|
|
634
|
+
model,
|
|
635
|
+
ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
636
|
+
apiKey: process.env.CHAT_API_KEY ?? ""
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function callOllama(cfg, messages) {
|
|
640
|
+
const res = await fetch(`${cfg.ollamaUrl}/api/chat`, {
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers: { "Content-Type": "application/json" },
|
|
643
|
+
body: JSON.stringify({ model: cfg.model, messages, stream: false, format: "json" })
|
|
644
|
+
});
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
const body = await res.text();
|
|
647
|
+
if (body.includes("not found") || body.includes("model")) {
|
|
648
|
+
throw new Error(`MODEL_NOT_FOUND:${cfg.model}`);
|
|
649
|
+
}
|
|
650
|
+
throw new Error(body);
|
|
651
|
+
}
|
|
652
|
+
const data = await res.json();
|
|
653
|
+
return data.message.content.trim();
|
|
654
|
+
}
|
|
655
|
+
async function callOpenAI(cfg, messages) {
|
|
656
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
657
|
+
method: "POST",
|
|
658
|
+
headers: {
|
|
659
|
+
"Content-Type": "application/json",
|
|
660
|
+
"Authorization": `Bearer ${cfg.apiKey}`
|
|
661
|
+
},
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
model: cfg.model,
|
|
664
|
+
messages,
|
|
665
|
+
response_format: { type: "json_object" }
|
|
666
|
+
})
|
|
667
|
+
});
|
|
668
|
+
if (!res.ok) throw new Error(`OpenAI API error ${res.status}: ${await res.text()}`);
|
|
669
|
+
const data = await res.json();
|
|
670
|
+
return data.choices[0].message.content.trim();
|
|
671
|
+
}
|
|
672
|
+
async function callAnthropic(cfg, messages) {
|
|
673
|
+
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
674
|
+
const userMessages = messages.filter((m) => m.role !== "system");
|
|
675
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
676
|
+
method: "POST",
|
|
677
|
+
headers: {
|
|
678
|
+
"Content-Type": "application/json",
|
|
679
|
+
"x-api-key": cfg.apiKey,
|
|
680
|
+
"anthropic-version": "2023-06-01"
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify({
|
|
683
|
+
model: cfg.model,
|
|
684
|
+
max_tokens: 4096,
|
|
685
|
+
system,
|
|
686
|
+
messages: userMessages
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
if (!res.ok) throw new Error(`Anthropic API error ${res.status}: ${await res.text()}`);
|
|
690
|
+
const data = await res.json();
|
|
691
|
+
return data.content[0].text.trim();
|
|
692
|
+
}
|
|
693
|
+
async function callMiniMax(cfg, messages) {
|
|
694
|
+
const res = await fetch("https://api.minimax.io/v1/chat/completions", {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
"Content-Type": "application/json",
|
|
698
|
+
"Authorization": `Bearer ${cfg.apiKey}`
|
|
699
|
+
},
|
|
700
|
+
body: JSON.stringify({
|
|
701
|
+
model: cfg.model,
|
|
702
|
+
messages,
|
|
703
|
+
response_format: { type: "json_object" }
|
|
704
|
+
})
|
|
705
|
+
});
|
|
706
|
+
if (!res.ok) throw new Error(`MiniMax API error ${res.status}: ${await res.text()}`);
|
|
707
|
+
const data = await res.json();
|
|
708
|
+
return data.choices[0].message.content.trim();
|
|
709
|
+
}
|
|
710
|
+
async function callChatModel(messages) {
|
|
711
|
+
const cfg = getChatConfig();
|
|
712
|
+
switch (cfg.provider) {
|
|
713
|
+
case "openai":
|
|
714
|
+
return callOpenAI(cfg, messages);
|
|
715
|
+
case "anthropic":
|
|
716
|
+
return callAnthropic(cfg, messages);
|
|
717
|
+
case "minimax":
|
|
718
|
+
return callMiniMax(cfg, messages);
|
|
719
|
+
default:
|
|
720
|
+
return callOllama(cfg, messages);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function getChatProviderLabel() {
|
|
724
|
+
const cfg = getChatConfig();
|
|
725
|
+
if (cfg.provider === "ollama") return `ollama (${cfg.model})`;
|
|
726
|
+
return `${cfg.provider} (${cfg.model})`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/memory-file.ts
|
|
730
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
731
|
+
import { join as join3 } from "path";
|
|
732
|
+
var MEMORY_FILE = "memories.json";
|
|
733
|
+
function toPortableMemory(memory) {
|
|
734
|
+
return {
|
|
735
|
+
type: memory.type,
|
|
736
|
+
scope: memory.scope,
|
|
737
|
+
architecture: memory.architecture,
|
|
738
|
+
projectName: memory.project_name,
|
|
739
|
+
title: memory.title,
|
|
740
|
+
content: memory.content,
|
|
741
|
+
reason: memory.reason,
|
|
742
|
+
tags: memory.tags ?? []
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function writeMemoryFile(memories, cwd = process.cwd()) {
|
|
746
|
+
const path = join3(cwd, MEMORY_FILE);
|
|
747
|
+
writeFileSync2(path, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
748
|
+
return path;
|
|
749
|
+
}
|
|
750
|
+
function readMemoryFile(cwd = process.cwd()) {
|
|
751
|
+
const path = join3(cwd, MEMORY_FILE);
|
|
752
|
+
if (!existsSync3(path)) {
|
|
753
|
+
throw new Error(`${MEMORY_FILE} not found. Run: memory-core export`);
|
|
725
754
|
}
|
|
726
|
-
return
|
|
755
|
+
return parseMemoryFile(readFileSync3(path, "utf-8"));
|
|
756
|
+
}
|
|
757
|
+
async function readMemoryFileFromUrl(url) {
|
|
758
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
759
|
+
if (!res.ok) throw new Error(`Failed to download ${url}: HTTP ${res.status}`);
|
|
760
|
+
return parseMemoryFile(await res.text());
|
|
761
|
+
}
|
|
762
|
+
function parseMemoryFile(raw) {
|
|
763
|
+
const parsed = JSON.parse(raw);
|
|
764
|
+
if (!Array.isArray(parsed)) {
|
|
765
|
+
throw new Error(`${MEMORY_FILE} must be a JSON array`);
|
|
766
|
+
}
|
|
767
|
+
return parsed.map((item, index) => {
|
|
768
|
+
if (!item || typeof item !== "object") {
|
|
769
|
+
throw new Error(`Memory at index ${index} must be an object`);
|
|
770
|
+
}
|
|
771
|
+
const record = item;
|
|
772
|
+
if (typeof record.content !== "string" || record.content.trim() === "") {
|
|
773
|
+
throw new Error(`Memory at index ${index} is missing content`);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: typeof record.type === "string" ? record.type : "rule",
|
|
777
|
+
scope: typeof record.scope === "string" ? record.scope : "project",
|
|
778
|
+
architecture: typeof record.architecture === "string" ? record.architecture : void 0,
|
|
779
|
+
projectName: typeof record.projectName === "string" ? record.projectName : void 0,
|
|
780
|
+
title: typeof record.title === "string" ? record.title : void 0,
|
|
781
|
+
content: record.content,
|
|
782
|
+
reason: typeof record.reason === "string" ? record.reason : void 0,
|
|
783
|
+
tags: Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : []
|
|
784
|
+
};
|
|
785
|
+
});
|
|
727
786
|
}
|
|
787
|
+
|
|
788
|
+
// src/hook.ts
|
|
728
789
|
var reasonMap = new Map(
|
|
729
790
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
730
791
|
);
|
|
@@ -745,6 +806,63 @@ else
|
|
|
745
806
|
fi
|
|
746
807
|
`;
|
|
747
808
|
}
|
|
809
|
+
function recordViolations(violations) {
|
|
810
|
+
const statsPath = join4(process.cwd(), ".memory-core-stats.json");
|
|
811
|
+
let stats = { rules: {}, files: {} };
|
|
812
|
+
if (existsSync4(statsPath)) {
|
|
813
|
+
try {
|
|
814
|
+
stats = JSON.parse(readFileSync4(statsPath, "utf-8"));
|
|
815
|
+
} catch {
|
|
816
|
+
stats = { rules: {}, files: {} };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const violation of violations) {
|
|
820
|
+
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
821
|
+
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
822
|
+
}
|
|
823
|
+
writeFileSync3(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
824
|
+
}
|
|
825
|
+
async function promptToSaveViolations(violations) {
|
|
826
|
+
if (!process.stdin.isTTY || violations.length === 0) return;
|
|
827
|
+
try {
|
|
828
|
+
const { confirm: confirm2, input: input2 } = await import("@inquirer/prompts");
|
|
829
|
+
const save = await confirm2({
|
|
830
|
+
message: "Save a caught violation as a project rule?",
|
|
831
|
+
default: false
|
|
832
|
+
});
|
|
833
|
+
if (!save) return;
|
|
834
|
+
const choices = violations.map((violation, index) => `${index + 1}. ${violation.rule}`);
|
|
835
|
+
const selected = violations.length === 1 ? violations[0] : violations[Number(await input2({ message: `Which violation? ${choices.join(" | ")}`, default: "1" })) - 1] ?? violations[0];
|
|
836
|
+
const reason = await input2({
|
|
837
|
+
message: "Why should this rule exist?",
|
|
838
|
+
default: selected.reason ?? selected.issue ?? ""
|
|
839
|
+
});
|
|
840
|
+
const { embed: embed2 } = await import("./embedding-PAYD2JYW.js");
|
|
841
|
+
const { upsertMemory: upsertMemory2 } = await import("./db-KU4EEG4Y.js");
|
|
842
|
+
await upsertMemory2({
|
|
843
|
+
type: "rule",
|
|
844
|
+
scope: "project",
|
|
845
|
+
content: selected.rule,
|
|
846
|
+
reason: reason || void 0,
|
|
847
|
+
tags: ["violation"],
|
|
848
|
+
embedding: await embed2(selected.rule)
|
|
849
|
+
});
|
|
850
|
+
console.log(chalk.green(" \u2713 Saved as project rule. Run memory-core sync to propagate it.\n"));
|
|
851
|
+
} catch (err) {
|
|
852
|
+
console.log(chalk.yellow(` Could not save violation: ${err.message}
|
|
853
|
+
`));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function loadIgnorePatterns() {
|
|
857
|
+
try {
|
|
858
|
+
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
|
|
859
|
+
const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
|
|
860
|
+
await closePool2();
|
|
861
|
+
return ignores.map((ignore) => ignore.content);
|
|
862
|
+
} catch {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
}
|
|
748
866
|
function installHook(advisory = true) {
|
|
749
867
|
if (!existsSync4(".git")) {
|
|
750
868
|
console.error(chalk.red("\n Not a git repository. Run from project root.\n"));
|
|
@@ -752,19 +870,19 @@ function installHook(advisory = true) {
|
|
|
752
870
|
}
|
|
753
871
|
const script = buildHookScript(advisory);
|
|
754
872
|
if (existsSync4(HOOK_PATH)) {
|
|
755
|
-
const existing =
|
|
873
|
+
const existing = readFileSync4(HOOK_PATH, "utf-8");
|
|
756
874
|
if (existing.includes(HOOK_MARKER)) {
|
|
757
875
|
const markerIndex = existing.indexOf(HOOK_MARKER);
|
|
758
876
|
const before = markerIndex > 1 ? existing.slice(0, markerIndex).trimEnd() + "\n\n" : "";
|
|
759
|
-
|
|
877
|
+
writeFileSync3(HOOK_PATH, before + script);
|
|
760
878
|
chmodSync(HOOK_PATH, 493);
|
|
761
879
|
const modeLabel2 = advisory ? chalk.cyan("advisory") : chalk.yellow("strict");
|
|
762
880
|
console.log(chalk.green("\n \u2713 Pre-commit hook updated") + chalk.dim(` (${modeLabel2} mode)`));
|
|
763
881
|
return;
|
|
764
882
|
}
|
|
765
|
-
|
|
883
|
+
writeFileSync3(HOOK_PATH, existing.trimEnd() + "\n\n" + script);
|
|
766
884
|
} else {
|
|
767
|
-
|
|
885
|
+
writeFileSync3(HOOK_PATH, script);
|
|
768
886
|
}
|
|
769
887
|
chmodSync(HOOK_PATH, 493);
|
|
770
888
|
const modeLabel = advisory ? "advisory (logs violations, never blocks)" : "strict (blocks commits on violations)";
|
|
@@ -777,14 +895,14 @@ function uninstallHook() {
|
|
|
777
895
|
console.log(chalk.yellow("\n No pre-commit hook found.\n"));
|
|
778
896
|
return;
|
|
779
897
|
}
|
|
780
|
-
const content =
|
|
898
|
+
const content = readFileSync4(HOOK_PATH, "utf-8");
|
|
781
899
|
if (!content.includes(HOOK_MARKER)) {
|
|
782
900
|
console.log(chalk.yellow("\n ArchMind hook not found in pre-commit \u2014 nothing to remove.\n"));
|
|
783
901
|
return;
|
|
784
902
|
}
|
|
785
903
|
const markerIndex = content.indexOf(HOOK_MARKER);
|
|
786
904
|
if (markerIndex > 1) {
|
|
787
|
-
|
|
905
|
+
writeFileSync3(HOOK_PATH, content.slice(0, markerIndex).trimEnd() + "\n");
|
|
788
906
|
} else {
|
|
789
907
|
unlinkSync(HOOK_PATH);
|
|
790
908
|
}
|
|
@@ -799,7 +917,8 @@ async function checkStaged(options = {}) {
|
|
|
799
917
|
if (options.verbose) console.log(chalk.gray(" No source files staged \u2014 skipping rule check."));
|
|
800
918
|
return;
|
|
801
919
|
}
|
|
802
|
-
|
|
920
|
+
const result = spawnSync("git", ["diff", "--cached", "--", ...stagedFiles], { encoding: "utf-8" });
|
|
921
|
+
diff = result.stdout ?? "";
|
|
803
922
|
} catch {
|
|
804
923
|
console.error(chalk.red(" Failed to read staged diff."));
|
|
805
924
|
process.exit(1);
|
|
@@ -810,38 +929,37 @@ async function checkStaged(options = {}) {
|
|
|
810
929
|
}
|
|
811
930
|
const configPath = join4(process.cwd(), ".memory-core.json");
|
|
812
931
|
if (!existsSync4(configPath)) return;
|
|
813
|
-
const
|
|
932
|
+
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
814
933
|
const rules = [];
|
|
815
934
|
const avoids = [];
|
|
816
|
-
if (
|
|
817
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
935
|
+
if (config.backendArchitecture) {
|
|
936
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
818
937
|
if (profile) {
|
|
819
938
|
rules.push(...profile.rules);
|
|
820
939
|
avoids.push(...profile.avoid);
|
|
821
940
|
}
|
|
822
941
|
}
|
|
823
|
-
if (
|
|
824
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
942
|
+
if (config.frontendFramework) {
|
|
943
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
825
944
|
if (profile) {
|
|
826
945
|
rules.push(...profile.rules);
|
|
827
946
|
avoids.push(...profile.avoid);
|
|
828
947
|
}
|
|
829
948
|
}
|
|
830
949
|
if (rules.length === 0) return;
|
|
831
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
832
|
-
const chatModel = await resolveModel(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
833
950
|
const MAX_DIFF = 8e3;
|
|
834
951
|
const truncated = diff.length > MAX_DIFF;
|
|
835
952
|
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
836
953
|
console.log(chalk.cyan("\n archmind \u2014 checking staged changes against rules\u2026"));
|
|
837
|
-
if (options.verbose) {
|
|
838
|
-
console.log(chalk.gray(` model: ${
|
|
954
|
+
if (options.verbose || options.debug) {
|
|
955
|
+
console.log(chalk.gray(` model: ${getChatProviderLabel()} rules: ${rules.length} diff: ${diff.length} chars${truncated ? " (truncated)" : ""}`));
|
|
839
956
|
}
|
|
840
957
|
const rulesWithReasons = rules.map((r, i) => {
|
|
841
958
|
const why = reasonMap.get(r);
|
|
842
959
|
return why ? `${i + 1}. ${r}
|
|
843
960
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
844
961
|
}).join("\n");
|
|
962
|
+
const ignorePatterns = await loadIgnorePatterns();
|
|
845
963
|
const systemPrompt = `You are a strict code reviewer enforcing architecture and framework rules.
|
|
846
964
|
Analyze the git diff and identify ONLY clear, definite rule violations \u2014 not style preferences.
|
|
847
965
|
Use the WHY for each rule to understand intent and judge edge cases correctly.
|
|
@@ -852,38 +970,34 @@ ${rulesWithReasons}
|
|
|
852
970
|
Things that must never appear:
|
|
853
971
|
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
854
972
|
|
|
973
|
+
Never flag these accepted project patterns:
|
|
974
|
+
${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
975
|
+
|
|
855
976
|
IMPORTANT: You MUST respond with a JSON object that has a "violations" key containing an array.
|
|
856
977
|
For each violation include a "reason" field \u2014 copy the WHY from the rule to explain to the developer why this matters.
|
|
857
978
|
Example with violations: {"violations":[{"rule":"Use functional components only","file":"User.tsx","line":3,"issue":"Class component used","suggestion":"Convert to a function component using hooks","reason":"Class components cannot use hooks and the entire React ecosystem now assumes functional components"}]}
|
|
858
979
|
Example with no violations: {"violations":[]}
|
|
859
980
|
Do not include any text outside the JSON object.`;
|
|
981
|
+
if (options.debug) {
|
|
982
|
+
console.log(chalk.gray("\n [debug] prompt:"));
|
|
983
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
984
|
+
console.log(systemPrompt);
|
|
985
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
986
|
+
console.log(chalk.gray(` [debug] diff length: ${diff.length} chars`));
|
|
987
|
+
console.log(chalk.dim(diffToSend));
|
|
988
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
989
|
+
}
|
|
860
990
|
let violations = [];
|
|
861
991
|
try {
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
body: JSON.stringify({
|
|
866
|
-
model: chatModel,
|
|
867
|
-
messages: [
|
|
868
|
-
{ role: "system", content: systemPrompt },
|
|
869
|
-
{ role: "user", content: `Review this diff:
|
|
992
|
+
const raw = await callChatModel([
|
|
993
|
+
{ role: "system", content: systemPrompt },
|
|
994
|
+
{ role: "user", content: `Review this diff:
|
|
870
995
|
|
|
871
996
|
${diffToSend}` }
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
})
|
|
876
|
-
});
|
|
877
|
-
if (!res.ok) {
|
|
878
|
-
const body = await res.text();
|
|
879
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
880
|
-
printModelMissing(chatModel);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
throw new Error(body);
|
|
997
|
+
]);
|
|
998
|
+
if (options.verbose || options.debug) {
|
|
999
|
+
console.log(chalk.gray(` raw response: ${options.debug ? raw : raw.slice(0, 200)}`));
|
|
884
1000
|
}
|
|
885
|
-
const data = await res.json();
|
|
886
|
-
const raw = data.message.content.trim();
|
|
887
1001
|
try {
|
|
888
1002
|
const parsed = JSON.parse(raw);
|
|
889
1003
|
if (Array.isArray(parsed)) {
|
|
@@ -898,10 +1012,11 @@ ${diffToSend}` }
|
|
|
898
1012
|
} catch {
|
|
899
1013
|
violations = [];
|
|
900
1014
|
}
|
|
901
|
-
if (options.verbose) {
|
|
902
|
-
console.log(chalk.gray(` raw response: ${raw.slice(0, 200)}`));
|
|
903
|
-
}
|
|
904
1015
|
} catch (err) {
|
|
1016
|
+
if (err.message?.startsWith("MODEL_NOT_FOUND:")) {
|
|
1017
|
+
printModelMissing(err.message.split(":")[1]);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
905
1020
|
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
|
|
906
1021
|
console.log(chalk.yellow("\n \u26A0 Ollama not running \u2014 skipping rule check."));
|
|
907
1022
|
console.log(chalk.gray(" Start it: ollama serve\n"));
|
|
@@ -937,6 +1052,96 @@ ${diffToSend}` }
|
|
|
937
1052
|
console.log(chalk.dim(" To bypass (not recommended): git commit --no-verify"));
|
|
938
1053
|
console.log(chalk.dim(' To save as memory: memory-core remember "<lesson>"'));
|
|
939
1054
|
console.log();
|
|
1055
|
+
recordViolations(violations);
|
|
1056
|
+
await promptToSaveViolations(violations);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
function extractForbiddenPhrases(content) {
|
|
1060
|
+
const phrases = [];
|
|
1061
|
+
const normalized = content.replace(/\s+/g, " ");
|
|
1062
|
+
const patterns = [
|
|
1063
|
+
/\bnever\s+([^.;]+)/gi,
|
|
1064
|
+
/\bmust not\s+([^.;]+)/gi,
|
|
1065
|
+
/\bdo not\s+([^.;]+)/gi
|
|
1066
|
+
];
|
|
1067
|
+
for (const pattern of patterns) {
|
|
1068
|
+
for (const match of normalized.matchAll(pattern)) {
|
|
1069
|
+
const phrase = match[1]?.trim();
|
|
1070
|
+
if (phrase && phrase.split(/\s+/).length >= 2) phrases.push(phrase.toLowerCase());
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return phrases;
|
|
1074
|
+
}
|
|
1075
|
+
function getCiDiff() {
|
|
1076
|
+
const baseRef = process.env.GITHUB_BASE_REF;
|
|
1077
|
+
const commands = [
|
|
1078
|
+
baseRef ? `git diff --unified=0 --diff-filter=ACMRT origin/${baseRef}...HEAD` : "",
|
|
1079
|
+
"git diff --unified=0 --diff-filter=ACMRT HEAD~1 HEAD",
|
|
1080
|
+
"git diff --cached --unified=0 --diff-filter=ACMRT"
|
|
1081
|
+
].filter(Boolean);
|
|
1082
|
+
for (const command of commands) {
|
|
1083
|
+
try {
|
|
1084
|
+
const diff = execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1085
|
+
if (diff.trim()) return diff;
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return "";
|
|
1090
|
+
}
|
|
1091
|
+
async function checkCi(options = {}) {
|
|
1092
|
+
let memories;
|
|
1093
|
+
try {
|
|
1094
|
+
memories = readMemoryFile();
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
console.error(chalk.red(`
|
|
1097
|
+
CI check failed: ${err.message}
|
|
1098
|
+
`));
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
const rules = memories.filter((memory) => memory.type !== "ignore");
|
|
1102
|
+
const ignores = memories.filter((memory) => memory.type === "ignore").map((memory) => memory.content.toLowerCase());
|
|
1103
|
+
const phrases = rules.flatMap(
|
|
1104
|
+
(memory) => extractForbiddenPhrases(memory.content).map((phrase) => ({ rule: memory.content, phrase }))
|
|
1105
|
+
);
|
|
1106
|
+
const diff = getCiDiff();
|
|
1107
|
+
const addedLines = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
|
|
1108
|
+
if (options.debug) {
|
|
1109
|
+
console.log(chalk.gray(`
|
|
1110
|
+
[debug] memories: ${memories.length}`));
|
|
1111
|
+
console.log(chalk.gray(` [debug] text rules: ${phrases.length}`));
|
|
1112
|
+
console.log(chalk.gray(` [debug] diff length: ${diff.length} chars
|
|
1113
|
+
`));
|
|
1114
|
+
}
|
|
1115
|
+
const violations = [];
|
|
1116
|
+
for (const line of addedLines) {
|
|
1117
|
+
const normalizedLine = line.toLowerCase();
|
|
1118
|
+
if (ignores.some((ignore) => normalizedLine.includes(ignore))) continue;
|
|
1119
|
+
for (const { rule, phrase } of phrases) {
|
|
1120
|
+
if (normalizedLine.includes(phrase)) {
|
|
1121
|
+
violations.push({
|
|
1122
|
+
rule,
|
|
1123
|
+
file: "diff",
|
|
1124
|
+
issue: `Added line contains forbidden phrase: "${phrase}"`
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (violations.length === 0) {
|
|
1130
|
+
console.log(chalk.green(`
|
|
1131
|
+
\u2713 CI memory check passed (${rules.length} rules loaded from memories.json)
|
|
1132
|
+
`));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
console.log(chalk.red.bold(`
|
|
1136
|
+
\u2717 ${violations.length} CI violation${violations.length > 1 ? "s" : ""} found
|
|
1137
|
+
`));
|
|
1138
|
+
violations.forEach((violation, index) => {
|
|
1139
|
+
console.log(chalk.bold(` [${index + 1}] ${violation.file}`));
|
|
1140
|
+
console.log(chalk.yellow(" Rule: ") + violation.rule);
|
|
1141
|
+
console.log(chalk.red(" Issue: ") + violation.issue);
|
|
1142
|
+
console.log();
|
|
1143
|
+
});
|
|
1144
|
+
recordViolations(violations);
|
|
940
1145
|
process.exit(1);
|
|
941
1146
|
}
|
|
942
1147
|
function printModelMissing(model) {
|
|
@@ -949,27 +1154,13 @@ function printModelMissing(model) {
|
|
|
949
1154
|
|
|
950
1155
|
// src/watcher.ts
|
|
951
1156
|
import { watch } from "chokidar";
|
|
952
|
-
import {
|
|
953
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1157
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1158
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
954
1159
|
import { join as join5, relative } from "path";
|
|
955
1160
|
import chalk2 from "chalk";
|
|
956
|
-
async function resolveModel2(ollamaUrl, chatModel) {
|
|
957
|
-
try {
|
|
958
|
-
const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3e3) });
|
|
959
|
-
if (!res.ok) return chatModel;
|
|
960
|
-
const data = await res.json();
|
|
961
|
-
const models = data.models ?? [];
|
|
962
|
-
const exact = models.find((m) => m.name === chatModel);
|
|
963
|
-
if (exact) return exact.name;
|
|
964
|
-
const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
|
|
965
|
-
if (prefixed) return prefixed.name;
|
|
966
|
-
} catch {
|
|
967
|
-
}
|
|
968
|
-
return chatModel;
|
|
969
|
-
}
|
|
970
1161
|
function getFileLines(filePath) {
|
|
971
1162
|
try {
|
|
972
|
-
return
|
|
1163
|
+
return readFileSync5(filePath, "utf-8").split("\n");
|
|
973
1164
|
} catch {
|
|
974
1165
|
return [];
|
|
975
1166
|
}
|
|
@@ -995,27 +1186,43 @@ var SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|php|rb|go|java|cs|swift|kt|rs|vue|s
|
|
|
995
1186
|
var reasonMap2 = new Map(
|
|
996
1187
|
seeds.filter((s) => s.reason).map((s) => [s.content, s.reason])
|
|
997
1188
|
);
|
|
1189
|
+
function recordViolations2(violations) {
|
|
1190
|
+
const statsPath = join5(process.cwd(), ".memory-core-stats.json");
|
|
1191
|
+
let stats = { rules: {}, files: {} };
|
|
1192
|
+
if (existsSync5(statsPath)) {
|
|
1193
|
+
try {
|
|
1194
|
+
stats = JSON.parse(readFileSync5(statsPath, "utf-8"));
|
|
1195
|
+
} catch {
|
|
1196
|
+
stats = { rules: {}, files: {} };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
for (const violation of violations) {
|
|
1200
|
+
stats.rules[violation.rule] = (stats.rules[violation.rule] ?? 0) + 1;
|
|
1201
|
+
if (violation.file) stats.files[violation.file] = (stats.files[violation.file] ?? 0) + 1;
|
|
1202
|
+
}
|
|
1203
|
+
writeFileSync4(statsPath, JSON.stringify(stats, null, 2) + "\n", "utf-8");
|
|
1204
|
+
}
|
|
998
1205
|
function loadConfig(cwd) {
|
|
999
1206
|
const configPath = join5(cwd, ".memory-core.json");
|
|
1000
1207
|
if (!existsSync5(configPath)) return null;
|
|
1001
1208
|
try {
|
|
1002
|
-
return JSON.parse(
|
|
1209
|
+
return JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1003
1210
|
} catch {
|
|
1004
1211
|
return null;
|
|
1005
1212
|
}
|
|
1006
1213
|
}
|
|
1007
|
-
function getProfileRules(
|
|
1214
|
+
function getProfileRules(config) {
|
|
1008
1215
|
const rules = [];
|
|
1009
1216
|
const avoids = [];
|
|
1010
|
-
if (
|
|
1011
|
-
const profile = listProfiles("backend").find((p) => p.name ===
|
|
1217
|
+
if (config.backendArchitecture) {
|
|
1218
|
+
const profile = listProfiles("backend").find((p) => p.name === config.backendArchitecture);
|
|
1012
1219
|
if (profile) {
|
|
1013
1220
|
rules.push(...profile.rules);
|
|
1014
1221
|
avoids.push(...profile.avoid);
|
|
1015
1222
|
}
|
|
1016
1223
|
}
|
|
1017
|
-
if (
|
|
1018
|
-
const profile = listProfiles("frontend").find((p) => p.name ===
|
|
1224
|
+
if (config.frontendFramework) {
|
|
1225
|
+
const profile = listProfiles("frontend").find((p) => p.name === config.frontendFramework);
|
|
1019
1226
|
if (profile) {
|
|
1020
1227
|
rules.push(...profile.rules);
|
|
1021
1228
|
avoids.push(...profile.avoid);
|
|
@@ -1023,30 +1230,33 @@ function getProfileRules(config2) {
|
|
|
1023
1230
|
}
|
|
1024
1231
|
return { rules, avoids };
|
|
1025
1232
|
}
|
|
1026
|
-
async function
|
|
1027
|
-
const rel = relative(cwd, filePath);
|
|
1028
|
-
let diff;
|
|
1233
|
+
async function loadIgnorePatterns2() {
|
|
1029
1234
|
try {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1235
|
+
const { listMemories: listMemories2, closePool: closePool2 } = await import("./db-KU4EEG4Y.js");
|
|
1236
|
+
const ignores = await listMemories2({ type: "ignore", limit: 1e3 });
|
|
1237
|
+
await closePool2();
|
|
1238
|
+
return ignores.map((ignore) => ignore.content);
|
|
1034
1239
|
} catch {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async function checkFile(filePath, cwd, config, verbose, debug) {
|
|
1244
|
+
const rel = relative(cwd, filePath);
|
|
1245
|
+
let diff;
|
|
1246
|
+
const headResult = spawnSync2("git", ["diff", "HEAD", "--", rel], { encoding: "utf-8", cwd });
|
|
1247
|
+
if (headResult.stdout?.trim()) {
|
|
1248
|
+
diff = headResult.stdout;
|
|
1249
|
+
} else {
|
|
1250
|
+
const noIndexResult = spawnSync2("git", ["diff", "--no-index", "/dev/null", rel], { encoding: "utf-8", cwd });
|
|
1251
|
+
diff = noIndexResult.stdout ?? "";
|
|
1040
1252
|
}
|
|
1041
1253
|
if (!diff.trim()) return;
|
|
1042
|
-
const { rules, avoids } = getProfileRules(
|
|
1254
|
+
const { rules, avoids } = getProfileRules(config);
|
|
1043
1255
|
if (rules.length === 0) return;
|
|
1044
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1045
|
-
const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
1046
1256
|
const MAX_DIFF = 6e3;
|
|
1047
1257
|
const truncated = diff.length > MAX_DIFF;
|
|
1048
1258
|
const diffToSend = truncated ? diff.slice(0, MAX_DIFF) + "\n\n[diff truncated]" : diff;
|
|
1049
|
-
if (verbose) {
|
|
1259
|
+
if (verbose || debug) {
|
|
1050
1260
|
console.log(chalk2.dim(`
|
|
1051
1261
|
[watch] checking ${rel} (${diff.length} chars)\u2026`));
|
|
1052
1262
|
}
|
|
@@ -1055,6 +1265,7 @@ async function checkFile(filePath, cwd, config2, verbose) {
|
|
|
1055
1265
|
return why ? `${i + 1}. ${r}
|
|
1056
1266
|
WHY: ${why}` : `${i + 1}. ${r}`;
|
|
1057
1267
|
}).join("\n");
|
|
1268
|
+
const ignorePatterns = await loadIgnorePatterns2();
|
|
1058
1269
|
const systemPrompt = `You are a strict code reviewer enforcing architecture rules.
|
|
1059
1270
|
Analyze the file diff and identify ONLY clear, definite rule violations.
|
|
1060
1271
|
Use the WHY for each rule to understand intent and judge edge cases.
|
|
@@ -1065,36 +1276,33 @@ ${rulesWithReasons}
|
|
|
1065
1276
|
Things that must never appear:
|
|
1066
1277
|
${avoids.map((a, i) => `${i + 1}. ${a}`).join("\n")}
|
|
1067
1278
|
|
|
1279
|
+
Never flag these accepted project patterns:
|
|
1280
|
+
${ignorePatterns.length ? ignorePatterns.map((a, i) => `${i + 1}. ${a}`).join("\n") : "(none)"}
|
|
1281
|
+
|
|
1068
1282
|
IMPORTANT: Respond with JSON: {"violations":[...]} or {"violations":[]}.
|
|
1069
1283
|
Each violation: {"rule":"...","file":"...","line":N,"issue":"...","suggestion":"...","reason":"..."}.
|
|
1070
1284
|
No text outside the JSON.`;
|
|
1285
|
+
if (debug) {
|
|
1286
|
+
console.log(chalk2.gray("\n [debug] prompt:"));
|
|
1287
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1288
|
+
console.log(systemPrompt);
|
|
1289
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1290
|
+
console.log(chalk2.gray(` [debug] diff length: ${diff.length} chars`));
|
|
1291
|
+
console.log(chalk2.dim(diffToSend));
|
|
1292
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1293
|
+
}
|
|
1071
1294
|
try {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
body: JSON.stringify({
|
|
1076
|
-
model: chatModel,
|
|
1077
|
-
messages: [
|
|
1078
|
-
{ role: "system", content: systemPrompt },
|
|
1079
|
-
{ role: "user", content: `Review this diff for ${rel}:
|
|
1295
|
+
const raw = await callChatModel([
|
|
1296
|
+
{ role: "system", content: systemPrompt },
|
|
1297
|
+
{ role: "user", content: `Review this diff for ${rel}:
|
|
1080
1298
|
|
|
1081
1299
|
${diffToSend}` }
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (!res.ok) {
|
|
1088
|
-
const body = await res.text();
|
|
1089
|
-
if (body.includes("not found") || body.includes("model")) {
|
|
1090
|
-
console.log(chalk2.yellow(`
|
|
1091
|
-
\u26A0 Chat model "${chatModel}" not found. Pull it: ollama pull ${chatModel}
|
|
1092
|
-
`));
|
|
1093
|
-
}
|
|
1094
|
-
return;
|
|
1300
|
+
]);
|
|
1301
|
+
if (debug) {
|
|
1302
|
+
console.log(chalk2.gray(" [debug] raw response:"));
|
|
1303
|
+
console.log(chalk2.dim(raw));
|
|
1304
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1095
1305
|
}
|
|
1096
|
-
const data = await res.json();
|
|
1097
|
-
const raw = data.message.content.trim();
|
|
1098
1306
|
let violations = [];
|
|
1099
1307
|
try {
|
|
1100
1308
|
const parsed = JSON.parse(raw);
|
|
@@ -1130,6 +1338,7 @@ ${diffToSend}` }
|
|
|
1130
1338
|
if (v.suggestion) console.log(chalk2.green(" Fix: ") + v.suggestion);
|
|
1131
1339
|
console.log();
|
|
1132
1340
|
});
|
|
1341
|
+
recordViolations2(violations);
|
|
1133
1342
|
console.log(chalk2.dim(' Fix violations or run: memory-core remember "<lesson>"'));
|
|
1134
1343
|
console.log();
|
|
1135
1344
|
} catch (err) {
|
|
@@ -1143,22 +1352,20 @@ ${diffToSend}` }
|
|
|
1143
1352
|
}
|
|
1144
1353
|
async function startWatch(options = {}) {
|
|
1145
1354
|
const cwd = process.cwd();
|
|
1146
|
-
const
|
|
1147
|
-
if (!
|
|
1355
|
+
const config = loadConfig(cwd);
|
|
1356
|
+
if (!config) {
|
|
1148
1357
|
console.error(chalk2.red("\n No .memory-core.json found. Run: memory-core init\n"));
|
|
1149
1358
|
process.exit(1);
|
|
1150
1359
|
}
|
|
1151
|
-
const { rules } = getProfileRules(
|
|
1360
|
+
const { rules } = getProfileRules(config);
|
|
1152
1361
|
if (rules.length === 0) {
|
|
1153
1362
|
console.log(chalk2.yellow("\n No architecture rules configured in .memory-core.json \u2014 nothing to watch.\n"));
|
|
1154
1363
|
process.exit(0);
|
|
1155
1364
|
}
|
|
1156
1365
|
const watchPath = options.path ?? cwd;
|
|
1157
|
-
const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
|
|
1158
|
-
const chatModel = await resolveModel2(ollamaUrl, process.env.OLLAMA_CHAT_MODEL ?? "llama3.2");
|
|
1159
1366
|
console.log(chalk2.cyan("\n archmind watch \u2014 real-time rule enforcement\n"));
|
|
1160
1367
|
console.log(chalk2.dim(` watching: ${watchPath}`));
|
|
1161
|
-
console.log(chalk2.dim(` model: ${
|
|
1368
|
+
console.log(chalk2.dim(` model: ${getChatProviderLabel()}`));
|
|
1162
1369
|
console.log(chalk2.dim(` rules: ${rules.length}`));
|
|
1163
1370
|
console.log(chalk2.dim(" ctrl+c to stop\n"));
|
|
1164
1371
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -1197,14 +1404,14 @@ async function startWatch(options = {}) {
|
|
|
1197
1404
|
}
|
|
1198
1405
|
return;
|
|
1199
1406
|
}
|
|
1200
|
-
await checkFile(filePath, cwd,
|
|
1407
|
+
await checkFile(filePath, cwd, config, options.verbose ?? false, options.debug ?? false);
|
|
1201
1408
|
}, 300);
|
|
1202
1409
|
pending.set(filePath, timer);
|
|
1203
1410
|
};
|
|
1204
1411
|
watcher.on("add", handle);
|
|
1205
1412
|
watcher.on("change", handle);
|
|
1206
1413
|
watcher.on("error", (err) => {
|
|
1207
|
-
console.error(chalk2.red(` watcher error: ${err.message}`));
|
|
1414
|
+
console.error(chalk2.red(` watcher error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1208
1415
|
});
|
|
1209
1416
|
process.on("SIGINT", () => {
|
|
1210
1417
|
console.log(chalk2.dim("\n\n archmind watch stopped.\n"));
|
|
@@ -1216,7 +1423,7 @@ async function startWatch(options = {}) {
|
|
|
1216
1423
|
|
|
1217
1424
|
// src/cli.ts
|
|
1218
1425
|
function printBanner(projectName, agentCount, status) {
|
|
1219
|
-
const
|
|
1426
|
+
const pg = status ? status.postgresOk ? chalk3.green(" \u2713 PostgreSQL ") + chalk3.bold("connected") : chalk3.red(" \u2717 PostgreSQL ") + chalk3.bold("not connected \u2014 check DATABASE_URL") : chalk3.green(" \u2713 Memory ") + chalk3.bold("PostgreSQL + pgvector ready");
|
|
1220
1427
|
const ol = status ? status.ollamaOk ? chalk3.green(" \u2713 Ollama ") + chalk3.bold(`connected (model: ${status.chatModel})`) : chalk3.red(" \u2717 Ollama ") + chalk3.bold("not running \u2014 start with: ollama serve") : null;
|
|
1221
1428
|
const lines = [
|
|
1222
1429
|
"",
|
|
@@ -1231,7 +1438,7 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1231
1438
|
"",
|
|
1232
1439
|
chalk3.green(` \u2713 Project `) + chalk3.bold(projectName),
|
|
1233
1440
|
chalk3.green(` \u2713 Agents `) + chalk3.bold(`${agentCount} AI agents configured`),
|
|
1234
|
-
|
|
1441
|
+
pg,
|
|
1235
1442
|
...ol ? [ol] : [],
|
|
1236
1443
|
"",
|
|
1237
1444
|
chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),
|
|
@@ -1247,13 +1454,13 @@ function printBanner(projectName, agentCount, status) {
|
|
|
1247
1454
|
];
|
|
1248
1455
|
lines.forEach((l) => console.log(l));
|
|
1249
1456
|
}
|
|
1250
|
-
async function checkConnections(dbUrl,
|
|
1457
|
+
async function checkConnections(dbUrl, ollamaUrl2, chatModel) {
|
|
1251
1458
|
const spinner = ora("Checking connections\u2026").start();
|
|
1252
1459
|
let postgresOk = false;
|
|
1253
1460
|
let ollamaOk = false;
|
|
1254
1461
|
try {
|
|
1255
|
-
const { Pool
|
|
1256
|
-
const testPool = new
|
|
1462
|
+
const { Pool } = (await import("pg")).default;
|
|
1463
|
+
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1257
1464
|
await testPool.query("SELECT 1");
|
|
1258
1465
|
await testPool.end();
|
|
1259
1466
|
postgresOk = true;
|
|
@@ -1261,7 +1468,7 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1261
1468
|
postgresOk = false;
|
|
1262
1469
|
}
|
|
1263
1470
|
try {
|
|
1264
|
-
const res = await fetch(`${
|
|
1471
|
+
const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1265
1472
|
ollamaOk = res.ok;
|
|
1266
1473
|
} catch {
|
|
1267
1474
|
ollamaOk = false;
|
|
@@ -1276,28 +1483,74 @@ async function checkConnections(dbUrl, ollamaUrl, chatModel) {
|
|
|
1276
1483
|
console.log();
|
|
1277
1484
|
return { postgresOk, ollamaOk, chatModel };
|
|
1278
1485
|
}
|
|
1279
|
-
var { version } = JSON.parse(
|
|
1486
|
+
var { version } = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1280
1487
|
var CONFIG_FILE = ".memory-core.json";
|
|
1281
1488
|
function readProjectConfig() {
|
|
1282
1489
|
const path = join6(process.cwd(), CONFIG_FILE);
|
|
1283
1490
|
if (!existsSync6(path)) return null;
|
|
1284
1491
|
try {
|
|
1285
|
-
return JSON.parse(
|
|
1492
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
1286
1493
|
} catch {
|
|
1287
1494
|
return null;
|
|
1288
1495
|
}
|
|
1289
1496
|
}
|
|
1290
|
-
function writeProjectConfig(
|
|
1291
|
-
|
|
1497
|
+
function writeProjectConfig(config) {
|
|
1498
|
+
writeFileSync5(join6(process.cwd(), CONFIG_FILE), JSON.stringify(config, null, 2));
|
|
1499
|
+
}
|
|
1500
|
+
function parseTags(tags) {
|
|
1501
|
+
return tags ? tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
1502
|
+
}
|
|
1503
|
+
function truncate(value, length) {
|
|
1504
|
+
if (!value) return "";
|
|
1505
|
+
return value.length > length ? `${value.slice(0, Math.max(0, length - 1))}\u2026` : value;
|
|
1506
|
+
}
|
|
1507
|
+
function printMemoryTable(memories, title = "Rules in memory") {
|
|
1508
|
+
console.log(chalk3.bold(`
|
|
1509
|
+
${title} (${memories.length} total)
|
|
1510
|
+
`));
|
|
1511
|
+
console.log(chalk3.dim(" ID Type Scope Title / Content"));
|
|
1512
|
+
console.log(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1513
|
+
memories.forEach((memory) => {
|
|
1514
|
+
const id = String(memory.id).padEnd(4);
|
|
1515
|
+
const type = memory.type.padEnd(10);
|
|
1516
|
+
const scope = memory.scope.padEnd(9);
|
|
1517
|
+
const label = truncate(memory.title || memory.content, 64);
|
|
1518
|
+
console.log(` ${id} ${type} ${scope} ${label}`);
|
|
1519
|
+
});
|
|
1520
|
+
console.log(chalk3.gray("\n Use: memory-core remove <id> | memory-core edit <id>\n"));
|
|
1292
1521
|
}
|
|
1293
1522
|
var program = new Command();
|
|
1294
1523
|
program.name("memory-core").description("Universal AI memory core \u2014 generate AI context files for all coding agents").version(version);
|
|
1295
|
-
program.command("init").description("Initialize memory-core in the current project").action(async () => {
|
|
1524
|
+
program.command("init").description("Initialize memory-core in the current project").option("--quick", "Use smart defaults and skip optional prompts").action(async (opts) => {
|
|
1296
1525
|
console.log(chalk3.bold.cyan("\n memory-core init\n"));
|
|
1297
1526
|
const detected = detectProject();
|
|
1527
|
+
const quick = opts.quick ?? false;
|
|
1298
1528
|
const envPath = join6(process.cwd(), ".memory-core.env");
|
|
1299
1529
|
const hasEnv = existsSync6(envPath) || existsSync6(join6(process.cwd(), ".env")) || !!process.env.DATABASE_URL;
|
|
1300
|
-
if (!hasEnv) {
|
|
1530
|
+
if (!hasEnv && quick) {
|
|
1531
|
+
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1532
|
+
const dbUrl = `postgresql://${dbUser}@localhost:5432/memory_core`;
|
|
1533
|
+
const ollamaUrl2 = "http://localhost:11434";
|
|
1534
|
+
const chatModel = "llama3.2";
|
|
1535
|
+
const envContent = [
|
|
1536
|
+
`DATABASE_URL=${dbUrl}`,
|
|
1537
|
+
`OLLAMA_URL=${ollamaUrl2}`,
|
|
1538
|
+
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1539
|
+
`OLLAMA_CHAT_MODEL=${chatModel}`
|
|
1540
|
+
].join("\n") + "\n";
|
|
1541
|
+
writeFileSync5(envPath, envContent);
|
|
1542
|
+
process.env.DATABASE_URL = dbUrl;
|
|
1543
|
+
process.env.OLLAMA_URL = ollamaUrl2;
|
|
1544
|
+
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1545
|
+
process.env.OLLAMA_CHAT_MODEL = chatModel;
|
|
1546
|
+
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
1547
|
+
const gitignore = existsSync6(gitignorePath2) ? readFileSync6(gitignorePath2, "utf-8") : "";
|
|
1548
|
+
if (!gitignore.includes(".memory-core.env")) {
|
|
1549
|
+
appendFileSync(gitignorePath2, `${gitignore ? "\n" : ""}.memory-core.env
|
|
1550
|
+
`);
|
|
1551
|
+
}
|
|
1552
|
+
console.log(chalk3.green(" \u2713 .memory-core.env created with local defaults"));
|
|
1553
|
+
} else if (!hasEnv) {
|
|
1301
1554
|
console.log(chalk3.dim(" No .memory-core.env found \u2014 let's set up your connection.\n"));
|
|
1302
1555
|
const dbUser = process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
1303
1556
|
let dbUrl = "";
|
|
@@ -1308,8 +1561,8 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1308
1561
|
});
|
|
1309
1562
|
const pgSpinner = ora(" Testing PostgreSQL connection\u2026").start();
|
|
1310
1563
|
try {
|
|
1311
|
-
const { Pool
|
|
1312
|
-
const testPool = new
|
|
1564
|
+
const { Pool } = (await import("pg")).default;
|
|
1565
|
+
const testPool = new Pool({ connectionString: dbUrl, connectionTimeoutMillis: 5e3 });
|
|
1313
1566
|
await testPool.query("SELECT 1");
|
|
1314
1567
|
await testPool.end();
|
|
1315
1568
|
pgSpinner.succeed(chalk3.green("PostgreSQL connected"));
|
|
@@ -1319,15 +1572,15 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1319
1572
|
console.log(chalk3.yellow(" Please check that PostgreSQL is running and the URL is correct.\n"));
|
|
1320
1573
|
}
|
|
1321
1574
|
}
|
|
1322
|
-
let
|
|
1575
|
+
let ollamaUrl2 = "";
|
|
1323
1576
|
while (true) {
|
|
1324
|
-
|
|
1577
|
+
ollamaUrl2 = await input({
|
|
1325
1578
|
message: "Ollama URL?",
|
|
1326
|
-
default:
|
|
1579
|
+
default: ollamaUrl2 || "http://localhost:11434"
|
|
1327
1580
|
});
|
|
1328
1581
|
const ollamaSpinner = ora(" Testing Ollama connection\u2026").start();
|
|
1329
1582
|
try {
|
|
1330
|
-
const res = await fetch(`${
|
|
1583
|
+
const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1331
1584
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1332
1585
|
ollamaSpinner.succeed(chalk3.green("Ollama connected"));
|
|
1333
1586
|
break;
|
|
@@ -1336,69 +1589,117 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1336
1589
|
console.log(chalk3.yellow(" Make sure Ollama is running: ollama serve\n"));
|
|
1337
1590
|
}
|
|
1338
1591
|
}
|
|
1592
|
+
const chatProvider = await select({
|
|
1593
|
+
message: "Which provider for code checking?",
|
|
1594
|
+
choices: [
|
|
1595
|
+
{ name: "Local \u2014 Ollama (no API key, free)", value: "ollama" },
|
|
1596
|
+
{ name: "OpenAI \u2014 gpt-4o, gpt-4o-mini", value: "openai" },
|
|
1597
|
+
{ name: "Anthropic \u2014 claude-sonnet, claude-haiku", value: "anthropic" },
|
|
1598
|
+
{ name: "MiniMax \u2014 MiniMax-Text-01, abab6.5s-chat", value: "minimax" }
|
|
1599
|
+
]
|
|
1600
|
+
});
|
|
1339
1601
|
let chatModel = "";
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1602
|
+
let chatApiKey = "";
|
|
1603
|
+
if (chatProvider === "ollama") {
|
|
1604
|
+
while (true) {
|
|
1605
|
+
const chatModelChoice = await select({
|
|
1606
|
+
message: "Which Ollama model for code checking?",
|
|
1607
|
+
choices: [
|
|
1608
|
+
{ name: "llama3.2 (fast, 2GB, recommended for most machines)", value: "llama3.2" },
|
|
1609
|
+
{ name: "qwen2.5-coder (better code understanding, 4.7GB)", value: "qwen2.5-coder" },
|
|
1610
|
+
{ name: "mistral (balanced, 4.1GB)", value: "mistral" },
|
|
1611
|
+
{ name: "codellama (code-focused, 3.8GB)", value: "codellama" },
|
|
1612
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1613
|
+
]
|
|
1614
|
+
});
|
|
1615
|
+
chatModel = chatModelChoice === "__custom__" ? await input({ message: "Model name?", default: "llama3.2" }) : chatModelChoice;
|
|
1616
|
+
const modelSpinner = ora(` Checking if ${chatModel} is installed\u2026`).start();
|
|
1617
|
+
try {
|
|
1618
|
+
const res = await fetch(`${ollamaUrl2}/api/tags`, { signal: AbortSignal.timeout(5e3) });
|
|
1619
|
+
const data = await res.json();
|
|
1620
|
+
const models = data.models ?? [];
|
|
1621
|
+
const exact = models.find((m) => m.name === chatModel);
|
|
1622
|
+
const prefixed = models.find((m) => m.name.startsWith(`${chatModel}:`));
|
|
1623
|
+
const match = exact ?? prefixed;
|
|
1624
|
+
if (match) {
|
|
1625
|
+
chatModel = match.name;
|
|
1626
|
+
modelSpinner.succeed(chalk3.green(`${chatModel} is installed and ready`));
|
|
1627
|
+
break;
|
|
1628
|
+
} else {
|
|
1629
|
+
modelSpinner.fail(chalk3.red(`${chatModel} is not installed in your Ollama`));
|
|
1630
|
+
console.log(chalk3.yellow(` Run: ollama pull ${chatModel} \u2014 or pick a different model.
|
|
1367
1631
|
`));
|
|
1632
|
+
}
|
|
1633
|
+
} catch {
|
|
1634
|
+
modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
|
|
1635
|
+
break;
|
|
1368
1636
|
}
|
|
1369
|
-
} catch {
|
|
1370
|
-
modelSpinner.warn(chalk3.yellow("Could not verify model \u2014 continuing anyway"));
|
|
1371
|
-
break;
|
|
1372
1637
|
}
|
|
1638
|
+
} else {
|
|
1639
|
+
const modelChoices = {
|
|
1640
|
+
openai: [
|
|
1641
|
+
{ name: "gpt-4o (best accuracy)", value: "gpt-4o" },
|
|
1642
|
+
{ name: "gpt-4o-mini (fast, cheaper)", value: "gpt-4o-mini" },
|
|
1643
|
+
{ name: "gpt-4-turbo (powerful, slower)", value: "gpt-4-turbo" },
|
|
1644
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1645
|
+
],
|
|
1646
|
+
anthropic: [
|
|
1647
|
+
{ name: "claude-sonnet-4-5 (best accuracy)", value: "claude-sonnet-4-5-20251001" },
|
|
1648
|
+
{ name: "claude-haiku-4-5 (fast, cheaper)", value: "claude-haiku-4-5-20251001" },
|
|
1649
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1650
|
+
],
|
|
1651
|
+
minimax: [
|
|
1652
|
+
{ name: "MiniMax-Text-01 (flagship)", value: "MiniMax-Text-01" },
|
|
1653
|
+
{ name: "abab6.5s-chat (fast, efficient)", value: "abab6.5s-chat" },
|
|
1654
|
+
{ name: "Other (enter manually)", value: "__custom__" }
|
|
1655
|
+
]
|
|
1656
|
+
};
|
|
1657
|
+
const modelChoice = await select({
|
|
1658
|
+
message: `Which ${chatProvider} model?`,
|
|
1659
|
+
choices: modelChoices[chatProvider]
|
|
1660
|
+
});
|
|
1661
|
+
chatModel = modelChoice === "__custom__" ? await input({ message: "Model name?" }) : modelChoice;
|
|
1662
|
+
chatApiKey = await input({
|
|
1663
|
+
message: `${chatProvider.charAt(0).toUpperCase() + chatProvider.slice(1)} API key?`
|
|
1664
|
+
});
|
|
1665
|
+
console.log(chalk3.green(` \u2713 ${chatProvider} / ${chatModel} configured`));
|
|
1373
1666
|
}
|
|
1374
|
-
const
|
|
1667
|
+
const envLines = [
|
|
1375
1668
|
`DATABASE_URL=${dbUrl}`,
|
|
1376
|
-
`OLLAMA_URL=${
|
|
1669
|
+
`OLLAMA_URL=${ollamaUrl2}`,
|
|
1377
1670
|
`OLLAMA_MODEL=nomic-embed-text`,
|
|
1378
|
-
`
|
|
1379
|
-
|
|
1380
|
-
|
|
1671
|
+
`CHAT_PROVIDER=${chatProvider}`,
|
|
1672
|
+
`CHAT_MODEL=${chatModel}`
|
|
1673
|
+
];
|
|
1674
|
+
if (chatProvider === "ollama") envLines.push(`OLLAMA_CHAT_MODEL=${chatModel}`);
|
|
1675
|
+
if (chatApiKey) envLines.push(`CHAT_API_KEY=${chatApiKey}`);
|
|
1676
|
+
const envContent = envLines.join("\n") + "\n";
|
|
1677
|
+
writeFileSync5(envPath, envContent);
|
|
1381
1678
|
process.env.DATABASE_URL = dbUrl;
|
|
1382
|
-
process.env.OLLAMA_URL =
|
|
1679
|
+
process.env.OLLAMA_URL = ollamaUrl2;
|
|
1383
1680
|
process.env.OLLAMA_MODEL = "nomic-embed-text";
|
|
1384
|
-
process.env.
|
|
1681
|
+
process.env.CHAT_PROVIDER = chatProvider;
|
|
1682
|
+
process.env.CHAT_MODEL = chatModel;
|
|
1683
|
+
if (chatProvider === "ollama") process.env.OLLAMA_CHAT_MODEL = chatModel;
|
|
1684
|
+
if (chatApiKey) process.env.CHAT_API_KEY = chatApiKey;
|
|
1385
1685
|
const gitignorePath2 = join6(process.cwd(), ".gitignore");
|
|
1386
1686
|
if (existsSync6(gitignorePath2)) {
|
|
1387
|
-
const gi =
|
|
1687
|
+
const gi = readFileSync6(gitignorePath2, "utf-8");
|
|
1388
1688
|
if (!gi.includes(".memory-core.env")) {
|
|
1389
1689
|
appendFileSync(gitignorePath2, "\n.memory-core.env\n");
|
|
1390
1690
|
}
|
|
1391
1691
|
} else {
|
|
1392
|
-
|
|
1692
|
+
writeFileSync5(gitignorePath2, ".memory-core.env\n");
|
|
1393
1693
|
}
|
|
1394
1694
|
console.log(chalk3.green("\n \u2713 .memory-core.env created"));
|
|
1395
1695
|
console.log(chalk3.gray(" Added to .gitignore \u2014 your DB credentials stay local.\n"));
|
|
1396
1696
|
}
|
|
1397
|
-
const projectName = await input({
|
|
1697
|
+
const projectName = quick ? process.cwd().split("/").pop() ?? "my-project" : await input({
|
|
1398
1698
|
message: "Project name?",
|
|
1399
1699
|
default: process.cwd().split("/").pop() ?? "my-project"
|
|
1400
1700
|
});
|
|
1401
|
-
const
|
|
1701
|
+
const inferredProjectType = ["Nuxt.js"].includes(detected.framework) ? "fullstack" : ["React", "Vue.js", "Svelte"].includes(detected.framework) ? "frontend" : "backend";
|
|
1702
|
+
const projectType = quick ? inferredProjectType : await select({
|
|
1402
1703
|
message: "Project type?",
|
|
1403
1704
|
choices: [
|
|
1404
1705
|
{ value: "backend", name: "Backend \u2014 API, server, microservice" },
|
|
@@ -1408,35 +1709,49 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1408
1709
|
});
|
|
1409
1710
|
let backendArchitecture;
|
|
1410
1711
|
if (projectType === "backend" || projectType === "fullstack") {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1712
|
+
if (quick) {
|
|
1713
|
+
backendArchitecture = detected.framework === "NestJS" ? "nestjs" : detected.framework === "Laravel" ? "laravel-service-repository" : detected.framework === "Go" ? "go-api" : "clean-architecture";
|
|
1714
|
+
} else {
|
|
1715
|
+
const backendProfiles = listProfiles("backend");
|
|
1716
|
+
backendArchitecture = await select({
|
|
1717
|
+
message: "Backend architecture?",
|
|
1718
|
+
choices: backendProfiles.map((p) => ({
|
|
1719
|
+
value: p.name,
|
|
1720
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
1721
|
+
}))
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1419
1724
|
}
|
|
1420
1725
|
let frontendFramework;
|
|
1421
1726
|
if (projectType === "frontend" || projectType === "fullstack") {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1727
|
+
if (quick) {
|
|
1728
|
+
const frameworkMap = {
|
|
1729
|
+
"Nuxt.js": "nuxt",
|
|
1730
|
+
React: "react",
|
|
1731
|
+
"Vue.js": "vue",
|
|
1732
|
+
Svelte: "svelte"
|
|
1733
|
+
};
|
|
1734
|
+
frontendFramework = frameworkMap[detected.framework] ?? "react";
|
|
1735
|
+
} else {
|
|
1736
|
+
const frontendProfiles = listProfiles("frontend");
|
|
1737
|
+
frontendFramework = await select({
|
|
1738
|
+
message: "Frontend framework?",
|
|
1739
|
+
choices: frontendProfiles.map((p) => ({
|
|
1740
|
+
value: p.name,
|
|
1741
|
+
name: `${p.displayName} \u2014 ${p.description}`
|
|
1742
|
+
}))
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1430
1745
|
}
|
|
1431
|
-
const language = await input({
|
|
1746
|
+
const language = quick ? detected.language : await input({
|
|
1432
1747
|
message: "Language?",
|
|
1433
1748
|
default: detected.language
|
|
1434
1749
|
});
|
|
1435
|
-
const pullMemories = await confirm({
|
|
1750
|
+
const pullMemories = quick ? true : await confirm({
|
|
1436
1751
|
message: "Pull relevant memories from previous projects?",
|
|
1437
1752
|
default: true
|
|
1438
1753
|
});
|
|
1439
|
-
const installCaveman = await confirm({
|
|
1754
|
+
const installCaveman = quick ? false : await confirm({
|
|
1440
1755
|
message: "Install caveman token saver? (~65-75% fewer tokens)",
|
|
1441
1756
|
default: false
|
|
1442
1757
|
});
|
|
@@ -1451,18 +1766,20 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1451
1766
|
]
|
|
1452
1767
|
});
|
|
1453
1768
|
}
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1769
|
+
const selectedAgents = quick ? AGENT_NAMES.filter((a) => a !== "Shared") : await (async () => {
|
|
1770
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
1771
|
+
return checkbox({
|
|
1772
|
+
message: "Which AI agents do you want to generate files for?",
|
|
1773
|
+
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({ name, value: name, checked: true })),
|
|
1774
|
+
instructions: " (Space to toggle, A to select all, Enter to confirm)"
|
|
1775
|
+
});
|
|
1776
|
+
})();
|
|
1777
|
+
const enableHook = quick ? true : await confirm({
|
|
1461
1778
|
message: "Enable pre-commit hook?",
|
|
1462
1779
|
default: true
|
|
1463
1780
|
});
|
|
1464
1781
|
let hookAdvisory = true;
|
|
1465
|
-
if (enableHook) {
|
|
1782
|
+
if (enableHook && !quick) {
|
|
1466
1783
|
const { select: selectMode } = await import("@inquirer/prompts");
|
|
1467
1784
|
hookAdvisory = await selectMode({
|
|
1468
1785
|
message: "Hook mode?",
|
|
@@ -1472,7 +1789,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1472
1789
|
]
|
|
1473
1790
|
});
|
|
1474
1791
|
}
|
|
1475
|
-
const
|
|
1792
|
+
const config = {
|
|
1476
1793
|
projectName,
|
|
1477
1794
|
projectType,
|
|
1478
1795
|
backendArchitecture,
|
|
@@ -1495,7 +1812,7 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1495
1812
|
if (installCaveman) {
|
|
1496
1813
|
const spinner2 = ora("Installing caveman token saver\u2026").start();
|
|
1497
1814
|
try {
|
|
1498
|
-
|
|
1815
|
+
execSync2(
|
|
1499
1816
|
"curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash",
|
|
1500
1817
|
{ stdio: "pipe", cwd: process.cwd() }
|
|
1501
1818
|
);
|
|
@@ -1506,16 +1823,16 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1506
1823
|
}
|
|
1507
1824
|
const spinner = ora("Generating AI agent context files\u2026").start();
|
|
1508
1825
|
const written = await generate(
|
|
1509
|
-
{ projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman:
|
|
1826
|
+
{ projectName, projectType, backendArchitecture, frontendFramework, language, memories, caveman: config.caveman },
|
|
1510
1827
|
process.cwd(),
|
|
1511
1828
|
[...selectedAgents, "Shared"]
|
|
1512
1829
|
);
|
|
1513
|
-
writeProjectConfig(
|
|
1830
|
+
writeProjectConfig(config);
|
|
1514
1831
|
spinner.succeed(`Generated ${written.written.length} files`);
|
|
1515
1832
|
const gitignorePath = join6(process.cwd(), ".gitignore");
|
|
1516
1833
|
const generatedPaths = written.written;
|
|
1517
1834
|
if (generatedPaths.length > 0) {
|
|
1518
|
-
const existing = existsSync6(gitignorePath) ?
|
|
1835
|
+
const existing = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
1519
1836
|
const toAdd = generatedPaths.filter((p) => !existing.includes(p));
|
|
1520
1837
|
if (toAdd.length > 0) {
|
|
1521
1838
|
const block = "\n# memory-core generated files\n" + toAdd.join("\n") + "\n";
|
|
@@ -1531,17 +1848,17 @@ program.command("init").description("Initialize memory-core in the current proje
|
|
|
1531
1848
|
process.env.OLLAMA_URL ?? "http://localhost:11434",
|
|
1532
1849
|
process.env.OLLAMA_CHAT_MODEL ?? "llama3.2"
|
|
1533
1850
|
);
|
|
1534
|
-
printBanner(
|
|
1851
|
+
printBanner(config.projectName, written.written.length, status);
|
|
1535
1852
|
await closePool();
|
|
1536
1853
|
});
|
|
1537
1854
|
program.command("sync").description("Re-pull memories and regenerate AI agent files").action(async () => {
|
|
1538
|
-
const
|
|
1539
|
-
if (!
|
|
1855
|
+
const config = readProjectConfig();
|
|
1856
|
+
if (!config) {
|
|
1540
1857
|
console.error(chalk3.red("No .memory-core.json found. Run: memory-core init"));
|
|
1541
1858
|
process.exit(1);
|
|
1542
1859
|
}
|
|
1543
1860
|
const { checkbox } = await import("@inquirer/prompts");
|
|
1544
|
-
const savedAgents = new Set(
|
|
1861
|
+
const savedAgents = new Set(config.agents ?? AGENT_NAMES.filter((a) => a !== "Shared"));
|
|
1545
1862
|
const selectedAgents = await checkbox({
|
|
1546
1863
|
message: "Which agents do you want to sync?",
|
|
1547
1864
|
choices: AGENT_NAMES.filter((a) => a !== "Shared").map((name) => ({
|
|
@@ -1558,21 +1875,21 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
1558
1875
|
const spinner = ora("Syncing memories\u2026").start();
|
|
1559
1876
|
let memories = [];
|
|
1560
1877
|
try {
|
|
1561
|
-
const archQuery = [
|
|
1562
|
-
memories = await retrieve(archQuery,
|
|
1878
|
+
const archQuery = [config.backendArchitecture, config.frontendFramework, config.language].filter(Boolean).join(" ");
|
|
1879
|
+
memories = await retrieve(archQuery, config.backendArchitecture ?? config.frontendFramework, 10);
|
|
1563
1880
|
spinner.text = `Found ${memories.length} memories \u2014 regenerating files\u2026`;
|
|
1564
1881
|
} catch (err) {
|
|
1565
1882
|
spinner.warn(`Could not retrieve memories: ${err.message}`);
|
|
1566
1883
|
}
|
|
1567
1884
|
const result = await generate(
|
|
1568
1885
|
{
|
|
1569
|
-
projectName:
|
|
1570
|
-
projectType:
|
|
1571
|
-
backendArchitecture:
|
|
1572
|
-
frontendFramework:
|
|
1573
|
-
language:
|
|
1886
|
+
projectName: config.projectName,
|
|
1887
|
+
projectType: config.projectType,
|
|
1888
|
+
backendArchitecture: config.backendArchitecture,
|
|
1889
|
+
frontendFramework: config.frontendFramework,
|
|
1890
|
+
language: config.language,
|
|
1574
1891
|
memories,
|
|
1575
|
-
caveman:
|
|
1892
|
+
caveman: config.caveman
|
|
1576
1893
|
},
|
|
1577
1894
|
process.cwd(),
|
|
1578
1895
|
[...selectedAgents, "Shared"]
|
|
@@ -1588,7 +1905,7 @@ program.command("sync").description("Re-pull memories and regenerate AI agent fi
|
|
|
1588
1905
|
await closePool();
|
|
1589
1906
|
});
|
|
1590
1907
|
program.command("remember <text>").description("Save a new memory to the central database").option("-t, --type <type>", "Memory type (decision|rule|pattern|note)", "decision").option("-s, --scope <scope>", "Scope (global|project)", "project").option("--tags <tags>", "Comma-separated tags").option("-r, --reason <reason>", "Why this rule exists \u2014 helps agents understand intent and debug violations").action(async (text, opts) => {
|
|
1591
|
-
const
|
|
1908
|
+
const config = readProjectConfig();
|
|
1592
1909
|
let reason = opts.reason;
|
|
1593
1910
|
if (!reason) {
|
|
1594
1911
|
reason = await input({
|
|
@@ -1602,8 +1919,8 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
1602
1919
|
await saveMemory({
|
|
1603
1920
|
type: opts.type,
|
|
1604
1921
|
scope: opts.scope,
|
|
1605
|
-
architecture:
|
|
1606
|
-
projectName:
|
|
1922
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
1923
|
+
projectName: config?.projectName,
|
|
1607
1924
|
content: text,
|
|
1608
1925
|
reason: reason || void 0,
|
|
1609
1926
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
@@ -1619,12 +1936,12 @@ program.command("remember <text>").description("Save a new memory to the central
|
|
|
1619
1936
|
await closePool();
|
|
1620
1937
|
});
|
|
1621
1938
|
program.command("search <query>").description("Search memories using semantic similarity").option("-n, --limit <n>", "Number of results", "5").action(async (query, opts) => {
|
|
1622
|
-
const
|
|
1939
|
+
const config = readProjectConfig();
|
|
1623
1940
|
const spinner = ora("Searching\u2026").start();
|
|
1624
1941
|
try {
|
|
1625
1942
|
const results = await retrieve(
|
|
1626
1943
|
query,
|
|
1627
|
-
|
|
1944
|
+
config?.backendArchitecture ?? config?.frontendFramework,
|
|
1628
1945
|
parseInt(opts.limit, 10)
|
|
1629
1946
|
);
|
|
1630
1947
|
spinner.stop();
|
|
@@ -1648,6 +1965,222 @@ program.command("search <query>").description("Search memories using semantic si
|
|
|
1648
1965
|
}
|
|
1649
1966
|
await closePool();
|
|
1650
1967
|
});
|
|
1968
|
+
program.command("export").description(`Export DB memories to ${MEMORY_FILE}`).option("-o, --output <file>", `Output file (default: ${MEMORY_FILE})`).action(async (opts) => {
|
|
1969
|
+
const spinner = ora("Exporting memories\u2026").start();
|
|
1970
|
+
try {
|
|
1971
|
+
const memories = await listMemories({ limit: 1e4 });
|
|
1972
|
+
const portable = memories.map(toPortableMemory);
|
|
1973
|
+
const outputPath = opts.output ? join6(process.cwd(), opts.output) : writeMemoryFile(portable);
|
|
1974
|
+
if (opts.output) {
|
|
1975
|
+
writeFileSync5(outputPath, JSON.stringify(portable, null, 2) + "\n", "utf-8");
|
|
1976
|
+
}
|
|
1977
|
+
spinner.succeed(`Exported ${portable.length} memories to ${outputPath}`);
|
|
1978
|
+
} catch (err) {
|
|
1979
|
+
spinner.fail(`Export failed: ${err.message}`);
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
} finally {
|
|
1982
|
+
await closePool();
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
program.command("import").description(`Import memories from ${MEMORY_FILE}`).option("--url <url>", "Import memories from a remote URL").option("-f, --file <file>", `Import file (default: ${MEMORY_FILE})`).action(async (opts) => {
|
|
1986
|
+
const spinner = ora("Reading memories\u2026").start();
|
|
1987
|
+
try {
|
|
1988
|
+
const memories = opts.url ? await readMemoryFileFromUrl(opts.url) : opts.file ? parseMemoryFile(readFileSync6(join6(process.cwd(), opts.file), "utf-8")) : readMemoryFile();
|
|
1989
|
+
let inserted = 0;
|
|
1990
|
+
let skipped = 0;
|
|
1991
|
+
spinner.text = `Importing ${memories.length} memories\u2026`;
|
|
1992
|
+
for (const memory of memories) {
|
|
1993
|
+
const embedding = await embed(memory.content);
|
|
1994
|
+
const result = await upsertMemory({
|
|
1995
|
+
type: memory.type,
|
|
1996
|
+
scope: memory.scope,
|
|
1997
|
+
architecture: memory.architecture,
|
|
1998
|
+
projectName: memory.projectName,
|
|
1999
|
+
title: memory.title,
|
|
2000
|
+
content: memory.content,
|
|
2001
|
+
reason: memory.reason,
|
|
2002
|
+
tags: memory.tags,
|
|
2003
|
+
embedding
|
|
2004
|
+
});
|
|
2005
|
+
if (result === "inserted") inserted++;
|
|
2006
|
+
else skipped++;
|
|
2007
|
+
}
|
|
2008
|
+
spinner.succeed(`Imported ${inserted} memories, skipped ${skipped} duplicates`);
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
spinner.fail(`Import failed: ${err.message}`);
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
} finally {
|
|
2013
|
+
await closePool();
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
program.command("list").description("List memories from the local database").option("--type <type>", "Filter by type").option("--scope <scope>", "Filter by scope").option("--arch <architecture>", "Filter by architecture").option("-n, --limit <n>", "Maximum rows to show", "200").action(async (opts) => {
|
|
2017
|
+
try {
|
|
2018
|
+
const memories = await listMemories({
|
|
2019
|
+
type: opts.type,
|
|
2020
|
+
scope: opts.scope,
|
|
2021
|
+
architecture: opts.arch,
|
|
2022
|
+
limit: parseInt(opts.limit, 10)
|
|
2023
|
+
});
|
|
2024
|
+
printMemoryTable(memories);
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
console.error(chalk3.red(`List failed: ${err.message}`));
|
|
2027
|
+
process.exit(1);
|
|
2028
|
+
} finally {
|
|
2029
|
+
await closePool();
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
program.command("remove <id>").description("Remove a memory by ID").action(async (id) => {
|
|
2033
|
+
try {
|
|
2034
|
+
const deleted = await deleteMemory(parseInt(id, 10));
|
|
2035
|
+
if (!deleted) {
|
|
2036
|
+
console.log(chalk3.yellow(`No memory found with ID ${id}`));
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
console.log(chalk3.green(`Removed memory ${id}`));
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
console.error(chalk3.red(`Remove failed: ${err.message}`));
|
|
2042
|
+
process.exit(1);
|
|
2043
|
+
} finally {
|
|
2044
|
+
await closePool();
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
program.command("edit <id>").description("Edit a memory interactively").action(async (id) => {
|
|
2048
|
+
const memoryId = parseInt(id, 10);
|
|
2049
|
+
try {
|
|
2050
|
+
const existing = await getMemory(memoryId);
|
|
2051
|
+
if (!existing) {
|
|
2052
|
+
console.log(chalk3.yellow(`No memory found with ID ${id}`));
|
|
2053
|
+
process.exit(1);
|
|
2054
|
+
}
|
|
2055
|
+
const type = await input({ message: "Type?", default: existing.type });
|
|
2056
|
+
const scope = await input({ message: "Scope?", default: existing.scope });
|
|
2057
|
+
const title = await input({ message: "Title?", default: existing.title ?? "" });
|
|
2058
|
+
const content = await input({ message: "Content?", default: existing.content });
|
|
2059
|
+
const reason = await input({ message: "Reason?", default: existing.reason ?? "" });
|
|
2060
|
+
const tags = await input({ message: "Tags?", default: existing.tags.join(",") });
|
|
2061
|
+
const embedding = content === existing.content ? void 0 : await embed(content);
|
|
2062
|
+
await updateMemory(memoryId, {
|
|
2063
|
+
type,
|
|
2064
|
+
scope,
|
|
2065
|
+
title: title || void 0,
|
|
2066
|
+
content,
|
|
2067
|
+
reason: reason || void 0,
|
|
2068
|
+
tags: parseTags(tags),
|
|
2069
|
+
embedding
|
|
2070
|
+
});
|
|
2071
|
+
console.log(chalk3.green(`Updated memory ${id}`));
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
console.error(chalk3.red(`Edit failed: ${err.message}`));
|
|
2074
|
+
process.exit(1);
|
|
2075
|
+
} finally {
|
|
2076
|
+
await closePool();
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
program.command("ignore [pattern]").description("Manage project-scoped false-positive ignore patterns").option("--list", "List ignored patterns").option("--remove <id>", "Remove an ignored pattern by ID").action(async (pattern, opts) => {
|
|
2080
|
+
try {
|
|
2081
|
+
if (opts.list) {
|
|
2082
|
+
printMemoryTable(await listMemories({ type: "ignore", limit: 1e3 }), "Ignored patterns");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (opts.remove) {
|
|
2086
|
+
const deleted = await deleteMemory(parseInt(opts.remove, 10));
|
|
2087
|
+
if (!deleted) {
|
|
2088
|
+
console.log(chalk3.yellow(`No ignore pattern found with ID ${opts.remove}`));
|
|
2089
|
+
process.exit(1);
|
|
2090
|
+
}
|
|
2091
|
+
console.log(chalk3.green(`Removed ignore pattern ${opts.remove}`));
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (!pattern) {
|
|
2095
|
+
console.error(chalk3.red("Provide a pattern, --list, or --remove <id>"));
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
2098
|
+
const config = readProjectConfig();
|
|
2099
|
+
const embedding = await embed(pattern);
|
|
2100
|
+
await upsertMemory({
|
|
2101
|
+
type: "ignore",
|
|
2102
|
+
scope: "project",
|
|
2103
|
+
architecture: config?.backendArchitecture ?? config?.frontendFramework,
|
|
2104
|
+
projectName: config?.projectName,
|
|
2105
|
+
content: pattern,
|
|
2106
|
+
tags: ["ignore"],
|
|
2107
|
+
embedding
|
|
2108
|
+
});
|
|
2109
|
+
console.log(chalk3.green(`Ignored pattern saved: "${pattern}"`));
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
console.error(chalk3.red(`Ignore failed: ${err.message}`));
|
|
2112
|
+
process.exit(1);
|
|
2113
|
+
} finally {
|
|
2114
|
+
await closePool();
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
program.command("ci-setup").description("Generate GitHub Actions workflow for memory-core").action(() => {
|
|
2118
|
+
const workflowPath = join6(process.cwd(), ".github", "workflows", "memory-core.yml");
|
|
2119
|
+
mkdirSync2(dirname2(workflowPath), { recursive: true });
|
|
2120
|
+
writeFileSync5(workflowPath, `name: memory-core
|
|
2121
|
+
on: [pull_request]
|
|
2122
|
+
jobs:
|
|
2123
|
+
check:
|
|
2124
|
+
runs-on: ubuntu-latest
|
|
2125
|
+
steps:
|
|
2126
|
+
- uses: actions/checkout@v4
|
|
2127
|
+
with:
|
|
2128
|
+
fetch-depth: 0
|
|
2129
|
+
- run: npx @shahmilsaari/memory-core check --ci
|
|
2130
|
+
`, "utf-8");
|
|
2131
|
+
console.log(chalk3.green(`Generated ${workflowPath}`));
|
|
2132
|
+
});
|
|
2133
|
+
program.command("reset").description("Remove memory-core generated files and local project config").option("--soft", "Only remove generated files; keep config and DB").option("--db", "Also drop the memories table after confirmation").action(async (opts) => {
|
|
2134
|
+
const generated = [...new Set(OUTPUT_FILES.map((file) => file.path))];
|
|
2135
|
+
let removed = 0;
|
|
2136
|
+
for (const relativePath of generated) {
|
|
2137
|
+
const target = join6(process.cwd(), relativePath);
|
|
2138
|
+
if (existsSync6(target)) {
|
|
2139
|
+
rmSync(target, { force: true });
|
|
2140
|
+
removed++;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (!opts.soft) {
|
|
2144
|
+
const configPath = join6(process.cwd(), CONFIG_FILE);
|
|
2145
|
+
if (existsSync6(configPath)) {
|
|
2146
|
+
unlinkSync2(configPath);
|
|
2147
|
+
removed++;
|
|
2148
|
+
}
|
|
2149
|
+
uninstallHook();
|
|
2150
|
+
}
|
|
2151
|
+
if (opts.db) {
|
|
2152
|
+
const ok = await confirm({
|
|
2153
|
+
message: "Drop the memories table from the configured database?",
|
|
2154
|
+
default: false
|
|
2155
|
+
});
|
|
2156
|
+
if (ok) {
|
|
2157
|
+
const { getPool } = await import("./db-KU4EEG4Y.js");
|
|
2158
|
+
await getPool().query("DROP TABLE IF EXISTS memories");
|
|
2159
|
+
await closePool();
|
|
2160
|
+
console.log(chalk3.yellow("Dropped memories table"));
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
console.log(chalk3.green(`Reset complete. Removed ${removed} files.`));
|
|
2164
|
+
});
|
|
2165
|
+
program.command("stats").description("Show violation counters recorded by check and watch").action(() => {
|
|
2166
|
+
const statsPath = join6(process.cwd(), ".memory-core-stats.json");
|
|
2167
|
+
if (!existsSync6(statsPath)) {
|
|
2168
|
+
console.log(chalk3.yellow("\n No violation stats recorded yet.\n"));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const stats = JSON.parse(readFileSync6(statsPath, "utf-8"));
|
|
2172
|
+
const printTop = (label, values = {}) => {
|
|
2173
|
+
console.log(chalk3.bold(`
|
|
2174
|
+
${label}
|
|
2175
|
+
`));
|
|
2176
|
+
Object.entries(values).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([name, count], index) => {
|
|
2177
|
+
console.log(` ${index + 1}. ${truncate(name, 44).padEnd(46)} ${count}`);
|
|
2178
|
+
});
|
|
2179
|
+
};
|
|
2180
|
+
printTop("Top rules", stats.rules);
|
|
2181
|
+
printTop("Top files", stats.files);
|
|
2182
|
+
console.log();
|
|
2183
|
+
});
|
|
1651
2184
|
program.command("seed").description("Load all predefined memories into the database").option("--arch <architecture>", "Only seed a specific architecture (e.g. clean-architecture)").option("--force", "Re-seed even if memories already exist", false).action(async (opts) => {
|
|
1652
2185
|
await runMigrations();
|
|
1653
2186
|
const filtered = opts.arch ? seeds.filter((s) => s.architecture === opts.arch || s.architecture === "global") : seeds;
|
|
@@ -1660,7 +2193,7 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1660
2193
|
const spinner = ora(`[${seed.architecture}] ${seed.title}`).start();
|
|
1661
2194
|
try {
|
|
1662
2195
|
const embedding = await embed(seed.content);
|
|
1663
|
-
|
|
2196
|
+
const payload = {
|
|
1664
2197
|
type: seed.type,
|
|
1665
2198
|
scope: seed.scope,
|
|
1666
2199
|
architecture: seed.architecture === "global" ? void 0 : seed.architecture,
|
|
@@ -1669,9 +2202,21 @@ program.command("seed").description("Load all predefined memories into the datab
|
|
|
1669
2202
|
reason: seed.reason,
|
|
1670
2203
|
tags: seed.tags,
|
|
1671
2204
|
embedding
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
|
|
2205
|
+
};
|
|
2206
|
+
if (opts.force) {
|
|
2207
|
+
await saveMemory(payload);
|
|
2208
|
+
saved++;
|
|
2209
|
+
spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2210
|
+
} else {
|
|
2211
|
+
const result = await upsertMemory(payload);
|
|
2212
|
+
if (result === "inserted") {
|
|
2213
|
+
saved++;
|
|
2214
|
+
spinner.succeed(chalk3.gray(`[${seed.architecture}] ${seed.title}`));
|
|
2215
|
+
} else {
|
|
2216
|
+
skipped++;
|
|
2217
|
+
spinner.info(chalk3.gray(`Already exists \u2014 [${seed.architecture}] ${seed.title}`));
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
1675
2220
|
} catch (err) {
|
|
1676
2221
|
spinner.warn(`Skipped \u2014 ${err.message}`);
|
|
1677
2222
|
skipped++;
|
|
@@ -1725,12 +2270,12 @@ ${rulesText}
|
|
|
1725
2270
|
const skipped = [];
|
|
1726
2271
|
const writeFile2 = (filePath, content) => {
|
|
1727
2272
|
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
1728
|
-
|
|
2273
|
+
writeFileSync5(filePath, content, "utf-8");
|
|
1729
2274
|
};
|
|
1730
2275
|
const readJson = (filePath) => {
|
|
1731
2276
|
if (!existsSync6(filePath)) return {};
|
|
1732
2277
|
try {
|
|
1733
|
-
return JSON.parse(
|
|
2278
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
1734
2279
|
} catch {
|
|
1735
2280
|
return {};
|
|
1736
2281
|
}
|
|
@@ -1755,9 +2300,9 @@ ${rulesText}
|
|
|
1755
2300
|
writeFile2(target.path, JSON.stringify(processedVscode.settings, null, 2));
|
|
1756
2301
|
written.push(target.label);
|
|
1757
2302
|
} else if (target.type === "continue") {
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
writeFile2(target.path, JSON.stringify(
|
|
2303
|
+
const config = readJson(target.path);
|
|
2304
|
+
config["systemMessage"] = systemPrompt;
|
|
2305
|
+
writeFile2(target.path, JSON.stringify(config, null, 2));
|
|
1761
2306
|
written.push(target.label);
|
|
1762
2307
|
} else if (target.type === "aider") {
|
|
1763
2308
|
const aiderContent = `# Aider global config \u2014 synced by memory-core
|
|
@@ -1767,11 +2312,11 @@ read:
|
|
|
1767
2312
|
writeFile2(target.path, aiderContent);
|
|
1768
2313
|
written.push(target.label);
|
|
1769
2314
|
} else if (target.type === "zed") {
|
|
1770
|
-
const
|
|
1771
|
-
const assistant =
|
|
2315
|
+
const config = readJson(target.path);
|
|
2316
|
+
const assistant = config["assistant"] ?? {};
|
|
1772
2317
|
assistant["system_prompt"] = systemPrompt;
|
|
1773
|
-
|
|
1774
|
-
writeFile2(target.path, JSON.stringify(
|
|
2318
|
+
config["assistant"] = assistant;
|
|
2319
|
+
writeFile2(target.path, JSON.stringify(config, null, 2));
|
|
1775
2320
|
written.push(target.label);
|
|
1776
2321
|
}
|
|
1777
2322
|
} catch {
|
|
@@ -1796,10 +2341,14 @@ hook.command("install").description("Install pre-commit hook (advisory mode by d
|
|
|
1796
2341
|
hook.command("uninstall").description("Remove the pre-commit hook").action(() => {
|
|
1797
2342
|
uninstallHook();
|
|
1798
2343
|
});
|
|
1799
|
-
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--verbose", "Show model and diff details").action(async (opts) => {
|
|
1800
|
-
|
|
2344
|
+
program.command("check").description("Check staged changes against architecture rules (used by pre-commit hook)").option("--staged", "Check git staged diff (default behaviour)").option("--ci", `Check CI diff using ${MEMORY_FILE}`).option("--verbose", "Show model and diff details").option("--debug", "Show prompt, diff, and raw model response").action(async (opts) => {
|
|
2345
|
+
if (opts.ci) {
|
|
2346
|
+
await checkCi({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
await checkStaged({ verbose: opts.verbose ?? false, debug: opts.debug ?? false });
|
|
1801
2350
|
});
|
|
1802
|
-
program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--verbose", "Show diff size and model details per file").action((opts) => {
|
|
1803
|
-
startWatch({ path: opts.path, verbose: opts.verbose });
|
|
2351
|
+
program.command("watch").description("Watch source files and check violations in real-time on every save").option("--path <dir>", "Directory to watch (default: current directory)").option("--verbose", "Show diff size and model details per file").option("--debug", "Show prompt, diff, and raw model response").action((opts) => {
|
|
2352
|
+
startWatch({ path: opts.path, verbose: opts.verbose, debug: opts.debug });
|
|
1804
2353
|
});
|
|
1805
2354
|
program.parseAsync(process.argv);
|