march-cli 0.1.22 → 0.1.23

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 ADDED
@@ -0,0 +1,88 @@
1
+ <p align="center">
2
+ <img src="docs/assets/march-banner.png" alt="March CLI banner" width="800">
3
+ </p>
4
+
5
+ <p align="center"><strong>Forget Skills. Embrace Memory. Keep your model in the sweet spot.</strong></p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/march-cli"><img alt="npm" src="https://img.shields.io/npm/v/march-cli?style=flat-square" /></a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="README.md">English</a> |
13
+ <a href="README.zh.md">简体中文</a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ### Installation
19
+
20
+ ```bash
21
+ npm install -g march-cli
22
+ ```
23
+
24
+ ### Why Token Efficient?
25
+
26
+ March is obsessively token-efficient. After each turn, context resets to ~8K — we **discard** all intermediate execution and keep only two things: the user's question and the AI's final response.
27
+
28
+ Most agents fight context bloat with compression, truncation, retrieval, summarization. March's answer: just throw away what you don't need.
29
+
30
+ The result:
31
+
32
+ - **91% cache hit rate**, individual model calls rarely exceed 50K tokens
33
+ - Context never grows unbounded — **no context rot**
34
+ - Your model always operates in the sweet spot, not drowning in 100K of noise
35
+
36
+ ### Memory System
37
+
38
+ March has a built-in memory system. Anything you tell March — preferences, project conventions, technical decisions — it remembers. When you need it again, March **automatically recalls** relevant memories during its thinking process. No manual search required.
39
+
40
+ #### No Skill Files
41
+
42
+ The problem with Skill systems: Skill files inject context upfront. What happens when you have too many?
43
+
44
+ March takes a different approach: every memory is a "latent skill," recalled on-demand rather than always sitting in context. What you've discussed is the best prompt.
45
+
46
+ #### Managing Memories
47
+
48
+ March stores memories as Markdown files under `~/.march/March Memories/`. You can directly edit, delete, or add files — March auto-detects changes.
49
+
50
+ ### Built-in Capabilities
51
+
52
+ **Image Generation**: If you have access to ChatGPT Codex, March can generate images directly — no extra API keys or third-party services.
53
+
54
+ **Web Search**: Connect SuperGrok and **all your configured models** gain web search. March dispatches Grok to search and injects results into the current conversation.
55
+
56
+ **More Search**: Tavily Search and Brave Search are built in.
57
+
58
+ ### Configuration
59
+
60
+ March uses `~/.march/config.json` (global) or `<project>/.march/config.json` (project-level) for model and provider configuration. Compatible with any OpenAI-compatible API.
61
+
62
+ ```json
63
+ {
64
+ "provider": "openai",
65
+ "model": "gpt-5.1"
66
+ }
67
+ ```
68
+
69
+ See the [docs](docs/custom-provider.md) for custom providers, multi-model setup, and more.
70
+
71
+ ### FAQ
72
+
73
+ #### How is this different from Claude Code?
74
+
75
+ March is similarly capable but takes a fundamentally different approach to context. Instead of keeping everything in context and relying on compression, March resets context each turn — you get ~8K clean context every time, with 91% cache hit rate. March also replaces Skill files with a built-in memory system that recalls on demand.
76
+
77
+ #### How is this different from OpenCode?
78
+
79
+ Both are open source, terminal-native agents. March's key differentiators: extreme token efficiency via per-turn context reset, a built-in Markdown memory system with automatic recall, and the philosophy that memories should be recalled on-demand — not injected upfront like Skills.
80
+
81
+ ### Documentation
82
+
83
+ - [Full Documentation](docs/) — configuration, context management, memory system
84
+ - [Custom Provider](docs/custom-provider.md) — connect local models or third-party APIs
85
+ - [Context Management](docs/context-core.md) — March's context architecture explained
86
+ - [Memory System](docs/markdown-memory-system.md) — storage and recall mechanism
87
+
88
+
package/README.zh.md ADDED
@@ -0,0 +1,88 @@
1
+ <p align="center">
2
+ <img src="docs/assets/march-banner.png" alt="March CLI banner" width="800">
3
+ </p>
4
+
5
+ <p align="center"><strong>忘记 Skill,拥抱记忆,让你的模型永远工作在甜点区。</strong></p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/march-cli"><img alt="npm" src="https://img.shields.io/npm/v/march-cli?style=flat-square" /></a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="README.md">English</a> |
13
+ <a href="README.zh.md">简体中文</a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ### 安装
19
+
20
+ ```bash
21
+ npm install -g march-cli
22
+ ```
23
+
24
+ ### 为什么省 Token?
25
+
26
+ March 极端地省 Token。每轮对话结束后,上下文回滚到约 8K——我们**丢弃**模型中间的所有执行过程,只保留两样东西:用户的问题和 AI 的最终回复。
27
+
28
+ 大多数 Agent 系统用压缩、裁剪、检索、摘要来对抗上下文膨胀。March 的答案是:直接扔掉不需要的。
29
+
30
+ 结果:
31
+
32
+ - **缓存命中率 91%**,单次模型调用几乎不超过 50K
33
+ - 上下文不会越聊越大,**不存在上下文腐烂**
34
+ - 你的模型永远在甜点区工作,而不是在 100K 的噪音里大海捞针
35
+
36
+ ### 记忆系统
37
+
38
+ March 内置了记忆系统。你在对话中告诉 March 的任何东西——偏好、项目约定、技术决策——它都能记住。当你再次需要时,March 会在思考过程中**自动召回**相关记忆,你不需要手动检索。
39
+
40
+ #### 不需要 Skill 文件
41
+
42
+ Skill 系统的问题是:Skill 文件在一开始就注入了上下文。Skill 多了怎么办?
43
+
44
+ March 换了一种方式:每条记忆就是一条"潜在的 Skill",由 March 在需要时动态召回,而不是常驻在上下文里。你聊过的内容就是最好的提示词。
45
+
46
+ #### 管理记忆
47
+
48
+ March 在 `~/.march/March Memories/` 目录下以 Markdown 文件存储记忆。你可以直接编辑、删除或新增这些文件,March 会自动感知变化。
49
+
50
+ ### 更多内置能力
51
+
52
+ **生图**:如果你有 ChatGPT Codex 权限,March 可以直接生图,不需要额外的 API Key 或第三方服务。
53
+
54
+ **联网搜索**:接入 SuperGrok 后,你配置的**所有模型**都会获得联网搜索能力——March 会派遣 Grok 去搜索,搜索结果注入当前对话。
55
+
56
+ **更多搜索渠道**:Tavily Search、Brave Search 均已内置。
57
+
58
+ ### 配置
59
+
60
+ March 通过 `~/.march/config.json`(全局)或 `<project>/.march/config.json`(项目级)配置模型和 provider。支持所有 OpenAI 兼容接口。
61
+
62
+ ```json
63
+ {
64
+ "provider": "openai",
65
+ "model": "gpt-5.1"
66
+ }
67
+ ```
68
+
69
+ 自定义 provider、多模型切换等详细配置见 [文档](docs/custom-provider.md)。
70
+
71
+ ### FAQ
72
+
73
+ #### 和 Claude Code 有什么区别?
74
+
75
+ March 能力相当,但上下文策略截然不同。Claude Code 尽量保留上下文并用压缩应对膨胀;March 每轮重置上下文——你每次拿到的是干净的 ~8K 上下文,缓存命中率 91%。March 还用内置记忆系统替代了 Skill 文件,记忆按需召回而不是常驻注入。
76
+
77
+ #### 和 OpenCode 有什么区别?
78
+
79
+ 两者都是开源、终端原生的 Agent。March 的核心差异:极端的 Token 效率(每轮上下文重置)、内置 Markdown 记忆系统(自动召回),以及"记忆应该按需召回、而非像 Skill 一样提前注入"的设计哲学。
80
+
81
+ ### 文档
82
+
83
+ - [完整文档](docs/) — 配置、上下文管理、记忆系统
84
+ - [自定义 Provider](docs/custom-provider.md) — 接入本地模型或第三方 API
85
+ - [上下文管理](docs/context-core.md) — March 的上下文架构详解
86
+ - [记忆系统](docs/markdown-memory-system.md) — 记忆存储与召回机制
87
+
88
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -4,21 +4,28 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
4
4
  </identity>
