@johpaz/hive-sdk 0.0.18 → 0.1.4
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/bun.lock +291 -1
- package/docs/HIVE-HARNESS.md +113 -0
- package/package.json +36 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +13 -2
- package/packages/core/src/ace/Tracer.ts +1 -1
- package/packages/core/src/agent/AgentRunner.ts +12 -0
- package/packages/core/src/agent/ContextCompiler.ts +4 -4
- package/packages/core/src/agent/ConversationStore.ts +30 -20
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +50 -76
- package/packages/core/src/agent/selectors/SkillSelector.ts +106 -262
- package/packages/core/src/agent/selectors/ToolSelector.ts +53 -89
- package/packages/core/src/auth/auth.ts +36 -23
- package/packages/core/src/harness/boot-id.ts +20 -0
- package/packages/core/src/harness/collections.ts +98 -0
- package/packages/core/src/harness/db-helpers.ts +87 -0
- package/packages/core/src/harness/durable-queue.ts +337 -0
- package/packages/core/src/harness/goal-verifier.ts +141 -0
- package/packages/core/src/harness/harness.test.ts +236 -0
- package/packages/core/src/harness/index.ts +34 -0
- package/packages/core/src/harness/job-store.ts +399 -0
- package/packages/core/src/harness/proof-packet.ts +69 -0
- package/packages/core/src/harness/reconcile.ts +149 -0
- package/packages/core/src/harness/run-epoch.ts +32 -0
- package/packages/core/src/harness/run-store.ts +334 -0
- package/packages/core/src/index.ts +13 -0
- package/packages/core/src/memory/Scratchpad.test.ts +23 -21
- package/packages/core/src/memory/Scratchpad.ts +41 -24
- package/packages/core/src/storage/HiveDBStorage.ts +64 -0
- package/packages/core/src/storage/SQLiteStorage.ts +7 -0
- package/packages/core/src/storage/hiveSeed.ts +308 -0
- package/packages/core/src/storage/hiveStorage.test.ts +38 -0
- package/packages/core/src/storage/index.ts +11 -0
- package/packages/core/src/storage/seed.ts +5 -1
- package/packages/core/src/storage/usage.ts +106 -167
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +11 -3
- package/packages/core/src/tools/agents/get-available-models.ts +52 -56
- package/packages/core/src/tools/agents/index.ts +77 -60
- package/packages/core/src/tools/core/index.ts +106 -291
- package/packages/core/src/tools/meeting/index.ts +83 -93
- package/packages/core/src/utils/toon.ts +4 -4
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* HiveDB-based Dynamic Skill Selector Module
|
|
3
|
+
*
|
|
4
4
|
* Context Compiler Level 4 - Intelligent Skill Selection
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* skills (0-5) based on the user message, similar to tool selection.
|
|
8
|
-
*
|
|
9
|
-
* DESIGN DECISIONS:
|
|
10
|
-
*
|
|
11
|
-
* 1. Reads from skills table in database (not hardcoded catalog)
|
|
12
|
-
* 2. Maximum 5 skills per turn for balanced context injection
|
|
13
|
-
* 3. Relevance threshold for conversational messages
|
|
14
|
-
* 4. Uses skill descriptions for FTS5 matching
|
|
15
|
-
* 5. Returns skill content for injection into system prompt
|
|
5
|
+
*
|
|
6
|
+
* Uses HiveDB hybrid search (BM25 + optional vector) over the skills index.
|
|
16
7
|
*/
|
|
17
8
|
|
|
18
|
-
import {
|
|
9
|
+
import { getHiveDB } from "../../storage/HiveDBStorage.ts"
|
|
19
10
|
import { logger } from "../../utils/logger.ts"
|
|
11
|
+
import type { HiveSkillDoc } from "../../storage/hiveSeed.ts"
|
|
12
|
+
import type { IndexDoc } from "@johpaz/hive-db"
|
|
20
13
|
|
|
21
14
|
const log = logger.child("skill-selector")
|
|
22
15
|
|
|
23
16
|
// ─── Minimal Skill Set ─────────────────────────────────────────────────────────
|
|
24
17
|
|
|
25
|
-
/**
|
|
26
|
-
* Skills mínimas que SIEMPRE están disponibles (asociadas a las 4 tools iniciales)
|
|
27
|
-
* - memory_manager: usa save_note (notas persistentes)
|
|
28
|
-
* - canvas_report: usa report_progress (reportes de progreso)
|
|
29
|
-
* - task_orchestrator: usa notify (comunicación entre agentes)
|
|
30
|
-
*/
|
|
31
18
|
export const MINIMAL_SKILL_NAMES = new Set([
|
|
32
|
-
"memory_manager",
|
|
33
|
-
"canvas_report",
|
|
34
|
-
"task_orchestrator",
|
|
19
|
+
"memory_manager",
|
|
20
|
+
"canvas_report",
|
|
21
|
+
"task_orchestrator",
|
|
35
22
|
])
|
|
36
23
|
|
|
37
24
|
// ─── Types ───────────────────────────────────────────────────────────────────────
|
|
@@ -68,19 +55,10 @@ export interface SkillSelectorResult {
|
|
|
68
55
|
|
|
69
56
|
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
const MAX_SKILLS_PER_TURN = 4 // Increased from 2 to allow more skills
|
|
58
|
+
const MAX_SKILLS_PER_TURN = 4
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
* Minimum bm25 score threshold. Below this = conversational, no skills needed.
|
|
76
|
-
*
|
|
77
|
-
* CRITICAL: bm25() returns NEGATIVE scores where closer to 0 = more relevant.
|
|
78
|
-
* - Score of -5 is MORE relevant than -20
|
|
79
|
-
* - We use -15 as threshold to allow reasonable matching while filtering noise
|
|
80
|
-
*/
|
|
81
|
-
const MIN_RELEVANCE_THRESHOLD = -15 // Increased from -5 to allow more matches
|
|
60
|
+
const MIN_RELEVANCE_THRESHOLD = 0.5
|
|
82
61
|
|
|
83
|
-
/** Stopwords to filter out before FTS5 query construction */
|
|
84
62
|
const STOPWORDS = new Set([
|
|
85
63
|
"que", "con", "para", "por", "una", "uno", "los", "las", "del",
|
|
86
64
|
"como", "esta", "esto", "ese", "eso", "the", "and", "for",
|
|
@@ -92,7 +70,6 @@ const STOPWORDS = new Set([
|
|
|
92
70
|
"puedes", "necesito", "quiero", "podés", "necesitás", "querés",
|
|
93
71
|
])
|
|
94
72
|
|
|
95
|
-
/** Conversational patterns that should return empty skill list */
|
|
96
73
|
const CONVERSATIONAL_PATTERNS = [
|
|
97
74
|
/^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
|
|
98
75
|
/^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
|
|
@@ -101,46 +78,29 @@ const CONVERSATIONAL_PATTERNS = [
|
|
|
101
78
|
/^(adiós|bye|nos vemos|see you|later|chau)/i,
|
|
102
79
|
/^(entiendo|understand|i see|ya veo|got it)/i,
|
|
103
80
|
/^(bien|good|great|excelente|awesome|perfect)/i,
|
|
104
|
-
/^(?:\?|¿)$/,
|
|
81
|
+
/^(?:\?|¿)$/,
|
|
105
82
|
]
|
|
106
83
|
|
|
107
84
|
// ─── Helper Functions ───────────────────────────────────────────────────────
|
|
108
85
|
|
|
109
|
-
/**
|
|
110
|
-
* Check if message is purely conversational (no skills needed)
|
|
111
|
-
*/
|
|
112
86
|
function isConversational(message: string): boolean {
|
|
113
87
|
const trimmed = message.trim()
|
|
114
|
-
|
|
115
|
-
// Empty or very short messages
|
|
116
88
|
if (trimmed.length < 2) return true
|
|
117
|
-
|
|
118
|
-
// Check conversational patterns
|
|
119
89
|
for (const pattern of CONVERSATIONAL_PATTERNS) {
|
|
120
90
|
if (pattern.test(trimmed)) {
|
|
121
91
|
log.debug(`[skill-selector] Message matched conversational pattern: ${pattern}`)
|
|
122
92
|
return true
|
|
123
93
|
}
|
|
124
94
|
}
|
|
125
|
-
|
|
126
|
-
// Check if all words are stopwords (likely conversational)
|
|
127
95
|
const words = trimmed.toLowerCase().split(/\s+/)
|
|
128
96
|
const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
|
|
129
97
|
if (meaningfulWords.length === 0) {
|
|
130
98
|
log.debug(`[skill-selector] All words are stopwords - conversational`)
|
|
131
99
|
return true
|
|
132
100
|
}
|
|
133
|
-
|
|
134
101
|
return false
|
|
135
102
|
}
|
|
136
103
|
|
|
137
|
-
/**
|
|
138
|
-
* Build FTS5 query from user message
|
|
139
|
-
*
|
|
140
|
-
* Uses prefix matching for better recall:
|
|
141
|
-
* - "generar" matches "generando", "generación", "genera"
|
|
142
|
-
* - "código" matches "codigos", "codificar"
|
|
143
|
-
*/
|
|
144
104
|
function buildFTSQuery(message: string): string {
|
|
145
105
|
const words = message
|
|
146
106
|
.toLowerCase()
|
|
@@ -150,144 +110,94 @@ function buildFTSQuery(message: string): string {
|
|
|
150
110
|
.slice(0, 8)
|
|
151
111
|
|
|
152
112
|
if (words.length === 0) return ""
|
|
153
|
-
|
|
154
|
-
// Use prefix matching for better recall (e.g., "gener*" matches "generar", "generando", "generación")
|
|
155
|
-
return words.map(w => `${w}*`).join(" OR ")
|
|
113
|
+
return words.join(" ")
|
|
156
114
|
}
|
|
157
115
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
116
|
+
function matchTriggers(message: string, triggers: string[]): boolean {
|
|
117
|
+
if (!triggers || triggers.length === 0) return false
|
|
118
|
+
const lowerMessage = message.toLowerCase()
|
|
119
|
+
return triggers.some(trigger =>
|
|
120
|
+
lowerMessage.includes(trigger.toLowerCase())
|
|
121
|
+
)
|
|
122
|
+
}
|
|
163
123
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
124
|
+
function toSkillDescriptor(doc: HiveSkillDoc): SkillDescriptor {
|
|
125
|
+
return {
|
|
126
|
+
id: doc.id,
|
|
127
|
+
name: doc.name,
|
|
128
|
+
description: doc.description,
|
|
129
|
+
category: doc.category,
|
|
130
|
+
tools: Array.isArray(doc.tools) ? doc.tools.join(",") : String(doc.tools ?? ""),
|
|
131
|
+
triggers: Array.isArray(doc.triggers) ? doc.triggers.join(",") : String(doc.triggers ?? ""),
|
|
132
|
+
preferred_agents: Array.isArray(doc.preferredAgents) ? doc.preferredAgents.join(",") : String(doc.preferredAgents ?? ""),
|
|
133
|
+
body: doc.body,
|
|
134
|
+
version: doc.version,
|
|
135
|
+
version_num: doc.versionNum,
|
|
136
|
+
active: doc.active ? 1 : 0,
|
|
176
137
|
}
|
|
177
138
|
}
|
|
178
139
|
|
|
179
140
|
// ─── Main Selection Function ─────────────────────────────────────────────────
|
|
180
141
|
|
|
181
|
-
|
|
182
|
-
* Select skills for a given user message using hybrid matching:
|
|
183
|
-
* 1. First check explicit triggers (high confidence match)
|
|
184
|
-
* 2. Fallback to FTS5 bm25() scoring for semantic matching
|
|
185
|
-
*
|
|
186
|
-
* @param userMessage - The raw user message
|
|
187
|
-
* @returns Array of 0-5 selected skills with scores
|
|
188
|
-
*
|
|
189
|
-
* ALGORITHM:
|
|
190
|
-
* 1. If conversational → return []
|
|
191
|
-
* 2. Check explicit triggers from all enabled skills
|
|
192
|
-
* 3. If trigger match found → return matching skill immediately
|
|
193
|
-
* 4. Build FTS5 query from message keywords
|
|
194
|
-
* 5. Query skills_fts with bm25() scoring
|
|
195
|
-
* 6. Filter results below MIN_RELEVANCE_THRESHOLD
|
|
196
|
-
* 7. Return top MAX_SKILLS_PER_TURN results
|
|
197
|
-
*/
|
|
198
|
-
export function selectSkills(userMessage: string): SkillDescriptor[] {
|
|
142
|
+
export async function selectSkills(userMessage: string): Promise<SkillDescriptor[]> {
|
|
199
143
|
const startTime = performance.now()
|
|
200
144
|
|
|
201
145
|
log.debug(`[skill-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
|
|
202
146
|
|
|
203
|
-
// Step 1: Check if conversational
|
|
204
147
|
if (isConversational(userMessage)) {
|
|
205
148
|
log.debug(`[skill-selector] Conversational message, returning empty array`)
|
|
206
149
|
return []
|
|
207
150
|
}
|
|
208
151
|
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
FROM skills
|
|
214
|
-
WHERE active = 1
|
|
215
|
-
`).all() as SkillDescriptor[]
|
|
152
|
+
const db = await getHiveDB()
|
|
153
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
154
|
+
const entries = await skillsCol.scan()
|
|
155
|
+
const allSkills = entries.map(e => e.doc).filter(s => s.active)
|
|
216
156
|
|
|
217
|
-
// Check trigger match - if found, return immediately with high confidence
|
|
218
157
|
for (const skill of allSkills) {
|
|
219
|
-
if (
|
|
158
|
+
if (matchTriggers(userMessage, skill.triggers)) {
|
|
220
159
|
log.info(`[skill-selector] Trigger match found: ${skill.name}`)
|
|
221
|
-
return [skill]
|
|
160
|
+
return [toSkillDescriptor(skill)]
|
|
222
161
|
}
|
|
223
162
|
}
|
|
224
163
|
|
|
225
|
-
// Step 3: Build FTS5 query for semantic matching
|
|
226
164
|
const ftsQuery = buildFTSQuery(userMessage)
|
|
227
165
|
if (!ftsQuery) {
|
|
228
|
-
log.debug(`[skill-selector] No valid
|
|
166
|
+
log.debug(`[skill-selector] No valid query terms, returning empty array`)
|
|
229
167
|
return []
|
|
230
168
|
}
|
|
231
169
|
|
|
232
|
-
log.debug(`[skill-selector]
|
|
233
|
-
|
|
234
|
-
// Step 4: Execute FTS5 query with bm25 scoring
|
|
235
|
-
// Use bm25() with column weights for relevance scoring
|
|
236
|
-
// FTS5 table columns: id, name, description, category, tools, triggers, body
|
|
237
|
-
// Weights: id=1.0, name=4.0, description=5.0, category=1.0, tools=1.0, triggers=5.0, body=2.0
|
|
238
|
-
// Higher weight on description (5.0) and triggers (5.0) for best semantic matching
|
|
239
|
-
const ftsResults = db.query(`
|
|
240
|
-
SELECT id, bm25(skills_fts, 1.0, 4.0, 5.0, 1.0, 1.0, 5.0, 2.0) as bm25_score
|
|
241
|
-
FROM skills_fts
|
|
242
|
-
WHERE skills_fts MATCH ?
|
|
243
|
-
ORDER BY bm25_score ASC
|
|
244
|
-
LIMIT 20
|
|
245
|
-
`).all(ftsQuery) as { id: string; bm25_score: number }[]
|
|
246
|
-
|
|
247
|
-
if (ftsResults.length === 0) {
|
|
248
|
-
log.debug(`[skill-selector] No FTS matches, returning empty array`)
|
|
249
|
-
return []
|
|
250
|
-
}
|
|
170
|
+
log.debug(`[skill-selector] Search query: "${ftsQuery}"`)
|
|
251
171
|
|
|
252
|
-
|
|
253
|
-
|
|
172
|
+
const hits = await db.queryHybrid({
|
|
173
|
+
text: ftsQuery,
|
|
174
|
+
k: 20,
|
|
175
|
+
boosts: { name: 4.0, body: 5.0, tags: 3.0 },
|
|
176
|
+
})
|
|
254
177
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (relevantResults.length === 0) {
|
|
259
|
-
log.debug(`[skill-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
|
|
178
|
+
if (hits.length === 0) {
|
|
179
|
+
log.debug(`[skill-selector] No index matches, returning empty array`)
|
|
260
180
|
return []
|
|
261
181
|
}
|
|
262
182
|
|
|
263
|
-
|
|
264
|
-
const skillIds = relevantResults.map(r => r.id)
|
|
183
|
+
log.info(`[skill-selector] Raw scores: ${hits.slice(0, 10).map(r => `id=${r.id}, score=${r.score.toFixed(2)}`).join(", ")}`)
|
|
265
184
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
dbSkills = db.query(`
|
|
270
|
-
SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
|
|
271
|
-
FROM skills
|
|
272
|
-
WHERE id IN (${skillIds.map(() => '?').join(',')})
|
|
273
|
-
AND active = 1
|
|
274
|
-
`).all(...skillIds) as SkillDescriptor[]
|
|
275
|
-
} catch (err) {
|
|
276
|
-
log.warn(`[skill-selector] Failed to fetch skills from DB:`, err)
|
|
185
|
+
const relevantResults = hits.filter(r => r.score >= MIN_RELEVANCE_THRESHOLD)
|
|
186
|
+
if (relevantResults.length === 0) {
|
|
187
|
+
log.debug(`[skill-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
|
|
277
188
|
return []
|
|
278
189
|
}
|
|
279
190
|
|
|
280
|
-
|
|
281
|
-
const skillMap = new Map(dbSkills.map(s => [s.id, s]))
|
|
191
|
+
const skillMap = new Map(allSkills.map(s => [s.id, s]))
|
|
282
192
|
const scoredSkills: SelectedSkill[] = []
|
|
283
193
|
|
|
284
|
-
for (const
|
|
285
|
-
const skill = skillMap.get(
|
|
194
|
+
for (const hit of relevantResults) {
|
|
195
|
+
const skill = skillMap.get(hit.id)
|
|
286
196
|
if (skill) {
|
|
287
197
|
scoredSkills.push({
|
|
288
198
|
id: skill.id,
|
|
289
199
|
name: skill.name,
|
|
290
|
-
score:
|
|
200
|
+
score: hit.score,
|
|
291
201
|
category: skill.category,
|
|
292
202
|
description: skill.description || "",
|
|
293
203
|
body: skill.body,
|
|
@@ -295,11 +205,8 @@ export function selectSkills(userMessage: string): SkillDescriptor[] {
|
|
|
295
205
|
}
|
|
296
206
|
}
|
|
297
207
|
|
|
298
|
-
// Step 7: Take top N skills
|
|
299
208
|
const topSkills = scoredSkills.slice(0, MAX_SKILLS_PER_TURN)
|
|
300
|
-
|
|
301
|
-
// Step 8: Return as SkillDescriptor array
|
|
302
|
-
const result = topSkills.map(t => skillMap.get(t.id)!).filter(Boolean)
|
|
209
|
+
const result = topSkills.map(t => toSkillDescriptor(skillMap.get(t.id)!)).filter(Boolean)
|
|
303
210
|
|
|
304
211
|
const timing = performance.now() - startTime
|
|
305
212
|
|
|
@@ -315,23 +222,15 @@ export function selectSkills(userMessage: string): SkillDescriptor[] {
|
|
|
315
222
|
|
|
316
223
|
// ─── Minimal Skills Loader ───────────────────────────────────────────────────
|
|
317
224
|
|
|
318
|
-
|
|
319
|
-
* Load minimal skills that are ALWAYS available (associated with MINIMAL_TOOLS)
|
|
320
|
-
* These are loaded at startup, not via FTS5 search.
|
|
321
|
-
*
|
|
322
|
-
* @returns Array of minimal skills (memory_manager, canvas_report, task_orchestrator)
|
|
323
|
-
*/
|
|
324
|
-
export function getMinimalSkills(): SkillDescriptor[] {
|
|
325
|
-
const db = getDb()
|
|
326
|
-
|
|
225
|
+
export async function getMinimalSkills(): Promise<SkillDescriptor[]> {
|
|
327
226
|
try {
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
227
|
+
const db = await getHiveDB()
|
|
228
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
229
|
+
const entries = await skillsCol.scan()
|
|
230
|
+
const skills = entries
|
|
231
|
+
.map(e => e.doc)
|
|
232
|
+
.filter(s => s.active && MINIMAL_SKILL_NAMES.has(s.name))
|
|
233
|
+
.map(toSkillDescriptor)
|
|
335
234
|
|
|
336
235
|
log.info(`[skill-selector] Loaded ${skills.length} minimal skills: ${skills.map(s => s.name).join(", ")}`)
|
|
337
236
|
return skills
|
|
@@ -341,136 +240,81 @@ export function getMinimalSkills(): SkillDescriptor[] {
|
|
|
341
240
|
}
|
|
342
241
|
}
|
|
343
242
|
|
|
344
|
-
// ─── Sync Skills to
|
|
243
|
+
// ─── Sync Skills to Index ───────────────────────────────────────────────────
|
|
345
244
|
|
|
346
|
-
/**
|
|
347
|
-
* Sync all enabled skills from database to FTS5
|
|
348
|
-
* Should be called on initialization from gateway/initializer.ts
|
|
349
|
-
* The skills_fts table is created by schema.ts (v0.0.28 includes description)
|
|
350
|
-
*/
|
|
351
245
|
export async function syncSkillsToFTS(): Promise<void> {
|
|
352
|
-
const db =
|
|
246
|
+
const db = await getHiveDB()
|
|
353
247
|
|
|
354
248
|
try {
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
FROM skills
|
|
359
|
-
WHERE active = 1
|
|
360
|
-
`).all() as Array<{
|
|
361
|
-
id: string
|
|
362
|
-
name: string
|
|
363
|
-
description: string
|
|
364
|
-
category: string
|
|
365
|
-
tools: string
|
|
366
|
-
triggers: string
|
|
367
|
-
body: string
|
|
368
|
-
}>
|
|
249
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
250
|
+
const entries = await skillsCol.scan()
|
|
251
|
+
const dbSkills = entries.map(e => e.doc).filter(s => s.active)
|
|
369
252
|
|
|
370
253
|
if (dbSkills.length === 0) {
|
|
371
254
|
log.debug(`[skill-selector] No skills found in DB to sync`)
|
|
255
|
+
return
|
|
372
256
|
}
|
|
373
257
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// B: Prepare insertion (v0.0.28 schema with description)
|
|
386
|
-
const insert = db.prepare(`
|
|
387
|
-
INSERT INTO skills_fts(id, name, description, category, tools, triggers, body)
|
|
388
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
389
|
-
`)
|
|
390
|
-
|
|
391
|
-
// C: Re-populate
|
|
392
|
-
for (const skill of dbSkills) {
|
|
393
|
-
insert.run(
|
|
394
|
-
skill.id,
|
|
395
|
-
skill.name,
|
|
396
|
-
skill.description || "",
|
|
397
|
-
skill.category,
|
|
398
|
-
skill.tools,
|
|
399
|
-
skill.triggers,
|
|
400
|
-
skill.body
|
|
401
|
-
)
|
|
402
|
-
}
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
// Execute transaction
|
|
406
|
-
syncTransaction()
|
|
407
|
-
|
|
408
|
-
log.info(`[skill-selector] Atomic sync complete: ${dbSkills.length} skills indexed in FTS5`)
|
|
258
|
+
const docs: IndexDoc[] = dbSkills.map(skill => ({
|
|
259
|
+
id: skill.id,
|
|
260
|
+
name: skill.name,
|
|
261
|
+
body: `${skill.description || ""} ${skill.body || ""}`,
|
|
262
|
+
tags: [skill.category, ...skill.tools, ...skill.triggers].join(" "),
|
|
263
|
+
filters: [{ field: "type", value: "skill" }],
|
|
264
|
+
}))
|
|
265
|
+
|
|
266
|
+
await db.upsertBatch(docs)
|
|
267
|
+
|
|
268
|
+
log.info(`[skill-selector] Atomic sync complete: ${dbSkills.length} skills indexed in HiveDB`)
|
|
409
269
|
|
|
410
270
|
} catch (err) {
|
|
411
271
|
log.error(`[skill-selector] Transactional sync failed:`, err)
|
|
412
|
-
throw err
|
|
272
|
+
throw err
|
|
413
273
|
}
|
|
414
274
|
}
|
|
275
|
+
|
|
415
276
|
// ─── Initialization ───────────────────────────────────────────────────────
|
|
416
277
|
|
|
417
|
-
/**
|
|
418
|
-
* Initialize the skill selector
|
|
419
|
-
* DEPRECATED: syncSkillsToFTS() is now called from gateway/initializer.ts
|
|
420
|
-
* This function is kept for backward compatibility but is no longer needed
|
|
421
|
-
*/
|
|
422
278
|
export function initializeSkillSelector(): void {
|
|
423
|
-
log.info(`[skill-selector] Initializing skill selector (deprecated - sync is done in
|
|
424
|
-
// syncSkillsToFTS() - No longer needed here, done in gateway/initializer.ts
|
|
279
|
+
log.info(`[skill-selector] Initializing skill selector (deprecated - sync is done in seed)`)
|
|
425
280
|
}
|
|
426
281
|
|
|
427
282
|
// ─── Debug/Test Helpers ─────────────────────────────────────────────────────
|
|
428
283
|
|
|
429
|
-
|
|
430
|
-
* Get all enabled skills from database (for debugging/testing)
|
|
431
|
-
*/
|
|
432
|
-
export function getAllSkillsFromDB(): SkillDescriptor[] {
|
|
284
|
+
export async function getAllSkillsFromDB(): Promise<SkillDescriptor[]> {
|
|
433
285
|
try {
|
|
434
|
-
const db =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
WHERE active = 1
|
|
439
|
-
`).all() as SkillDescriptor[]
|
|
286
|
+
const db = await getHiveDB()
|
|
287
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
288
|
+
const entries = await skillsCol.scan()
|
|
289
|
+
return entries.map(e => toSkillDescriptor(e.doc)).filter(s => s.active)
|
|
440
290
|
} catch (err) {
|
|
441
291
|
log.error(`[skill-selector] Failed to fetch skills:`, err)
|
|
442
292
|
return []
|
|
443
293
|
}
|
|
444
294
|
}
|
|
445
295
|
|
|
446
|
-
|
|
447
|
-
* Get skill by name
|
|
448
|
-
*/
|
|
449
|
-
export function getSkillByName(name: string): SkillDescriptor | undefined {
|
|
296
|
+
export async function getSkillByName(name: string): Promise<SkillDescriptor | undefined> {
|
|
450
297
|
try {
|
|
451
|
-
const db =
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
`).get(name) as SkillDescriptor | undefined
|
|
298
|
+
const db = await getHiveDB()
|
|
299
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
300
|
+
const entries = await skillsCol.scan()
|
|
301
|
+
const skill = entries.map(e => e.doc).find(s => s.name === name && s.active)
|
|
302
|
+
return skill ? toSkillDescriptor(skill) : undefined
|
|
457
303
|
} catch (err) {
|
|
458
304
|
log.error(`[skill-selector] Failed to fetch skill by name:`, err)
|
|
459
305
|
return undefined
|
|
460
306
|
}
|
|
461
307
|
}
|
|
462
308
|
|
|
463
|
-
|
|
464
|
-
* Get skills by category
|
|
465
|
-
*/
|
|
466
|
-
export function getSkillsByCategory(category: string): SkillDescriptor[] {
|
|
309
|
+
export async function getSkillsByCategory(category: string): Promise<SkillDescriptor[]> {
|
|
467
310
|
try {
|
|
468
|
-
const db =
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
311
|
+
const db = await getHiveDB()
|
|
312
|
+
const skillsCol = db.collection<HiveSkillDoc>("skills")
|
|
313
|
+
const entries = await skillsCol.scan()
|
|
314
|
+
return entries
|
|
315
|
+
.map(e => e.doc)
|
|
316
|
+
.filter(s => s.active && s.category === category)
|
|
317
|
+
.map(toSkillDescriptor)
|
|
474
318
|
} catch (err) {
|
|
475
319
|
log.error(`[skill-selector] Failed to fetch skills by category:`, err)
|
|
476
320
|
return []
|