superintent 0.0.1
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 +226 -0
- package/bin/superintent.js +2 -0
- package/dist/commands/extract.d.ts +2 -0
- package/dist/commands/extract.js +66 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/knowledge.d.ts +2 -0
- package/dist/commands/knowledge.js +647 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +153 -0
- package/dist/commands/spec.d.ts +2 -0
- package/dist/commands/spec.js +283 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/ticket.d.ts +4 -0
- package/dist/commands/ticket.js +942 -0
- package/dist/commands/ui.d.ts +2 -0
- package/dist/commands/ui.js +954 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +26 -0
- package/dist/db/init-schema.d.ts +2 -0
- package/dist/db/init-schema.js +28 -0
- package/dist/db/parsers.d.ts +24 -0
- package/dist/db/parsers.js +79 -0
- package/dist/db/schema.d.ts +7 -0
- package/dist/db/schema.js +64 -0
- package/dist/db/usage.d.ts +8 -0
- package/dist/db/usage.js +24 -0
- package/dist/embed/model.d.ts +5 -0
- package/dist/embed/model.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +1 -0
- package/dist/ui/components/index.d.ts +6 -0
- package/dist/ui/components/index.js +13 -0
- package/dist/ui/components/knowledge.d.ts +33 -0
- package/dist/ui/components/knowledge.js +238 -0
- package/dist/ui/components/layout.d.ts +1 -0
- package/dist/ui/components/layout.js +323 -0
- package/dist/ui/components/search.d.ts +15 -0
- package/dist/ui/components/search.js +114 -0
- package/dist/ui/components/spec.d.ts +11 -0
- package/dist/ui/components/spec.js +253 -0
- package/dist/ui/components/ticket.d.ts +90 -0
- package/dist/ui/components/ticket.js +604 -0
- package/dist/ui/components/utils.d.ts +26 -0
- package/dist/ui/components/utils.js +34 -0
- package/dist/ui/styles.css +2 -0
- package/dist/utils/cli.d.ts +21 -0
- package/dist/utils/cli.js +31 -0
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/config.js +116 -0
- package/dist/utils/id.d.ts +6 -0
- package/dist/utils/id.js +13 -0
- package/dist/utils/io.d.ts +8 -0
- package/dist/utils/io.js +15 -0
- package/package.json +60 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getClient, closeClient } from '../db/client.js';
|
|
3
|
+
import { parseKnowledgeRow } from '../db/parsers.js';
|
|
4
|
+
import { embed } from '../embed/model.js';
|
|
5
|
+
import { readStdin } from '../utils/io.js';
|
|
6
|
+
import { generateId } from '../utils/id.js';
|
|
7
|
+
/**
|
|
8
|
+
* Parse markdown knowledge format matching SKILL.md:
|
|
9
|
+
*
|
|
10
|
+
* # {Title}
|
|
11
|
+
*
|
|
12
|
+
* **Namespace:** {project-namespace}
|
|
13
|
+
* **Category:** architecture|pattern|truth|principle|gotcha
|
|
14
|
+
* **Source:** discovery|ticket|manual
|
|
15
|
+
* **Origin Ticket:** {ticket-id} (optional, from ticket skill)
|
|
16
|
+
* **Origin Ticket Type:** {ticket-type} (optional, from ticket skill)
|
|
17
|
+
* **Confidence:** {0.75-0.95}
|
|
18
|
+
* **Scope:** new-only|global|backward-compatible|legacy-frozen
|
|
19
|
+
* **Tags:** {kebab-case, comma-separated}
|
|
20
|
+
*
|
|
21
|
+
* ## Content
|
|
22
|
+
*
|
|
23
|
+
* {content body}
|
|
24
|
+
*/
|
|
25
|
+
function parseMarkdownKnowledge(markdown) {
|
|
26
|
+
const lines = markdown.split('\n');
|
|
27
|
+
const result = {
|
|
28
|
+
title: '',
|
|
29
|
+
namespace: '',
|
|
30
|
+
source: 'manual',
|
|
31
|
+
confidence: 0.8,
|
|
32
|
+
scope: 'global',
|
|
33
|
+
content: '',
|
|
34
|
+
};
|
|
35
|
+
let inContent = false;
|
|
36
|
+
const contentLines = [];
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
// Parse title: # {Title}
|
|
40
|
+
if (trimmed.startsWith('# ') && !trimmed.startsWith('## ')) {
|
|
41
|
+
result.title = trimmed.substring(2).trim();
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Start content section — everything after ## Content is content
|
|
45
|
+
if (trimmed === '## Content') {
|
|
46
|
+
inContent = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Collect content lines
|
|
50
|
+
if (inContent) {
|
|
51
|
+
contentLines.push(line);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Parse metadata fields — **Field:** format only
|
|
55
|
+
if (trimmed.startsWith('**Namespace:**')) {
|
|
56
|
+
result.namespace = trimmed.replace('**Namespace:**', '').trim();
|
|
57
|
+
}
|
|
58
|
+
else if (trimmed.startsWith('**Category:**')) {
|
|
59
|
+
result.category = trimmed.replace('**Category:**', '').trim();
|
|
60
|
+
}
|
|
61
|
+
else if (trimmed.startsWith('**Source:**')) {
|
|
62
|
+
result.source = trimmed.replace('**Source:**', '').trim();
|
|
63
|
+
}
|
|
64
|
+
else if (trimmed.startsWith('**Origin Ticket:**')) {
|
|
65
|
+
result.originTicketId = trimmed.replace('**Origin Ticket:**', '').trim();
|
|
66
|
+
if (result.originTicketId)
|
|
67
|
+
result.source = 'ticket';
|
|
68
|
+
}
|
|
69
|
+
else if (trimmed.startsWith('**Origin Ticket Type:**')) {
|
|
70
|
+
const typeValue = trimmed.replace('**Origin Ticket Type:**', '').trim().toLowerCase();
|
|
71
|
+
if (['feature', 'bugfix', 'refactor', 'docs', 'chore', 'test'].includes(typeValue)) {
|
|
72
|
+
result.originTicketType = typeValue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (trimmed.startsWith('**Confidence:**')) {
|
|
76
|
+
result.confidence = parseFloat(trimmed.replace('**Confidence:**', '').trim());
|
|
77
|
+
}
|
|
78
|
+
else if (trimmed.startsWith('**Scope:**')) {
|
|
79
|
+
result.scope = trimmed.replace('**Scope:**', '').trim();
|
|
80
|
+
}
|
|
81
|
+
else if (trimmed.startsWith('**Tags:**')) {
|
|
82
|
+
const tagStr = trimmed.replace('**Tags:**', '').trim();
|
|
83
|
+
result.tags = tagStr.split(',').map(t => t.trim()).filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
result.content = contentLines.join('\n').trim();
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
export const knowledgeCommand = new Command('knowledge')
|
|
90
|
+
.description('Manage knowledge entries');
|
|
91
|
+
// Create subcommand
|
|
92
|
+
knowledgeCommand
|
|
93
|
+
.command('create')
|
|
94
|
+
.description('Create a new knowledge entry')
|
|
95
|
+
.option('--stdin', 'Read markdown from stdin')
|
|
96
|
+
.option('--title <title>', 'Knowledge title')
|
|
97
|
+
.option('--content <content>', 'Knowledge content')
|
|
98
|
+
.option('--namespace <namespace>', 'Project namespace (use domain, not "global")')
|
|
99
|
+
.option('--category <category>', 'Category: pattern|truth|principle|architecture')
|
|
100
|
+
.option('--tags <tags...>', 'Tags (kebab-case, intent-aware)')
|
|
101
|
+
.option('--source <source>', 'Source: ticket|discovery|manual', 'manual')
|
|
102
|
+
.option('--origin <ticketId>', 'Origin ticket ID (sets origin-type to ticket)')
|
|
103
|
+
.option('--confidence <n>', 'Confidence 0-1 (0.7-0.8 for patterns, 1.0 for invariants)', '0.8')
|
|
104
|
+
.option('--scope <scope>', 'Decision scope: new-only|backward-compatible|global|legacy-frozen', 'global')
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
try {
|
|
107
|
+
let id;
|
|
108
|
+
let title;
|
|
109
|
+
let content;
|
|
110
|
+
let namespace;
|
|
111
|
+
let category;
|
|
112
|
+
let tags;
|
|
113
|
+
let source;
|
|
114
|
+
let originTicketId;
|
|
115
|
+
let originTicketType;
|
|
116
|
+
let confidence;
|
|
117
|
+
let scope;
|
|
118
|
+
if (options.stdin) {
|
|
119
|
+
// Parse from stdin markdown
|
|
120
|
+
const markdown = await readStdin();
|
|
121
|
+
const parsed = parseMarkdownKnowledge(markdown);
|
|
122
|
+
// Field-level validation
|
|
123
|
+
const missing = [];
|
|
124
|
+
if (!parsed.title)
|
|
125
|
+
missing.push('title: Missing # Title header');
|
|
126
|
+
if (!parsed.namespace)
|
|
127
|
+
missing.push('namespace: Missing **Namespace:** field');
|
|
128
|
+
if (!parsed.content)
|
|
129
|
+
missing.push('content: Missing ## Content section or content is empty');
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
const response = {
|
|
132
|
+
success: false,
|
|
133
|
+
error: missing.join('; '),
|
|
134
|
+
};
|
|
135
|
+
console.log(JSON.stringify(response));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
id = generateId('KNOWLEDGE');
|
|
139
|
+
title = parsed.title;
|
|
140
|
+
content = parsed.content;
|
|
141
|
+
namespace = parsed.namespace;
|
|
142
|
+
category = parsed.category || null;
|
|
143
|
+
tags = parsed.tags || null;
|
|
144
|
+
source = parsed.source;
|
|
145
|
+
originTicketId = parsed.originTicketId || null;
|
|
146
|
+
originTicketType = parsed.originTicketType || null;
|
|
147
|
+
confidence = parsed.confidence;
|
|
148
|
+
scope = parsed.scope;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Use CLI options
|
|
152
|
+
if (!options.title || !options.namespace || !options.content) {
|
|
153
|
+
const response = {
|
|
154
|
+
success: false,
|
|
155
|
+
error: 'Required: --title, --namespace, --content (or use --stdin)',
|
|
156
|
+
};
|
|
157
|
+
console.log(JSON.stringify(response));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
id = generateId('KNOWLEDGE');
|
|
161
|
+
title = options.title;
|
|
162
|
+
content = options.content;
|
|
163
|
+
namespace = options.namespace;
|
|
164
|
+
category = options.category || null;
|
|
165
|
+
tags = options.tags || null;
|
|
166
|
+
source = options.origin ? 'ticket' : options.source;
|
|
167
|
+
originTicketId = options.origin || null;
|
|
168
|
+
originTicketType = null; // CLI doesn't support this yet, use stdin for full control
|
|
169
|
+
confidence = parseFloat(options.confidence);
|
|
170
|
+
scope = options.scope;
|
|
171
|
+
}
|
|
172
|
+
const client = await getClient();
|
|
173
|
+
// Generate embedding from title + content + tags
|
|
174
|
+
const tagsText = tags?.length ? ' ' + tags.join(' ') : '';
|
|
175
|
+
const textToEmbed = `${title} ${content}${tagsText}`;
|
|
176
|
+
const embedding = await embed(textToEmbed);
|
|
177
|
+
await client.execute({
|
|
178
|
+
sql: `INSERT INTO knowledge (
|
|
179
|
+
id, namespace, chunk_index, title, content, embedding,
|
|
180
|
+
category, tags, source, origin_ticket_id, origin_ticket_type, confidence, active, decision_scope
|
|
181
|
+
) VALUES (?, ?, 0, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
182
|
+
args: [
|
|
183
|
+
id,
|
|
184
|
+
namespace,
|
|
185
|
+
title,
|
|
186
|
+
content,
|
|
187
|
+
JSON.stringify(embedding),
|
|
188
|
+
category,
|
|
189
|
+
tags ? JSON.stringify(tags) : null,
|
|
190
|
+
source,
|
|
191
|
+
originTicketId,
|
|
192
|
+
originTicketType,
|
|
193
|
+
confidence,
|
|
194
|
+
scope,
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
// Bidirectional linking: update ticket's derived_knowledge
|
|
198
|
+
if (originTicketId) {
|
|
199
|
+
const ticketResult = await client.execute({
|
|
200
|
+
sql: 'SELECT derived_knowledge FROM tickets WHERE id = ?',
|
|
201
|
+
args: [originTicketId],
|
|
202
|
+
});
|
|
203
|
+
if (ticketResult.rows.length > 0) {
|
|
204
|
+
const row = ticketResult.rows[0];
|
|
205
|
+
const existing = row.derived_knowledge ? JSON.parse(row.derived_knowledge) : [];
|
|
206
|
+
existing.push(id);
|
|
207
|
+
await client.execute({
|
|
208
|
+
sql: 'UPDATE tickets SET derived_knowledge = ? WHERE id = ?',
|
|
209
|
+
args: [JSON.stringify(existing), originTicketId],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
closeClient();
|
|
214
|
+
const response = {
|
|
215
|
+
success: true,
|
|
216
|
+
data: { id, namespace, source, status: 'created' },
|
|
217
|
+
};
|
|
218
|
+
console.log(JSON.stringify(response));
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const response = {
|
|
222
|
+
success: false,
|
|
223
|
+
error: `Failed to create knowledge: ${error.message}`,
|
|
224
|
+
};
|
|
225
|
+
console.log(JSON.stringify(response));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Get subcommand
|
|
230
|
+
knowledgeCommand
|
|
231
|
+
.command('get')
|
|
232
|
+
.description('Get a knowledge entry by ID')
|
|
233
|
+
.argument('<id>', 'Knowledge ID')
|
|
234
|
+
.action(async (id) => {
|
|
235
|
+
try {
|
|
236
|
+
const client = await getClient();
|
|
237
|
+
const result = await client.execute({
|
|
238
|
+
sql: `SELECT id, namespace, chunk_index, title, content,
|
|
239
|
+
category, tags, source, origin_ticket_id, origin_ticket_type, confidence, active, decision_scope,
|
|
240
|
+
usage_count, last_used_at, created_at
|
|
241
|
+
FROM knowledge WHERE id = ?`,
|
|
242
|
+
args: [id],
|
|
243
|
+
});
|
|
244
|
+
closeClient();
|
|
245
|
+
if (result.rows.length === 0) {
|
|
246
|
+
const response = {
|
|
247
|
+
success: false,
|
|
248
|
+
error: `Knowledge ${id} not found`,
|
|
249
|
+
};
|
|
250
|
+
console.log(JSON.stringify(response));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const knowledge = parseKnowledgeRow(result.rows[0]);
|
|
254
|
+
const response = {
|
|
255
|
+
success: true,
|
|
256
|
+
data: knowledge,
|
|
257
|
+
};
|
|
258
|
+
console.log(JSON.stringify(response));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const response = {
|
|
262
|
+
success: false,
|
|
263
|
+
error: `Failed to get knowledge: ${error.message}`,
|
|
264
|
+
};
|
|
265
|
+
console.log(JSON.stringify(response));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// List subcommand
|
|
270
|
+
knowledgeCommand
|
|
271
|
+
.command('list')
|
|
272
|
+
.description('List knowledge entries')
|
|
273
|
+
.option('--namespace <namespace>', 'Filter by namespace')
|
|
274
|
+
.option('--category <category>', 'Filter by category')
|
|
275
|
+
.option('--scope <scope>', 'Filter by decision scope (new-only|backward-compatible|global|legacy-frozen)')
|
|
276
|
+
.option('--source <source>', 'Filter by source (ticket|discovery|manual)')
|
|
277
|
+
.option('--status <status>', 'Filter by status (active|inactive|all)', 'active')
|
|
278
|
+
.option('--limit <n>', 'Limit results', '20')
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
try {
|
|
281
|
+
const client = await getClient();
|
|
282
|
+
const conditions = [];
|
|
283
|
+
const args = [];
|
|
284
|
+
if (options.status === 'active') {
|
|
285
|
+
conditions.push('active = 1');
|
|
286
|
+
}
|
|
287
|
+
else if (options.status === 'inactive') {
|
|
288
|
+
conditions.push('active = 0');
|
|
289
|
+
}
|
|
290
|
+
// 'all' = no filter on active
|
|
291
|
+
if (options.namespace) {
|
|
292
|
+
conditions.push('namespace = ?');
|
|
293
|
+
args.push(options.namespace);
|
|
294
|
+
}
|
|
295
|
+
if (options.category) {
|
|
296
|
+
conditions.push('category = ?');
|
|
297
|
+
args.push(options.category);
|
|
298
|
+
}
|
|
299
|
+
if (options.scope) {
|
|
300
|
+
conditions.push('decision_scope = ?');
|
|
301
|
+
args.push(options.scope);
|
|
302
|
+
}
|
|
303
|
+
if (options.source) {
|
|
304
|
+
conditions.push('source = ?');
|
|
305
|
+
args.push(options.source);
|
|
306
|
+
}
|
|
307
|
+
let sql = `SELECT id, namespace, chunk_index, title, content,
|
|
308
|
+
category, tags, source, origin_ticket_id, origin_ticket_type, confidence, active, decision_scope,
|
|
309
|
+
usage_count, last_used_at, created_at
|
|
310
|
+
FROM knowledge`;
|
|
311
|
+
if (conditions.length > 0) {
|
|
312
|
+
sql += ` WHERE ${conditions.join(' AND ')}`;
|
|
313
|
+
}
|
|
314
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
315
|
+
args.push(parseInt(options.limit, 10));
|
|
316
|
+
const result = await client.execute({ sql, args });
|
|
317
|
+
closeClient();
|
|
318
|
+
const knowledge = result.rows.map((row) => parseKnowledgeRow(row));
|
|
319
|
+
const response = {
|
|
320
|
+
success: true,
|
|
321
|
+
data: knowledge,
|
|
322
|
+
};
|
|
323
|
+
console.log(JSON.stringify(response));
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const response = {
|
|
327
|
+
success: false,
|
|
328
|
+
error: `Failed to list knowledge: ${error.message}`,
|
|
329
|
+
};
|
|
330
|
+
console.log(JSON.stringify(response));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Update subcommand
|
|
335
|
+
knowledgeCommand
|
|
336
|
+
.command('update')
|
|
337
|
+
.description('Update a knowledge entry')
|
|
338
|
+
.argument('<id>', 'Knowledge ID')
|
|
339
|
+
.option('--title <title>', 'New title')
|
|
340
|
+
.option('--content-stdin', 'Read new content from stdin')
|
|
341
|
+
.option('--namespace <namespace>', 'New namespace')
|
|
342
|
+
.option('--category <category>', 'New category')
|
|
343
|
+
.option('--tags <tags...>', 'New tags')
|
|
344
|
+
.option('--origin <ticketId>', 'Origin ticket ID')
|
|
345
|
+
.option('--confidence <n>', 'Confidence score 0-1')
|
|
346
|
+
.option('--scope <scope>', 'Decision scope: new-only|backward-compatible|global|legacy-frozen')
|
|
347
|
+
.action(async (id, options) => {
|
|
348
|
+
try {
|
|
349
|
+
const client = await getClient();
|
|
350
|
+
// Read content from stdin — parse as full markdown if it has ## Content section
|
|
351
|
+
let stdinContent;
|
|
352
|
+
let stdinParsed;
|
|
353
|
+
if (options.contentStdin) {
|
|
354
|
+
const raw = await readStdin();
|
|
355
|
+
if (raw.includes('## Content')) {
|
|
356
|
+
// Full knowledge markdown format — parse all fields
|
|
357
|
+
stdinParsed = parseMarkdownKnowledge(raw);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Plain content text
|
|
361
|
+
stdinContent = raw;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Build dynamic update
|
|
365
|
+
const updates = [];
|
|
366
|
+
const args = [];
|
|
367
|
+
// CLI flags take priority over parsed stdin fields
|
|
368
|
+
if (options.title) {
|
|
369
|
+
updates.push('title = ?');
|
|
370
|
+
args.push(options.title);
|
|
371
|
+
}
|
|
372
|
+
else if (stdinParsed?.title) {
|
|
373
|
+
updates.push('title = ?');
|
|
374
|
+
args.push(stdinParsed.title);
|
|
375
|
+
}
|
|
376
|
+
if (stdinParsed?.content) {
|
|
377
|
+
updates.push('content = ?');
|
|
378
|
+
args.push(stdinParsed.content);
|
|
379
|
+
}
|
|
380
|
+
else if (stdinContent) {
|
|
381
|
+
updates.push('content = ?');
|
|
382
|
+
args.push(stdinContent);
|
|
383
|
+
}
|
|
384
|
+
if (options.namespace) {
|
|
385
|
+
updates.push('namespace = ?');
|
|
386
|
+
args.push(options.namespace);
|
|
387
|
+
}
|
|
388
|
+
else if (stdinParsed?.namespace) {
|
|
389
|
+
updates.push('namespace = ?');
|
|
390
|
+
args.push(stdinParsed.namespace);
|
|
391
|
+
}
|
|
392
|
+
if (options.category) {
|
|
393
|
+
updates.push('category = ?');
|
|
394
|
+
args.push(options.category);
|
|
395
|
+
}
|
|
396
|
+
else if (stdinParsed?.category) {
|
|
397
|
+
updates.push('category = ?');
|
|
398
|
+
args.push(stdinParsed.category);
|
|
399
|
+
}
|
|
400
|
+
if (options.tags) {
|
|
401
|
+
updates.push('tags = ?');
|
|
402
|
+
args.push(JSON.stringify(options.tags));
|
|
403
|
+
}
|
|
404
|
+
else if (stdinParsed?.tags?.length) {
|
|
405
|
+
updates.push('tags = ?');
|
|
406
|
+
args.push(JSON.stringify(stdinParsed.tags));
|
|
407
|
+
}
|
|
408
|
+
if (options.origin) {
|
|
409
|
+
updates.push('origin_ticket_id = ?');
|
|
410
|
+
args.push(options.origin);
|
|
411
|
+
}
|
|
412
|
+
else if (stdinParsed?.originTicketId) {
|
|
413
|
+
updates.push('origin_ticket_id = ?');
|
|
414
|
+
args.push(stdinParsed.originTicketId);
|
|
415
|
+
}
|
|
416
|
+
if (options.confidence) {
|
|
417
|
+
updates.push('confidence = ?');
|
|
418
|
+
args.push(parseFloat(options.confidence));
|
|
419
|
+
}
|
|
420
|
+
else if (stdinParsed?.confidence) {
|
|
421
|
+
updates.push('confidence = ?');
|
|
422
|
+
args.push(stdinParsed.confidence);
|
|
423
|
+
}
|
|
424
|
+
if (options.scope) {
|
|
425
|
+
updates.push('decision_scope = ?');
|
|
426
|
+
args.push(options.scope);
|
|
427
|
+
}
|
|
428
|
+
else if (stdinParsed?.scope) {
|
|
429
|
+
updates.push('decision_scope = ?');
|
|
430
|
+
args.push(stdinParsed.scope);
|
|
431
|
+
}
|
|
432
|
+
if (updates.length === 0) {
|
|
433
|
+
const response = {
|
|
434
|
+
success: false,
|
|
435
|
+
error: 'No fields to update',
|
|
436
|
+
};
|
|
437
|
+
console.log(JSON.stringify(response));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
// Re-generate embedding if title, content, or tags changed
|
|
441
|
+
if (options.title || stdinContent || stdinParsed || options.tags) {
|
|
442
|
+
const current = await client.execute({
|
|
443
|
+
sql: 'SELECT title, content, tags FROM knowledge WHERE id = ?',
|
|
444
|
+
args: [id],
|
|
445
|
+
});
|
|
446
|
+
if (current.rows.length > 0) {
|
|
447
|
+
const row = current.rows[0];
|
|
448
|
+
const newTitle = options.title || row.title;
|
|
449
|
+
const newContent = stdinContent || row.content;
|
|
450
|
+
const newTags = options.tags || (row.tags ? JSON.parse(row.tags) : []);
|
|
451
|
+
const tagsText = newTags?.length ? ' ' + newTags.join(' ') : '';
|
|
452
|
+
const embedding = await embed(`${newTitle} ${newContent}${tagsText}`);
|
|
453
|
+
updates.push('embedding = vector32(?)');
|
|
454
|
+
args.push(JSON.stringify(embedding));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
updates.push("updated_at = datetime('now')");
|
|
458
|
+
args.push(id);
|
|
459
|
+
const sql = `UPDATE knowledge SET ${updates.join(', ')} WHERE id = ?`;
|
|
460
|
+
const result = await client.execute({ sql, args });
|
|
461
|
+
closeClient();
|
|
462
|
+
if (result.rowsAffected === 0) {
|
|
463
|
+
const response = {
|
|
464
|
+
success: false,
|
|
465
|
+
error: `Knowledge ${id} not found`,
|
|
466
|
+
};
|
|
467
|
+
console.log(JSON.stringify(response));
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
const response = {
|
|
471
|
+
success: true,
|
|
472
|
+
data: { id, status: 'updated', updated: Object.keys(options).filter(k => options[k]) },
|
|
473
|
+
};
|
|
474
|
+
console.log(JSON.stringify(response));
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
const response = {
|
|
478
|
+
success: false,
|
|
479
|
+
error: `Failed to update knowledge: ${error.message}`,
|
|
480
|
+
};
|
|
481
|
+
console.log(JSON.stringify(response));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// Deactivate subcommand
|
|
486
|
+
knowledgeCommand
|
|
487
|
+
.command('deactivate')
|
|
488
|
+
.description('Mark a knowledge entry as inactive')
|
|
489
|
+
.argument('<id>', 'Knowledge ID')
|
|
490
|
+
.action(async (id) => {
|
|
491
|
+
try {
|
|
492
|
+
const client = await getClient();
|
|
493
|
+
const result = await client.execute({
|
|
494
|
+
sql: 'UPDATE knowledge SET active = 0 WHERE id = ?',
|
|
495
|
+
args: [id],
|
|
496
|
+
});
|
|
497
|
+
closeClient();
|
|
498
|
+
if (result.rowsAffected === 0) {
|
|
499
|
+
const response = {
|
|
500
|
+
success: false,
|
|
501
|
+
error: `Knowledge ${id} not found`,
|
|
502
|
+
};
|
|
503
|
+
console.log(JSON.stringify(response));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
const response = {
|
|
507
|
+
success: true,
|
|
508
|
+
data: { id, status: 'deactivated' },
|
|
509
|
+
};
|
|
510
|
+
console.log(JSON.stringify(response));
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
const response = {
|
|
514
|
+
success: false,
|
|
515
|
+
error: `Failed to deactivate knowledge: ${error.message}`,
|
|
516
|
+
};
|
|
517
|
+
console.log(JSON.stringify(response));
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
// Activate subcommand
|
|
522
|
+
knowledgeCommand
|
|
523
|
+
.command('activate')
|
|
524
|
+
.description('Mark a knowledge entry as active')
|
|
525
|
+
.argument('<id>', 'Knowledge ID')
|
|
526
|
+
.action(async (id) => {
|
|
527
|
+
try {
|
|
528
|
+
const client = await getClient();
|
|
529
|
+
const result = await client.execute({
|
|
530
|
+
sql: 'UPDATE knowledge SET active = 1 WHERE id = ?',
|
|
531
|
+
args: [id],
|
|
532
|
+
});
|
|
533
|
+
closeClient();
|
|
534
|
+
if (result.rowsAffected === 0) {
|
|
535
|
+
const response = {
|
|
536
|
+
success: false,
|
|
537
|
+
error: `Knowledge ${id} not found`,
|
|
538
|
+
};
|
|
539
|
+
console.log(JSON.stringify(response));
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
const response = {
|
|
543
|
+
success: true,
|
|
544
|
+
data: { id, status: 'activated' },
|
|
545
|
+
};
|
|
546
|
+
console.log(JSON.stringify(response));
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const response = {
|
|
550
|
+
success: false,
|
|
551
|
+
error: `Failed to activate knowledge: ${error.message}`,
|
|
552
|
+
};
|
|
553
|
+
console.log(JSON.stringify(response));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
// Recalculate confidence subcommand
|
|
558
|
+
knowledgeCommand
|
|
559
|
+
.command('recalculate')
|
|
560
|
+
.description('Recalculate confidence scores based on usage patterns')
|
|
561
|
+
.option('--dry-run', 'Preview changes without applying')
|
|
562
|
+
.action(async (options) => {
|
|
563
|
+
try {
|
|
564
|
+
const client = await getClient();
|
|
565
|
+
// Fetch all active knowledge with usage data
|
|
566
|
+
const result = await client.execute({
|
|
567
|
+
sql: `SELECT id, title, confidence, usage_count, last_used_at, created_at
|
|
568
|
+
FROM knowledge WHERE active = 1`,
|
|
569
|
+
args: [],
|
|
570
|
+
});
|
|
571
|
+
const now = new Date();
|
|
572
|
+
const adjustments = [];
|
|
573
|
+
for (const row of result.rows) {
|
|
574
|
+
const id = row.id;
|
|
575
|
+
const title = row.title;
|
|
576
|
+
const currentConfidence = row.confidence;
|
|
577
|
+
const usageCount = row.usage_count || 0;
|
|
578
|
+
const lastUsedAt = row.last_used_at;
|
|
579
|
+
const createdAt = row.created_at;
|
|
580
|
+
let adjustment = 0;
|
|
581
|
+
const reasons = [];
|
|
582
|
+
// Usage-based growth
|
|
583
|
+
if (usageCount > 10) {
|
|
584
|
+
adjustment += 0.10;
|
|
585
|
+
reasons.push(`high usage (${usageCount}): +0.10`);
|
|
586
|
+
}
|
|
587
|
+
else if (usageCount > 5) {
|
|
588
|
+
adjustment += 0.05;
|
|
589
|
+
reasons.push(`good usage (${usageCount}): +0.05`);
|
|
590
|
+
}
|
|
591
|
+
// Staleness-based decay
|
|
592
|
+
const referenceDate = lastUsedAt || createdAt;
|
|
593
|
+
if (referenceDate) {
|
|
594
|
+
const daysSince = Math.floor((now.getTime() - new Date(referenceDate).getTime()) / (1000 * 60 * 60 * 24));
|
|
595
|
+
if (daysSince > 180) {
|
|
596
|
+
adjustment -= 0.20;
|
|
597
|
+
reasons.push(`very stale (${daysSince}d): -0.20`);
|
|
598
|
+
}
|
|
599
|
+
else if (daysSince > 90) {
|
|
600
|
+
adjustment -= 0.10;
|
|
601
|
+
reasons.push(`stale (${daysSince}d): -0.10`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Skip if no adjustment needed
|
|
605
|
+
if (adjustment === 0)
|
|
606
|
+
continue;
|
|
607
|
+
// Calculate new confidence, clamped between 0.1 and 1.0
|
|
608
|
+
const newConfidence = Math.max(0.1, Math.min(1.0, currentConfidence + adjustment));
|
|
609
|
+
// Skip if no actual change (already at bounds)
|
|
610
|
+
if (Math.abs(newConfidence - currentConfidence) < 0.001)
|
|
611
|
+
continue;
|
|
612
|
+
adjustments.push({
|
|
613
|
+
id,
|
|
614
|
+
title: title.slice(0, 50),
|
|
615
|
+
oldConfidence: currentConfidence,
|
|
616
|
+
newConfidence: Math.round(newConfidence * 100) / 100,
|
|
617
|
+
reason: reasons.join(', '),
|
|
618
|
+
});
|
|
619
|
+
// Apply update unless dry-run
|
|
620
|
+
if (!options.dryRun) {
|
|
621
|
+
await client.execute({
|
|
622
|
+
sql: 'UPDATE knowledge SET confidence = ? WHERE id = ?',
|
|
623
|
+
args: [newConfidence, id],
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
closeClient();
|
|
628
|
+
const response = {
|
|
629
|
+
success: true,
|
|
630
|
+
data: {
|
|
631
|
+
dryRun: !!options.dryRun,
|
|
632
|
+
total: result.rows.length,
|
|
633
|
+
adjusted: adjustments.length,
|
|
634
|
+
adjustments,
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
console.log(JSON.stringify(response));
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
const response = {
|
|
641
|
+
success: false,
|
|
642
|
+
error: `Failed to recalculate confidence: ${error.message}`,
|
|
643
|
+
};
|
|
644
|
+
console.log(JSON.stringify(response));
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
});
|