mindlore 0.4.1 → 0.4.3
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 +3 -2
- package/dist/scripts/init.js +33 -21
- package/dist/scripts/init.js.map +1 -1
- package/dist/scripts/mindlore-backup.d.ts +1 -0
- package/dist/scripts/mindlore-backup.d.ts.map +1 -1
- package/dist/scripts/mindlore-backup.js +125 -24
- package/dist/scripts/mindlore-backup.js.map +1 -1
- package/dist/scripts/mindlore-episodes.js +2 -2
- package/dist/scripts/mindlore-episodes.js.map +1 -1
- package/dist/tests/backup.test.js +12 -7
- package/dist/tests/backup.test.js.map +1 -1
- package/dist/tests/hook-smoke.test.js +2 -2
- package/dist/tests/hook-smoke.test.js.map +1 -1
- package/dist/tests/research-guard.test.d.ts +2 -0
- package/dist/tests/research-guard.test.d.ts.map +1 -0
- package/dist/tests/research-guard.test.js +143 -0
- package/dist/tests/research-guard.test.js.map +1 -0
- package/hooks/lib/mindlore-common.cjs +62 -0
- package/hooks/mindlore-research-guard.cjs +144 -0
- package/hooks/mindlore-search.cjs +1 -44
- package/hooks/mindlore-session-end.cjs +129 -9
- package/package.json +1 -1
- package/plugin.json +6 -1
- package/templates/config.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-smoke.test.js","sourceRoot":"","sources":["../../tests/hook-smoke.test.ts"],"names":[],"mappings":";;;;;AAAA,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,MAAM,aAAa,GAAG;QACpB,4BAA4B;QAC5B,qBAAqB;QACrB,oBAAoB;QACpB,wBAAwB;QACxB,0BAA0B;QAC1B,0BAA0B;QAC1B,2BAA2B;KAC5B,CAAC;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElD,2EAA2E;YAC3E,MAAM,CAAC,GAAG,EAAE;gBACV,IAAI,YAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAChD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAE5C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"hook-smoke.test.js","sourceRoot":"","sources":["../../tests/hook-smoke.test.ts"],"names":[],"mappings":";;;;;AAAA,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,MAAM,aAAa,GAAG;QACpB,4BAA4B;QAC5B,qBAAqB;QACrB,oBAAoB;QACpB,wBAAwB;QACxB,0BAA0B;QAC1B,0BAA0B;QAC1B,2BAA2B;KAC5B,CAAC;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElD,2EAA2E;YAC3E,MAAM,CAAC,GAAG,EAAE;gBACV,IAAI,YAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAChD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAE5C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACnD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAElE,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"research-guard.test.d.ts","sourceRoot":"","sources":["../../tests/research-guard.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const db_1 = require("./helpers/db");
|
|
11
|
+
const HOOK_PATH = path_1.default.join(__dirname, '..', 'hooks', 'mindlore-research-guard.cjs');
|
|
12
|
+
let tmpDir;
|
|
13
|
+
let dbPath;
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
// Create isolated .mindlore/ with test DB
|
|
16
|
+
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'mindlore-rg-'));
|
|
17
|
+
dbPath = path_1.default.join(tmpDir, 'mindlore.db');
|
|
18
|
+
const db = (0, db_1.createTestDb)(dbPath);
|
|
19
|
+
// Insert a high-quality recent source
|
|
20
|
+
(0, db_1.insertFts)(db, {
|
|
21
|
+
path: 'sources/cc-hooks-reference.md',
|
|
22
|
+
slug: 'cc-hooks-reference',
|
|
23
|
+
description: 'Claude Code Hooks Reference',
|
|
24
|
+
type: 'source',
|
|
25
|
+
category: 'sources',
|
|
26
|
+
title: 'Claude Code Hooks — Comprehensive Reference',
|
|
27
|
+
content: 'Claude Code hooks SessionEnd timeout behavior exit code PreToolUse PostToolUse lifecycle',
|
|
28
|
+
tags: 'claude-code, hooks, timeout, SessionEnd',
|
|
29
|
+
quality: 'high',
|
|
30
|
+
dateCaptured: new Date().toISOString().slice(0, 10),
|
|
31
|
+
project: 'mindlore',
|
|
32
|
+
});
|
|
33
|
+
// Insert a medium-quality source
|
|
34
|
+
(0, db_1.insertFts)(db, {
|
|
35
|
+
path: 'sources/obsidian-integration.md',
|
|
36
|
+
slug: 'obsidian-integration',
|
|
37
|
+
description: 'Obsidian integration notes',
|
|
38
|
+
type: 'source',
|
|
39
|
+
category: 'sources',
|
|
40
|
+
title: 'Obsidian Vault Integration',
|
|
41
|
+
content: 'obsidian vault wikilinks export integration markdown',
|
|
42
|
+
tags: 'obsidian, vault, export',
|
|
43
|
+
quality: 'medium',
|
|
44
|
+
dateCaptured: new Date().toISOString().slice(0, 10),
|
|
45
|
+
project: 'mindlore',
|
|
46
|
+
});
|
|
47
|
+
db.close();
|
|
48
|
+
});
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
function runHook(input) {
|
|
53
|
+
const result = (0, child_process_1.spawnSync)('node', [HOOK_PATH], {
|
|
54
|
+
input: JSON.stringify(input),
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 10000,
|
|
57
|
+
env: { ...process.env, MINDLORE_HOME: tmpDir },
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
stdout: result.stdout || '',
|
|
61
|
+
stderr: result.stderr || '',
|
|
62
|
+
exitCode: result.status ?? 1,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
describe('mindlore-research-guard', () => {
|
|
66
|
+
test('should pass silently for non-Agent tools', () => {
|
|
67
|
+
const result = runHook({ tool_name: 'Read', tool_input: { file_path: '/tmp/test.md' } });
|
|
68
|
+
expect(result.exitCode).toBe(0);
|
|
69
|
+
expect(result.stdout).toBe('');
|
|
70
|
+
});
|
|
71
|
+
test('should pass silently for non-research Agent prompts', () => {
|
|
72
|
+
const result = runHook({
|
|
73
|
+
tool_name: 'Agent',
|
|
74
|
+
tool_input: { prompt: 'Fix the bug in auth module', description: 'coder fix' },
|
|
75
|
+
});
|
|
76
|
+
expect(result.exitCode).toBe(0);
|
|
77
|
+
expect(result.stdout).toBe('');
|
|
78
|
+
});
|
|
79
|
+
test('should pass silently for mindlore ingest operations', () => {
|
|
80
|
+
const result = runHook({
|
|
81
|
+
tool_name: 'Agent',
|
|
82
|
+
tool_input: { prompt: '[mindlore:ingest] Fetch URL and save to raw/', description: 'ingest' },
|
|
83
|
+
});
|
|
84
|
+
expect(result.exitCode).toBe(0);
|
|
85
|
+
expect(result.stdout).toBe('');
|
|
86
|
+
});
|
|
87
|
+
test('should pass silently for [research-override] bypass', () => {
|
|
88
|
+
const result = runHook({
|
|
89
|
+
tool_name: 'Agent',
|
|
90
|
+
tool_input: {
|
|
91
|
+
prompt: '[research-override] Research the Claude Code hooks timeout',
|
|
92
|
+
description: 'research',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
expect(result.exitCode).toBe(0);
|
|
96
|
+
expect(result.stdout).toBe('');
|
|
97
|
+
});
|
|
98
|
+
test('should block (exit 2) when high-quality recent match exists', () => {
|
|
99
|
+
const result = runHook({
|
|
100
|
+
tool_name: 'Agent',
|
|
101
|
+
tool_input: {
|
|
102
|
+
prompt: 'Research the Claude Code SessionEnd hook timeout behavior',
|
|
103
|
+
description: 'SessionEnd hook research',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(result.exitCode).toBe(2);
|
|
107
|
+
expect(result.stderr).toContain('BLOK');
|
|
108
|
+
expect(result.stderr).toContain('cc-hooks-reference');
|
|
109
|
+
});
|
|
110
|
+
test('should pass silently when keywords are too few', () => {
|
|
111
|
+
const result = runHook({
|
|
112
|
+
tool_name: 'Agent',
|
|
113
|
+
tool_input: { prompt: 'Research it', description: 'research' },
|
|
114
|
+
});
|
|
115
|
+
expect(result.exitCode).toBe(0);
|
|
116
|
+
expect(result.stdout).toBe('');
|
|
117
|
+
});
|
|
118
|
+
test('should return additionalContext for medium-quality matches', () => {
|
|
119
|
+
const result = runHook({
|
|
120
|
+
tool_name: 'Agent',
|
|
121
|
+
tool_input: {
|
|
122
|
+
prompt: 'Research obsidian vault integration wikilinks export',
|
|
123
|
+
description: 'obsidian research',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
expect(result.exitCode).toBe(0);
|
|
127
|
+
if (result.stdout) {
|
|
128
|
+
const parsed = JSON.parse(result.stdout);
|
|
129
|
+
expect(parsed.hookSpecificOutput).toBeDefined();
|
|
130
|
+
expect(parsed.hookSpecificOutput.additionalContext).toContain('mindlore-research-guard');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
test('should handle empty stdin gracefully', () => {
|
|
134
|
+
const result = (0, child_process_1.spawnSync)('node', [HOOK_PATH], {
|
|
135
|
+
input: '',
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
timeout: 5000,
|
|
138
|
+
env: { ...process.env, MINDLORE_HOME: tmpDir },
|
|
139
|
+
});
|
|
140
|
+
expect(result.status).toBe(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
//# sourceMappingURL=research-guard.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"research-guard.test.js","sourceRoot":"","sources":["../../tests/research-guard.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -532,6 +532,64 @@ function formatMultiSessionEpisodes(episodes) {
|
|
|
532
532
|
return lines.join('\n');
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// Shared FTS5 search utilities (used by mindlore-search + mindlore-research-guard)
|
|
536
|
+
const STOP_WORDS = new Set([
|
|
537
|
+
// English
|
|
538
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
539
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
540
|
+
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
541
|
+
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
542
|
+
'it', 'its', 'this', 'that', 'these', 'those', 'what', 'which', 'who',
|
|
543
|
+
'whom', 'how', 'when', 'where', 'why', 'not', 'no', 'nor', 'so',
|
|
544
|
+
'if', 'or', 'but', 'all', 'each', 'every', 'both', 'few', 'more',
|
|
545
|
+
'most', 'other', 'some', 'such', 'only', 'own', 'same', 'than',
|
|
546
|
+
'and', 'about', 'between', 'after', 'before', 'above', 'below',
|
|
547
|
+
'up', 'down', 'out', 'very', 'just', 'also', 'now', 'then',
|
|
548
|
+
'here', 'there', 'too', 'yet', 'my', 'your', 'his', 'her', 'our',
|
|
549
|
+
'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she', 'we', 'they',
|
|
550
|
+
// Turkish
|
|
551
|
+
'bir', 'bu', 'su', 'ne', 'nasil', 'neden', 'var', 'yok', 'mi', 'mu',
|
|
552
|
+
'ile', 'icin', 'de', 'da', 've', 'veya', 'ama', 'ise', 'hem',
|
|
553
|
+
'bakalim', 'gel', 'git', 'yap', 'et', 'al', 'ver',
|
|
554
|
+
'evet', 'hayir', 'tamam', 'ok', 'oldu', 'olur', 'dur',
|
|
555
|
+
'simdi', 'sonra', 'once', 'hemen', 'biraz',
|
|
556
|
+
'lan', 'ya', 'ki', 'abi', 'hadi', 'hey', 'selam',
|
|
557
|
+
'olarak', 'olan', 'gibi', 'kadar', 'daha', 'cok', 'hem',
|
|
558
|
+
'bunu', 'buna', 'icinde', 'uzerinde', 'arasinda',
|
|
559
|
+
'sonucu', 'tarafindan', 'zaten', 'gayet',
|
|
560
|
+
'acaba', 'nedir', 'midir', 'mudur',
|
|
561
|
+
// Generic technical (appears everywhere, not distinctive)
|
|
562
|
+
'hook', 'file', 'dosya', 'kullan', 'ekle', 'yaz', 'oku', 'calistir',
|
|
563
|
+
'kontrol', 'test', 'check', 'run', 'add', 'update', 'config',
|
|
564
|
+
'setup', 'install', 'start', 'stop', 'create', 'delete', 'remove', 'set',
|
|
565
|
+
'get', 'list', 'show', 'view', 'open', 'close', 'save', 'load',
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Extract topic keywords from text. Preserves Turkish chars.
|
|
570
|
+
* @param {string} text - Input text
|
|
571
|
+
* @param {number} [maxKeywords=8] - Max keywords to return
|
|
572
|
+
* @returns {string[]} Unique keywords
|
|
573
|
+
*/
|
|
574
|
+
function extractKeywords(text, maxKeywords = 8) {
|
|
575
|
+
const words = text
|
|
576
|
+
.toLowerCase()
|
|
577
|
+
.replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
|
|
578
|
+
.split(/\s+/)
|
|
579
|
+
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
|
|
580
|
+
return [...new Set(words)].slice(0, maxKeywords);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Sanitize keyword for FTS5 MATCH — strip special chars, quote-wrap.
|
|
585
|
+
* @param {string} kw - Raw keyword
|
|
586
|
+
* @returns {string|null} Quoted keyword or null if too short
|
|
587
|
+
*/
|
|
588
|
+
function sanitizeKeyword(kw) {
|
|
589
|
+
const clean = kw.replace(/["*(){}[\]^~:]/g, '').replace(/-/g, ' ').trim();
|
|
590
|
+
return clean.length >= 2 ? `"${clean}"` : null;
|
|
591
|
+
}
|
|
592
|
+
|
|
535
593
|
module.exports = {
|
|
536
594
|
MINDLORE_DIR,
|
|
537
595
|
GLOBAL_MINDLORE_DIR,
|
|
@@ -571,4 +629,8 @@ module.exports = {
|
|
|
571
629
|
formatSupersededChains,
|
|
572
630
|
queryMultiSessionEpisodes,
|
|
573
631
|
formatMultiSessionEpisodes,
|
|
632
|
+
// FTS5 search utilities (v0.4.3)
|
|
633
|
+
STOP_WORDS,
|
|
634
|
+
extractKeywords,
|
|
635
|
+
sanitizeKeyword,
|
|
574
636
|
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mindlore-research-guard — PreToolUse (Agent) hook
|
|
6
|
+
*
|
|
7
|
+
* Before spawning a researcher agent, checks FTS5 for existing knowledge.
|
|
8
|
+
* - High quality + recent (30 days) match → exit 2 (block)
|
|
9
|
+
* - Old or low quality match → exit 0 with warning (additionalContext)
|
|
10
|
+
* - No match → silent pass
|
|
11
|
+
*
|
|
12
|
+
* Prevents redundant web research when knowledge already exists in DB.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { getAllDbs, requireDatabase, extractKeywords, sanitizeKeyword } = require('./lib/mindlore-common.cjs');
|
|
18
|
+
|
|
19
|
+
// Keywords that signal a research/web-search intent in agent prompts
|
|
20
|
+
// Note: entries with dots/stars are regex patterns, rest are literals
|
|
21
|
+
const RESEARCH_SIGNALS = [
|
|
22
|
+
'research', 'araştır', 'arastir', 'investigate', 'search for',
|
|
23
|
+
'web search', 'websearch', 'webfetch', 'fetch.*url', 'look up',
|
|
24
|
+
'find out', 'check.*docs', 'documentation.*for',
|
|
25
|
+
];
|
|
26
|
+
const RESEARCH_REGEX = new RegExp(RESEARCH_SIGNALS.join('|'), 'i');
|
|
27
|
+
|
|
28
|
+
// Exclude ingest/internal operations (they intentionally fetch URLs)
|
|
29
|
+
const EXCLUDE_REGEX = /\[mindlore:|\bmindlore-ingest\b|ingest.*url|save.*raw|\[research-override\]/i;
|
|
30
|
+
|
|
31
|
+
const MAX_AGE_DAYS = 30;
|
|
32
|
+
|
|
33
|
+
function isRecent(dateStr) {
|
|
34
|
+
if (!dateStr) return false;
|
|
35
|
+
const d = new Date(dateStr);
|
|
36
|
+
if (isNaN(d.getTime())) return false;
|
|
37
|
+
const diff = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24);
|
|
38
|
+
return diff <= MAX_AGE_DAYS;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Search FTS5 using a single OR query instead of per-path×keyword loop.
|
|
43
|
+
* Returns top matches with quality and date from FTS5 columns (no file I/O).
|
|
44
|
+
*/
|
|
45
|
+
function searchDbs(keywords) {
|
|
46
|
+
const Database = requireDatabase();
|
|
47
|
+
if (!Database) return [];
|
|
48
|
+
|
|
49
|
+
const sanitized = keywords.map(sanitizeKeyword).filter(Boolean);
|
|
50
|
+
if (sanitized.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
const matchQuery = sanitized.join(' OR ');
|
|
53
|
+
const dbPaths = getAllDbs();
|
|
54
|
+
const results = [];
|
|
55
|
+
|
|
56
|
+
for (const dbPath of dbPaths) {
|
|
57
|
+
try {
|
|
58
|
+
const db = new Database(dbPath, { readonly: true });
|
|
59
|
+
|
|
60
|
+
// Single FTS5 query — O(1) instead of O(paths × keywords)
|
|
61
|
+
const rows = db.prepare(
|
|
62
|
+
`SELECT path, slug, title, description, quality, date_captured, rank
|
|
63
|
+
FROM mindlore_fts
|
|
64
|
+
WHERE mindlore_fts MATCH ?
|
|
65
|
+
ORDER BY rank
|
|
66
|
+
LIMIT 10`
|
|
67
|
+
).all(matchQuery);
|
|
68
|
+
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
const quality = (row.quality || 'medium').toLowerCase();
|
|
71
|
+
const date_captured = row.date_captured || null;
|
|
72
|
+
|
|
73
|
+
results.push({
|
|
74
|
+
slug: row.slug || path.basename(row.path, '.md'),
|
|
75
|
+
title: row.title || row.description || row.slug || '',
|
|
76
|
+
quality,
|
|
77
|
+
date_captured,
|
|
78
|
+
recent: isRecent(date_captured),
|
|
79
|
+
rank: row.rank,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
db.close();
|
|
84
|
+
} catch (_err) { /* db open or query failed */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Sort by rank (lower = better match in FTS5)
|
|
88
|
+
results.sort((a, b) => a.rank - b.rank);
|
|
89
|
+
return results.slice(0, 5);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function main() {
|
|
93
|
+
let input;
|
|
94
|
+
try {
|
|
95
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
96
|
+
if (!raw) return;
|
|
97
|
+
input = JSON.parse(raw);
|
|
98
|
+
} catch (_err) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const toolName = input.tool_name || '';
|
|
103
|
+
if (toolName !== 'Agent') return;
|
|
104
|
+
|
|
105
|
+
const toolInput = input.tool_input || {};
|
|
106
|
+
const prompt = (toolInput.prompt || '') + ' ' + (toolInput.description || '');
|
|
107
|
+
|
|
108
|
+
// Skip mindlore internal operations and explicit overrides
|
|
109
|
+
if (EXCLUDE_REGEX.test(prompt)) return;
|
|
110
|
+
|
|
111
|
+
// Only trigger on research-intent agents
|
|
112
|
+
if (!RESEARCH_REGEX.test(prompt)) return;
|
|
113
|
+
|
|
114
|
+
const keywords = extractKeywords(prompt, 10);
|
|
115
|
+
if (keywords.length < 2) return;
|
|
116
|
+
|
|
117
|
+
const matches = searchDbs(keywords);
|
|
118
|
+
if (matches.length === 0) return;
|
|
119
|
+
|
|
120
|
+
// Check for high-quality recent matches
|
|
121
|
+
const strongMatches = matches.filter((m) => m.quality === 'high' && m.recent);
|
|
122
|
+
|
|
123
|
+
if (strongMatches.length > 0) {
|
|
124
|
+
// BLOCK: high quality + recent knowledge exists
|
|
125
|
+
const slugList = strongMatches.map((m) => ` - ${m.slug} (${m.title})`).join('\n');
|
|
126
|
+
const msg = `[mindlore-research-guard] BLOK: Bu konuda guncel, yuksek kaliteli bilgi DB'de zaten var.\n` +
|
|
127
|
+
`Once mevcut bilgiyi oku:\n${slugList}\n` +
|
|
128
|
+
`Eger bilgi yetersizse, prompt'a "[research-override]" ekleyerek tekrar dene.`;
|
|
129
|
+
process.stderr.write(msg);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// WARN: old or low-quality matches exist
|
|
134
|
+
const slugList = matches.map((m) => `${m.slug} (${m.quality}, ${m.date_captured || 'tarih yok'})`).join(', ');
|
|
135
|
+
const output = {
|
|
136
|
+
hookSpecificOutput: {
|
|
137
|
+
hookEventName: 'PreToolUse',
|
|
138
|
+
additionalContext: `[mindlore-research-guard] DB'de ilgili bilgi var ama eski/dusuk kalite: ${slugList}. Guncelleme gerekebilir — arastirma sonrasi DB'yi guncelle.`,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
process.stdout.write(JSON.stringify(output));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main();
|
|
@@ -10,55 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { getAllDbs, requireDatabase, extractHeadings, readHookStdin } = require('./lib/mindlore-common.cjs');
|
|
13
|
+
const { getAllDbs, requireDatabase, extractHeadings, readHookStdin, extractKeywords } = require('./lib/mindlore-common.cjs');
|
|
14
14
|
|
|
15
15
|
const MAX_RESULTS = 3;
|
|
16
16
|
const MIN_QUERY_WORDS = 3;
|
|
17
17
|
const MIN_KEYWORD_HITS = 2;
|
|
18
18
|
|
|
19
|
-
// Extended stop words (~70 TR + EN) matching old knowledge system
|
|
20
|
-
const STOP_WORDS = new Set([
|
|
21
|
-
// English
|
|
22
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
23
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
24
|
-
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
25
|
-
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
26
|
-
'it', 'its', 'this', 'that', 'these', 'those', 'what', 'which', 'who',
|
|
27
|
-
'whom', 'how', 'when', 'where', 'why', 'not', 'no', 'nor', 'so',
|
|
28
|
-
'if', 'or', 'but', 'all', 'each', 'every', 'both', 'few', 'more',
|
|
29
|
-
'most', 'other', 'some', 'such', 'only', 'own', 'same', 'than',
|
|
30
|
-
'and', 'about', 'between', 'after', 'before', 'above', 'below',
|
|
31
|
-
'up', 'down', 'out', 'very', 'just', 'also', 'now', 'then',
|
|
32
|
-
'here', 'there', 'too', 'yet', 'my', 'your', 'his', 'her', 'our',
|
|
33
|
-
'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she', 'we', 'they',
|
|
34
|
-
// Turkish
|
|
35
|
-
'bir', 'bu', 'su', 'ne', 'nasil', 'neden', 'var', 'yok', 'mi', 'mu',
|
|
36
|
-
'ile', 'icin', 'de', 'da', 've', 'veya', 'ama', 'ise', 'hem',
|
|
37
|
-
'bakalim', 'gel', 'git', 'yap', 'et', 'al', 'ver',
|
|
38
|
-
'evet', 'hayir', 'tamam', 'ok', 'oldu', 'olur', 'dur',
|
|
39
|
-
'simdi', 'sonra', 'once', 'hemen', 'biraz',
|
|
40
|
-
'lan', 'ya', 'ki', 'abi', 'hadi', 'hey', 'selam',
|
|
41
|
-
'olarak', 'olan', 'gibi', 'kadar', 'daha', 'cok', 'hem',
|
|
42
|
-
'bunu', 'buna', 'icinde', 'uzerinde', 'arasinda',
|
|
43
|
-
'sonucu', 'tarafindan', 'zaten', 'gayet',
|
|
44
|
-
'acaba', 'nedir', 'midir', 'mudur',
|
|
45
|
-
// Generic technical (appears everywhere, not distinctive)
|
|
46
|
-
'hook', 'file', 'dosya', 'kullan', 'ekle', 'yaz', 'oku', 'calistir',
|
|
47
|
-
'kontrol', 'test', 'check', 'run', 'add', 'update', 'config',
|
|
48
|
-
'setup', 'install', 'start', 'stop', 'create', 'delete', 'remove', 'set',
|
|
49
|
-
'get', 'list', 'show', 'view', 'open', 'close', 'save', 'load',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
function extractKeywords(text) {
|
|
53
|
-
const words = text
|
|
54
|
-
.toLowerCase()
|
|
55
|
-
.replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
|
|
56
|
-
.split(/\s+/)
|
|
57
|
-
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
|
|
58
|
-
|
|
59
|
-
return [...new Set(words)].slice(0, 8);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
19
|
/**
|
|
63
20
|
* Search a single DB and return scored results with their baseDir.
|
|
64
21
|
*/
|
|
@@ -11,9 +11,26 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execSync, spawn } = require('child_process');
|
|
15
16
|
const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow } = require('./lib/mindlore-common.cjs');
|
|
16
17
|
|
|
18
|
+
// --worker mode: heavy ops run in detached child process (survives parent exit)
|
|
19
|
+
if (process.argv.includes('--worker')) {
|
|
20
|
+
const dataPath = process.argv[process.argv.indexOf('--worker') + 1];
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(dataPath, 'utf8');
|
|
23
|
+
fs.unlinkSync(dataPath); // cleanup temp file before any processing
|
|
24
|
+
const { baseDir, project, commits, changedFiles, reads } = JSON.parse(raw);
|
|
25
|
+
writeBareEpisode(baseDir, project, commits, changedFiles, reads);
|
|
26
|
+
syncObsidian(baseDir);
|
|
27
|
+
syncGlobalRepo();
|
|
28
|
+
} catch (_err) {
|
|
29
|
+
// Graceful fail — worker errors are silent
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
function formatDate(date) {
|
|
18
35
|
const y = date.getFullYear();
|
|
19
36
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
@@ -140,11 +157,25 @@ function main() {
|
|
|
140
157
|
fs.appendFileSync(logPath, logEntry, 'utf8');
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
// Heavy ops: detach into child process so CC can exit immediately.
|
|
161
|
+
// Fixes "Hook cancelled" when CC kills the hook before completion.
|
|
162
|
+
// See: https://github.com/anthropics/claude-code/issues/41577
|
|
163
|
+
try {
|
|
164
|
+
const workerData = JSON.stringify({ baseDir, project, commits, changedFiles, reads });
|
|
165
|
+
const tmpFile = path.join(os.tmpdir(), `mindlore-worker-${Date.now()}.json`);
|
|
166
|
+
fs.writeFileSync(tmpFile, workerData, 'utf8');
|
|
167
|
+
const child = spawn(process.execPath, [__filename, '--worker', tmpFile], {
|
|
168
|
+
detached: true,
|
|
169
|
+
stdio: 'ignore',
|
|
170
|
+
cwd: process.cwd(),
|
|
171
|
+
});
|
|
172
|
+
child.unref();
|
|
173
|
+
} catch (_err) {
|
|
174
|
+
// Fallback: run inline if spawn fails
|
|
175
|
+
writeBareEpisode(baseDir, project, commits, changedFiles, reads);
|
|
176
|
+
syncObsidian(baseDir);
|
|
177
|
+
syncGlobalRepo();
|
|
178
|
+
}
|
|
148
179
|
}
|
|
149
180
|
|
|
150
181
|
/**
|
|
@@ -201,7 +232,7 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
|
|
|
201
232
|
description: truncatedSummary,
|
|
202
233
|
type: 'episode',
|
|
203
234
|
category: 'episodes',
|
|
204
|
-
title: truncatedSummary
|
|
235
|
+
title: truncatedSummary,
|
|
205
236
|
content: [truncatedSummary, body ?? ''].join('\n').trim(),
|
|
206
237
|
tags: 'session',
|
|
207
238
|
quality: null,
|
|
@@ -215,8 +246,97 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
|
|
|
215
246
|
writeBoth();
|
|
216
247
|
|
|
217
248
|
db.close();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
process.stderr.write(`[mindlore] episode write failed: ${err?.message ?? err}\n`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const EXPORT_DIRS = ['analyses', 'decisions', 'diary', 'raw', 'sources', 'domains', 'connections', 'insights', 'learnings'];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Load obsidian-helpers from compiled dist (single source of truth for wikilink conversion).
|
|
258
|
+
* Returns null if helpers not available (e.g. dev environment without build).
|
|
259
|
+
*/
|
|
260
|
+
function loadObsidianHelpers() {
|
|
261
|
+
try {
|
|
262
|
+
// Resolve from package root (hooks/ is sibling to dist/)
|
|
263
|
+
const hookDir = __dirname;
|
|
264
|
+
const pkgRoot = path.dirname(hookDir);
|
|
265
|
+
const helpersPath = path.join(pkgRoot, 'dist', 'scripts', 'lib', 'obsidian-helpers.js');
|
|
266
|
+
if (!fs.existsSync(helpersPath)) return null;
|
|
267
|
+
return require(helpersPath);
|
|
218
268
|
} catch (_err) {
|
|
219
|
-
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Export a single .md file to Obsidian vault with wikilink conversion.
|
|
275
|
+
* Uses obsidian-helpers.convertToWikilinks for consistent behavior.
|
|
276
|
+
* Returns true if file was exported.
|
|
277
|
+
*/
|
|
278
|
+
function exportMdFile(srcPath, destPath, convertFn) {
|
|
279
|
+
try {
|
|
280
|
+
const destStat = fs.statSync(destPath);
|
|
281
|
+
const srcStat = fs.statSync(srcPath);
|
|
282
|
+
if (srcStat.mtimeMs <= destStat.mtimeMs) return false;
|
|
283
|
+
} catch (_err) {
|
|
284
|
+
// dest doesn't exist — proceed with export
|
|
285
|
+
}
|
|
286
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
287
|
+
content = convertFn(content);
|
|
288
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Auto-export .md files to Obsidian vault if configured.
|
|
294
|
+
* Skips if no vault configured, vault missing, or nothing changed since last export.
|
|
295
|
+
*/
|
|
296
|
+
function syncObsidian(baseDir) {
|
|
297
|
+
try {
|
|
298
|
+
const configPath = path.join(baseDir, 'config.json');
|
|
299
|
+
if (!fs.existsSync(configPath)) return;
|
|
300
|
+
|
|
301
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
302
|
+
const vaultPath = config?.obsidian?.vault;
|
|
303
|
+
if (!vaultPath || typeof vaultPath !== 'string') return;
|
|
304
|
+
if (!fs.existsSync(vaultPath)) return;
|
|
305
|
+
|
|
306
|
+
const helpers = loadObsidianHelpers();
|
|
307
|
+
// Fallback regex if helpers unavailable (strips path prefixes like the canonical version)
|
|
308
|
+
const convertFn = helpers?.convertToWikilinks
|
|
309
|
+
?? ((c) => c.replace(/\[([^\]]+)\]\((?:\.\.?\/)?(?:[\w-]+\/)*([^/)]+)\.md\)/g, '[[$2]]'));
|
|
310
|
+
|
|
311
|
+
const destBase = path.join(vaultPath, 'mindlore');
|
|
312
|
+
let exported = 0;
|
|
313
|
+
|
|
314
|
+
for (const dir of EXPORT_DIRS) {
|
|
315
|
+
const srcDir = path.join(baseDir, dir);
|
|
316
|
+
if (!fs.existsSync(srcDir)) continue;
|
|
317
|
+
|
|
318
|
+
const destDir = path.join(destBase, dir);
|
|
319
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
320
|
+
|
|
321
|
+
for (const file of fs.readdirSync(srcDir).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
|
|
322
|
+
if (exportMdFile(path.join(srcDir, file), path.join(destDir, file), convertFn)) exported++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const rootFile of ['INDEX.md', 'log.md']) {
|
|
327
|
+
const srcPath = path.join(baseDir, rootFile);
|
|
328
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
329
|
+
if (!fs.existsSync(destBase)) fs.mkdirSync(destBase, { recursive: true });
|
|
330
|
+
if (exportMdFile(srcPath, path.join(destBase, rootFile), convertFn)) exported++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (exported > 0) {
|
|
334
|
+
config.obsidian.lastExport = new Date().toISOString();
|
|
335
|
+
config.obsidian.lastExportCount = exported;
|
|
336
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
process.stderr.write(`[mindlore] obsidian sync failed: ${err?.message ?? err}\n`);
|
|
220
340
|
}
|
|
221
341
|
}
|
|
222
342
|
|
|
@@ -240,7 +360,7 @@ function syncGlobalRepo() {
|
|
|
240
360
|
|
|
241
361
|
if (!status) return; // nothing to commit
|
|
242
362
|
|
|
243
|
-
execSync('git add
|
|
363
|
+
execSync('git add *.md mindlore.db config.json diary/ sources/ domains/ analyses/ decisions/ raw/ connections/ insights/ learnings/', { cwd: gDir, timeout: 5000, stdio: 'pipe' });
|
|
244
364
|
const now = new Date().toISOString().slice(0, 19);
|
|
245
365
|
execSync(`git commit -m "mindlore auto-sync ${now}"`, {
|
|
246
366
|
cwd: gDir,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mindlore",
|
|
3
3
|
"description": "AI-native knowledge system for Claude Code. Persistent, searchable, evolving knowledge base with FTS5.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"name": "mindlore-ingest",
|
|
@@ -94,6 +94,11 @@
|
|
|
94
94
|
"event": "PreToolUse",
|
|
95
95
|
"script": "hooks/mindlore-model-router.cjs",
|
|
96
96
|
"if": "Agent"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"event": "PreToolUse",
|
|
100
|
+
"script": "hooks/mindlore-research-guard.cjs",
|
|
101
|
+
"if": "Agent"
|
|
97
102
|
}
|
|
98
103
|
]
|
|
99
104
|
}
|
package/templates/config.json
CHANGED