5
5
 
6
6
  <communication_contract>
7
- - Be concise and direct. Match the response shape to the task; simple questions get simple answers.
8
- - Assume users may not see tool calls. Before the first tool call, say in one sentence what you are about to do. While working, give brief updates when you find something important, change direction, or hit a blocker.
9
- - Don't narrate hidden reasoning. State decisions, results, and relevant next steps.
10
- - End with a brief summary of what you did during the task, including what changed, verification status, and what's next if anything; keep it concise, but don't omit the execution overview.
11
- - Report outcomes truthfully. If tests fail or a step was skipped, say so plainly with the relevant output or reason.
7
+ - Be concise and direct. Match the response shape to the task; simple questions get simple answers.
8
+ - Surface assumptions and ambiguity before acting. If intent, constraints, or code organization are unclear, ask or state the uncertainty instead of guessing.
9
+ - Assume users may not see tool calls. Before the first tool call, say in one sentence what you are about to do. While working, give brief updates when you find something important, change direction, or hit a blocker.
10
+ - For multi-step work, checkpoint after meaningful milestones: what changed, what was verified, and what remains.
11
+ - Keep context use bounded. If the task is sprawling or the conversation is losing state, summarize and restart the plan instead of pushing forward blindly.
12
+ - Don't narrate hidden reasoning. State decisions, results, and relevant next steps.
13
+ - End with a brief summary of what you did during the task, including what changed, verification status, and what's next if anything; keep it concise, but don't omit the execution overview.
14
+ - Report outcomes truthfully. If tests fail, checks are skipped, data is ignored, or success is uncertain, say so plainly.
12
15
  </communication_contract>
