solvdex 1.0.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/hooks/error-lookup.d.ts +4 -0
- package/dist/hooks/error-lookup.d.ts.map +1 -0
- package/dist/hooks/error-lookup.js +92 -0
- package/dist/hooks/error-lookup.js.map +1 -0
- package/dist/hooks/post-task.d.ts +15 -0
- package/dist/hooks/post-task.d.ts.map +1 -0
- package/dist/hooks/post-task.js +246 -0
- package/dist/hooks/post-task.js.map +1 -0
- package/dist/hooks/prompt-enrich.d.ts +16 -0
- package/dist/hooks/prompt-enrich.d.ts.map +1 -0
- package/dist/hooks/prompt-enrich.js +141 -0
- package/dist/hooks/prompt-enrich.js.map +1 -0
- package/dist/hooks/session-start-cli.d.ts +3 -0
- package/dist/hooks/session-start-cli.d.ts.map +1 -0
- package/dist/hooks/session-start-cli.js +81 -0
- package/dist/hooks/session-start-cli.js.map +1 -0
- package/dist/hooks/session-start.d.ts +4 -0
- package/dist/hooks/session-start.d.ts.map +1 -0
- package/dist/hooks/session-start.js +134 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/src/audit.d.ts +63 -0
- package/dist/src/audit.d.ts.map +1 -0
- package/dist/src/audit.js +229 -0
- package/dist/src/audit.js.map +1 -0
- package/dist/src/cache.d.ts +54 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +167 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/config.d.ts +52 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +175 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/entry.d.ts +154 -0
- package/dist/src/entry.d.ts.map +1 -0
- package/dist/src/entry.js +469 -0
- package/dist/src/entry.js.map +1 -0
- package/dist/src/errors.d.ts +65 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +121 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/frontmatter.d.ts +28 -0
- package/dist/src/frontmatter.d.ts.map +1 -0
- package/dist/src/frontmatter.js +111 -0
- package/dist/src/frontmatter.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +188 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/maturity.d.ts +31 -0
- package/dist/src/maturity.d.ts.map +1 -0
- package/dist/src/maturity.js +96 -0
- package/dist/src/maturity.js.map +1 -0
- package/dist/src/quality.d.ts +23 -0
- package/dist/src/quality.d.ts.map +1 -0
- package/dist/src/quality.js +236 -0
- package/dist/src/quality.js.map +1 -0
- package/dist/src/search.d.ts +35 -0
- package/dist/src/search.d.ts.map +1 -0
- package/dist/src/search.js +263 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/similarity.d.ts +42 -0
- package/dist/src/similarity.d.ts.map +1 -0
- package/dist/src/similarity.js +111 -0
- package/dist/src/similarity.js.map +1 -0
- package/dist/src/stats.d.ts +56 -0
- package/dist/src/stats.d.ts.map +1 -0
- package/dist/src/stats.js +198 -0
- package/dist/src/stats.js.map +1 -0
- package/dist/src/templates.d.ts +63 -0
- package/dist/src/templates.d.ts.map +1 -0
- package/dist/src/templates.js +347 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/transfer.d.ts +92 -0
- package/dist/src/transfer.d.ts.map +1 -0
- package/dist/src/transfer.js +215 -0
- package/dist/src/transfer.js.map +1 -0
- package/dist/src/types.d.ts +270 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +153 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validate.d.ts +90 -0
- package/dist/src/validate.d.ts.map +1 -0
- package/dist/src/validate.js +295 -0
- package/dist/src/validate.js.map +1 -0
- package/hooks/error-lookup.ts +110 -0
- package/hooks/hooks.json +49 -0
- package/hooks/post-task.ts +309 -0
- package/hooks/prompt-enrich.ts +162 -0
- package/hooks/session-start-cli.ts +96 -0
- package/hooks/session-start.ts +159 -0
- package/package.json +40 -0
- package/scripts/error-lookup.py +64 -0
- package/scripts/post-task.py +60 -0
- package/scripts/prompt-enrich.py +64 -0
- package/scripts/session-start.py +64 -0
- package/skills/wiki/SKILL.md +61 -0
- package/skills/wiki-add/SKILL.md +90 -0
- package/skills/wiki-browse/SKILL.md +108 -0
- package/skills/wiki-capture/SKILL.md +265 -0
- package/skills/wiki-explorer/SKILL.md +223 -0
- package/skills/wiki-export/SKILL.md +101 -0
- package/skills/wiki-fix/SKILL.md +86 -0
- package/skills/wiki-flag/SKILL.md +47 -0
- package/skills/wiki-import/SKILL.md +128 -0
- package/skills/wiki-init/SKILL.md +72 -0
- package/skills/wiki-scan/SKILL.md +98 -0
- package/skills/wiki-search/SKILL.md +86 -0
- package/skills/wiki-stats/SKILL.md +129 -0
- package/skills/wiki-status/SKILL.md +78 -0
- package/skills/wiki-test-trigger/SKILL.md +173 -0
- package/skills/wiki-validate/SKILL.md +62 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// hooks/post-task.ts
|
|
2
|
+
import {
|
|
3
|
+
wikiExists,
|
|
4
|
+
createEntry,
|
|
5
|
+
generateStubContent,
|
|
6
|
+
WikiEntry,
|
|
7
|
+
CONFIDENCE,
|
|
8
|
+
PostTaskOutput
|
|
9
|
+
} from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
interface TaskContext {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
taskDescription: string;
|
|
14
|
+
outcome: 'success' | 'failure';
|
|
15
|
+
errorEncountered?: string;
|
|
16
|
+
errorResolved?: boolean;
|
|
17
|
+
filesModified?: string[];
|
|
18
|
+
userConfirmation?: string;
|
|
19
|
+
codeChanges?: string; // For detecting workarounds
|
|
20
|
+
actionHistory?: string[]; // For repeated_pattern detection
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default enabled signals (can be overridden by future config)
|
|
24
|
+
const DEFAULT_SIGNALS = [
|
|
25
|
+
'error_resolved',
|
|
26
|
+
'user_confirmation',
|
|
27
|
+
'explicit_save',
|
|
28
|
+
'repeated_pattern',
|
|
29
|
+
'workaround_applied'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export async function onPostTask(context: TaskContext): Promise<PostTaskOutput | null> {
|
|
33
|
+
try {
|
|
34
|
+
const { projectRoot } = context;
|
|
35
|
+
|
|
36
|
+
if (!wikiExists(projectRoot)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check signals
|
|
41
|
+
const signal = detectSignal(context, DEFAULT_SIGNALS);
|
|
42
|
+
|
|
43
|
+
if (!signal) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract entry details
|
|
48
|
+
const entryDetails = extractEntryDetails(context, signal);
|
|
49
|
+
|
|
50
|
+
// Check for duplicates before creating (but don't block on it)
|
|
51
|
+
let isDuplicate = false;
|
|
52
|
+
try {
|
|
53
|
+
const { checkDuplicate } = await import('../src/index.js');
|
|
54
|
+
const dupCheck = await checkDuplicate(
|
|
55
|
+
projectRoot,
|
|
56
|
+
entryDetails.title,
|
|
57
|
+
entryDetails.tags,
|
|
58
|
+
entryDetails.category
|
|
59
|
+
);
|
|
60
|
+
isDuplicate = dupCheck.isDuplicate;
|
|
61
|
+
|
|
62
|
+
if (isDuplicate) {
|
|
63
|
+
console.log(`📚 Solvdex: Skipping duplicate "${entryDetails.title}"`);
|
|
64
|
+
console.log(` Similar to: "${dupCheck.matchedEntry?.frontmatter.title}"`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Duplicate check failure is non-critical, proceed with creation
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create entry
|
|
72
|
+
const entry = createEntry(projectRoot, {
|
|
73
|
+
category: entryDetails.category,
|
|
74
|
+
title: entryDetails.title,
|
|
75
|
+
tags: entryDetails.tags,
|
|
76
|
+
content: entryDetails.content,
|
|
77
|
+
trigger: entryDetails.trigger,
|
|
78
|
+
autoCapture: true,
|
|
79
|
+
stub: true,
|
|
80
|
+
source: `auto-capture:${signal}`
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Output for Claude Code Stop hook integration (user-facing console logs)
|
|
84
|
+
console.log(`📚 Solvdex: Captured "${entry.frontmatter.title}"`);
|
|
85
|
+
console.log(` Category: ${entry.category}`);
|
|
86
|
+
console.log(` Signal: ${signal}`);
|
|
87
|
+
|
|
88
|
+
// Structured output for Claude Code (includes both old and new format for backward compatibility)
|
|
89
|
+
const output: PostTaskOutput = {
|
|
90
|
+
// Old format (for backward compatibility)
|
|
91
|
+
type: 'wiki_captured',
|
|
92
|
+
message: `Wiki: Saved "${entry.frontmatter.title}" to ${entry.category}/`,
|
|
93
|
+
entry,
|
|
94
|
+
signal,
|
|
95
|
+
// New format (Claude Code integration)
|
|
96
|
+
continue: true,
|
|
97
|
+
hookSpecificOutput: {
|
|
98
|
+
type: 'entry_captured',
|
|
99
|
+
message: `Captured "${entry.frontmatter.title}" to ${entry.category}/`,
|
|
100
|
+
entryPath: entry.path
|
|
101
|
+
},
|
|
102
|
+
additionalContext: formatEntryForContext(entry, signal)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Output JSON marker + JSON (MUST be last output)
|
|
106
|
+
console.log('\n__HOOK_OUTPUT__');
|
|
107
|
+
console.log(JSON.stringify(output));
|
|
108
|
+
|
|
109
|
+
return output;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Graceful degradation - log error but don't crash
|
|
112
|
+
console.error(`[Solvdex] Error in post-task hook: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Detects if similar actions have been performed 3+ times.
|
|
119
|
+
* Returns the repeated action if found.
|
|
120
|
+
*/
|
|
121
|
+
function detectRepeatedPattern(actionHistory: string[]): string | null {
|
|
122
|
+
if (!actionHistory || actionHistory.length < 3) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Count occurrences of each action
|
|
127
|
+
const counts: Record<string, number> = {};
|
|
128
|
+
for (const action of actionHistory) {
|
|
129
|
+
const normalized = action.toLowerCase().trim();
|
|
130
|
+
counts[normalized] = (counts[normalized] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Find action repeated 3+ times
|
|
134
|
+
for (const [action, count] of Object.entries(counts)) {
|
|
135
|
+
if (count >= 3) {
|
|
136
|
+
return action;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function detectSignal(context: TaskContext, enabledSignals: string[]): string | null {
|
|
144
|
+
// Signal 1: Error resolved
|
|
145
|
+
if (
|
|
146
|
+
enabledSignals.includes('error_resolved') &&
|
|
147
|
+
context.errorEncountered &&
|
|
148
|
+
context.errorResolved &&
|
|
149
|
+
context.outcome === 'success'
|
|
150
|
+
) {
|
|
151
|
+
return 'error_resolved';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Signal 2: User confirmation
|
|
155
|
+
if (
|
|
156
|
+
enabledSignals.includes('user_confirmation') &&
|
|
157
|
+
context.userConfirmation &&
|
|
158
|
+
isPositiveConfirmation(context.userConfirmation)
|
|
159
|
+
) {
|
|
160
|
+
return 'user_confirmation';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Signal 3: Explicit save request
|
|
164
|
+
if (
|
|
165
|
+
enabledSignals.includes('explicit_save') &&
|
|
166
|
+
context.userConfirmation &&
|
|
167
|
+
isExplicitSaveRequest(context.userConfirmation)
|
|
168
|
+
) {
|
|
169
|
+
return 'explicit_save';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Signal 4: Repeated pattern
|
|
173
|
+
if (enabledSignals.includes('repeated_pattern') && context.actionHistory) {
|
|
174
|
+
const repeatedAction = detectRepeatedPattern(context.actionHistory);
|
|
175
|
+
if (repeatedAction) {
|
|
176
|
+
return 'repeated_pattern';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Signal 5: Workaround applied (detect HACK/WORKAROUND comments)
|
|
181
|
+
if (
|
|
182
|
+
enabledSignals.includes('workaround_applied') &&
|
|
183
|
+
context.codeChanges &&
|
|
184
|
+
hasWorkaroundMarkers(context.codeChanges)
|
|
185
|
+
) {
|
|
186
|
+
return 'workaround_applied';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isPositiveConfirmation(text: string): boolean {
|
|
193
|
+
const positivePatterns = [
|
|
194
|
+
'that fixed it',
|
|
195
|
+
'works now',
|
|
196
|
+
'perfect',
|
|
197
|
+
'that worked',
|
|
198
|
+
'thank you',
|
|
199
|
+
'great',
|
|
200
|
+
'awesome',
|
|
201
|
+
'solved'
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const textLower = text.toLowerCase();
|
|
205
|
+
return positivePatterns.some(p => textLower.includes(p));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isExplicitSaveRequest(text: string): boolean {
|
|
209
|
+
const savePatterns = [
|
|
210
|
+
'save this to wiki',
|
|
211
|
+
'remember this',
|
|
212
|
+
'add to wiki',
|
|
213
|
+
'wiki this',
|
|
214
|
+
'document this'
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const textLower = text.toLowerCase();
|
|
218
|
+
return savePatterns.some(p => textLower.includes(p));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasWorkaroundMarkers(codeChanges: string): boolean {
|
|
222
|
+
const workaroundPatterns = [
|
|
223
|
+
/\/\/\s*(HACK|WORKAROUND|TODO|FIXME):/i,
|
|
224
|
+
/\/\*\s*(HACK|WORKAROUND|TODO|FIXME):/i,
|
|
225
|
+
/#\s*(HACK|WORKAROUND|TODO|FIXME):/i
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
return workaroundPatterns.some(pattern => pattern.test(codeChanges));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function extractEntryDetails(context: TaskContext, signal: string) {
|
|
232
|
+
// Determine category based on signal and task keywords (7 categories)
|
|
233
|
+
let category = 'issues'; // default
|
|
234
|
+
|
|
235
|
+
if (signal === 'error_resolved') {
|
|
236
|
+
category = 'issues';
|
|
237
|
+
} else if (signal === 'workaround_applied') {
|
|
238
|
+
category = 'gotchas';
|
|
239
|
+
} else if (signal === 'repeated_pattern') {
|
|
240
|
+
category = 'patterns';
|
|
241
|
+
} else if (context.taskDescription) {
|
|
242
|
+
const task = context.taskDescription.toLowerCase();
|
|
243
|
+
|
|
244
|
+
// Testing signals
|
|
245
|
+
if (task.includes('test') || task.includes('mock') || task.includes('fixture')) {
|
|
246
|
+
category = 'testing';
|
|
247
|
+
}
|
|
248
|
+
// Documentation signals
|
|
249
|
+
else if (task.includes('document') || task.includes('readme') || task.includes('guide')) {
|
|
250
|
+
category = 'docs';
|
|
251
|
+
}
|
|
252
|
+
// Security signals
|
|
253
|
+
else if (task.includes('auth') || task.includes('security') || task.includes('permission')) {
|
|
254
|
+
category = 'security';
|
|
255
|
+
}
|
|
256
|
+
// Performance signals
|
|
257
|
+
else if (task.includes('slow') || task.includes('optimize') || task.includes('performance')) {
|
|
258
|
+
category = 'performance';
|
|
259
|
+
}
|
|
260
|
+
// Pattern signals
|
|
261
|
+
else if (task.includes('pattern') || task.includes('reusable')) {
|
|
262
|
+
category = 'patterns';
|
|
263
|
+
}
|
|
264
|
+
// Gotcha signals
|
|
265
|
+
else if (task.includes('careful') || task.includes('avoid') || task.includes('pitfall')) {
|
|
266
|
+
category = 'gotchas';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Generate title from task or error
|
|
271
|
+
const title = context.errorEncountered
|
|
272
|
+
? `Fix: ${context.errorEncountered.substring(0, 50)}`
|
|
273
|
+
: context.taskDescription?.substring(0, 50) || 'Auto-captured entry';
|
|
274
|
+
|
|
275
|
+
// Extract tags from context
|
|
276
|
+
const tags: string[] = [];
|
|
277
|
+
if (context.filesModified) {
|
|
278
|
+
// Add file extension tags
|
|
279
|
+
const extensions = new Set(
|
|
280
|
+
context.filesModified
|
|
281
|
+
.map(f => f.split('.').pop())
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
);
|
|
284
|
+
tags.push(...Array.from(extensions) as string[]);
|
|
285
|
+
}
|
|
286
|
+
if (signal) {
|
|
287
|
+
tags.push(`auto:${signal}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Generate stub content
|
|
291
|
+
const stubContent = generateStubContent(category);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
category,
|
|
295
|
+
title: title.replace(/[^\w\s-]/g, '').trim(),
|
|
296
|
+
tags,
|
|
297
|
+
content: stubContent,
|
|
298
|
+
trigger: context.errorEncountered
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Formats a captured entry into a readable context string for Claude Code.
|
|
304
|
+
*/
|
|
305
|
+
function formatEntryForContext(entry: WikiEntry, signal: string): string {
|
|
306
|
+
return `## Captured Entry: ${entry.frontmatter.title}\nCategory: ${entry.category}\nSignal: ${signal}\nPath: ${entry.path}\n\n${entry.content.substring(0, 300)}...`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export default onPostTask;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// hooks/prompt-enrich.ts
|
|
2
|
+
import { wikiExists, searchWiki, PromptEnrichOutput, SearchResult } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook input from Claude Code's UserPromptSubmit event.
|
|
6
|
+
*/
|
|
7
|
+
interface HookInput {
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
prompt: string;
|
|
10
|
+
filePaths?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Category keywords for detecting relevant wiki categories from prompt.
|
|
15
|
+
*/
|
|
16
|
+
const CATEGORY_KEYWORDS: Record<string, string[]> = {
|
|
17
|
+
issues: ['error', 'fix', 'bug', 'broken', 'fails', 'crash', 'exception'],
|
|
18
|
+
patterns: ['pattern', 'template', 'reusable', 'design', 'approach'],
|
|
19
|
+
gotchas: ['careful', 'watch out', 'pitfall', 'gotcha', 'trap', 'avoid'],
|
|
20
|
+
testing: ['test', 'mock', 'fixture', 'assert', 'spec', 'coverage'],
|
|
21
|
+
docs: ['document', 'readme', 'guide', 'tutorial', 'api docs'],
|
|
22
|
+
security: ['auth', 'token', 'vulnerability', 'permission', 'csrf', 'xss', 'injection'],
|
|
23
|
+
performance: ['slow', 'optimize', 'memory', 'latency', 'benchmark', 'profiling']
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Path hints for detecting relevant categories from file paths.
|
|
28
|
+
*/
|
|
29
|
+
const PATH_HINTS: Record<string, string[]> = {
|
|
30
|
+
testing: ['test', 'spec', 'fixture', 'mock', '__tests__'],
|
|
31
|
+
docs: ['doc', 'readme', 'wiki', 'guide'],
|
|
32
|
+
security: ['auth', 'secret', 'cred', 'permission'],
|
|
33
|
+
performance: ['perf', 'bench', 'profile', 'optim']
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* UserPromptSubmit hook - enriches prompts with relevant wiki context.
|
|
38
|
+
* Outputs to stdout for Claude Code to include in context.
|
|
39
|
+
*/
|
|
40
|
+
export async function onPromptEnrich(input: HookInput): Promise<PromptEnrichOutput | null> {
|
|
41
|
+
try {
|
|
42
|
+
const { projectRoot, prompt, filePaths } = input;
|
|
43
|
+
|
|
44
|
+
if (!wikiExists(projectRoot)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Detect relevant categories from prompt keywords
|
|
49
|
+
const relevantCategories = detectRelevantCategories(prompt, filePaths || []);
|
|
50
|
+
|
|
51
|
+
// Search wiki for relevant entries
|
|
52
|
+
const searchResults = await searchWiki(projectRoot, { query: prompt });
|
|
53
|
+
|
|
54
|
+
if (searchResults.length === 0 && relevantCategories.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Format entries for output
|
|
59
|
+
const entries = searchResults.slice(0, 5).map(r => ({
|
|
60
|
+
title: r.entry.frontmatter.title,
|
|
61
|
+
category: r.entry.category,
|
|
62
|
+
path: r.entry.path,
|
|
63
|
+
confidence: r.entry.frontmatter.confidence
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Record usage for matched entries
|
|
67
|
+
if (entries.length > 0) {
|
|
68
|
+
try {
|
|
69
|
+
const { recordEntryUsage } = await import('../src/index.js');
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
recordEntryUsage(projectRoot, entry.path);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Usage tracking failure is non-critical
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Output for Claude Code hook integration (user-facing console logs)
|
|
79
|
+
if (entries.length > 0) {
|
|
80
|
+
console.log(`\u{1F4DA} Solvdex: Found ${entries.length} relevant entries for your task:`);
|
|
81
|
+
for (const entry of entries.slice(0, 3)) {
|
|
82
|
+
console.log(` - ${entry.title} (${entry.category}, confidence: ${entry.confidence})`);
|
|
83
|
+
}
|
|
84
|
+
if (entries.length > 3) {
|
|
85
|
+
console.log(` ... and ${entries.length - 3} more`);
|
|
86
|
+
}
|
|
87
|
+
} else if (relevantCategories.length > 0) {
|
|
88
|
+
console.log(`\u{1F4DA} Solvdex: Check ${relevantCategories.join(', ')} for related knowledge`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Structured output for Claude Code (includes both old and new format for backward compatibility)
|
|
92
|
+
const output: PromptEnrichOutput = {
|
|
93
|
+
// Old format (for backward compatibility)
|
|
94
|
+
type: 'wiki_context',
|
|
95
|
+
message: entries.length > 0
|
|
96
|
+
? `Found ${entries.length} relevant wiki entries`
|
|
97
|
+
: `Relevant categories: ${relevantCategories.join(', ')}`,
|
|
98
|
+
entries,
|
|
99
|
+
// New format (Claude Code integration)
|
|
100
|
+
continue: true,
|
|
101
|
+
hookSpecificOutput: {
|
|
102
|
+
type: 'context_added',
|
|
103
|
+
message: entries.length > 0
|
|
104
|
+
? `Found ${entries.length} relevant wiki entries`
|
|
105
|
+
: `Relevant categories: ${relevantCategories.join(', ')}`,
|
|
106
|
+
entryCount: entries.length
|
|
107
|
+
},
|
|
108
|
+
additionalContext: entries.length > 0
|
|
109
|
+
? formatEntriesForContext(searchResults.slice(0, 5))
|
|
110
|
+
: undefined
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Output JSON marker + JSON (MUST be last output)
|
|
114
|
+
console.log('\n__HOOK_OUTPUT__');
|
|
115
|
+
console.log(JSON.stringify(output));
|
|
116
|
+
|
|
117
|
+
return output;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Graceful degradation - log error but don't crash
|
|
120
|
+
console.error(`[Solvdex] Error in prompt-enrich hook: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detects relevant categories based on prompt keywords and file paths.
|
|
127
|
+
*/
|
|
128
|
+
function detectRelevantCategories(prompt: string, filePaths: string[]): string[] {
|
|
129
|
+
const detected: Set<string> = new Set();
|
|
130
|
+
const promptLower = prompt.toLowerCase();
|
|
131
|
+
|
|
132
|
+
// Check keywords in prompt
|
|
133
|
+
for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
|
|
134
|
+
if (keywords.some(kw => promptLower.includes(kw))) {
|
|
135
|
+
detected.add(category);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check path hints
|
|
140
|
+
for (const filePath of filePaths) {
|
|
141
|
+
const pathLower = filePath.toLowerCase();
|
|
142
|
+
for (const [category, hints] of Object.entries(PATH_HINTS)) {
|
|
143
|
+
if (hints.some(hint => pathLower.includes(hint))) {
|
|
144
|
+
detected.add(category);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Array.from(detected);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Formats search results into a readable context string for Claude Code.
|
|
154
|
+
*/
|
|
155
|
+
function formatEntriesForContext(results: SearchResult[]): string {
|
|
156
|
+
return results.map(r => {
|
|
157
|
+
const e = r.entry;
|
|
158
|
+
return `## ${e.frontmatter.title} (${e.category})\nConfidence: ${e.frontmatter.confidence}\nTags: ${e.frontmatter.tags.join(', ')}\n\n${e.content.substring(0, 300)}...`;
|
|
159
|
+
}).join('\n\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default onPromptEnrich;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI wrapper for session-start hook
|
|
3
|
+
import { wikiExists, searchWiki, DEFAULT_CATEGORIES } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
interface SessionStartInput {
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
taskDescription?: string;
|
|
8
|
+
filePaths?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CATEGORY_KEYWORDS: Record<string, string[]> = {
|
|
12
|
+
issues: ['error', 'fix', 'bug', 'broken', 'fails', 'crash', 'exception', 'trouble'],
|
|
13
|
+
patterns: ['pattern', 'template', 'reusable', 'design', 'approach', 'best practice'],
|
|
14
|
+
gotchas: ['careful', 'watch out', 'pitfall', 'gotcha', 'trap', 'avoid', 'never'],
|
|
15
|
+
testing: ['test', 'mock', 'fixture', 'assert', 'spec', 'coverage', 'jest', 'vitest'],
|
|
16
|
+
docs: ['document', 'readme', 'guide', 'tutorial', 'api docs', 'documentation'],
|
|
17
|
+
security: ['auth', 'token', 'vulnerability', 'permission', 'csrf', 'xss', 'injection', 'security'],
|
|
18
|
+
performance: ['slow', 'optimize', 'memory', 'latency', 'benchmark', 'profiling', 'performance']
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CATEGORY_PATH_HINTS: Record<string, string[]> = {
|
|
22
|
+
testing: ['test', 'spec', 'fixture', 'mock', '__tests__'],
|
|
23
|
+
docs: ['doc', 'readme', 'wiki', 'guide', 'docs'],
|
|
24
|
+
security: ['auth', 'secret', 'cred', 'permission', 'security'],
|
|
25
|
+
performance: ['perf', 'bench', 'profile', 'optim']
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function detectCategoryFromPaths(paths: string[]): string[] {
|
|
29
|
+
const detected: Set<string> = new Set();
|
|
30
|
+
for (const filePath of paths) {
|
|
31
|
+
const pathLower = filePath.toLowerCase();
|
|
32
|
+
for (const [category, hints] of Object.entries(CATEGORY_PATH_HINTS)) {
|
|
33
|
+
if (hints.some(hint => pathLower.includes(hint))) {
|
|
34
|
+
detected.add(category);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Array.from(detected);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRelevantCategory(task: string, category: string): boolean {
|
|
42
|
+
const categoryKeywords = CATEGORY_KEYWORDS[category] || [];
|
|
43
|
+
const taskLower = task.toLowerCase();
|
|
44
|
+
return categoryKeywords.some(kw => taskLower.includes(kw));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
try {
|
|
49
|
+
// Read JSON from stdin
|
|
50
|
+
const chunks: Buffer[] = [];
|
|
51
|
+
for await (const chunk of process.stdin) {
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
}
|
|
54
|
+
const inputData: SessionStartInput = JSON.parse(Buffer.concat(chunks).toString());
|
|
55
|
+
|
|
56
|
+
const { projectRoot, taskDescription, filePaths } = inputData;
|
|
57
|
+
|
|
58
|
+
if (!wikiExists(projectRoot) || !taskDescription) {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check which categories might be relevant
|
|
63
|
+
const relevantCategories: string[] = [];
|
|
64
|
+
for (const category of DEFAULT_CATEGORIES) {
|
|
65
|
+
if (isRelevantCategory(taskDescription, category)) {
|
|
66
|
+
relevantCategories.push(category);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Also check paths for category hints
|
|
71
|
+
if (filePaths && filePaths.length > 0) {
|
|
72
|
+
const pathCategories = detectCategoryFromPaths(filePaths);
|
|
73
|
+
for (const cat of pathCategories) {
|
|
74
|
+
if (!relevantCategories.includes(cat)) {
|
|
75
|
+
relevantCategories.push(cat);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Search wiki for relevant entries
|
|
81
|
+
const searchResults = await searchWiki(projectRoot, { query: taskDescription });
|
|
82
|
+
|
|
83
|
+
if (searchResults.length > 0) {
|
|
84
|
+
console.log(`📚 Solvdex: Found ${searchResults.length} relevant entries`);
|
|
85
|
+
for (const r of searchResults.slice(0, 3)) {
|
|
86
|
+
console.log(` - ${r.entry.frontmatter.title} (${r.entry.category})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
process.exit(0);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
process.exit(0); // Silent fail
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main();
|