memorix 1.1.7 → 1.1.9
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/CHANGELOG.md +22 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +852 -214
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +129 -62
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +22 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +129 -62
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/API_REFERENCE.md +2 -0
- package/docs/CONFIGURATION.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +23 -30
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/codegraph.ts +4 -0
- package/src/cli/commands/config-get.ts +9 -2
- package/src/cli/commands/doctor.ts +26 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/auto-context.ts +6 -0
- package/src/codegraph/context-pack.ts +7 -6
- package/src/codegraph/exclude.ts +47 -0
- package/src/codegraph/lite-provider.ts +5 -24
- package/src/codegraph/project-context.ts +13 -15
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/config/resolved-config.ts +6 -0
- package/src/config/toml-loader.ts +4 -0
- package/src/config/yaml-loader.ts +7 -0
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +3 -0
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/orama-store.ts +15 -8
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -3,6 +3,7 @@ import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
import type { CodeEdge, CodeFile, CodeSymbol } from './types.js';
|
|
5
5
|
import { makeCodeEdgeId, makeCodeFileId, makeCodeSymbolId, normalizeCodePath } from './ids.js';
|
|
6
|
+
import { isCodeGraphExcludedPath, normalizeCodeGraphExcludePatterns } from './exclude.js';
|
|
6
7
|
|
|
7
8
|
export interface LiteIndexOptions {
|
|
8
9
|
projectId: string;
|
|
@@ -169,19 +170,6 @@ function languageForPath(path: string): string {
|
|
|
169
170
|
return LANGUAGE_BY_EXTENSION.get(ext) ?? 'unknown';
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
function isExcluded(path: string, exclude: string[]): boolean {
|
|
173
|
-
const normalized = normalizeCodePath(path);
|
|
174
|
-
return exclude.some((pattern) => {
|
|
175
|
-
const p = normalizeCodePath(pattern);
|
|
176
|
-
if (p.endsWith('/**')) {
|
|
177
|
-
const base = p.slice(0, -3);
|
|
178
|
-
return normalized === base || normalized.startsWith(`${base}/`);
|
|
179
|
-
}
|
|
180
|
-
if (p.startsWith('**/')) return normalized.endsWith(p.slice(3));
|
|
181
|
-
return normalized === p || normalized.startsWith(`${p}/`);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
173
|
function walk(root: string, exclude: string[], maxFiles: number): string[] {
|
|
186
174
|
const out: string[] = [];
|
|
187
175
|
const visit = (dir: string) => {
|
|
@@ -196,9 +184,10 @@ function walk(root: string, exclude: string[], maxFiles: number): string[] {
|
|
|
196
184
|
for (const entry of entries) {
|
|
197
185
|
const abs = join(dir, entry.name);
|
|
198
186
|
const rel = normalizeCodePath(relative(root, abs));
|
|
199
|
-
if (
|
|
187
|
+
if (isCodeGraphExcludedPath(rel, exclude)) continue;
|
|
200
188
|
if (entry.isDirectory()) {
|
|
201
|
-
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
189
|
+
if (entry.name === '.git' || entry.name === '.worktrees' || entry.name === '.tmp' || entry.name === 'node_modules') continue;
|
|
190
|
+
if (rel === '.claude/worktrees' || rel.startsWith('.claude/worktrees/')) continue;
|
|
202
191
|
visit(abs);
|
|
203
192
|
if (out.length >= maxFiles) return;
|
|
204
193
|
continue;
|
|
@@ -289,15 +278,7 @@ function extractImportEdges(projectId: string, file: CodeFile, text: string, ind
|
|
|
289
278
|
}
|
|
290
279
|
|
|
291
280
|
export async function indexProjectLite(options: LiteIndexOptions): Promise<LiteIndexResult> {
|
|
292
|
-
const exclude = options.exclude
|
|
293
|
-
'node_modules/**',
|
|
294
|
-
'dist/**',
|
|
295
|
-
'build/**',
|
|
296
|
-
'coverage/**',
|
|
297
|
-
'.next/**',
|
|
298
|
-
'.turbo/**',
|
|
299
|
-
'.git/**',
|
|
300
|
-
];
|
|
281
|
+
const exclude = normalizeCodeGraphExcludePatterns(options.exclude);
|
|
301
282
|
const maxFiles = options.maxFiles ?? 5000;
|
|
302
283
|
const indexedAt = new Date().toISOString();
|
|
303
284
|
const paths = walk(options.projectRoot, exclude, maxFiles);
|
|
@@ -2,6 +2,7 @@ import type { ProjectInfo } from '../types.js';
|
|
|
2
2
|
import { evaluateCodeRefFreshness } from './freshness.js';
|
|
3
3
|
import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
|
|
4
4
|
import type { CodeGraphStore } from './store.js';
|
|
5
|
+
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
5
6
|
|
|
6
7
|
export interface ProjectContextObservation {
|
|
7
8
|
id: number;
|
|
@@ -77,25 +78,17 @@ function countLanguages(files: CodeFile[]): LanguageSummary[] {
|
|
|
77
78
|
.sort((a, b) => a.language.localeCompare(b.language));
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function normalizePath(path: string): string {
|
|
81
|
-
return path.replace(/\\/g, '/');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isGeneratedPath(path: string): boolean {
|
|
85
|
-
return /(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules)\//i.test(normalizePath(path));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
81
|
function suggestedReadRank(path: string): number {
|
|
89
|
-
const normalized =
|
|
82
|
+
const normalized = path.replace(/\\/g, '/');
|
|
90
83
|
if (normalized.startsWith('src/')) return 0;
|
|
91
84
|
if (normalized.startsWith('tests/') || normalized.startsWith('test/')) return 0;
|
|
92
85
|
if (normalized.startsWith('packages/') && normalized.includes('/src/')) return 2;
|
|
93
86
|
return 3;
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
function compactSuggestedReads(paths: string[], limit = 8): string[] {
|
|
89
|
+
function compactSuggestedReads(paths: string[], limit = 8, exclude?: string[]): string[] {
|
|
97
90
|
return uniq(paths)
|
|
98
|
-
.filter(path => !
|
|
91
|
+
.filter(path => !isCodeGraphExcludedPath(path, exclude))
|
|
99
92
|
.sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b))
|
|
100
93
|
.slice(0, limit);
|
|
101
94
|
}
|
|
@@ -104,6 +97,7 @@ function collectGraph(
|
|
|
104
97
|
store: CodeGraphStore,
|
|
105
98
|
projectId: string,
|
|
106
99
|
observations: ProjectContextObservation[],
|
|
100
|
+
exclude?: string[],
|
|
107
101
|
): {
|
|
108
102
|
files: CodeFile[];
|
|
109
103
|
symbols: CodeSymbol[];
|
|
@@ -135,7 +129,9 @@ function collectGraph(
|
|
|
135
129
|
|
|
136
130
|
const observation = observationsById.get(ref.observationId);
|
|
137
131
|
if (!observation) continue;
|
|
138
|
-
|
|
132
|
+
const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
|
|
133
|
+
if (result.status === 'current' && file && !excluded) suggestedReads.push(file.path);
|
|
134
|
+
if (excluded) continue;
|
|
139
135
|
sources.push({
|
|
140
136
|
observationId: observation.id,
|
|
141
137
|
title: observation.title,
|
|
@@ -153,7 +149,7 @@ function collectGraph(
|
|
|
153
149
|
refs,
|
|
154
150
|
freshness,
|
|
155
151
|
sources,
|
|
156
|
-
suggestedReads: compactSuggestedReads(suggestedReads),
|
|
152
|
+
suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude),
|
|
157
153
|
};
|
|
158
154
|
}
|
|
159
155
|
|
|
@@ -161,9 +157,10 @@ export function buildProjectContextOverview(input: {
|
|
|
161
157
|
project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
|
|
162
158
|
store: CodeGraphStore;
|
|
163
159
|
observations: ProjectContextObservation[];
|
|
160
|
+
exclude?: string[];
|
|
164
161
|
}): ProjectContextOverview {
|
|
165
162
|
const active = activeObservations(input.observations, input.project.id);
|
|
166
|
-
const graph = collectGraph(input.store, input.project.id, active);
|
|
163
|
+
const graph = collectGraph(input.store, input.project.id, active, input.exclude);
|
|
167
164
|
const status = input.store.status(input.project.id);
|
|
168
165
|
|
|
169
166
|
return {
|
|
@@ -190,10 +187,11 @@ export function buildProjectContextExplain(input: {
|
|
|
190
187
|
project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
|
|
191
188
|
store: CodeGraphStore;
|
|
192
189
|
observations: ProjectContextObservation[];
|
|
190
|
+
exclude?: string[];
|
|
193
191
|
}): ProjectContextExplain {
|
|
194
192
|
const overview = buildProjectContextOverview(input);
|
|
195
193
|
const active = activeObservations(input.observations, input.project.id);
|
|
196
|
-
const graph = collectGraph(input.store, input.project.id, active);
|
|
194
|
+
const graph = collectGraph(input.store, input.project.id, active, input.exclude);
|
|
197
195
|
return {
|
|
198
196
|
project: input.project,
|
|
199
197
|
sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Token Budget Manager
|
|
3
|
-
*
|
|
4
|
-
* Provides token counting and budget management for Progressive Disclosure.
|
|
5
|
-
* Source: gpt-tokenizer (737 stars, JS port of OpenAI's tiktoken)
|
|
6
|
-
*
|
|
7
|
-
* Used by the Compact Engine to determine which layer of detail
|
|
8
|
-
* fits within the caller's token budget.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { countTokens, isWithinTokenLimit } from 'gpt-tokenizer';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Count tokens in a string.
|
|
15
|
-
*/
|
|
16
|
-
export function countTextTokens(text: string): number {
|
|
17
|
-
return countTokens(text);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Check if text fits within a token limit.
|
|
22
|
-
* Returns the token count if within limit, false otherwise.
|
|
23
|
-
*/
|
|
24
|
-
export function fitsInBudget(text: string, limit: number): number | false {
|
|
25
|
-
return isWithinTokenLimit(text, limit);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Truncate text to fit within a token budget.
|
|
30
|
-
* Truncates at sentence boundaries when possible.
|
|
31
|
-
*/
|
|
32
|
-
export function truncateToTokenBudget(text: string, budget: number): string {
|
|
33
|
-
if (fitsInBudget(text, budget) !== false) {
|
|
34
|
-
return text;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Binary search for the right length
|
|
38
|
-
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
39
|
-
let result = '';
|
|
40
|
-
|
|
41
|
-
for (const sentence of sentences) {
|
|
42
|
-
const candidate = result ? `${result} ${sentence}` : sentence;
|
|
43
|
-
if (fitsInBudget(candidate, budget) === false) {
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
result = candidate;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// If no complete sentence fits, truncate by characters
|
|
50
|
-
if (!result) {
|
|
51
|
-
// Rough estimate: 1 token ≈ 4 chars for English, ≈ 1.5 chars for Chinese
|
|
52
|
-
const estimatedChars = budget * 2;
|
|
53
|
-
result = text.slice(0, estimatedChars);
|
|
54
|
-
// Refine
|
|
55
|
-
while (fitsInBudget(result, budget) === false && result.length > 0) {
|
|
56
|
-
result = result.slice(0, Math.floor(result.length * 0.9));
|
|
57
|
-
}
|
|
58
|
-
if (result.length < text.length) {
|
|
59
|
-
result += '...';
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Estimate the token cost of an IndexEntry line.
|
|
68
|
-
* Used to predict compact index size.
|
|
69
|
-
*/
|
|
70
|
-
export function estimateIndexEntryTokens(title: string): number {
|
|
71
|
-
// Format: "| #ID | Time | Icon | Title | ~Tokens |"
|
|
72
|
-
// Overhead is roughly 15 tokens for formatting
|
|
73
|
-
return countTextTokens(title) + 15;
|
|
74
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Token Budget Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides token counting and budget management for Progressive Disclosure.
|
|
5
|
+
* Source: gpt-tokenizer (737 stars, JS port of OpenAI's tiktoken)
|
|
6
|
+
*
|
|
7
|
+
* Used by the Compact Engine to determine which layer of detail
|
|
8
|
+
* fits within the caller's token budget.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { countTokens, isWithinTokenLimit } from 'gpt-tokenizer';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count tokens in a string.
|
|
15
|
+
*/
|
|
16
|
+
export function countTextTokens(text: string): number {
|
|
17
|
+
return countTokens(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if text fits within a token limit.
|
|
22
|
+
* Returns the token count if within limit, false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export function fitsInBudget(text: string, limit: number): number | false {
|
|
25
|
+
return isWithinTokenLimit(text, limit);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Truncate text to fit within a token budget.
|
|
30
|
+
* Truncates at sentence boundaries when possible.
|
|
31
|
+
*/
|
|
32
|
+
export function truncateToTokenBudget(text: string, budget: number): string {
|
|
33
|
+
if (fitsInBudget(text, budget) !== false) {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Binary search for the right length
|
|
38
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
39
|
+
let result = '';
|
|
40
|
+
|
|
41
|
+
for (const sentence of sentences) {
|
|
42
|
+
const candidate = result ? `${result} ${sentence}` : sentence;
|
|
43
|
+
if (fitsInBudget(candidate, budget) === false) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
result = candidate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If no complete sentence fits, truncate by characters
|
|
50
|
+
if (!result) {
|
|
51
|
+
// Rough estimate: 1 token ≈ 4 chars for English, ≈ 1.5 chars for Chinese
|
|
52
|
+
const estimatedChars = budget * 2;
|
|
53
|
+
result = text.slice(0, estimatedChars);
|
|
54
|
+
// Refine
|
|
55
|
+
while (fitsInBudget(result, budget) === false && result.length > 0) {
|
|
56
|
+
result = result.slice(0, Math.floor(result.length * 0.9));
|
|
57
|
+
}
|
|
58
|
+
if (result.length < text.length) {
|
|
59
|
+
result += '...';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Estimate the token cost of an IndexEntry line.
|
|
68
|
+
* Used to predict compact index size.
|
|
69
|
+
*/
|
|
70
|
+
export function estimateIndexEntryTokens(title: string): number {
|
|
71
|
+
// Format: "| #ID | Time | Icon | Title | ~Tokens |"
|
|
72
|
+
// Overhead is roughly 15 tokens for formatting
|
|
73
|
+
return countTextTokens(title) + 15;
|
|
74
|
+
}
|
package/src/config/behavior.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Behavior Configuration Reader
|
|
3
|
-
*
|
|
4
|
-
* Reads behavior settings from ~/.memorix/config.json.
|
|
5
|
-
* Falls back to sensible defaults if config is missing.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
|
|
12
|
-
export interface BehaviorConfig {
|
|
13
|
-
sessionInject: 'full' | 'minimal' | 'silent';
|
|
14
|
-
syncAdvisory: boolean;
|
|
15
|
-
autoCleanup: boolean;
|
|
16
|
-
formationMode: 'shadow' | 'active' | 'fallback';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const DEFAULTS: BehaviorConfig = {
|
|
20
|
-
sessionInject: 'minimal',
|
|
21
|
-
syncAdvisory: true,
|
|
22
|
-
autoCleanup: true,
|
|
23
|
-
formationMode: 'active',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
let cached: BehaviorConfig | null = null;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Load behavior config from ~/.memorix/config.json.
|
|
30
|
-
* Caches after first read. Returns defaults if file is missing.
|
|
31
|
-
*/
|
|
32
|
-
export function getBehaviorConfig(): BehaviorConfig {
|
|
33
|
-
if (cached) return cached;
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const configPath = join(homedir(), '.memorix', 'config.json');
|
|
37
|
-
const raw = readFileSync(configPath, 'utf-8');
|
|
38
|
-
const config = JSON.parse(raw);
|
|
39
|
-
const behavior = config.behavior ?? {};
|
|
40
|
-
|
|
41
|
-
cached = {
|
|
42
|
-
sessionInject: behavior.sessionInject ?? DEFAULTS.sessionInject,
|
|
43
|
-
syncAdvisory: behavior.syncAdvisory ?? DEFAULTS.syncAdvisory,
|
|
44
|
-
autoCleanup: behavior.autoCleanup ?? DEFAULTS.autoCleanup,
|
|
45
|
-
formationMode: behavior.formationMode ?? DEFAULTS.formationMode,
|
|
46
|
-
};
|
|
47
|
-
} catch {
|
|
48
|
-
cached = { ...DEFAULTS };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return cached;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Reset cached config (for testing or after config change).
|
|
56
|
-
*/
|
|
57
|
-
export function resetBehaviorConfigCache(): void {
|
|
58
|
-
cached = null;
|
|
59
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Configuration Reader
|
|
3
|
+
*
|
|
4
|
+
* Reads behavior settings from ~/.memorix/config.json.
|
|
5
|
+
* Falls back to sensible defaults if config is missing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
export interface BehaviorConfig {
|
|
13
|
+
sessionInject: 'full' | 'minimal' | 'silent';
|
|
14
|
+
syncAdvisory: boolean;
|
|
15
|
+
autoCleanup: boolean;
|
|
16
|
+
formationMode: 'shadow' | 'active' | 'fallback';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULTS: BehaviorConfig = {
|
|
20
|
+
sessionInject: 'minimal',
|
|
21
|
+
syncAdvisory: true,
|
|
22
|
+
autoCleanup: true,
|
|
23
|
+
formationMode: 'active',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let cached: BehaviorConfig | null = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load behavior config from ~/.memorix/config.json.
|
|
30
|
+
* Caches after first read. Returns defaults if file is missing.
|
|
31
|
+
*/
|
|
32
|
+
export function getBehaviorConfig(): BehaviorConfig {
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const configPath = join(homedir(), '.memorix', 'config.json');
|
|
37
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
38
|
+
const config = JSON.parse(raw);
|
|
39
|
+
const behavior = config.behavior ?? {};
|
|
40
|
+
|
|
41
|
+
cached = {
|
|
42
|
+
sessionInject: behavior.sessionInject ?? DEFAULTS.sessionInject,
|
|
43
|
+
syncAdvisory: behavior.syncAdvisory ?? DEFAULTS.syncAdvisory,
|
|
44
|
+
autoCleanup: behavior.autoCleanup ?? DEFAULTS.autoCleanup,
|
|
45
|
+
formationMode: behavior.formationMode ?? DEFAULTS.formationMode,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
cached = { ...DEFAULTS };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return cached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset cached config (for testing or after config change).
|
|
56
|
+
*/
|
|
57
|
+
export function resetBehaviorConfigCache(): void {
|
|
58
|
+
cached = null;
|
|
59
|
+
}
|
|
@@ -51,6 +51,9 @@ export interface ResolvedMemorixConfig {
|
|
|
51
51
|
excludePatterns?: string[];
|
|
52
52
|
noiseKeywords?: string[];
|
|
53
53
|
};
|
|
54
|
+
codegraph: {
|
|
55
|
+
excludePatterns?: string[];
|
|
56
|
+
};
|
|
54
57
|
server: {
|
|
55
58
|
transport?: 'stdio' | 'http';
|
|
56
59
|
dashboard?: boolean;
|
|
@@ -147,6 +150,9 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
147
150
|
excludePatterns: firstArray(toml.git?.exclude_patterns, yaml.git?.excludePatterns),
|
|
148
151
|
noiseKeywords: firstArray(toml.git?.noise_keywords, yaml.git?.noiseKeywords),
|
|
149
152
|
},
|
|
153
|
+
codegraph: {
|
|
154
|
+
excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
|
|
155
|
+
},
|
|
150
156
|
server: {
|
|
151
157
|
transport: first(toml.server?.transport, yaml.server?.transport),
|
|
152
158
|
dashboard: firstBool(toml.server?.dashboard, yaml.server?.dashboard),
|
|
@@ -39,6 +39,9 @@ export interface MemorixTomlConfig {
|
|
|
39
39
|
exclude_patterns?: string[];
|
|
40
40
|
noise_keywords?: string[];
|
|
41
41
|
};
|
|
42
|
+
codegraph?: {
|
|
43
|
+
exclude_patterns?: string[];
|
|
44
|
+
};
|
|
42
45
|
server?: {
|
|
43
46
|
transport?: 'stdio' | 'http';
|
|
44
47
|
dashboard?: boolean;
|
|
@@ -91,6 +94,7 @@ function mergeTomlConfig(base: MemorixTomlConfig, override: MemorixTomlConfig):
|
|
|
91
94
|
embedding: { ...base.embedding, ...override.embedding },
|
|
92
95
|
hooks: { ...base.hooks, ...override.hooks },
|
|
93
96
|
git: { ...base.git, ...override.git },
|
|
97
|
+
codegraph: { ...base.codegraph, ...override.codegraph },
|
|
94
98
|
server: { ...base.server, ...override.server },
|
|
95
99
|
};
|
|
96
100
|
}
|
|
@@ -64,6 +64,12 @@ export interface MemorixYamlConfig {
|
|
|
64
64
|
noiseKeywords?: string[];
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
/** CodeGraph / Project Context configuration */
|
|
68
|
+
codegraph?: {
|
|
69
|
+
/** File patterns to exclude from CodeGraph indexing and context suggestions */
|
|
70
|
+
excludePatterns?: string[];
|
|
71
|
+
};
|
|
72
|
+
|
|
67
73
|
/** Behavior settings */
|
|
68
74
|
behavior?: {
|
|
69
75
|
/** Session start injection mode */
|
|
@@ -184,6 +190,7 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
|
|
|
184
190
|
agent: { ...userConfig.agent, ...projectConfig.agent },
|
|
185
191
|
embedding: { ...userConfig.embedding, ...projectConfig.embedding },
|
|
186
192
|
git: { ...userConfig.git, ...projectConfig.git },
|
|
193
|
+
codegraph: { ...userConfig.codegraph, ...projectConfig.codegraph },
|
|
187
194
|
behavior: { ...userConfig.behavior, ...projectConfig.behavior },
|
|
188
195
|
server: { ...userConfig.server, ...projectConfig.server },
|
|
189
196
|
team: { ...userConfig.team, ...projectConfig.team },
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project classification helpers — shared by /api/projects and /api/identity.
|
|
3
|
-
*
|
|
4
|
-
* Three kinds:
|
|
5
|
-
* - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
|
|
6
|
-
* - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
|
|
7
|
-
* - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
|
|
8
|
-
*
|
|
9
|
-
* A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
|
|
10
|
-
* 'dirty' and 'temporary' are orthogonal axes:
|
|
11
|
-
* - local/task-abc → temporary, NOT dirty
|
|
12
|
-
* - placeholder/xxx → placeholder, dirty
|
|
13
|
-
* - System32\something → real-looking location but dirty (broken ID)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export type ProjectKind = 'real' | 'temporary' | 'placeholder';
|
|
17
|
-
|
|
18
|
-
/** Regex list — anything matching is temporary (scratch projects) */
|
|
19
|
-
const TEMPORARY_PATTERNS: RegExp[] = [
|
|
20
|
-
/^local\/task-/i,
|
|
21
|
-
/^local\/smoke-/i,
|
|
22
|
-
/^local\/release-smoke-/i,
|
|
23
|
-
/^local\/memorix-e2e-/i,
|
|
24
|
-
/^local\/orchestrate-/i,
|
|
25
|
-
/^local\/scratch-/i,
|
|
26
|
-
/^local\/tmp-/i,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
/** Regex list — anything matching is placeholder/unresolved */
|
|
30
|
-
const PLACEHOLDER_PATTERNS: RegExp[] = [
|
|
31
|
-
/^__unresolved__$/,
|
|
32
|
-
/^placeholder\//i,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/** Regex list — IDs that indicate a broken canonical ID (dirty). */
|
|
36
|
-
const DIRTY_PATTERNS: RegExp[] = [
|
|
37
|
-
/^placeholder\//i,
|
|
38
|
-
/System32/i,
|
|
39
|
-
/Microsoft VS Code/i,
|
|
40
|
-
/node_modules/i,
|
|
41
|
-
/\.vscode/i,
|
|
42
|
-
/^local\/[A-Z]:\\/i,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export function classifyProjectId(id: string): ProjectKind {
|
|
46
|
-
if (!id) return 'placeholder';
|
|
47
|
-
if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
|
|
48
|
-
if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
|
|
49
|
-
return 'real';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function isDirtyProjectId(id: string): boolean {
|
|
53
|
-
if (!id) return false;
|
|
54
|
-
return DIRTY_PATTERNS.some(p => p.test(id));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Friendly label for UI badges */
|
|
58
|
-
export function projectKindLabel(kind: ProjectKind): string {
|
|
59
|
-
switch (kind) {
|
|
60
|
-
case 'real': return 'real';
|
|
61
|
-
case 'temporary': return 'temporary';
|
|
62
|
-
case 'placeholder': return 'placeholder';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Project classification helpers — shared by /api/projects and /api/identity.
|
|
3
|
+
*
|
|
4
|
+
* Three kinds:
|
|
5
|
+
* - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
|
|
6
|
+
* - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
|
|
7
|
+
* - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
|
|
8
|
+
*
|
|
9
|
+
* A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
|
|
10
|
+
* 'dirty' and 'temporary' are orthogonal axes:
|
|
11
|
+
* - local/task-abc → temporary, NOT dirty
|
|
12
|
+
* - placeholder/xxx → placeholder, dirty
|
|
13
|
+
* - System32\something → real-looking location but dirty (broken ID)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ProjectKind = 'real' | 'temporary' | 'placeholder';
|
|
17
|
+
|
|
18
|
+
/** Regex list — anything matching is temporary (scratch projects) */
|
|
19
|
+
const TEMPORARY_PATTERNS: RegExp[] = [
|
|
20
|
+
/^local\/task-/i,
|
|
21
|
+
/^local\/smoke-/i,
|
|
22
|
+
/^local\/release-smoke-/i,
|
|
23
|
+
/^local\/memorix-e2e-/i,
|
|
24
|
+
/^local\/orchestrate-/i,
|
|
25
|
+
/^local\/scratch-/i,
|
|
26
|
+
/^local\/tmp-/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Regex list — anything matching is placeholder/unresolved */
|
|
30
|
+
const PLACEHOLDER_PATTERNS: RegExp[] = [
|
|
31
|
+
/^__unresolved__$/,
|
|
32
|
+
/^placeholder\//i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Regex list — IDs that indicate a broken canonical ID (dirty). */
|
|
36
|
+
const DIRTY_PATTERNS: RegExp[] = [
|
|
37
|
+
/^placeholder\//i,
|
|
38
|
+
/System32/i,
|
|
39
|
+
/Microsoft VS Code/i,
|
|
40
|
+
/node_modules/i,
|
|
41
|
+
/\.vscode/i,
|
|
42
|
+
/^local\/[A-Z]:\\/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function classifyProjectId(id: string): ProjectKind {
|
|
46
|
+
if (!id) return 'placeholder';
|
|
47
|
+
if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
|
|
48
|
+
if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
|
|
49
|
+
return 'real';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isDirtyProjectId(id: string): boolean {
|
|
53
|
+
if (!id) return false;
|
|
54
|
+
return DIRTY_PATTERNS.some(p => p.test(id));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Friendly label for UI badges */
|
|
58
|
+
export function projectKindLabel(kind: ProjectKind): string {
|
|
59
|
+
switch (kind) {
|
|
60
|
+
case 'real': return 'real';
|
|
61
|
+
case 'temporary': return 'temporary';
|
|
62
|
+
case 'placeholder': return 'placeholder';
|
|
63
|
+
}
|
|
64
|
+
}
|