13
16
 
14
17
  <operating_contract>
15
- - Default to doing the requested work in the repository, not giving abstract advice.
16
- - Build context from current project facts before editing. Inspect existing code and conventions first.
17
- - Keep the change scoped to the request. Don't add features, refactors, abstractions, files, or docs beyond what's needed.
18
- - Three similar lines beats a premature abstraction. No half-finished implementations.
19
- - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal guarantees; validate at real boundaries such as user input and external APIs.
20
- - Avoid backwards-compatibility hacks. If unused code is truly unused, delete it rather than leaving shims or markers.
21
- - Default to add one short comment when the WHY is helpful.
18
+ - Default to doing the requested work in the repository, not giving abstract advice.
19
+ - Define the success condition for non-trivial tasks, then iterate until it is actually met or a blocker is clear.
20
+ - Build context from current project facts before editing. Inspect existing code, exports, direct callers, shared utilities, and conventions first.
21
+ - Keep the change scoped to the request. Don't add features, refactors, abstractions, files, or docs beyond what's needed.
22
+ - Prefer the simplest correct solution. Three similar lines beats a premature abstraction; no speculative code and no half-finished implementations.
23
+ - When existing patterns conflict, do not blend them. Choose the newer, better-tested, or more local convention, state why, and note the other as cleanup if relevant.
24
+ - Follow repository conventions even when another style seems preferable. Raise harmful conventions explicitly; don't silently introduce a second pattern.
25
+ - Use model judgment only where judgment is needed, such as classification, drafting, summarization, or extracting from unstructured text. Deterministic routing, retry, status-code handling, and data transforms belong in code.
26
+ - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal guarantees; validate at real boundaries such as user input and external APIs.
27
+ - Avoid backwards-compatibility hacks. If unused code is truly unused, delete it rather than leaving shims or markers.
28
+ - Default to add one short comment when the WHY is helpful.
22
29
  </operating_contract>
23
30
 
24
31
  <safety_contract>
@@ -40,9 +47,10 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
40
47
  </editing_contract>
41
48
 
42
49
  <verification_contract>
43
- - Run the most relevant tests, type checks, or linters when practical after code changes.
44
- - If you cannot verify, say what was not run and why.
45
- - Do not claim success beyond what you actually checked.
50
+ - Run the most relevant tests, type checks, or linters when practical after code changes.
51
+ - Prefer tests that verify intent and would fail if the underlying behavior is wrong, not tests that only exercise superficial output.
52
+ - If you cannot verify, say what was not run and why.
53
+ - Do not claim success beyond what you actually checked.
46
54
  </verification_contract>
47
55
 
48
56
  <git_contract>
