byterover-cli 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/core/domain/swarm/types.d.ts +132 -0
- package/dist/agent/core/domain/swarm/types.js +128 -0
- package/dist/agent/core/domain/tools/constants.d.ts +2 -0
- package/dist/agent/core/domain/tools/constants.js +2 -0
- package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
- package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
- package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
- package/dist/agent/infra/agent/service-initializer.js +48 -0
- package/dist/agent/infra/map/map-shared.d.ts +2 -2
- package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
- package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
- package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
- package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
- package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
- package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
- package/dist/agent/infra/swarm/provider-factory.js +67 -0
- package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
- package/dist/agent/infra/swarm/search-precision.js +141 -0
- package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
- package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
- package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
- package/dist/agent/infra/swarm/swarm-graph.js +167 -0
- package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
- package/dist/agent/infra/swarm/swarm-merger.js +66 -0
- package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
- package/dist/agent/infra/swarm/swarm-router.js +40 -0
- package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
- package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
- package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
- package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
- package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
- package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
- package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
- package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +25 -1
- package/dist/agent/resources/tools/code_exec.txt +2 -0
- package/dist/agent/resources/tools/swarm_query.txt +38 -0
- package/dist/agent/resources/tools/swarm_store.txt +35 -0
- package/dist/oclif/commands/connectors/install.d.ts +2 -2
- package/dist/oclif/commands/connectors/install.js +15 -7
- package/dist/oclif/commands/swarm/curate.d.ts +13 -0
- package/dist/oclif/commands/swarm/curate.js +81 -0
- package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
- package/dist/oclif/commands/swarm/onboard.js +233 -0
- package/dist/oclif/commands/swarm/query.d.ts +14 -0
- package/dist/oclif/commands/swarm/query.js +84 -0
- package/dist/oclif/commands/swarm/status.d.ts +41 -0
- package/dist/oclif/commands/swarm/status.js +278 -0
- package/dist/server/constants.d.ts +3 -2
- package/dist/server/constants.js +10 -9
- package/dist/server/core/domain/entities/agent.js +4 -0
- package/dist/server/core/domain/source/source-schema.d.ts +6 -6
- package/dist/server/core/domain/transport/schemas.d.ts +4 -4
- package/dist/server/infra/connectors/mcp/claude-desktop-config-path.d.ts +20 -0
- package/dist/server/infra/connectors/mcp/claude-desktop-config-path.js +47 -0
- package/dist/server/infra/connectors/mcp/mcp-connector-config.d.ts +19 -0
- package/dist/server/infra/connectors/mcp/mcp-connector-config.js +9 -0
- package/dist/server/infra/connectors/mcp/mcp-connector.js +12 -3
- package/dist/server/infra/http/provider-model-fetchers.js +1 -0
- package/dist/server/infra/process/feature-handlers.js +13 -0
- package/dist/server/infra/project/project-registry.js +13 -1
- package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
- package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
- package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
- package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
- package/dist/server/templates/skill/SKILL.md +163 -0
- package/dist/server/utils/gitignore.d.ts +1 -0
- package/dist/server/utils/gitignore.js +36 -4
- package/dist/shared/types/agent.d.ts +2 -1
- package/dist/shared/types/agent.js +2 -0
- package/dist/tui/features/connectors/components/connectors-flow.js +7 -2
- package/oclif.manifest.json +503 -323
- package/package.json +2 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { ADAPTER_CONTENT_LIMIT, applyGapRatio, POST_EXPANSION_GAP_RATIO, searchWithPrecision } from '../search-precision.js';
|
|
5
|
+
/** Wikilink decay factor for graph-expanded results */
|
|
6
|
+
const WIKILINK_DECAY = 0.7;
|
|
7
|
+
/**
|
|
8
|
+
* Extract `[[target]]` and `[[target|alias]]` wikilinks from markdown content.
|
|
9
|
+
*/
|
|
10
|
+
function extractWikilinks(content) {
|
|
11
|
+
const links = [];
|
|
12
|
+
const regex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = regex.exec(content)) !== null) {
|
|
15
|
+
links.push(match[1].trim());
|
|
16
|
+
}
|
|
17
|
+
return links;
|
|
18
|
+
}
|
|
19
|
+
function findMarkdownFiles(dirPath, basePath) {
|
|
20
|
+
const results = [];
|
|
21
|
+
try {
|
|
22
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.name.startsWith('.'))
|
|
25
|
+
continue;
|
|
26
|
+
const fullPath = join(dirPath, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
results.push(...findMarkdownFiles(fullPath, basePath));
|
|
29
|
+
}
|
|
30
|
+
else if (entry.name.endsWith('.md')) {
|
|
31
|
+
const stats = statSync(fullPath);
|
|
32
|
+
results.push({
|
|
33
|
+
fullPath,
|
|
34
|
+
relativePath: relative(basePath, fullPath),
|
|
35
|
+
signature: `${stats.mtimeMs}:${stats.size}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Skip inaccessible directories
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Derive a filename from note content (first heading or fallback).
|
|
47
|
+
*/
|
|
48
|
+
function deriveFilename(content) {
|
|
49
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
50
|
+
const title = titleMatch?.[1] ?? `note-${Date.now()}`;
|
|
51
|
+
const slug = title
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replaceAll(/[^\w-]+/g, '-')
|
|
54
|
+
.replaceAll(/-+/g, '-')
|
|
55
|
+
.replaceAll(/^-+|-+$/g, '');
|
|
56
|
+
return `${slug || `note-${Date.now()}`}.md`;
|
|
57
|
+
}
|
|
58
|
+
function buildIndexSignature(files) {
|
|
59
|
+
return files
|
|
60
|
+
.map((file) => `${file.relativePath}:${file.signature}`)
|
|
61
|
+
.sort()
|
|
62
|
+
.join('|');
|
|
63
|
+
}
|
|
64
|
+
function resolveUniqueFilename(folderPath, preferredFilename) {
|
|
65
|
+
const baseName = preferredFilename.replace(/\.md$/u, '');
|
|
66
|
+
const MAX_SUFFIX = 10_000;
|
|
67
|
+
let suffix = 0;
|
|
68
|
+
while (suffix <= MAX_SUFFIX) {
|
|
69
|
+
const candidate = suffix === 0 ? `${baseName}.md` : `${baseName}-${suffix}.md`;
|
|
70
|
+
if (!existsSync(join(folderPath, candidate))) {
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
suffix++;
|
|
74
|
+
}
|
|
75
|
+
return `${baseName}-${Date.now()}.md`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Local markdown folder adapter — indexes .md files via MiniSearch,
|
|
79
|
+
* optionally follows [[wikilinks]] one hop, supports write unless read-only.
|
|
80
|
+
*
|
|
81
|
+
* Each folder instance has a unique id: `local-markdown:{name}`.
|
|
82
|
+
*/
|
|
83
|
+
export class LocalMarkdownAdapter {
|
|
84
|
+
folderPath;
|
|
85
|
+
name;
|
|
86
|
+
capabilities;
|
|
87
|
+
id;
|
|
88
|
+
type = 'local-markdown';
|
|
89
|
+
documents = [];
|
|
90
|
+
followWikilinks;
|
|
91
|
+
index;
|
|
92
|
+
indexSignature;
|
|
93
|
+
pathToDoc = new Map();
|
|
94
|
+
readOnly;
|
|
95
|
+
watchForChanges;
|
|
96
|
+
constructor(folderPath, name, options) {
|
|
97
|
+
this.folderPath = folderPath;
|
|
98
|
+
this.name = name;
|
|
99
|
+
this.id = `local-markdown:${name}`;
|
|
100
|
+
this.readOnly = options?.readOnly ?? false;
|
|
101
|
+
this.followWikilinks = options?.followWikilinks ?? true;
|
|
102
|
+
this.watchForChanges = options?.watchForChanges ?? true;
|
|
103
|
+
this.capabilities = {
|
|
104
|
+
avgLatencyMs: 80,
|
|
105
|
+
graphTraversal: this.followWikilinks,
|
|
106
|
+
keywordSearch: true,
|
|
107
|
+
localOnly: true,
|
|
108
|
+
maxTokensPerQuery: 6000,
|
|
109
|
+
semanticSearch: false,
|
|
110
|
+
temporalQuery: false,
|
|
111
|
+
userModeling: false,
|
|
112
|
+
writeSupported: !this.readOnly,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async delete(_id) {
|
|
116
|
+
throw new Error('Local markdown delete not implemented.');
|
|
117
|
+
}
|
|
118
|
+
estimateCost(_request) {
|
|
119
|
+
return {
|
|
120
|
+
estimatedCostCents: 0,
|
|
121
|
+
estimatedLatencyMs: this.capabilities.avgLatencyMs,
|
|
122
|
+
estimatedTokens: 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async healthCheck() {
|
|
126
|
+
return { available: existsSync(this.folderPath) };
|
|
127
|
+
}
|
|
128
|
+
async query(request) {
|
|
129
|
+
this.ensureIndex();
|
|
130
|
+
const maxResults = request.maxResults ?? 10;
|
|
131
|
+
if (!this.index)
|
|
132
|
+
return [];
|
|
133
|
+
// T1/T2/T3: Precision-filtered search (stop words, AND-first, score floor, gap ratio)
|
|
134
|
+
const precisionResults = searchWithPrecision(this.index, request.query, { maxResults });
|
|
135
|
+
if (precisionResults.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
// Collect direct matches
|
|
138
|
+
const resultMap = new Map();
|
|
139
|
+
for (const pr of precisionResults) {
|
|
140
|
+
const doc = this.documents[pr.id];
|
|
141
|
+
if (!doc)
|
|
142
|
+
continue;
|
|
143
|
+
resultMap.set(doc.path, { doc, matchType: 'keyword', score: pr.normalizedScore });
|
|
144
|
+
}
|
|
145
|
+
// Without wikilink expansion: return direct matches
|
|
146
|
+
if (!this.followWikilinks) {
|
|
147
|
+
const sorted = [...resultMap.values()].sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
148
|
+
return sorted.map((entry, index) => ({
|
|
149
|
+
content: entry.doc.content.slice(0, ADAPTER_CONTENT_LIMIT),
|
|
150
|
+
id: `local-md-${this.name}-${index}`,
|
|
151
|
+
metadata: {
|
|
152
|
+
matchType: entry.matchType,
|
|
153
|
+
path: entry.doc.path,
|
|
154
|
+
source: entry.doc.path,
|
|
155
|
+
},
|
|
156
|
+
provider: this.id,
|
|
157
|
+
providerType: 'local-markdown',
|
|
158
|
+
score: entry.score,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
// Expand wikilinks one hop from direct matches
|
|
162
|
+
for (const [, entry] of resultMap) {
|
|
163
|
+
for (const linkTarget of entry.doc.wikilinks) {
|
|
164
|
+
const candidates = [
|
|
165
|
+
`${linkTarget}.md`,
|
|
166
|
+
linkTarget,
|
|
167
|
+
...[...this.pathToDoc.keys()].filter((p) => p.toLowerCase().endsWith(`${linkTarget.toLowerCase()}.md`) ||
|
|
168
|
+
p.toLowerCase() === linkTarget.toLowerCase()),
|
|
169
|
+
];
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
const linkedDoc = this.pathToDoc.get(candidate);
|
|
172
|
+
if (linkedDoc && !resultMap.has(linkedDoc.path)) {
|
|
173
|
+
resultMap.set(linkedDoc.path, {
|
|
174
|
+
doc: linkedDoc,
|
|
175
|
+
matchType: 'graph',
|
|
176
|
+
score: entry.score * WIKILINK_DECAY,
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Second gap-ratio pass on combined direct + expanded results (T3 only)
|
|
184
|
+
const combined = [...resultMap.entries()].map(([path, entry]) => ({
|
|
185
|
+
doc: entry.doc,
|
|
186
|
+
matchType: entry.matchType,
|
|
187
|
+
normalizedScore: entry.score,
|
|
188
|
+
path,
|
|
189
|
+
}));
|
|
190
|
+
const gapFiltered = applyGapRatio(combined.map((c) => ({ id: c.path, normalizedScore: c.normalizedScore, queryTerms: [], rawScore: 0 })), POST_EXPANSION_GAP_RATIO);
|
|
191
|
+
const keptPaths = new Set(gapFiltered.map((r) => r.id));
|
|
192
|
+
const sorted = combined
|
|
193
|
+
.filter((c) => keptPaths.has(c.path))
|
|
194
|
+
.sort((a, b) => b.normalizedScore - a.normalizedScore)
|
|
195
|
+
.slice(0, maxResults);
|
|
196
|
+
return sorted.map((entry, index) => ({
|
|
197
|
+
content: entry.doc.content.slice(0, ADAPTER_CONTENT_LIMIT),
|
|
198
|
+
id: `local-md-${this.name}-${index}`,
|
|
199
|
+
metadata: {
|
|
200
|
+
matchType: entry.matchType,
|
|
201
|
+
path: entry.path,
|
|
202
|
+
source: entry.path,
|
|
203
|
+
},
|
|
204
|
+
provider: this.id,
|
|
205
|
+
providerType: 'local-markdown',
|
|
206
|
+
score: entry.normalizedScore,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
async store(entry) {
|
|
210
|
+
if (this.readOnly) {
|
|
211
|
+
throw new Error(`Local markdown folder '${this.name}' is read-only.`);
|
|
212
|
+
}
|
|
213
|
+
const filename = resolveUniqueFilename(this.folderPath, deriveFilename(entry.content));
|
|
214
|
+
const filePath = join(this.folderPath, filename);
|
|
215
|
+
writeFileSync(filePath, entry.content);
|
|
216
|
+
// Invalidate index so next query picks up the new file
|
|
217
|
+
this.index = undefined;
|
|
218
|
+
this.indexSignature = undefined;
|
|
219
|
+
return { id: filename, provider: this.id, success: true };
|
|
220
|
+
}
|
|
221
|
+
async update(_id, _entry) {
|
|
222
|
+
throw new Error('Local markdown update not implemented.');
|
|
223
|
+
}
|
|
224
|
+
ensureIndex() {
|
|
225
|
+
// When watchForChanges is false, reuse the existing index after initial build
|
|
226
|
+
if (this.index && !this.watchForChanges) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const files = findMarkdownFiles(this.folderPath, this.folderPath);
|
|
230
|
+
const nextSignature = buildIndexSignature(files);
|
|
231
|
+
if (this.index && this.indexSignature === nextSignature) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.documents = files.map((file, index) => {
|
|
235
|
+
const content = readFileSync(file.fullPath, 'utf8');
|
|
236
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
237
|
+
return {
|
|
238
|
+
content,
|
|
239
|
+
id: index,
|
|
240
|
+
path: file.relativePath,
|
|
241
|
+
title: titleMatch?.[1] ?? file.relativePath,
|
|
242
|
+
wikilinks: extractWikilinks(content),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
this.index = new MiniSearch({
|
|
246
|
+
fields: ['title', 'content'],
|
|
247
|
+
storeFields: ['title', 'path'],
|
|
248
|
+
});
|
|
249
|
+
this.index.addAll(this.documents);
|
|
250
|
+
this.pathToDoc.clear();
|
|
251
|
+
for (const doc of this.documents) {
|
|
252
|
+
this.pathToDoc.set(doc.path, doc);
|
|
253
|
+
}
|
|
254
|
+
this.indexSignature = nextSignature;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CostEstimate, HealthStatus, MemoryEntry, ProviderCapabilities, QueryRequest, QueryResult, StoreResult } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
export interface MemoryWikiAdapterOptions {
|
|
4
|
+
boostFresh?: boolean;
|
|
5
|
+
vaultPath: string;
|
|
6
|
+
writePageType?: 'concept' | 'entity';
|
|
7
|
+
}
|
|
8
|
+
export declare class MemoryWikiAdapter implements IMemoryProvider {
|
|
9
|
+
readonly capabilities: ProviderCapabilities;
|
|
10
|
+
readonly id = "memory-wiki";
|
|
11
|
+
readonly type: "memory-wiki";
|
|
12
|
+
private readonly boostFresh;
|
|
13
|
+
private digest;
|
|
14
|
+
private documents;
|
|
15
|
+
private index;
|
|
16
|
+
private indexSignature;
|
|
17
|
+
private readonly vaultPath;
|
|
18
|
+
private readonly writePageType;
|
|
19
|
+
constructor(options: MemoryWikiAdapterOptions);
|
|
20
|
+
delete(_id: string): Promise<void>;
|
|
21
|
+
estimateCost(_request: QueryRequest): CostEstimate;
|
|
22
|
+
healthCheck(): Promise<HealthStatus>;
|
|
23
|
+
query(request: QueryRequest): Promise<QueryResult[]>;
|
|
24
|
+
store(entry: MemoryEntry): Promise<StoreResult>;
|
|
25
|
+
update(_id: string, _entry: Partial<MemoryEntry>): Promise<void>;
|
|
26
|
+
private ensureIndex;
|
|
27
|
+
private loadDigest;
|
|
28
|
+
private scanPages;
|
|
29
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { searchWithPrecision } from '../search-precision.js';
|
|
6
|
+
/** Directories to scan for wiki pages */
|
|
7
|
+
const PAGE_DIRS = ['entities', 'concepts', 'syntheses', 'sources'];
|
|
8
|
+
/** Score boost for fresh pages */
|
|
9
|
+
const FRESH_BOOST = 1.2;
|
|
10
|
+
/** Score boost for entities/concepts over sources */
|
|
11
|
+
const KIND_BOOST = {
|
|
12
|
+
concept: 1.3,
|
|
13
|
+
entity: 1.4,
|
|
14
|
+
source: 1,
|
|
15
|
+
synthesis: 1.2,
|
|
16
|
+
};
|
|
17
|
+
function slugify(text) {
|
|
18
|
+
return text
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replaceAll(/[^\w\s-]/g, '')
|
|
21
|
+
.trim()
|
|
22
|
+
.replaceAll(/\s+/g, '_');
|
|
23
|
+
}
|
|
24
|
+
function buildIndexSignature(vaultPath) {
|
|
25
|
+
const digestPath = join(vaultPath, '.openclaw-wiki', 'cache', 'agent-digest.json');
|
|
26
|
+
try {
|
|
27
|
+
const stat = statSync(digestPath);
|
|
28
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function extractContentSection(fullContent) {
|
|
35
|
+
// Extract content from openclaw:wiki:content markers (written by store())
|
|
36
|
+
const wikiMarkerMatch = fullContent.match(/<!-- openclaw:wiki:content:start -->\n([\s\S]*?)<!-- openclaw:wiki:content:end -->/);
|
|
37
|
+
if (wikiMarkerMatch) {
|
|
38
|
+
return wikiMarkerMatch[1].trim();
|
|
39
|
+
}
|
|
40
|
+
// Extract from ```text code block (wiki source page format)
|
|
41
|
+
const codeBlockMatch = fullContent.match(/```text\n([\s\S]*?)```/);
|
|
42
|
+
if (codeBlockMatch) {
|
|
43
|
+
return codeBlockMatch[1].trim();
|
|
44
|
+
}
|
|
45
|
+
// Fallback: strip frontmatter and return everything
|
|
46
|
+
const withoutFrontmatter = fullContent.replace(/^---[\s\S]*?---\n*/, '');
|
|
47
|
+
return withoutFrontmatter.trim();
|
|
48
|
+
}
|
|
49
|
+
export class MemoryWikiAdapter {
|
|
50
|
+
capabilities = {
|
|
51
|
+
avgLatencyMs: 60,
|
|
52
|
+
graphTraversal: false,
|
|
53
|
+
keywordSearch: true,
|
|
54
|
+
localOnly: true,
|
|
55
|
+
maxTokensPerQuery: 8000,
|
|
56
|
+
semanticSearch: false,
|
|
57
|
+
temporalQuery: false,
|
|
58
|
+
userModeling: false,
|
|
59
|
+
writeSupported: true,
|
|
60
|
+
};
|
|
61
|
+
id = 'memory-wiki';
|
|
62
|
+
type = 'memory-wiki';
|
|
63
|
+
boostFresh;
|
|
64
|
+
digest = null;
|
|
65
|
+
documents = [];
|
|
66
|
+
index = null;
|
|
67
|
+
indexSignature = '';
|
|
68
|
+
vaultPath;
|
|
69
|
+
writePageType;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.vaultPath = options.vaultPath;
|
|
72
|
+
this.boostFresh = options.boostFresh ?? true;
|
|
73
|
+
this.writePageType = options.writePageType ?? 'concept';
|
|
74
|
+
}
|
|
75
|
+
async delete(_id) {
|
|
76
|
+
throw new Error('Memory Wiki pages should be managed via wiki_apply.');
|
|
77
|
+
}
|
|
78
|
+
estimateCost(_request) {
|
|
79
|
+
return { estimatedCostCents: 0, estimatedLatencyMs: 60, estimatedTokens: 0 };
|
|
80
|
+
}
|
|
81
|
+
async healthCheck() {
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(this.vaultPath)) {
|
|
84
|
+
return { available: false, error: `Wiki vault not found at ${this.vaultPath}` };
|
|
85
|
+
}
|
|
86
|
+
return { available: true };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return { available: false, error: error instanceof Error ? error.message : String(error) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async query(request) {
|
|
93
|
+
this.ensureIndex();
|
|
94
|
+
// Safety net: ensureIndex() could leave this.index null if file reads fail
|
|
95
|
+
if (!this.index) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const maxResults = request.maxResults ?? 10;
|
|
99
|
+
const precisionResults = searchWithPrecision(this.index, request.query, { maxResults });
|
|
100
|
+
if (precisionResults.length === 0) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const mapped = [];
|
|
104
|
+
for (const pr of precisionResults) {
|
|
105
|
+
const doc = this.documents[pr.id];
|
|
106
|
+
if (!doc)
|
|
107
|
+
continue;
|
|
108
|
+
let score = pr.normalizedScore;
|
|
109
|
+
// Freshness boost
|
|
110
|
+
if (this.boostFresh && doc.freshnessLevel === 'fresh') {
|
|
111
|
+
score *= FRESH_BOOST;
|
|
112
|
+
}
|
|
113
|
+
// Kind boost
|
|
114
|
+
score *= KIND_BOOST[doc.kind] ?? 1;
|
|
115
|
+
mapped.push({
|
|
116
|
+
content: extractContentSection(doc.content).slice(0, 5000),
|
|
117
|
+
id: doc.pageId,
|
|
118
|
+
metadata: {
|
|
119
|
+
matchType: 'keyword',
|
|
120
|
+
path: doc.path,
|
|
121
|
+
source: doc.path,
|
|
122
|
+
},
|
|
123
|
+
provider: 'memory-wiki',
|
|
124
|
+
providerType: 'memory-wiki',
|
|
125
|
+
score,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return mapped.sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
129
|
+
}
|
|
130
|
+
async store(entry) {
|
|
131
|
+
const { content } = entry;
|
|
132
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
133
|
+
const title = titleMatch?.[1] ?? content.slice(0, 60).trim();
|
|
134
|
+
const slug = slugify(title);
|
|
135
|
+
const pageType = this.writePageType;
|
|
136
|
+
const dir = pageType === 'entity' ? 'entities' : 'concepts';
|
|
137
|
+
const dirPath = join(this.vaultPath, dir);
|
|
138
|
+
mkdirSync(dirPath, { recursive: true });
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
// Resolve unique filename
|
|
141
|
+
const MAX_SUFFIX = 10_000;
|
|
142
|
+
let filename = `${slug}.md`;
|
|
143
|
+
let filePath = join(dirPath, filename);
|
|
144
|
+
let suffix = 1;
|
|
145
|
+
while (existsSync(filePath) && suffix <= MAX_SUFFIX) {
|
|
146
|
+
filename = `${slug}-${suffix}.md`;
|
|
147
|
+
filePath = join(dirPath, filename);
|
|
148
|
+
suffix++;
|
|
149
|
+
}
|
|
150
|
+
if (existsSync(filePath)) {
|
|
151
|
+
filename = `${slug}-${Date.now()}.md`;
|
|
152
|
+
filePath = join(dirPath, filename);
|
|
153
|
+
}
|
|
154
|
+
const pageId = `${pageType}.swarm.${slug}`;
|
|
155
|
+
const pageContent = [
|
|
156
|
+
'---',
|
|
157
|
+
`pageType: ${pageType}`,
|
|
158
|
+
`id: ${pageId}`,
|
|
159
|
+
`title: ${JSON.stringify(title)}`,
|
|
160
|
+
'status: active',
|
|
161
|
+
`updatedAt: "${now}"`,
|
|
162
|
+
'sourceType: swarm-curate',
|
|
163
|
+
'---',
|
|
164
|
+
'',
|
|
165
|
+
'<!-- openclaw:wiki:content:start -->',
|
|
166
|
+
content,
|
|
167
|
+
'<!-- openclaw:wiki:content:end -->',
|
|
168
|
+
'',
|
|
169
|
+
'<!-- openclaw:human:start -->',
|
|
170
|
+
'<!-- openclaw:human:end -->',
|
|
171
|
+
'',
|
|
172
|
+
].join('\n');
|
|
173
|
+
await writeFile(filePath, pageContent);
|
|
174
|
+
// Invalidate index so next query picks up the new page
|
|
175
|
+
this.index = null;
|
|
176
|
+
this.indexSignature = '';
|
|
177
|
+
return { id: pageId, provider: 'memory-wiki', success: true };
|
|
178
|
+
}
|
|
179
|
+
async update(_id, _entry) {
|
|
180
|
+
throw new Error('Memory Wiki pages should be managed via wiki_apply.');
|
|
181
|
+
}
|
|
182
|
+
ensureIndex() {
|
|
183
|
+
const nextSignature = buildIndexSignature(this.vaultPath);
|
|
184
|
+
if (this.index && this.indexSignature === nextSignature) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.digest = this.loadDigest();
|
|
188
|
+
this.documents = this.scanPages();
|
|
189
|
+
this.index = new MiniSearch({
|
|
190
|
+
fields: ['title', 'content'],
|
|
191
|
+
idField: 'id',
|
|
192
|
+
storeFields: ['title', 'path'],
|
|
193
|
+
});
|
|
194
|
+
this.index.addAll(this.documents);
|
|
195
|
+
this.indexSignature = nextSignature;
|
|
196
|
+
}
|
|
197
|
+
loadDigest() {
|
|
198
|
+
const digestPath = join(this.vaultPath, '.openclaw-wiki', 'cache', 'agent-digest.json');
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(readFileSync(digestPath, 'utf8'));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
scanPages() {
|
|
207
|
+
const digestMap = new Map();
|
|
208
|
+
if (this.digest) {
|
|
209
|
+
for (const page of this.digest.pages) {
|
|
210
|
+
digestMap.set(page.path, page);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const docs = [];
|
|
214
|
+
let docId = 0;
|
|
215
|
+
for (const dir of PAGE_DIRS) {
|
|
216
|
+
const dirPath = join(this.vaultPath, dir);
|
|
217
|
+
if (!existsSync(dirPath))
|
|
218
|
+
continue;
|
|
219
|
+
for (const entry of readdirSync(dirPath)) {
|
|
220
|
+
if (!entry.endsWith('.md') || entry === 'index.md')
|
|
221
|
+
continue;
|
|
222
|
+
try {
|
|
223
|
+
const content = readFileSync(join(dirPath, entry), 'utf8');
|
|
224
|
+
const relPath = `${dir}/${entry}`;
|
|
225
|
+
const digestPage = digestMap.get(relPath);
|
|
226
|
+
const titleMatch = content.match(/^title:\s*"?(.+?)"?\s*$/m) ?? content.match(/^#\s+(.+)$/m);
|
|
227
|
+
docs.push({
|
|
228
|
+
content,
|
|
229
|
+
freshnessLevel: digestPage?.freshnessLevel ?? 'unknown',
|
|
230
|
+
id: docId++,
|
|
231
|
+
kind: digestPage?.kind ?? dir.replace(/s$/, ''),
|
|
232
|
+
pageId: digestPage?.id ?? `${dir}.${entry.replace(/\.md$/, '')}`,
|
|
233
|
+
path: relPath,
|
|
234
|
+
title: titleMatch?.[1] ?? entry.replace(/\.md$/, ''),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Skip unreadable files
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return docs;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CostEstimate, HealthStatus, MemoryEntry, ProviderCapabilities, QueryRequest, QueryResult, StoreResult } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the Obsidian adapter.
|
|
5
|
+
*/
|
|
6
|
+
export interface ObsidianAdapterOptions {
|
|
7
|
+
/** Additional folder names to ignore when scanning the vault */
|
|
8
|
+
ignorePatterns?: string[];
|
|
9
|
+
/** Whether to rescan files on each query (default: true). When false, the index is built once and frozen. */
|
|
10
|
+
watchForChanges?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Obsidian vault adapter — indexes .md files via MiniSearch,
|
|
14
|
+
* follows [[wikilinks]] one hop for graph expansion.
|
|
15
|
+
*
|
|
16
|
+
* Read-only by default (does not modify the user's vault).
|
|
17
|
+
*/
|
|
18
|
+
export declare class ObsidianAdapter implements IMemoryProvider {
|
|
19
|
+
private readonly vaultPath;
|
|
20
|
+
readonly capabilities: ProviderCapabilities;
|
|
21
|
+
readonly id = "obsidian";
|
|
22
|
+
readonly type: "obsidian";
|
|
23
|
+
private documents;
|
|
24
|
+
private readonly ignoreSet;
|
|
25
|
+
private index?;
|
|
26
|
+
private indexSignature?;
|
|
27
|
+
private pathToDoc;
|
|
28
|
+
private readonly watchForChanges;
|
|
29
|
+
constructor(vaultPath: string, options?: ObsidianAdapterOptions);
|
|
30
|
+
delete(_id: string): Promise<void>;
|
|
31
|
+
estimateCost(_request: QueryRequest): CostEstimate;
|
|
32
|
+
healthCheck(): Promise<HealthStatus>;
|
|
33
|
+
query(request: QueryRequest): Promise<QueryResult[]>;
|
|
34
|
+
store(_entry: MemoryEntry): Promise<StoreResult>;
|
|
35
|
+
update(_id: string, _entry: Partial<MemoryEntry>): Promise<void>;
|
|
36
|
+
private ensureIndex;
|
|
37
|
+
}
|