byterover-cli 1.0.4 → 1.1.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/README.md +24 -11
- package/dist/commands/curate.js +1 -1
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/main.d.ts +13 -0
- package/dist/commands/main.js +53 -2
- package/dist/commands/query.js +1 -1
- package/dist/commands/status.js +8 -3
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/core/domain/cipher/llm/registry.js +53 -2
- package/dist/core/domain/cipher/llm/types.d.ts +2 -0
- package/dist/core/domain/cipher/process/types.d.ts +7 -0
- package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
- package/dist/core/domain/cipher/session/session-metadata.js +147 -0
- package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
- package/dist/core/domain/cipher/tools/constants.js +1 -0
- package/dist/core/domain/entities/agent.d.ts +16 -0
- package/dist/core/domain/entities/agent.js +24 -0
- package/dist/core/domain/entities/connector-type.d.ts +9 -0
- package/dist/core/domain/entities/connector-type.js +8 -0
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +2 -0
- package/dist/core/domain/errors/task-error.d.ts +4 -0
- package/dist/core/domain/errors/task-error.js +7 -0
- package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
- package/dist/core/domain/knowledge/markdown-writer.js +232 -34
- package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
- package/dist/core/domain/knowledge/relation-parser.js +39 -61
- package/dist/core/domain/transport/schemas.d.ts +77 -2
- package/dist/core/domain/transport/schemas.js +51 -2
- package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
- package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
- package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
- package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
- package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
- package/dist/core/interfaces/connectors/i-connector.js +1 -0
- package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
- package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
- package/dist/core/interfaces/i-file-service.d.ts +7 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
- package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
- package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
- package/dist/hooks/init/update-notifier.d.ts +1 -0
- package/dist/hooks/init/update-notifier.js +10 -1
- package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
- package/dist/infra/cipher/agent/service-initializer.js +4 -4
- package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
- package/dist/infra/cipher/file-system/binary-utils.js +46 -31
- package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
- package/dist/infra/cipher/file-system/file-system-service.js +1 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
- package/dist/infra/cipher/interactive-loop.js +3 -1
- package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
- package/dist/infra/cipher/llm/context/context-manager.js +63 -18
- package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
- package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
- package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
- package/dist/infra/cipher/llm/thought-parser.js +27 -0
- package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
- package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
- package/dist/infra/cipher/process/process-service.js +11 -3
- package/dist/infra/cipher/session/chat-session.d.ts +7 -2
- package/dist/infra/cipher/session/chat-session.js +90 -52
- package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
- package/dist/infra/cipher/session/session-metadata-store.js +406 -0
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
- package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
- package/dist/infra/cipher/tools/index.d.ts +1 -0
- package/dist/infra/cipher/tools/index.js +1 -0
- package/dist/infra/cipher/tools/tool-manager.js +1 -0
- package/dist/infra/cipher/tools/tool-registry.js +7 -0
- package/dist/infra/connectors/connector-manager.d.ts +32 -0
- package/dist/infra/connectors/connector-manager.js +156 -0
- package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
- package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
- package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
- package/dist/infra/connectors/hook/hook-connector.js +231 -0
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
- package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
- package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
- package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
- package/dist/infra/connectors/rules/rules-connector.js +204 -0
- package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
- package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
- package/dist/infra/context-tree/file-context-file-reader.js +4 -0
- package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
- package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
- package/dist/infra/core/executors/curate-executor.d.ts +2 -2
- package/dist/infra/core/executors/curate-executor.js +7 -7
- package/dist/infra/core/executors/query-executor.d.ts +12 -0
- package/dist/infra/core/executors/query-executor.js +62 -1
- package/dist/infra/core/task-processor.d.ts +2 -2
- package/dist/infra/file/fs-file-service.d.ts +7 -0
- package/dist/infra/file/fs-file-service.js +15 -1
- package/dist/infra/process/agent-worker.d.ts +2 -2
- package/dist/infra/process/agent-worker.js +626 -142
- package/dist/infra/process/constants.d.ts +1 -1
- package/dist/infra/process/constants.js +1 -1
- package/dist/infra/process/ipc-types.d.ts +17 -4
- package/dist/infra/process/ipc-types.js +3 -3
- package/dist/infra/process/parent-heartbeat.d.ts +47 -0
- package/dist/infra/process/parent-heartbeat.js +118 -0
- package/dist/infra/process/process-manager.d.ts +89 -1
- package/dist/infra/process/process-manager.js +293 -9
- package/dist/infra/process/task-queue-manager.d.ts +13 -0
- package/dist/infra/process/task-queue-manager.js +19 -0
- package/dist/infra/process/transport-handlers.d.ts +3 -0
- package/dist/infra/process/transport-handlers.js +82 -5
- package/dist/infra/process/transport-worker.js +9 -69
- package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
- package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
- package/dist/infra/repl/commands/index.js +8 -4
- package/dist/infra/repl/commands/init-command.js +11 -7
- package/dist/infra/repl/commands/new-command.d.ts +14 -0
- package/dist/infra/repl/commands/new-command.js +61 -0
- package/dist/infra/repl/commands/query-command.js +22 -2
- package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
- package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
- package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
- package/dist/infra/transport/socket-io-transport-client.js +283 -7
- package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
- package/dist/infra/usecase/connectors-use-case.js +203 -0
- package/dist/infra/usecase/init-use-case.d.ts +8 -43
- package/dist/infra/usecase/init-use-case.js +29 -253
- package/dist/infra/usecase/logout-use-case.js +2 -2
- package/dist/infra/usecase/pull-use-case.js +5 -5
- package/dist/infra/usecase/push-use-case.js +5 -5
- package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
- package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
- package/dist/infra/usecase/space-list-use-case.js +3 -3
- package/dist/infra/usecase/space-switch-use-case.js +3 -3
- package/dist/resources/prompts/curate.yml +75 -13
- package/dist/resources/prompts/explore.yml +34 -0
- package/dist/resources/prompts/query-orchestrator.yml +112 -0
- package/dist/resources/prompts/system-prompt.yml +12 -2
- package/dist/resources/tools/curate.txt +60 -15
- package/dist/resources/tools/search_knowledge.txt +32 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
- package/dist/tui/components/onboarding/welcome-box.js +1 -1
- package/dist/tui/contexts/onboarding-context.d.ts +4 -0
- package/dist/tui/contexts/onboarding-context.js +14 -2
- package/dist/tui/views/command-view.js +19 -0
- package/dist/utils/file-validator.d.ts +1 -1
- package/dist/utils/file-validator.js +34 -35
- package/dist/utils/type-guards.d.ts +5 -0
- package/dist/utils/type-guards.js +7 -0
- package/oclif.manifest.json +32 -6
- package/package.json +4 -1
- package/dist/config/context-tree-domains.d.ts +0 -29
- package/dist/config/context-tree-domains.js +0 -29
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
- package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
- package/dist/infra/rule/agent-rule-config.d.ts +0 -19
- package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
- package/dist/infra/usecase/generate-rules-use-case.js +0 -285
- /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
- /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
|
|
@@ -10,14 +9,43 @@ import { toSnakeCase } from '../../../../utils/file-helpers.js';
|
|
|
10
9
|
* Inspired by ACE Curator patterns.
|
|
11
10
|
*/
|
|
12
11
|
const OperationType = z.enum(['ADD', 'UPDATE', 'MERGE', 'DELETE']);
|
|
12
|
+
/**
|
|
13
|
+
* Raw Concept schema for structured metadata and technical footprint.
|
|
14
|
+
*/
|
|
15
|
+
const RawConceptSchema = z.object({
|
|
16
|
+
changes: z.array(z.string()).optional().describe('What changes in the codebase are induced by this concept'),
|
|
17
|
+
files: z.array(z.string()).optional().describe('Which files are related to this concept'),
|
|
18
|
+
flow: z.string().optional().describe('What is the flow included in this concept'),
|
|
19
|
+
task: z.string().optional().describe('What is the task related to this concept'),
|
|
20
|
+
timestamp: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('When the concept was created or modified (ISO 8601 format, e.g., 2025-03-18)'),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Narrative schema for descriptive and structural context.
|
|
27
|
+
*/
|
|
28
|
+
const NarrativeSchema = z.object({
|
|
29
|
+
dependencies: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Dependency management information (e.g., "Singleton, init when service starts, hard dependency in smoke test")'),
|
|
33
|
+
features: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Feature documentation for this concept (e.g., "User permission can be stale for up to 300 seconds due to Redis cache")'),
|
|
37
|
+
structure: z.string().optional().describe('Code structure documentation (e.g., "clients/redis_client.go")'),
|
|
38
|
+
});
|
|
13
39
|
/**
|
|
14
40
|
* Content structure for ADD and UPDATE operations.
|
|
15
41
|
*/
|
|
16
42
|
const ContentSchema = z.object({
|
|
43
|
+
narrative: NarrativeSchema.optional().describe('Narrative section with descriptive and structural context'),
|
|
44
|
+
rawConcept: RawConceptSchema.optional().describe('Raw concept section with metadata and technical footprint'),
|
|
17
45
|
relations: z
|
|
18
46
|
.array(z.string())
|
|
19
47
|
.optional()
|
|
20
|
-
.describe('Related topics using domain/topic or domain/topic/subtopic notation'),
|
|
48
|
+
.describe('Related topics using domain/topic/title.md or domain/topic/subtopic/title.md notation'),
|
|
21
49
|
snippets: z.array(z.string()).optional().describe('Code/text snippets'),
|
|
22
50
|
});
|
|
23
51
|
/**
|
|
@@ -27,9 +55,12 @@ const OperationSchema = z.object({
|
|
|
27
55
|
content: ContentSchema.optional().describe('Content for ADD/UPDATE operations'),
|
|
28
56
|
mergeTarget: z.string().optional().describe('Target path for MERGE operation'),
|
|
29
57
|
mergeTargetTitle: z.string().optional().describe('Title of the target file for MERGE operation'),
|
|
30
|
-
path: z.string().describe('Path: domain/topic or domain/topic/subtopic'),
|
|
58
|
+
path: z.string().describe('Path: domain/topic/title.md or domain/topic/subtopic/title.md'),
|
|
31
59
|
reason: z.string().describe('Reasoning for this operation'),
|
|
32
|
-
title: z
|
|
60
|
+
title: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Title for the context file (saved as {title}.md in snake_case). Required for ADD/UPDATE/MERGE, optional for DELETE'),
|
|
33
64
|
type: OperationType.describe('Operation type: ADD, UPDATE, MERGE, or DELETE'),
|
|
34
65
|
});
|
|
35
66
|
/**
|
|
@@ -53,33 +84,17 @@ function parsePath(path) {
|
|
|
53
84
|
topic: parts[1],
|
|
54
85
|
};
|
|
55
86
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Get existing domain names from the context tree.
|
|
58
|
-
* Returns domain folder names that exist in the context tree.
|
|
59
|
-
*/
|
|
60
|
-
async function getExistingDomains(basePath) {
|
|
61
|
-
try {
|
|
62
|
-
const entries = await fs.readdir(basePath, { withFileTypes: true });
|
|
63
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Directory doesn't exist yet
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
87
|
/**
|
|
71
88
|
* Validate domain name format.
|
|
72
89
|
* Dynamic domains are allowed - no predefined list or limits.
|
|
73
90
|
* The agent is responsible for creating semantically meaningful domains.
|
|
74
91
|
*/
|
|
75
|
-
|
|
92
|
+
function validateDomain(domainName) {
|
|
76
93
|
const normalizedDomain = toSnakeCase(domainName);
|
|
77
|
-
const existingDomains = await getExistingDomains(basePath);
|
|
78
94
|
// Validate domain name format (must be non-empty and valid for filesystem)
|
|
79
95
|
if (!normalizedDomain || normalizedDomain.length === 0) {
|
|
80
96
|
return {
|
|
81
97
|
allowed: false,
|
|
82
|
-
existingDomains,
|
|
83
98
|
reason: 'Domain name cannot be empty.',
|
|
84
99
|
};
|
|
85
100
|
}
|
|
@@ -87,12 +102,11 @@ async function validateDomain(basePath, domainName) {
|
|
|
87
102
|
if (!/^[\w-]+$/.test(normalizedDomain)) {
|
|
88
103
|
return {
|
|
89
104
|
allowed: false,
|
|
90
|
-
existingDomains,
|
|
91
105
|
reason: `Domain name "${normalizedDomain}" contains invalid characters. Use only letters, numbers, underscores, and hyphens.`,
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
// All valid domain names are allowed - dynamic domain creation enabled
|
|
95
|
-
return { allowed: true
|
|
109
|
+
return { allowed: true };
|
|
96
110
|
}
|
|
97
111
|
/**
|
|
98
112
|
* Build the full filesystem path from base path and knowledge path.
|
|
@@ -142,7 +156,7 @@ async function executeAdd(basePath, operation) {
|
|
|
142
156
|
};
|
|
143
157
|
}
|
|
144
158
|
// Validate domain before creating
|
|
145
|
-
const domainValidation =
|
|
159
|
+
const domainValidation = validateDomain(parsed.domain);
|
|
146
160
|
if (!domainValidation.allowed) {
|
|
147
161
|
return {
|
|
148
162
|
message: domainValidation.reason,
|
|
@@ -159,8 +173,10 @@ async function executeAdd(basePath, operation) {
|
|
|
159
173
|
// Note: writeFileAtomic creates parent directories as needed, avoiding empty folder creation
|
|
160
174
|
const contextContent = MarkdownWriter.generateContext({
|
|
161
175
|
name: title,
|
|
176
|
+
narrative: content.narrative,
|
|
177
|
+
rawConcept: content.rawConcept,
|
|
162
178
|
relations: content.relations,
|
|
163
|
-
snippets: content.snippets
|
|
179
|
+
snippets: content.snippets ?? [],
|
|
164
180
|
});
|
|
165
181
|
const filename = `${toSnakeCase(title)}.md`;
|
|
166
182
|
const contextPath = join(finalPath, filename);
|
|
@@ -220,8 +236,10 @@ async function executeUpdate(basePath, operation) {
|
|
|
220
236
|
// Generate and write updated content (full replacement)
|
|
221
237
|
const contextContent = MarkdownWriter.generateContext({
|
|
222
238
|
name: title,
|
|
239
|
+
narrative: content.narrative,
|
|
240
|
+
rawConcept: content.rawConcept,
|
|
223
241
|
relations: content.relations,
|
|
224
|
-
snippets: content.snippets
|
|
242
|
+
snippets: content.snippets ?? [],
|
|
225
243
|
});
|
|
226
244
|
await DirectoryManager.writeFileAtomic(contextPath, contextContent);
|
|
227
245
|
return {
|
|
@@ -381,7 +399,27 @@ async function executeDelete(basePath, operation) {
|
|
|
381
399
|
* Execute curate operations on knowledge topics.
|
|
382
400
|
*/
|
|
383
401
|
async function executeCurate(input, _context) {
|
|
384
|
-
const
|
|
402
|
+
const parseResult = CurateInputSchema.safeParse(input);
|
|
403
|
+
if (!parseResult.success) {
|
|
404
|
+
return {
|
|
405
|
+
applied: [
|
|
406
|
+
{
|
|
407
|
+
message: `Invalid input: ${parseResult.error.message}`,
|
|
408
|
+
path: '',
|
|
409
|
+
status: 'failed',
|
|
410
|
+
type: 'ADD',
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
summary: {
|
|
414
|
+
added: 0,
|
|
415
|
+
deleted: 0,
|
|
416
|
+
failed: 1,
|
|
417
|
+
merged: 0,
|
|
418
|
+
updated: 0,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const { basePath, operations } = parseResult.data;
|
|
385
423
|
const applied = [];
|
|
386
424
|
const summary = {
|
|
387
425
|
added: 0,
|
|
@@ -420,8 +458,10 @@ async function executeCurate(input, _context) {
|
|
|
420
458
|
break;
|
|
421
459
|
}
|
|
422
460
|
default: {
|
|
461
|
+
// Exhaustive type check - TypeScript will error if any case is missed
|
|
462
|
+
const exhaustiveCheck = operation.type;
|
|
423
463
|
result = {
|
|
424
|
-
message: `Unknown operation type: ${
|
|
464
|
+
message: `Unknown operation type: ${exhaustiveCheck}`,
|
|
425
465
|
path: operation.path,
|
|
426
466
|
status: 'failed',
|
|
427
467
|
type: operation.type,
|
|
@@ -446,30 +486,84 @@ async function executeCurate(input, _context) {
|
|
|
446
486
|
*/
|
|
447
487
|
export function createCurateTool() {
|
|
448
488
|
return {
|
|
449
|
-
description: `Curate knowledge topics with atomic operations. This tool manages the knowledge structure using four operation types:
|
|
489
|
+
description: `Curate knowledge topics with atomic operations. This tool manages the knowledge structure using four operation types and supports a two-part context model: Raw Concept + Narrative.
|
|
490
|
+
|
|
491
|
+
**Content Structure (Two-Part Model):**
|
|
492
|
+
- **rawConcept**: Captures essential metadata and technical footprint
|
|
493
|
+
- task: What is the task related to this concept
|
|
494
|
+
- changes: Array of changes induced in the codebase
|
|
495
|
+
- files: Array of related files
|
|
496
|
+
- flow: The execution flow of this concept
|
|
497
|
+
- timestamp: When created/modified (ISO 8601 format)
|
|
498
|
+
- **narrative**: Captures descriptive and structural context
|
|
499
|
+
- structure: Code structure documentation
|
|
500
|
+
- dependencies: Dependency management information
|
|
501
|
+
- features: Feature documentation
|
|
502
|
+
- **snippets**: Code/text snippets (legacy support)
|
|
503
|
+
- **relations**: Related topics using @domain/topic notation
|
|
450
504
|
|
|
451
505
|
**Operations:**
|
|
452
506
|
1. **ADD** - Create new titled context file in domain/topic/subtopic
|
|
453
507
|
- Requires: path, title, content (snippets and/or relations), reason
|
|
454
|
-
-
|
|
455
|
-
-
|
|
508
|
+
- Relations must be in the format of "domain/topic/title.md" or "domain/topic/subtopic/title.md"
|
|
509
|
+
- Example with Raw Concept + Narrative:
|
|
510
|
+
{
|
|
511
|
+
type: "ADD",
|
|
512
|
+
path: "structure/caching",
|
|
513
|
+
title: "Redis User Permissions",
|
|
514
|
+
content: {
|
|
515
|
+
rawConcept: {
|
|
516
|
+
task: "Introduce Redis cache for getUserPermissions(userId)",
|
|
517
|
+
changes: ["Cached result using remote Redis", "Redis client: singleton"],
|
|
518
|
+
files: ["services/permission_service.go", "clients/redis_client.go"],
|
|
519
|
+
flow: "getUserPermissions -> check Redis -> on miss query DB -> store result -> return",
|
|
520
|
+
timestamp: "2025-03-18"
|
|
521
|
+
},
|
|
522
|
+
narrative: {
|
|
523
|
+
structure: "# Redis client\\n- clients/redis_client.go",
|
|
524
|
+
dependencies: "# Redis client\\n- Singleton, init when service starts",
|
|
525
|
+
features: "# Authorization\\n- User permission can be stale for up to 300 seconds"
|
|
526
|
+
},
|
|
527
|
+
relations: ["structure/api-endpoints/validation.md", "structure/api-endpoints/error-handling/retry-logic.md"]
|
|
528
|
+
},
|
|
529
|
+
reason: "New caching pattern"
|
|
530
|
+
}
|
|
531
|
+
- Creates: structure/caching/redis_user_permissions.md
|
|
456
532
|
|
|
457
533
|
2. **UPDATE** - Modify existing titled context file (full replacement)
|
|
458
534
|
- Requires: path, title, content, reason
|
|
459
|
-
-
|
|
535
|
+
- Relations must be in the format of "domain/topic/title.md" or "domain/topic/subtopic/title.md"
|
|
536
|
+
- Supports same content structure as ADD
|
|
460
537
|
|
|
461
538
|
3. **MERGE** - Combine source file into target file, delete source
|
|
462
539
|
- Requires: path (source), title (source file), mergeTarget (destination path), mergeTargetTitle (destination file), reason
|
|
463
540
|
- Example: { type: "MERGE", path: "code_style/old_topic", title: "Old Guide", mergeTarget: "code_style/new_topic", mergeTargetTitle: "New Guide", reason: "Consolidating" }
|
|
541
|
+
- Raw concepts and narratives are intelligently merged
|
|
464
542
|
|
|
465
543
|
4. **DELETE** - Remove specific file or entire folder
|
|
466
544
|
- Requires: path, title (optional), reason
|
|
467
545
|
- With title: deletes specific file; without title: deletes entire folder
|
|
468
|
-
- Example (file): { type: "DELETE", path: "code_style/deprecated", title: "Old Guide", reason: "No longer relevant" }
|
|
469
|
-
- Example (folder): { type: "DELETE", path: "code_style/deprecated", title: "", reason: "Removing topic" }
|
|
470
546
|
|
|
471
|
-
**
|
|
472
|
-
|
|
547
|
+
**CRITICAL - Path vs Title separation:**
|
|
548
|
+
- "path" = folder location only (domain/topic or domain/topic/subtopic) - NEVER include file extension suffixes
|
|
549
|
+
- "title" = the context name (becomes {title}.md file automatically)
|
|
550
|
+
- The system auto-generates the .md file from title - DO NOT put .md or _md anywhere in path
|
|
551
|
+
|
|
552
|
+
**Path format:** domain/topic OR domain/topic/subtopic (2-3 segments, NO filename, NO extension)
|
|
553
|
+
**File naming:** Title is auto-converted to snake_case and .md is auto-appended (e.g., title "Best Practices" -> best_practices.md)
|
|
554
|
+
|
|
555
|
+
**Good path examples:**
|
|
556
|
+
- path: "authentication/jwt", title: "Token Refresh" -> creates authentication/jwt/token_refresh.md
|
|
557
|
+
- path: "api_design/error_handling", title: "Retry Logic" -> creates api_design/error_handling/retry_logic.md
|
|
558
|
+
- path: "database/migrations/versioning", title: "Schema Changes" -> creates database/migrations/versioning/schema_changes.md
|
|
559
|
+
|
|
560
|
+
**Bad path examples (NEVER DO THIS):**
|
|
561
|
+
- "code_style/error_handling_md" - WRONG: _md suffix in path
|
|
562
|
+
- "code_style/error_handling.md" - WRONG: .md extension in path
|
|
563
|
+
- "authentication/jwt/token_refresh.md" - WRONG: filename in path (use title parameter instead)
|
|
564
|
+
- "authentication/jwt/token_refresh_md" - WRONG: _md suffix (this is NOT how to specify filename)
|
|
565
|
+
- "api/auth/jwt/tokens" - WRONG: 4 levels deep (max 3 allowed)
|
|
566
|
+
- "a/b" - WRONG: too vague, use descriptive names
|
|
473
567
|
|
|
474
568
|
**Dynamic Domain Creation:**
|
|
475
569
|
- Domains are created dynamically based on the context being curated
|
|
@@ -485,6 +579,8 @@ export function createCurateTool() {
|
|
|
485
579
|
- Avoid overly specific names that only fit one topic
|
|
486
580
|
- Keep domain count reasonable by consolidating related concepts
|
|
487
581
|
|
|
582
|
+
**Backward Compatibility:** Existing context entries using only snippets and relations continue to work.
|
|
583
|
+
|
|
488
584
|
**Output:** Returns applied operations with status (success/failed), filePath (for created/modified files), and a summary of counts.`,
|
|
489
585
|
execute: executeCurate,
|
|
490
586
|
id: ToolName.CURATE,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
|
|
3
|
+
import { isImageFile } from '../../file-system/binary-utils.js';
|
|
3
4
|
/**
|
|
4
5
|
* Input schema for read file tool.
|
|
5
6
|
*/
|
|
@@ -31,23 +32,43 @@ export function createReadFileTool(fileSystemService) {
|
|
|
31
32
|
description: 'Read the contents of a file. Supports relative/absolute paths, pagination, and returns images/PDFs as base64 attachments.',
|
|
32
33
|
async execute(input, _context) {
|
|
33
34
|
const { filePath, limit, offset } = input;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
try {
|
|
36
|
+
// Call file system service
|
|
37
|
+
const result = await fileSystemService.readFile(filePath, {
|
|
38
|
+
limit,
|
|
39
|
+
offset,
|
|
40
|
+
});
|
|
41
|
+
// Transform attachment format (singular → plural array)
|
|
42
|
+
let attachments;
|
|
43
|
+
if (result.attachment) {
|
|
44
|
+
const type = isImageFile(filePath) ? 'image' : 'file';
|
|
45
|
+
attachments = [{
|
|
46
|
+
data: result.attachment.base64,
|
|
47
|
+
filename: result.attachment.fileName,
|
|
48
|
+
mimeType: result.attachment.mimeType,
|
|
49
|
+
type,
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
// Return formatted result with all metadata
|
|
53
|
+
return {
|
|
54
|
+
attachments,
|
|
55
|
+
content: result.formattedContent,
|
|
56
|
+
lines: result.lines,
|
|
57
|
+
message: result.message,
|
|
58
|
+
preview: result.preview,
|
|
59
|
+
size: result.size,
|
|
60
|
+
success: true,
|
|
61
|
+
totalLines: result.totalLines,
|
|
62
|
+
truncated: result.truncated,
|
|
63
|
+
truncatedLineCount: result.truncatedLineCount,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
error: error instanceof Error ? error.message : String(error),
|
|
69
|
+
success: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
51
72
|
},
|
|
52
73
|
id: ToolName.READ_FILE,
|
|
53
74
|
inputSchema: ReadFileInputSchema,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Tool } from '../../../../core/domain/cipher/tools/types.js';
|
|
2
|
+
import type { IFileSystem } from '../../../../core/interfaces/cipher/i-file-system.js';
|
|
3
|
+
export interface SearchKnowledgeToolConfig {
|
|
4
|
+
baseDirectory?: string;
|
|
5
|
+
cacheTtlMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createSearchKnowledgeTool(fileSystem: IFileSystem, config?: SearchKnowledgeToolConfig): Tool;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { removeStopwords } from 'stopword';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../../../constants.js';
|
|
6
|
+
import { ToolName } from '../../../../core/domain/cipher/tools/constants.js';
|
|
7
|
+
const MAX_CONTEXT_TREE_FILES = 10_000;
|
|
8
|
+
const CACHE_TTL_MS = 5000;
|
|
9
|
+
const MINISEARCH_OPTIONS = {
|
|
10
|
+
fields: ['title', 'content'],
|
|
11
|
+
idField: 'id',
|
|
12
|
+
searchOptions: {
|
|
13
|
+
boost: { title: 2 },
|
|
14
|
+
fuzzy: 0.2,
|
|
15
|
+
prefix: true,
|
|
16
|
+
},
|
|
17
|
+
storeFields: ['title', 'path'],
|
|
18
|
+
};
|
|
19
|
+
const SearchKnowledgeInputSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
limit: z
|
|
22
|
+
.number()
|
|
23
|
+
.int()
|
|
24
|
+
.positive()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(10)
|
|
27
|
+
.describe('Maximum number of results to return (default: 10)'),
|
|
28
|
+
query: z.string().min(1).describe('Natural language query string to search for in the knowledge base'),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
function filterStopWords(query) {
|
|
32
|
+
const words = query.toLowerCase().split(/\s+/);
|
|
33
|
+
const filtered = removeStopwords(words);
|
|
34
|
+
return filtered.length > 0 ? filtered.join(' ') : query;
|
|
35
|
+
}
|
|
36
|
+
function extractTitle(content, fallbackTitle) {
|
|
37
|
+
const match = /^# (.+)$/m.exec(content);
|
|
38
|
+
return match ? match[1].trim() : fallbackTitle;
|
|
39
|
+
}
|
|
40
|
+
function extractExcerpt(content, query, maxLength = 300) {
|
|
41
|
+
const relationsMatch = /^## Relations\n([\S\s]*?)(?=\n## |\n# |$)/m.exec(content);
|
|
42
|
+
let cleanContent = content;
|
|
43
|
+
if (relationsMatch) {
|
|
44
|
+
cleanContent = content.replace(relationsMatch[0], '').trim();
|
|
45
|
+
}
|
|
46
|
+
cleanContent = cleanContent.replace(/^# .+$/m, '').trim();
|
|
47
|
+
const queryTerms = query
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.split(/\s+/)
|
|
50
|
+
.filter((t) => t.length >= 2);
|
|
51
|
+
const lines = cleanContent.split('\n');
|
|
52
|
+
let bestStartIndex = 0;
|
|
53
|
+
let bestScore = 0;
|
|
54
|
+
for (const [i, line] of lines.entries()) {
|
|
55
|
+
const lineLower = line.toLowerCase();
|
|
56
|
+
let score = 0;
|
|
57
|
+
for (const term of queryTerms) {
|
|
58
|
+
if (lineLower.includes(term)) {
|
|
59
|
+
score++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (score > bestScore) {
|
|
63
|
+
bestScore = score;
|
|
64
|
+
bestStartIndex = i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let excerpt = '';
|
|
68
|
+
for (const line of lines.slice(bestStartIndex)) {
|
|
69
|
+
if (excerpt.length >= maxLength)
|
|
70
|
+
break;
|
|
71
|
+
excerpt += line + '\n';
|
|
72
|
+
}
|
|
73
|
+
excerpt = excerpt.trim();
|
|
74
|
+
if (excerpt.length > maxLength) {
|
|
75
|
+
excerpt = excerpt.slice(0, maxLength).trim() + '...';
|
|
76
|
+
}
|
|
77
|
+
else if (bestStartIndex > 0 || excerpt.length < cleanContent.length) {
|
|
78
|
+
excerpt += '...';
|
|
79
|
+
}
|
|
80
|
+
return excerpt || cleanContent.slice(0, maxLength) + (cleanContent.length > maxLength ? '...' : '');
|
|
81
|
+
}
|
|
82
|
+
async function findMarkdownFilesWithMtime(fileSystem, contextTreePath) {
|
|
83
|
+
try {
|
|
84
|
+
const globResult = await fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
|
|
85
|
+
cwd: contextTreePath,
|
|
86
|
+
includeMetadata: true,
|
|
87
|
+
maxResults: MAX_CONTEXT_TREE_FILES,
|
|
88
|
+
respectGitignore: false,
|
|
89
|
+
});
|
|
90
|
+
return globResult.files.map((f) => {
|
|
91
|
+
let relativePath = f.path;
|
|
92
|
+
if (f.path.startsWith(contextTreePath)) {
|
|
93
|
+
relativePath = f.path.slice(contextTreePath.length + 1);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
mtime: f.modified?.getTime() ?? 0,
|
|
97
|
+
path: relativePath,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isCacheValid(cache, currentFiles) {
|
|
106
|
+
if (cache.fileMtimes.size !== currentFiles.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
for (const file of currentFiles) {
|
|
110
|
+
const cachedMtime = cache.fileMtimes.get(file.path);
|
|
111
|
+
if (cachedMtime === undefined || cachedMtime !== file.mtime) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
if (filesWithMtime.length === 0) {
|
|
120
|
+
const index = new MiniSearch(MINISEARCH_OPTIONS);
|
|
121
|
+
return {
|
|
122
|
+
contextTreePath,
|
|
123
|
+
documentMap: new Map(),
|
|
124
|
+
fileMtimes: new Map(),
|
|
125
|
+
index,
|
|
126
|
+
lastValidatedAt: now,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const documentPromises = filesWithMtime.map(async ({ mtime, path: filePath }) => {
|
|
130
|
+
try {
|
|
131
|
+
const fullPath = join(contextTreePath, filePath);
|
|
132
|
+
const { content } = await fileSystem.readFile(fullPath);
|
|
133
|
+
const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
|
|
134
|
+
return {
|
|
135
|
+
content,
|
|
136
|
+
id: filePath,
|
|
137
|
+
mtime,
|
|
138
|
+
path: filePath,
|
|
139
|
+
title,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const results = await Promise.all(documentPromises);
|
|
147
|
+
const documents = results.filter((doc) => doc !== null);
|
|
148
|
+
const documentMap = new Map();
|
|
149
|
+
const fileMtimes = new Map();
|
|
150
|
+
for (const doc of documents) {
|
|
151
|
+
documentMap.set(doc.id, doc);
|
|
152
|
+
fileMtimes.set(doc.path, doc.mtime);
|
|
153
|
+
}
|
|
154
|
+
const index = new MiniSearch(MINISEARCH_OPTIONS);
|
|
155
|
+
index.addAll(documents);
|
|
156
|
+
return {
|
|
157
|
+
contextTreePath,
|
|
158
|
+
documentMap,
|
|
159
|
+
fileMtimes,
|
|
160
|
+
index,
|
|
161
|
+
lastValidatedAt: now,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Acquires the search index, using cached data when valid or building a fresh index.
|
|
166
|
+
* Uses promise-based locking to prevent duplicate builds during parallel execution.
|
|
167
|
+
*
|
|
168
|
+
* @param state - Mutable state object for caching and locking
|
|
169
|
+
* @param fileSystem - File system service
|
|
170
|
+
* @param contextTreePath - Path to the context tree directory
|
|
171
|
+
* @param ttlMs - Cache TTL in milliseconds
|
|
172
|
+
* @returns The cached index or an error result
|
|
173
|
+
*/
|
|
174
|
+
async function acquireIndex(state, fileSystem, contextTreePath, ttlMs) {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
// Fast path: TTL-based cache hit (no I/O needed)
|
|
177
|
+
if (state.cachedIndex &&
|
|
178
|
+
state.cachedIndex.contextTreePath === contextTreePath &&
|
|
179
|
+
ttlMs > 0 &&
|
|
180
|
+
now - state.cachedIndex.lastValidatedAt < ttlMs) {
|
|
181
|
+
return state.cachedIndex;
|
|
182
|
+
}
|
|
183
|
+
// If another call is already building the index, wait for it
|
|
184
|
+
if (state.buildingPromise) {
|
|
185
|
+
return state.buildingPromise;
|
|
186
|
+
}
|
|
187
|
+
// Create and store the build promise SYNCHRONOUSLY before any await
|
|
188
|
+
// This prevents race conditions where multiple parallel calls all start building
|
|
189
|
+
const buildPromise = (async () => {
|
|
190
|
+
// Check if context tree exists (only if no cache or different path)
|
|
191
|
+
if (!state.cachedIndex || state.cachedIndex.contextTreePath !== contextTreePath) {
|
|
192
|
+
try {
|
|
193
|
+
await fileSystem.listDirectory(contextTreePath);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Return empty index to signal error - caller will handle
|
|
197
|
+
const emptyIndex = new MiniSearch(MINISEARCH_OPTIONS);
|
|
198
|
+
return {
|
|
199
|
+
contextTreePath: '',
|
|
200
|
+
documentMap: new Map(),
|
|
201
|
+
fileMtimes: new Map(),
|
|
202
|
+
index: emptyIndex,
|
|
203
|
+
lastValidatedAt: 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const currentFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
|
|
208
|
+
// Re-check cache validity after getting file list (another call may have finished)
|
|
209
|
+
if (state.cachedIndex &&
|
|
210
|
+
state.cachedIndex.contextTreePath === contextTreePath &&
|
|
211
|
+
isCacheValid(state.cachedIndex, currentFiles)) {
|
|
212
|
+
// Update timestamp atomically by creating a new object
|
|
213
|
+
const updatedCache = {
|
|
214
|
+
...state.cachedIndex,
|
|
215
|
+
lastValidatedAt: Date.now(),
|
|
216
|
+
};
|
|
217
|
+
state.cachedIndex = updatedCache;
|
|
218
|
+
return updatedCache;
|
|
219
|
+
}
|
|
220
|
+
// Build fresh index
|
|
221
|
+
const freshIndex = await buildFreshIndex(fileSystem, contextTreePath, currentFiles);
|
|
222
|
+
state.cachedIndex = freshIndex;
|
|
223
|
+
return freshIndex;
|
|
224
|
+
})();
|
|
225
|
+
// Store promise IMMEDIATELY (synchronously) so parallel calls can wait on it
|
|
226
|
+
state.buildingPromise = buildPromise;
|
|
227
|
+
try {
|
|
228
|
+
const result = await buildPromise;
|
|
229
|
+
// Check for error signal (empty contextTreePath means listDirectory failed)
|
|
230
|
+
if (result.contextTreePath === '') {
|
|
231
|
+
return {
|
|
232
|
+
error: true,
|
|
233
|
+
result: {
|
|
234
|
+
message: 'Context tree not initialized. Run /init to create it.',
|
|
235
|
+
results: [],
|
|
236
|
+
totalFound: 0,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
// Clear the lock after completion (success or failure)
|
|
244
|
+
state.buildingPromise = undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export function createSearchKnowledgeTool(fileSystem, config = {}) {
|
|
248
|
+
// Shared state for caching and locking across parallel executions
|
|
249
|
+
const state = {
|
|
250
|
+
buildingPromise: undefined,
|
|
251
|
+
cachedIndex: undefined,
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
description: 'Search the curated knowledge base in .brv/context-tree/ for relevant topics. ' +
|
|
255
|
+
'Use natural language queries to find knowledge about specific topics (e.g., "auth design", "API patterns"). ' +
|
|
256
|
+
'Returns matching file paths, titles, and relevant excerpts.',
|
|
257
|
+
async execute(input, _context) {
|
|
258
|
+
const { limit, query } = SearchKnowledgeInputSchema.parse(input);
|
|
259
|
+
const baseDir = config.baseDirectory ?? process.cwd();
|
|
260
|
+
const contextTreePath = join(baseDir, BRV_DIR, CONTEXT_TREE_DIR);
|
|
261
|
+
const ttlMs = config.cacheTtlMs ?? CACHE_TTL_MS;
|
|
262
|
+
// Acquire index with parallel-safe locking
|
|
263
|
+
const indexResult = await acquireIndex(state, fileSystem, contextTreePath, ttlMs);
|
|
264
|
+
// Handle error case (context tree not initialized)
|
|
265
|
+
if ('error' in indexResult) {
|
|
266
|
+
return indexResult.result;
|
|
267
|
+
}
|
|
268
|
+
const { documentMap, index } = indexResult;
|
|
269
|
+
if (documentMap.size === 0) {
|
|
270
|
+
return {
|
|
271
|
+
message: 'Context tree is empty. Use /curate to add knowledge.',
|
|
272
|
+
results: [],
|
|
273
|
+
totalFound: 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const filteredQuery = filterStopWords(query);
|
|
277
|
+
const searchResults = index.search(filteredQuery, { combineWith: 'OR' });
|
|
278
|
+
const results = [];
|
|
279
|
+
const resultLimit = Math.min(limit, searchResults.length);
|
|
280
|
+
for (let i = 0; i < resultLimit; i++) {
|
|
281
|
+
const result = searchResults[i];
|
|
282
|
+
const document = documentMap.get(result.id);
|
|
283
|
+
if (document) {
|
|
284
|
+
results.push({
|
|
285
|
+
excerpt: extractExcerpt(document.content, query),
|
|
286
|
+
path: document.path,
|
|
287
|
+
score: Math.round(result.score * 100) / 100,
|
|
288
|
+
title: document.title,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
message: results.length > 0
|
|
294
|
+
? `Found ${searchResults.length} result(s). Use read_file to view full content.`
|
|
295
|
+
: 'No matching knowledge found. Try different search terms or check available topics with /query.',
|
|
296
|
+
results,
|
|
297
|
+
totalFound: searchResults.length,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
id: ToolName.SEARCH_KNOWLEDGE,
|
|
301
|
+
inputSchema: SearchKnowledgeInputSchema,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -95,6 +95,7 @@ export function createTaskTool(dependencies) {
|
|
|
95
95
|
const registry = getAgentRegistry();
|
|
96
96
|
return {
|
|
97
97
|
description: buildTaskToolDescription(registry),
|
|
98
|
+
// eslint-disable-next-line complexity -- Inherent complexity: validates agent, manages sessions, handles errors
|
|
98
99
|
async execute(input, context) {
|
|
99
100
|
const params = input;
|
|
100
101
|
const { contextTreeOnly, description, prompt, sessionId, subagentType } = params;
|