@@ -1,219 +0,0 @@
1
- import { DatabaseSync } from "node:sqlite";
2
- import { mkdirSync } from "node:fs";
3
- import { dirname } from "node:path";
4
-
5
- export const ROOT_NODE_UUID = "00000000-0000-0000-0000-000000000000";
6
-
7
- const SCHEMA = `
8
- CREATE TABLE IF NOT EXISTS nodes (
9
- uuid TEXT PRIMARY KEY,
10
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
11
- last_accessed_at TEXT
12
- );
13
-
14
- CREATE TABLE IF NOT EXISTS memories (
15
- id INTEGER PRIMARY KEY AUTOINCREMENT,
16
- node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
17
- content TEXT NOT NULL,
18
- deprecated INTEGER NOT NULL DEFAULT 0,
19
- migrated_to INTEGER REFERENCES memories(id),
20
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
21
- );
22
-
23
- CREATE TABLE IF NOT EXISTS edges (
24
- id INTEGER PRIMARY KEY AUTOINCREMENT,
25
- parent_uuid TEXT NOT NULL REFERENCES nodes(uuid),
26
- child_uuid TEXT NOT NULL REFERENCES nodes(uuid),
27
- name TEXT NOT NULL,
28
- priority INTEGER NOT NULL DEFAULT 0,
29
- disclosure TEXT
30
- );
31
-
32
- CREATE TABLE IF NOT EXISTS paths (
33
- namespace TEXT NOT NULL DEFAULT '',
34
- domain TEXT NOT NULL DEFAULT 'core',
35
- path TEXT NOT NULL,
36
- edge_id INTEGER NOT NULL REFERENCES edges(id),
37
- node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
38
- PRIMARY KEY (namespace, domain, path)
39
- );
40
-
41
- CREATE TABLE IF NOT EXISTS glossary_keywords (
42
- id INTEGER PRIMARY KEY AUTOINCREMENT,
43
- keyword TEXT NOT NULL,
44
- node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
45
- namespace TEXT NOT NULL DEFAULT ''
46
- );
47
-
48
- CREATE TABLE IF NOT EXISTS search_documents (
49
- node_uuid TEXT NOT NULL,
50
- namespace TEXT NOT NULL DEFAULT '',
51
- content TEXT NOT NULL,
52
- PRIMARY KEY (node_uuid, namespace)
53
- );
54
-
55
- CREATE TABLE IF NOT EXISTS memory_access_log (
56
- id INTEGER PRIMARY KEY AUTOINCREMENT,
57
- node_uuid TEXT NOT NULL REFERENCES nodes(uuid),
58
- namespace TEXT NOT NULL DEFAULT '',
59
- context TEXT,
60
- accessed_at TEXT NOT NULL DEFAULT (datetime('now'))
61
- );
62
-
63
- CREATE INDEX IF NOT EXISTS idx_memories_node ON memories(node_uuid, deprecated);
64
- CREATE INDEX IF NOT EXISTS idx_memories_migrated ON memories(migrated_to);
65
- CREATE INDEX IF NOT EXISTS idx_edges_parent ON edges(parent_uuid);
66
- CREATE INDEX IF NOT EXISTS idx_edges_child ON edges(child_uuid);
67
- CREATE INDEX IF NOT EXISTS idx_paths_node ON paths(node_uuid);
68
- CREATE INDEX IF NOT EXISTS idx_paths_edge ON paths(edge_id);
69
- CREATE INDEX IF NOT EXISTS idx_glossary_node ON glossary_keywords(node_uuid);
70
- CREATE INDEX IF NOT EXISTS idx_glossary_keyword ON glossary_keywords(keyword);
71
- `;
72
-
73
- export function openDatabase(dbPath) {
74
- mkdirSync(dirname(dbPath), { recursive: true });
75
- const db = new DatabaseSync(dbPath);
76
- db.exec("PRAGMA journal_mode = WAL");
77
- db.exec("PRAGMA foreign_keys = ON");
78
- db.exec(SCHEMA);
79
- ensureRootNode(db);
80
- return db;
81
- }
82
-
83
- function ensureRootNode(db) {
84
- const row = db.prepare("SELECT uuid FROM nodes WHERE uuid = ?").get(ROOT_NODE_UUID);
85
- if (!row) {
86
- db.prepare("INSERT INTO nodes (uuid) VALUES (?)").run(ROOT_NODE_UUID);
87
- db.prepare("INSERT INTO memories (node_uuid, content, deprecated) VALUES (?, ?, 0)").run(ROOT_NODE_UUID, "Root node");
88
- }
89
- }
90
-
91
- // ── Node operations ────────────────────────────────────────────────────
92
-
93
- export function createNode(db, uuid) {
94
- db.prepare("INSERT OR IGNORE INTO nodes (uuid) VALUES (?)").run(uuid);
95
- return { uuid };
96
- }
97
-
98
- export function getNode(db, uuid) {
99
- return db.prepare("SELECT * FROM nodes WHERE uuid = ?").get(uuid);
100
- }
101
-
102
- function touchNode(db, uuid) {
103
- db.prepare("UPDATE nodes SET last_accessed_at = datetime('now') WHERE uuid = ?").run(uuid);
104
- }
105
-
106
- // ── Memory operations ───────────────────────────────────────────────────
107
-
108
- export function createMemory(db, nodeUuid, content) {
109
- const result = db.prepare("INSERT INTO memories (node_uuid, content) VALUES (?, ?)").run(nodeUuid, content);
110
- touchNode(db, nodeUuid);
111
- return { id: Number(result.lastInsertRowid), node_uuid: nodeUuid, content };
112
- }
113
-
114
- export function getCurrentMemory(db, nodeUuid) {
115
- return db.prepare("SELECT * FROM memories WHERE node_uuid = ? AND deprecated = 0 ORDER BY id DESC LIMIT 1").get(nodeUuid);
116
- }
117
-
118
- export function getMemoryHistory(db, nodeUuid) {
119
- return db.prepare("SELECT * FROM memories WHERE node_uuid = ? ORDER BY id DESC").all(nodeUuid);
120
- }
121
-
122
- export function updateMemory(db, nodeUuid, newContent) {
123
- const current = getCurrentMemory(db, nodeUuid);
124
- if (!current) return null;
125
- db.prepare("UPDATE memories SET deprecated = 1 WHERE id = ?").run(current.id);
126
- const result = db.prepare("INSERT INTO memories (node_uuid, content) VALUES (?, ?)").run(nodeUuid, newContent);
127
- db.prepare("UPDATE memories SET migrated_to = ? WHERE id = ?").run(Number(result.lastInsertRowid), current.id);
128
- touchNode(db, nodeUuid);
129
- return { id: Number(result.lastInsertRowid), node_uuid: nodeUuid, content: newContent, previous_id: current.id };
130
- }
131
-
132
- export function deleteMemory(db, nodeUuid) {
133
- const pathRows = db.prepare("SELECT edge_id FROM paths WHERE node_uuid = ?").all(nodeUuid);
134
- db.prepare("DELETE FROM paths WHERE node_uuid = ?").run(nodeUuid);
135
- for (const row of pathRows) {
136
- try { db.prepare("DELETE FROM edges WHERE id = ?").run(row.edge_id); } catch {}
137
- }
138
- db.prepare("DELETE FROM edges WHERE parent_uuid = ? OR child_uuid = ?").run(nodeUuid, nodeUuid);
139
- db.prepare("DELETE FROM glossary_keywords WHERE node_uuid = ?").run(nodeUuid);
140
- db.prepare("DELETE FROM search_documents WHERE node_uuid = ?").run(nodeUuid);
141
- db.prepare("DELETE FROM memory_access_log WHERE node_uuid = ?").run(nodeUuid);
142
- db.prepare("DELETE FROM memories WHERE node_uuid = ?").run(nodeUuid);
143
- const result = db.prepare("DELETE FROM nodes WHERE uuid = ? AND uuid != ?").run(nodeUuid, ROOT_NODE_UUID);
144
- return result.changes > 0;
145
- }
146
-
147
- // ── Edge operations ─────────────────────────────────────────────────────
148
-
149
- export function createEdge(db, parentUuid, childUuid, { name = "related", priority = 0, disclosure = null } = {}) {
150
- const result = db.prepare(
151
- "INSERT INTO edges (parent_uuid, child_uuid, name, priority, disclosure) VALUES (?, ?, ?, ?, ?)",
152
- ).run(parentUuid, childUuid, name, priority, disclosure);
153
- return { id: Number(result.lastInsertRowid), parent_uuid: parentUuid, child_uuid: childUuid, name };
154
- }
155
-
156
- export function getEdges(db, nodeUuid, { direction = "both" } = {}) {
157
- if (direction === "children") return db.prepare("SELECT * FROM edges WHERE parent_uuid = ? ORDER BY priority DESC").all(nodeUuid);
158
- if (direction === "parents") return db.prepare("SELECT * FROM edges WHERE child_uuid = ? ORDER BY priority DESC").all(nodeUuid);
159
- return db.prepare("SELECT * FROM edges WHERE parent_uuid = ? OR child_uuid = ? ORDER BY priority DESC").all(nodeUuid, nodeUuid);
160
- }
161
-
162
- export function deleteEdge(db, edgeId) {
163
- db.prepare("DELETE FROM paths WHERE edge_id = ?").run(edgeId);
164
- return db.prepare("DELETE FROM edges WHERE id = ?").run(edgeId).changes > 0;
165
- }
166
-
167
- // ── Path operations ─────────────────────────────────────────────────────
168
-
169
- export function createPath(db, { namespace = "", domain = "core", path, edgeId, nodeUuid }) {
170
- db.prepare("INSERT OR REPLACE INTO paths (namespace, domain, path, edge_id, node_uuid) VALUES (?, ?, ?, ?, ?)").run(namespace, domain, path, edgeId, nodeUuid);
171
- return { namespace, domain, path, edge_id: edgeId, node_uuid: nodeUuid };
172
- }
173
-
174
- export function getPath(db, namespace, domain, path) {
175
- return db.prepare("SELECT * FROM paths WHERE namespace = ? AND domain = ? AND path = ?").get(namespace, domain, path);
176
- }
177
-
178
- export function getPathsForNode(db, nodeUuid) {
179
- return db.prepare("SELECT * FROM paths WHERE node_uuid = ?").all(nodeUuid);
180
- }
181
-
182
- export function listPaths(db, namespace = "", domain = "core", parentPath = "") {
183
- const prefix = parentPath ? `${parentPath}/` : "";
184
- return db.prepare("SELECT * FROM paths WHERE namespace = ? AND domain = ? AND path LIKE ? ORDER BY path").all(namespace, domain, `${prefix}%`);
185
- }
186
-
187
- // ── Glossary operations ─────────────────────────────────────────────────
188
-
189
- export function addGlossaryKeyword(db, keyword, nodeUuid, namespace = "") {
190
- db.prepare("INSERT OR IGNORE INTO glossary_keywords (keyword, node_uuid, namespace) VALUES (?, ?, ?)").run(keyword, nodeUuid, namespace);
191
- }
192
-
193
- export function findNodeByKeyword(db, keyword, namespace = "") {
194
- return db.prepare("SELECT * FROM glossary_keywords WHERE keyword = ? AND (namespace = ? OR namespace = '')").get(keyword, namespace);
195
- }
196
-
197
- export function getGlossaryKeywords(db, namespace = "") {
198
- return db.prepare("SELECT * FROM glossary_keywords WHERE namespace = ? OR namespace = '' ORDER BY keyword").all(namespace);
199
- }
200
-
201
- // ── Search operations ───────────────────────────────────────────────────
202
-
203
- export function indexSearchDocument(db, nodeUuid, content, namespace = "") {
204
- db.prepare("INSERT OR REPLACE INTO search_documents (node_uuid, namespace, content) VALUES (?, ?, ?)").run(nodeUuid, namespace, content);
205
- }
206
-
207
- export function searchByContent(db, query, namespace = "") {
208
- return db.prepare("SELECT * FROM search_documents WHERE (namespace = ? OR namespace = '') AND content LIKE ?").all(namespace, `%${query}%`);
209
- }
210
-
211
- // ── Access log ──────────────────────────────────────────────────────────
212
-
213
- export function logAccess(db, nodeUuid, context = null, namespace = "") {
214
- db.prepare("INSERT INTO memory_access_log (node_uuid, namespace, context) VALUES (?, ?, ?)").run(nodeUuid, namespace, context);
215
- }
216
-
217
- export function getRecentAccesses(db, limit = 20) {
218
- return db.prepare("SELECT * FROM memory_access_log ORDER BY accessed_at DESC LIMIT ?").all(limit);
219
- }
@@ -1,124 +0,0 @@
1
- export class GlossaryService {
2
- constructor(db, namespace = "") {
3
- this.db = db;
4
- this.namespace = namespace;
5
- this.fingerprint = null;
6
- this.automaton = null;
7
- }
8
-
9
- // ── Aho-Corasick automaton ────────────────────────────────────────
10
-
11
- #ensureAutomaton() {
12
- const fp = this.#computeFingerprint();
13
- if (fp === this.fingerprint && this.automaton) return;
14
- this.fingerprint = fp;
15
- this.automaton = this.#buildAutomaton();
16
- }
17
-
18
- #computeFingerprint() {
19
- const row = this.db.prepare(
20
- "SELECT COUNT(*) AS cnt, MAX(id) AS max_id FROM glossary_keywords WHERE namespace = ? OR namespace = 'global'"
21
- ).get(this.namespace);
22
- return `${row.cnt}:${row.max_id}`;
23
- }
24
-
25
- #buildAutomaton() {
26
- const keywords = this.db.prepare(
27
- "SELECT id, keyword, node_uuid FROM glossary_keywords WHERE namespace = ? OR namespace = 'global'"
28
- ).all(this.namespace);
29
-
30
- // Build trie
31
- const go = [new Map()];
32
- const fail = [0];
33
- const output = [new Map()];
34
-
35
- for (const kw of keywords) {
36
- let state = 0;
37
- for (const ch of kw.keyword) {
38
- let next = go[state].get(ch);
39
- if (next === undefined) {
40
- next = go.length;
41
- go.push(new Map());
42
- fail.push(0);
43
- output.push(new Map());
44
- go[state].set(ch, next);
45
- }
46
- state = next;
47
- }
48
- output[state].set(kw.id, kw.node_uuid);
49
- }
50
-
51
- // Build failure links (BFS)
52
- const queue = [];
53
- for (const [ch, next] of go[0]) {
54
- fail[next] = 0;
55
- queue.push(next);
56
- }
57
-
58
- while (queue.length > 0) {
59
- const r = queue.shift();
60
- for (const [ch, s] of go[r]) {
61
- queue.push(s);
62
- let f = fail[r];
63
- while (f > 0 && !go[f].has(ch)) f = fail[f];
64
- fail[s] = go[f].has(ch) ? go[f].get(ch) : 0;
65
- for (const [kwId, nodeUuid] of output[fail[s]]) {
66
- output[s].set(kwId, nodeUuid);
67
- }
68
- }
69
- }
70
-
71
- return { go, fail, output };
72
- }
73
-
74
- // ── Public API ─────────────────────────────────────────────────────
75
-
76
- addKeyword(keyword, nodeUuid, namespace = "") {
77
- this.db.prepare(
78
- "INSERT OR IGNORE INTO glossary_keywords (keyword, node_uuid, namespace) VALUES (?, ?, ?)"
79
- ).run(keyword, nodeUuid, namespace);
80
- this.fingerprint = null;
81
- }
82
-
83
- removeKeyword(keywordId) {
84
- this.db.prepare("DELETE FROM glossary_keywords WHERE id = ?").run(keywordId);
85
- this.fingerprint = null;
86
- }
87
-
88
- findInContent(content) {
89
- if (!content) return [];
90
- this.#ensureAutomaton();
91
- if (!this.automaton) return [];
92
-
93
- const { go, fail, output } = this.automaton;
94
- const matches = [];
95
- const seen = new Set();
96
- let state = 0;
97
-
98
- for (let i = 0; i < content.length; i++) {
99
- const ch = content[i];
100
- while (state > 0 && !go[state].has(ch)) state = fail[state];
101
- state = go[state].has(ch) ? go[state].get(ch) : 0;
102
-
103
- for (const [kwId, nodeUuid] of output[state]) {
104
- if (seen.has(kwId)) continue;
105
- seen.add(kwId);
106
- matches.push({ keyword_id: kwId, node_uuid: nodeUuid });
107
- }
108
- }
109
-
110
- return matches;
111
- }
112
-
113
- getAllKeywords() {
114
- return this.db.prepare(
115
- "SELECT * FROM glossary_keywords WHERE namespace = ? OR namespace = 'global' ORDER BY keyword"
116
- ).all(this.namespace);
117
- }
118
-
119
- getKeywordsForNode(nodeUuid) {
120
- return this.db.prepare(
121
- "SELECT * FROM glossary_keywords WHERE node_uuid = ? AND (namespace = ? OR namespace = 'global')"
122
- ).all(nodeUuid, this.namespace);
123
- }
124
- }