memorix 1.1.8 → 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 +9 -0
- package/dist/cli/index.js +132 -74
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +123 -63
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +9 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +123 -63
- package/dist/sdk.js.map +1 -1
- package/docs/API_REFERENCE.md +2 -0
- package/docs/CONFIGURATION.md +18 -0
- package/docs/dev-log/progress.txt +15 -31
- package/package.json +1 -1
- package/src/cli/commands/codegraph.ts +4 -0
- package/src/cli/commands/config-get.ts +9 -2
- package/src/cli/commands/doctor.ts +3 -0
- package/src/codegraph/auto-context.ts +6 -0
- package/src/codegraph/context-pack.ts +7 -10
- package/src/codegraph/exclude.ts +47 -0
- package/src/codegraph/lite-provider.ts +3 -26
- package/src/codegraph/project-context.ts +13 -19
- 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/server.ts +3 -0
- package/src/store/orama-store.ts +15 -8
package/docs/API_REFERENCE.md
CHANGED
|
@@ -103,6 +103,8 @@ MCP:
|
|
|
103
103
|
|
|
104
104
|
`memorix context` defaults to `--refresh auto`, so first use can seed CodeGraph Memory without a separate manual `memorix codegraph refresh`. Its brief puts live package/changelog/Git facts before memory hints and flags old `progress.txt` / dev-log notes as historical when they predate the latest changelog, so agents should treat current facts as the source of truth when files disagree. Task lenses keep the packet shaped to the work: bugfix briefs prefer failing tests and repros, release briefs prefer metadata/changelog/package checks, and onboarding briefs prefer docs and entry points while hiding unrelated suspect details. Use `--refresh never` for read-only inspection and `--refresh always` when you want to force a fresh scan.
|
|
105
105
|
|
|
106
|
+
Project-specific generated, vendored, or cache paths can be excluded from CodeGraph Memory with `[codegraph].exclude_patterns` in `memorix.toml` or `~/.memorix/config.toml` (`codegraph.excludePatterns` in legacy YAML). User patterns extend the built-in excludes and are applied to indexing, Project Context suggested reads, and Context Pack suggested reads.
|
|
107
|
+
|
|
106
108
|
SessionStart hooks keep the default minimal hint lightweight. When memory behavior is configured with `sessionInject=full`, Memorix injects the compact Memory Autopilot brief at session start instead of only listing recent text memories.
|
|
107
109
|
|
|
108
110
|
The intended loop for agents is: get the project brief when it helps, inspect the suggested current files, use stale or unbound memory only as a lead, store durable outcomes after the work changes the project, and resolve obsolete memories.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -60,6 +60,9 @@ skip_merge_commits = true
|
|
|
60
60
|
exclude_patterns = ["*.lock", "dist/**"]
|
|
61
61
|
noise_keywords = ["format", "typo"]
|
|
62
62
|
|
|
63
|
+
[codegraph]
|
|
64
|
+
exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]
|
|
65
|
+
|
|
63
66
|
[server]
|
|
64
67
|
transport = "stdio"
|
|
65
68
|
dashboard = true
|
|
@@ -197,6 +200,21 @@ Project identity is still resolved from the real `.git` root. A project
|
|
|
197
200
|
`memorix.toml` is an override file under that root; it does not create or rename
|
|
198
201
|
the Memorix project ID.
|
|
199
202
|
|
|
203
|
+
### `[codegraph]`
|
|
204
|
+
|
|
205
|
+
CodeGraph Memory and Project Context path filtering.
|
|
206
|
+
|
|
207
|
+
Common keys:
|
|
208
|
+
|
|
209
|
+
- `exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]`
|
|
210
|
+
|
|
211
|
+
Legacy YAML uses `codegraph.excludePatterns` for the same setting.
|
|
212
|
+
|
|
213
|
+
These patterns extend Memorix's built-in CodeGraph excludes (`node_modules`,
|
|
214
|
+
build outputs, worktrees, and similar generated directories). Matching paths are
|
|
215
|
+
skipped during CodeGraph indexing and hidden from Project Context / Context Pack
|
|
216
|
+
suggested reads.
|
|
217
|
+
|
|
200
218
|
### `[server]`
|
|
201
219
|
|
|
202
220
|
Server and dashboard behavior.
|
|
@@ -4,41 +4,26 @@
|
|
|
4
4
|
> exist under subdirectories; prefer this file for repository-wide state.
|
|
5
5
|
|
|
6
6
|
## Current State
|
|
7
|
-
- Phase: 1.1.
|
|
7
|
+
- Phase: 1.1.9 release finalization
|
|
8
8
|
- Branch: main
|
|
9
|
-
- PR: #
|
|
10
|
-
- Last updated: 2026-07-
|
|
11
|
-
- Goal: publish 1.1.
|
|
9
|
+
- PR: #120, #121, and #123 merged
|
|
10
|
+
- Last updated: 2026-07-12
|
|
11
|
+
- Goal: publish 1.1.9 after accepting the latest contributor fixes and closing local release hygiene gaps.
|
|
12
12
|
|
|
13
13
|
## Completed Recently
|
|
14
|
-
- Merged PR #
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Fixed Claude Code 2.x local MCP repair: doctor/repair now inspect `~/.claude.json` project-private entries and rewrite stale worktree commands to `memorix serve` with `alwaysLoad: true`.
|
|
20
|
-
- Fixed all-scope doctor noise: one healthy local, project, or global scope is enough in all-scope mode unless a stale actionable config is present.
|
|
21
|
-
- Fixed CodeGraph Memory noise: `.tmp`, `.worktrees`, and `.claude/worktrees` are ignored by the lite scan and Autopilot suggested reads.
|
|
22
|
-
- Bumped root and workspace package versions to `1.1.8` and updated lockfile/internal workspace dependencies.
|
|
14
|
+
- Merged PR #120: configurable CodeGraph exclude patterns for indexing, Project Context, context packs, diagnostics, and docs.
|
|
15
|
+
- Merged PR #121: `memorix config get` accepts TOML-style snake_case dotted keys against resolved camelCase config.
|
|
16
|
+
- Merged PR #123: Orama search access tracking preserves vector-backed documents, public/detail cache results strip embeddings, and hydration reconciles exact observation IDs even when mini-skills already populate the shared index.
|
|
17
|
+
- Cleaned the unrelated local model catalog refresh and deleted memcode image from the main working tree before release prep.
|
|
18
|
+
- Bumped root and workspace package versions to `1.1.9` and updated lockfile/internal workspace dependencies.
|
|
23
19
|
|
|
24
20
|
## Current Follow-Up
|
|
25
|
-
-
|
|
26
|
-
- Two unrelated local changes were temporarily stashed before final verification so they do not enter the release package: `packages/ai/src/models.generated.ts` and `packages/memcode/docs/images/exy.png`.
|
|
21
|
+
- Publish `1.1.9`, create the GitHub release, and confirm npm resolves the new root package version.
|
|
27
22
|
|
|
28
23
|
## Verification Snapshot
|
|
29
|
-
- `
|
|
30
|
-
-
|
|
31
|
-
- `npm run lint`
|
|
32
|
-
- `npm run build`
|
|
33
|
-
- `npm test`
|
|
34
|
-
- `npm pack --dry-run --json` -> `memorix@1.1.8`, about 14.1 MB packed / 60.3 MB unpacked
|
|
35
|
-
- `node dist/cli/index.js doctor agents --agent claude --scope local`
|
|
36
|
-
- `node dist/cli/index.js doctor agents --agent claude --scope all`
|
|
37
|
-
- `node dist/cli/index.js codegraph refresh --json`
|
|
38
|
-
- `node dist/cli/index.js context --refresh never --task "准备发布 1.1.8:判断当前进度、风险和下一步"`
|
|
39
|
-
- `claude mcp get memorix`
|
|
40
|
-
- `claude mcp list`
|
|
41
|
-
- Claude Code user-style smoke: a short print-mode handoff correctly blocked release because local changes were not committed yet; longer print-mode smoke can still time out.
|
|
24
|
+
- Latest main CI for `cd3f024` passed on Ubuntu and Windows before release prep.
|
|
25
|
+
- Targeted Orama/config/CodeGraph checks passed during PR review for #120, #121, and #123.
|
|
26
|
+
- Final 1.1.9 release verification is run from the release commit before publishing: `git diff --check`, `npm run lint`, `npm run build`, `npm test`, and package dry-runs.
|
|
42
27
|
|
|
43
28
|
## Known UX Notes
|
|
44
29
|
- Claude Code print-mode can be slow or fail to finish under budget during full user-style smoke. Use tool traces plus deterministic CLI/MCP smoke to distinguish product failures from model/runtime drift.
|
|
@@ -46,6 +31,5 @@
|
|
|
46
31
|
- Current code and release facts outrank stale handoff notes or older dev logs when they conflict.
|
|
47
32
|
|
|
48
33
|
## Next Steps
|
|
49
|
-
- Commit and push the 1.1.
|
|
50
|
-
-
|
|
51
|
-
- Restore the temporary local stash after publishing if those local model/image changes are still needed.
|
|
34
|
+
- Commit and push the 1.1.9 release patch.
|
|
35
|
+
- Tag and publish `memorix@1.1.9` plus workspaces in dependency order.
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { CodeGraphStore } from '../../codegraph/store.js';
|
|
|
3
3
|
import { indexProjectLite } from '../../codegraph/lite-provider.js';
|
|
4
4
|
import { assembleContextPackForTask, buildContextPackPrompt } from '../../codegraph/context-pack.js';
|
|
5
5
|
import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
|
|
6
|
+
import { getResolvedConfig } from '../../config/resolved-config.js';
|
|
6
7
|
import { getAllObservations } from '../../memory/observations.js';
|
|
7
8
|
import { emitError, emitResult, getCliProjectContext, parsePositiveInt } from './operator-shared.js';
|
|
8
9
|
|
|
@@ -49,6 +50,7 @@ export default defineCommand({
|
|
|
49
50
|
const store = new CodeGraphStore();
|
|
50
51
|
await store.init(dataDir);
|
|
51
52
|
const explicitAction = Boolean(positional[0] || (args.action as string | undefined));
|
|
53
|
+
const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
|
|
52
54
|
|
|
53
55
|
switch (action) {
|
|
54
56
|
case 'status': {
|
|
@@ -64,6 +66,7 @@ export default defineCommand({
|
|
|
64
66
|
const indexed = await indexProjectLite({
|
|
65
67
|
projectId: project.id,
|
|
66
68
|
projectRoot: project.rootPath,
|
|
69
|
+
exclude,
|
|
67
70
|
});
|
|
68
71
|
store.replaceProjectIndex(project.id, indexed);
|
|
69
72
|
const activeObservations = getAllObservations()
|
|
@@ -99,6 +102,7 @@ export default defineCommand({
|
|
|
99
102
|
task,
|
|
100
103
|
observations,
|
|
101
104
|
limit,
|
|
105
|
+
exclude,
|
|
102
106
|
});
|
|
103
107
|
emitResult({ project, pack }, buildContextPackPrompt(pack), asJson);
|
|
104
108
|
return;
|
|
@@ -29,12 +29,19 @@ export default defineCommand({
|
|
|
29
29
|
function readDotted(source: Record<string, unknown>, key: string): unknown {
|
|
30
30
|
let cursor: unknown = source;
|
|
31
31
|
for (const part of key.split('.')) {
|
|
32
|
-
if (!cursor || typeof cursor !== 'object'
|
|
33
|
-
|
|
32
|
+
if (!cursor || typeof cursor !== 'object') return undefined;
|
|
33
|
+
const record = cursor as Record<string, unknown>;
|
|
34
|
+
const normalizedPart = part in record ? part : snakeToCamel(part);
|
|
35
|
+
if (!(normalizedPart in record)) return undefined;
|
|
36
|
+
cursor = record[normalizedPart];
|
|
34
37
|
}
|
|
35
38
|
return cursor;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
function snakeToCamel(value: string): string {
|
|
42
|
+
return value.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
function formatValue(value: unknown, key: string): string {
|
|
39
46
|
if (typeof value === 'string') return shouldRedactKey(key) ? '<redacted>' : value;
|
|
40
47
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
@@ -318,8 +318,10 @@ export default defineCommand({
|
|
|
318
318
|
try {
|
|
319
319
|
const { CodeGraphStore } = await import('../../codegraph/store.js');
|
|
320
320
|
const { buildProjectContextOverview } = await import('../../codegraph/project-context.js');
|
|
321
|
+
const { getResolvedConfig } = await import('../../config/resolved-config.js');
|
|
321
322
|
const codeStore = new CodeGraphStore();
|
|
322
323
|
await codeStore.init(dataDir);
|
|
324
|
+
const exclude = getResolvedConfig({ projectRoot: projectRoot || process.cwd() }).codegraph.excludePatterns;
|
|
323
325
|
const overview = buildProjectContextOverview({
|
|
324
326
|
project: {
|
|
325
327
|
id: projectId,
|
|
@@ -328,6 +330,7 @@ export default defineCommand({
|
|
|
328
330
|
},
|
|
329
331
|
store: codeStore,
|
|
330
332
|
observations: projectObservations,
|
|
333
|
+
exclude,
|
|
331
334
|
});
|
|
332
335
|
const languageText = overview.code.languages.length > 0
|
|
333
336
|
? overview.code.languages.map((item: any) => `${item.language} ${item.files}`).join(', ')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { getResolvedConfig } from '../config/resolved-config.js';
|
|
3
4
|
import type { ProjectInfo } from '../types.js';
|
|
4
5
|
import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
|
|
5
6
|
import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-facts.js';
|
|
@@ -100,11 +101,13 @@ export async function buildAutoProjectContext(input: {
|
|
|
100
101
|
refresh?: AutoContextRefreshMode;
|
|
101
102
|
maxAgeMs?: number;
|
|
102
103
|
now?: Date;
|
|
104
|
+
exclude?: string[];
|
|
103
105
|
}): Promise<AutoProjectContext> {
|
|
104
106
|
const refreshMode = input.refresh ?? 'auto';
|
|
105
107
|
const now = input.now ?? new Date();
|
|
106
108
|
const task = input.task?.trim();
|
|
107
109
|
const lens = resolveTaskLens(task);
|
|
110
|
+
const exclude = input.exclude ?? getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph.excludePatterns;
|
|
108
111
|
const store = new CodeGraphStore();
|
|
109
112
|
await store.init(input.dataDir);
|
|
110
113
|
|
|
@@ -126,6 +129,7 @@ export async function buildAutoProjectContext(input: {
|
|
|
126
129
|
const indexed = await indexProjectLite({
|
|
127
130
|
projectId: input.project.id,
|
|
128
131
|
projectRoot: input.project.rootPath,
|
|
132
|
+
exclude,
|
|
129
133
|
});
|
|
130
134
|
store.replaceProjectIndex(input.project.id, indexed);
|
|
131
135
|
const backfill = await backfillMissingObservationCodeRefs(
|
|
@@ -147,11 +151,13 @@ export async function buildAutoProjectContext(input: {
|
|
|
147
151
|
project: input.project,
|
|
148
152
|
store,
|
|
149
153
|
observations: input.observations,
|
|
154
|
+
exclude,
|
|
150
155
|
});
|
|
151
156
|
const explain = buildProjectContextExplain({
|
|
152
157
|
project: input.project,
|
|
153
158
|
store,
|
|
154
159
|
observations: input.observations,
|
|
160
|
+
exclude,
|
|
155
161
|
});
|
|
156
162
|
|
|
157
163
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { evaluateCodeRefFreshness } from './freshness.js';
|
|
2
2
|
import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
|
|
3
3
|
import type { CodeGraphStore } from './store.js';
|
|
4
|
+
import { isCodeGraphExcludedPath } from './exclude.js';
|
|
4
5
|
|
|
5
6
|
export interface ContextPackMemory {
|
|
6
7
|
id: number;
|
|
@@ -52,6 +53,7 @@ export interface AssembleContextPackInput {
|
|
|
52
53
|
files: CodeFile[];
|
|
53
54
|
symbols: CodeSymbol[];
|
|
54
55
|
suggestedVerification?: string[];
|
|
56
|
+
exclude?: string[];
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
function uniq<T>(items: T[]): T[] {
|
|
@@ -68,14 +70,6 @@ function timestampOf(observation: ContextPackObservation): number {
|
|
|
68
70
|
return Date.parse(observation.updatedAt ?? observation.createdAt ?? '') || 0;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function isGeneratedPath(path: string): boolean {
|
|
72
|
-
const normalized = path.replace(/\\/g, '/');
|
|
73
|
-
return (
|
|
74
|
-
/(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules|\.git|\.tmp|\.worktrees)(\/|$)/i.test(normalized) ||
|
|
75
|
-
/(^|\/)\.claude\/worktrees(\/|$)/i.test(normalized)
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
73
|
function relevanceScore(observation: ContextPackObservation, taskTokens: string[]): number {
|
|
80
74
|
if (taskTokens.length === 0) return 0;
|
|
81
75
|
const title = observation.title.toLowerCase();
|
|
@@ -137,6 +131,7 @@ export function assembleContextPackForTask(input: {
|
|
|
137
131
|
observations: ContextPackObservation[];
|
|
138
132
|
limit?: number;
|
|
139
133
|
suggestedVerification?: string[];
|
|
134
|
+
exclude?: string[];
|
|
140
135
|
}): ContextPack {
|
|
141
136
|
const selected = selectRelevantObservations(input.observations, input.task, input.limit ?? 20);
|
|
142
137
|
const refs = selected.flatMap(obs => input.store.listObservationRefs(input.projectId, obs.id));
|
|
@@ -151,6 +146,7 @@ export function assembleContextPackForTask(input: {
|
|
|
151
146
|
files,
|
|
152
147
|
symbols,
|
|
153
148
|
suggestedVerification: input.suggestedVerification,
|
|
149
|
+
exclude: input.exclude,
|
|
154
150
|
});
|
|
155
151
|
}
|
|
156
152
|
|
|
@@ -172,6 +168,7 @@ export function assembleContextPack(input: AssembleContextPackInput): ContextPac
|
|
|
172
168
|
|
|
173
169
|
const file = ref.fileId ? files.get(ref.fileId) : undefined;
|
|
174
170
|
const symbol = ref.symbolId ? symbols.get(ref.symbolId) : undefined;
|
|
171
|
+
if (file && isCodeGraphExcludedPath(file.path, input.exclude)) continue;
|
|
175
172
|
const freshness = evaluateCodeRefFreshness(ref, file, symbol);
|
|
176
173
|
|
|
177
174
|
if (freshness.status === 'current') {
|
|
@@ -247,8 +244,8 @@ export function buildContextPackPrompt(pack: ContextPack): string {
|
|
|
247
244
|
const reliableMemories = pack.memories.filter(memory => memory.status === 'current');
|
|
248
245
|
const unboundMemories = pack.memories.filter(memory => memory.status === 'unbound');
|
|
249
246
|
const lines: string[] = ['## Task', pack.task, '', '## Reliable Memories'];
|
|
250
|
-
const visibleCodeFacts = pack.codeFacts.filter(fact => !
|
|
251
|
-
const visibleSuggestedReads = pack.suggestedReads.filter(path => !
|
|
247
|
+
const visibleCodeFacts = pack.codeFacts.filter(fact => !isCodeGraphExcludedPath(fact.path)).slice(0, 5);
|
|
248
|
+
const visibleSuggestedReads = pack.suggestedReads.filter(path => !isCodeGraphExcludedPath(path)).slice(0, 5);
|
|
252
249
|
|
|
253
250
|
if (reliableMemories.length === 0) lines.push('- none');
|
|
254
251
|
for (const memory of reliableMemories) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const DEFAULT_CODEGRAPH_EXCLUDES = [
|
|
2
|
+
'node_modules/**',
|
|
3
|
+
'dist/**',
|
|
4
|
+
'build/**',
|
|
5
|
+
'coverage/**',
|
|
6
|
+
'.next/**',
|
|
7
|
+
'.turbo/**',
|
|
8
|
+
'.git/**',
|
|
9
|
+
'.tmp/**',
|
|
10
|
+
'.worktrees/**',
|
|
11
|
+
'.claude/worktrees/**',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function normalizeCodeGraphExcludePatterns(exclude?: string[]): string[] {
|
|
15
|
+
return [...new Set([
|
|
16
|
+
...DEFAULT_CODEGRAPH_EXCLUDES,
|
|
17
|
+
...(exclude ?? []).map(pattern => pattern.trim()).filter(Boolean),
|
|
18
|
+
])];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isCodeGraphExcludedPath(path: string, exclude?: string[]): boolean {
|
|
22
|
+
const normalized = normalizeCodePath(path);
|
|
23
|
+
return normalizeCodeGraphExcludePatterns(exclude).some((pattern) => matchesPattern(normalized, normalizeCodePath(pattern)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeCodePath(path: string): string {
|
|
27
|
+
return path.replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function matchesPattern(path: string, pattern: string): boolean {
|
|
31
|
+
if (pattern.endsWith('/**')) {
|
|
32
|
+
const base = pattern.slice(0, -3);
|
|
33
|
+
if (base.startsWith('**/')) {
|
|
34
|
+
const suffix = base.slice(3);
|
|
35
|
+
return path === suffix || path.endsWith(`/${suffix}`) || path.includes(`/${suffix}/`);
|
|
36
|
+
}
|
|
37
|
+
if (!base.includes('/')) {
|
|
38
|
+
return path === base || path.startsWith(`${base}/`) || path.includes(`/${base}/`);
|
|
39
|
+
}
|
|
40
|
+
return path === base || path.startsWith(`${base}/`);
|
|
41
|
+
}
|
|
42
|
+
if (pattern.startsWith('**/')) {
|
|
43
|
+
const suffix = pattern.slice(3);
|
|
44
|
+
return path === suffix || path.endsWith(`/${suffix}`);
|
|
45
|
+
}
|
|
46
|
+
return path === pattern || path.startsWith(`${pattern}/`);
|
|
47
|
+
}
|
|
@@ -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,7 +184,7 @@ 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
189
|
if (entry.name === '.git' || entry.name === '.worktrees' || entry.name === '.tmp' || entry.name === 'node_modules') continue;
|
|
202
190
|
if (rel === '.claude/worktrees' || rel.startsWith('.claude/worktrees/')) continue;
|
|
@@ -290,18 +278,7 @@ function extractImportEdges(projectId: string, file: CodeFile, text: string, ind
|
|
|
290
278
|
}
|
|
291
279
|
|
|
292
280
|
export async function indexProjectLite(options: LiteIndexOptions): Promise<LiteIndexResult> {
|
|
293
|
-
const exclude = options.exclude
|
|
294
|
-
'node_modules/**',
|
|
295
|
-
'dist/**',
|
|
296
|
-
'build/**',
|
|
297
|
-
'coverage/**',
|
|
298
|
-
'.next/**',
|
|
299
|
-
'.turbo/**',
|
|
300
|
-
'.git/**',
|
|
301
|
-
'.tmp/**',
|
|
302
|
-
'.worktrees/**',
|
|
303
|
-
'.claude/worktrees/**',
|
|
304
|
-
];
|
|
281
|
+
const exclude = normalizeCodeGraphExcludePatterns(options.exclude);
|
|
305
282
|
const maxFiles = options.maxFiles ?? 5000;
|
|
306
283
|
const indexedAt = new Date().toISOString();
|
|
307
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,29 +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
|
-
const normalized = normalizePath(path);
|
|
86
|
-
return (
|
|
87
|
-
/(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules|\.git|\.tmp|\.worktrees)(\/|$)/i.test(normalized) ||
|
|
88
|
-
/(^|\/)\.claude\/worktrees(\/|$)/i.test(normalized)
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
81
|
function suggestedReadRank(path: string): number {
|
|
93
|
-
const normalized =
|
|
82
|
+
const normalized = path.replace(/\\/g, '/');
|
|
94
83
|
if (normalized.startsWith('src/')) return 0;
|
|
95
84
|
if (normalized.startsWith('tests/') || normalized.startsWith('test/')) return 0;
|
|
96
85
|
if (normalized.startsWith('packages/') && normalized.includes('/src/')) return 2;
|
|
97
86
|
return 3;
|
|
98
87
|
}
|
|
99
88
|
|
|
100
|
-
function compactSuggestedReads(paths: string[], limit = 8): string[] {
|
|
89
|
+
function compactSuggestedReads(paths: string[], limit = 8, exclude?: string[]): string[] {
|
|
101
90
|
return uniq(paths)
|
|
102
|
-
.filter(path => !
|
|
91
|
+
.filter(path => !isCodeGraphExcludedPath(path, exclude))
|
|
103
92
|
.sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b))
|
|
104
93
|
.slice(0, limit);
|
|
105
94
|
}
|
|
@@ -108,6 +97,7 @@ function collectGraph(
|
|
|
108
97
|
store: CodeGraphStore,
|
|
109
98
|
projectId: string,
|
|
110
99
|
observations: ProjectContextObservation[],
|
|
100
|
+
exclude?: string[],
|
|
111
101
|
): {
|
|
112
102
|
files: CodeFile[];
|
|
113
103
|
symbols: CodeSymbol[];
|
|
@@ -139,7 +129,9 @@ function collectGraph(
|
|
|
139
129
|
|
|
140
130
|
const observation = observationsById.get(ref.observationId);
|
|
141
131
|
if (!observation) continue;
|
|
142
|
-
|
|
132
|
+
const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
|
|
133
|
+
if (result.status === 'current' && file && !excluded) suggestedReads.push(file.path);
|
|
134
|
+
if (excluded) continue;
|
|
143
135
|
sources.push({
|
|
144
136
|
observationId: observation.id,
|
|
145
137
|
title: observation.title,
|
|
@@ -157,7 +149,7 @@ function collectGraph(
|
|
|
157
149
|
refs,
|
|
158
150
|
freshness,
|
|
159
151
|
sources,
|
|
160
|
-
suggestedReads: compactSuggestedReads(suggestedReads),
|
|
152
|
+
suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude),
|
|
161
153
|
};
|
|
162
154
|
}
|
|
163
155
|
|
|
@@ -165,9 +157,10 @@ export function buildProjectContextOverview(input: {
|
|
|
165
157
|
project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
|
|
166
158
|
store: CodeGraphStore;
|
|
167
159
|
observations: ProjectContextObservation[];
|
|
160
|
+
exclude?: string[];
|
|
168
161
|
}): ProjectContextOverview {
|
|
169
162
|
const active = activeObservations(input.observations, input.project.id);
|
|
170
|
-
const graph = collectGraph(input.store, input.project.id, active);
|
|
163
|
+
const graph = collectGraph(input.store, input.project.id, active, input.exclude);
|
|
171
164
|
const status = input.store.status(input.project.id);
|
|
172
165
|
|
|
173
166
|
return {
|
|
@@ -194,10 +187,11 @@ export function buildProjectContextExplain(input: {
|
|
|
194
187
|
project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
|
|
195
188
|
store: CodeGraphStore;
|
|
196
189
|
observations: ProjectContextObservation[];
|
|
190
|
+
exclude?: string[];
|
|
197
191
|
}): ProjectContextExplain {
|
|
198
192
|
const overview = buildProjectContextOverview(input);
|
|
199
193
|
const active = activeObservations(input.observations, input.project.id);
|
|
200
|
-
const graph = collectGraph(input.store, input.project.id, active);
|
|
194
|
+
const graph = collectGraph(input.store, input.project.id, active, input.exclude);
|
|
201
195
|
return {
|
|
202
196
|
project: input.project,
|
|
203
197
|
sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
@@ -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 },
|
package/src/server.ts
CHANGED
|
@@ -1295,8 +1295,10 @@ export async function createMemorixServer(
|
|
|
1295
1295
|
return withFreshIndex(async () => {
|
|
1296
1296
|
const { CodeGraphStore } = await import('./codegraph/store.js');
|
|
1297
1297
|
const { assembleContextPackForTask, buildContextPackPrompt } = await import('./codegraph/context-pack.js');
|
|
1298
|
+
const { getResolvedConfig } = await import('./config/resolved-config.js');
|
|
1298
1299
|
const store = new CodeGraphStore();
|
|
1299
1300
|
await store.init(projectDir);
|
|
1301
|
+
const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
|
|
1300
1302
|
|
|
1301
1303
|
const observations = getAllObservations()
|
|
1302
1304
|
.filter(obs => obs.projectId === project.id && (obs.status ?? 'active') === 'active')
|
|
@@ -1307,6 +1309,7 @@ export async function createMemorixServer(
|
|
|
1307
1309
|
task,
|
|
1308
1310
|
observations,
|
|
1309
1311
|
limit: typeof limit === 'number' ? limit : 20,
|
|
1312
|
+
exclude,
|
|
1310
1313
|
});
|
|
1311
1314
|
const text = buildContextPackPrompt(pack);
|
|
1312
1315
|
|
package/src/store/orama-store.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Vector search (embeddings) will be added in P1 phase.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { create, insert, search, remove, update, count, type AnyOrama } from '@orama/orama';
|
|
11
|
+
import { create, insert, search, remove, update, count, getByID, type AnyOrama } from '@orama/orama';
|
|
12
12
|
import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer } from '../types.js';
|
|
13
13
|
import { OBSERVATION_ICONS, type ObservationType } from '../types.js';
|
|
14
14
|
import { resolveKnowledgeLayer } from '../skills/mini-skills.js';
|
|
@@ -52,9 +52,13 @@ function makeEntryKey(projectId: string | undefined, observationId: number): str
|
|
|
52
52
|
return `${projectId ?? ''}::${observationId}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function rememberObservationDoc(doc: MemorixDocument):
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
function rememberObservationDoc(doc: MemorixDocument): MemorixDocument {
|
|
56
|
+
const publicDoc = { ...doc };
|
|
57
|
+
delete publicDoc.embedding;
|
|
58
|
+
if (doc.projectId && typeof doc.observationId === 'number') {
|
|
59
|
+
docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
|
|
60
|
+
}
|
|
61
|
+
return publicDoc;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function isCommandLikeQuery(query: string): boolean {
|
|
@@ -243,15 +247,15 @@ export async function batchGenerateEmbeddings(texts: string[]): Promise<(number[
|
|
|
243
247
|
*/
|
|
244
248
|
export async function hydrateIndex(observations: any[]): Promise<number> {
|
|
245
249
|
const database = await getDb();
|
|
246
|
-
const currentCount = await count(database);
|
|
247
|
-
if (currentCount > 0) return 0; // already hydrated
|
|
248
250
|
|
|
249
251
|
let inserted = 0;
|
|
250
252
|
for (const obs of observations) {
|
|
251
253
|
if (!obs || !obs.id || !obs.projectId) continue;
|
|
252
254
|
try {
|
|
255
|
+
const id = makeOramaObservationId(obs.projectId, obs.id);
|
|
256
|
+
if (getByID(database, id)) continue;
|
|
253
257
|
const doc: MemorixDocument = {
|
|
254
|
-
id
|
|
258
|
+
id,
|
|
255
259
|
observationId: obs.id,
|
|
256
260
|
entityName: obs.entityName || '',
|
|
257
261
|
type: obs.type || 'discovery',
|
|
@@ -376,6 +380,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
376
380
|
let searchParams: Record<string, unknown> = {
|
|
377
381
|
term: originalQuery,
|
|
378
382
|
limit: requestLimit,
|
|
383
|
+
includeVectors: true,
|
|
379
384
|
...(Object.keys(filters).length > 0 ? { where: filters } : {}),
|
|
380
385
|
// Search specific fields (not tokens, accessCount, etc.)
|
|
381
386
|
properties: ['title', 'entityName', 'narrative', 'facts', 'concepts', 'filesModified'],
|
|
@@ -458,6 +463,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
458
463
|
const vectorOnlyParams: Record<string, unknown> = {
|
|
459
464
|
term: '',
|
|
460
465
|
limit: requestLimit,
|
|
466
|
+
includeVectors: true,
|
|
461
467
|
...(Object.keys(filters).length > 0 ? { where: filters } : {}),
|
|
462
468
|
mode: 'vector',
|
|
463
469
|
vector: {
|
|
@@ -887,6 +893,7 @@ export async function getObservationsByIds(
|
|
|
887
893
|
|
|
888
894
|
const searchResult = await search(database, {
|
|
889
895
|
term: '',
|
|
896
|
+
includeVectors: true,
|
|
890
897
|
where: {
|
|
891
898
|
observationId: { eq: id },
|
|
892
899
|
...(projectId ? { projectId } : {}),
|
|
@@ -895,7 +902,7 @@ export async function getObservationsByIds(
|
|
|
895
902
|
});
|
|
896
903
|
|
|
897
904
|
if (searchResult.hits.length > 0) {
|
|
898
|
-
results.push(searchResult.hits[0].document as unknown as MemorixDocument);
|
|
905
|
+
results.push(rememberObservationDoc(searchResult.hits[0].document as unknown as MemorixDocument));
|
|
899
906
|
}
|
|
900
907
|
}
|
|
901
908
|
|