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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/bin/superintent.js +2 -0
  4. package/dist/commands/extract.d.ts +2 -0
  5. package/dist/commands/extract.js +66 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +56 -0
  8. package/dist/commands/knowledge.d.ts +2 -0
  9. package/dist/commands/knowledge.js +647 -0
  10. package/dist/commands/search.d.ts +2 -0
  11. package/dist/commands/search.js +153 -0
  12. package/dist/commands/spec.d.ts +2 -0
  13. package/dist/commands/spec.js +283 -0
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +43 -0
  16. package/dist/commands/ticket.d.ts +4 -0
  17. package/dist/commands/ticket.js +942 -0
  18. package/dist/commands/ui.d.ts +2 -0
  19. package/dist/commands/ui.js +954 -0
  20. package/dist/db/client.d.ts +4 -0
  21. package/dist/db/client.js +26 -0
  22. package/dist/db/init-schema.d.ts +2 -0
  23. package/dist/db/init-schema.js +28 -0
  24. package/dist/db/parsers.d.ts +24 -0
  25. package/dist/db/parsers.js +79 -0
  26. package/dist/db/schema.d.ts +7 -0
  27. package/dist/db/schema.js +64 -0
  28. package/dist/db/usage.d.ts +8 -0
  29. package/dist/db/usage.js +24 -0
  30. package/dist/embed/model.d.ts +5 -0
  31. package/dist/embed/model.js +34 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/types.d.ts +120 -0
  35. package/dist/types.js +1 -0
  36. package/dist/ui/components/index.d.ts +6 -0
  37. package/dist/ui/components/index.js +13 -0
  38. package/dist/ui/components/knowledge.d.ts +33 -0
  39. package/dist/ui/components/knowledge.js +238 -0
  40. package/dist/ui/components/layout.d.ts +1 -0
  41. package/dist/ui/components/layout.js +323 -0
  42. package/dist/ui/components/search.d.ts +15 -0
  43. package/dist/ui/components/search.js +114 -0
  44. package/dist/ui/components/spec.d.ts +11 -0
  45. package/dist/ui/components/spec.js +253 -0
  46. package/dist/ui/components/ticket.d.ts +90 -0
  47. package/dist/ui/components/ticket.js +604 -0
  48. package/dist/ui/components/utils.d.ts +26 -0
  49. package/dist/ui/components/utils.js +34 -0
  50. package/dist/ui/styles.css +2 -0
  51. package/dist/utils/cli.d.ts +21 -0
  52. package/dist/utils/cli.js +31 -0
  53. package/dist/utils/config.d.ts +12 -0
  54. package/dist/utils/config.js +116 -0
  55. package/dist/utils/id.d.ts +6 -0
  56. package/dist/utils/id.js +13 -0
  57. package/dist/utils/io.d.ts +8 -0
  58. package/dist/utils/io.js +15 -0
  59. package/package.json +60 -0
@@ -0,0 +1,153 @@
1
+ import { Command } from 'commander';
2
+ import { getClient, closeClient } from '../db/client.js';
3
+ import { parseSearchRow } from '../db/parsers.js';
4
+ import { trackUsage } from '../db/usage.js';
5
+ import { embed } from '../embed/model.js';
6
+ export const searchCommand = new Command('search')
7
+ .description('Semantic search knowledge base')
8
+ .argument('<query>', 'Search query')
9
+ .option('--namespace <namespace>', 'Filter by namespace (project)')
10
+ .option('--category <category>', 'Filter by category')
11
+ .option('--ticket-type <type>', 'Filter by origin ticket type (feature|bugfix|refactor|docs|chore|test)')
12
+ .option('--tags <tags...>', 'Filter by tags (OR logic)')
13
+ .option('--min-score <n>', 'Minimum similarity score 0-1', '0')
14
+ .option('--limit <n>', 'Max results', '5')
15
+ .action(async (query, options) => {
16
+ try {
17
+ const client = await getClient();
18
+ // Generate query embedding
19
+ const queryEmbedding = await embed(query);
20
+ // Build query with filters
21
+ const conditions = ['k.active = 1'];
22
+ const limit = parseInt(options.limit, 10);
23
+ const topK = limit * 2; // Fetch extra for filtering
24
+ const args = [
25
+ JSON.stringify(queryEmbedding),
26
+ JSON.stringify(queryEmbedding),
27
+ ];
28
+ if (options.namespace) {
29
+ conditions.push('k.namespace = ?');
30
+ args.push(options.namespace);
31
+ }
32
+ if (options.category) {
33
+ conditions.push('k.category = ?');
34
+ args.push(options.category);
35
+ }
36
+ if (options.ticketType) {
37
+ conditions.push('k.origin_ticket_type = ?');
38
+ args.push(options.ticketType);
39
+ }
40
+ // Build SQL with vector search (k must be literal, not bound parameter)
41
+ const sql = `
42
+ SELECT
43
+ k.id, k.namespace, k.chunk_index, k.title, k.content,
44
+ k.category, k.tags, k.source, k.origin_ticket_id, k.origin_ticket_type, k.confidence, k.active, k.decision_scope,
45
+ k.usage_count, k.last_used_at, k.created_at,
46
+ vector_distance_cos(k.embedding, vector32(?)) as distance
47
+ FROM vector_top_k('knowledge_embedding_idx', vector32(?), ${topK}) AS v
48
+ JOIN knowledge k ON k.rowid = v.id
49
+ WHERE ${conditions.join(' AND ')}
50
+ ORDER BY distance ASC
51
+ LIMIT ${limit}
52
+ `;
53
+ const result = await client.execute({ sql, args });
54
+ // Filter by min score and tags
55
+ const minScore = parseFloat(options.minScore);
56
+ let results = result.rows
57
+ .map((row) => parseSearchRow(row))
58
+ .filter((r) => r.score >= minScore);
59
+ // Filter by tags (OR logic) if provided
60
+ if (options.tags && options.tags.length > 0) {
61
+ results = results.filter((r) => {
62
+ if (!r.tags)
63
+ return false;
64
+ return options.tags.some((tag) => r.tags.includes(tag));
65
+ });
66
+ }
67
+ // Track usage for returned results
68
+ await trackUsage(results.map(r => r.id));
69
+ closeClient();
70
+ const response = {
71
+ success: true,
72
+ data: {
73
+ query,
74
+ results,
75
+ },
76
+ };
77
+ console.log(JSON.stringify(response));
78
+ }
79
+ catch (error) {
80
+ // Fallback to non-indexed search if vector index doesn't exist or fails
81
+ const errorMessage = error.message;
82
+ if (errorMessage.includes('vector_top_k') || errorMessage.includes('vector index') || errorMessage.includes('no such table')) {
83
+ try {
84
+ const client = await getClient();
85
+ const queryEmbedding = await embed(query);
86
+ const conditions = ['active = 1'];
87
+ const args = [JSON.stringify(queryEmbedding)];
88
+ if (options.namespace) {
89
+ conditions.push('namespace = ?');
90
+ args.push(options.namespace);
91
+ }
92
+ if (options.category) {
93
+ conditions.push('category = ?');
94
+ args.push(options.category);
95
+ }
96
+ if (options.ticketType) {
97
+ conditions.push('origin_ticket_type = ?');
98
+ args.push(options.ticketType);
99
+ }
100
+ const sql = `
101
+ SELECT
102
+ id, namespace, chunk_index, title, content,
103
+ category, tags, source, origin_ticket_id, origin_ticket_type, confidence, active, decision_scope,
104
+ usage_count, last_used_at, created_at,
105
+ vector_distance_cos(embedding, vector32(?)) as distance
106
+ FROM knowledge
107
+ WHERE ${conditions.join(' AND ')}
108
+ ORDER BY distance ASC
109
+ LIMIT ?
110
+ `;
111
+ args.push(parseInt(options.limit, 10));
112
+ const result = await client.execute({ sql, args });
113
+ const minScore = parseFloat(options.minScore);
114
+ let results = result.rows
115
+ .map((row) => parseSearchRow(row))
116
+ .filter((r) => r.score >= minScore);
117
+ if (options.tags && options.tags.length > 0) {
118
+ results = results.filter((r) => {
119
+ if (!r.tags)
120
+ return false;
121
+ return options.tags.some((tag) => r.tags.includes(tag));
122
+ });
123
+ }
124
+ // Track usage for returned results
125
+ await trackUsage(results.map(r => r.id));
126
+ closeClient();
127
+ const response = {
128
+ success: true,
129
+ data: {
130
+ query,
131
+ results,
132
+ },
133
+ };
134
+ console.log(JSON.stringify(response));
135
+ return;
136
+ }
137
+ catch (fallbackError) {
138
+ const response = {
139
+ success: false,
140
+ error: `Search failed: ${fallbackError.message}`,
141
+ };
142
+ console.log(JSON.stringify(response));
143
+ process.exit(1);
144
+ }
145
+ }
146
+ const response = {
147
+ success: false,
148
+ error: `Search failed: ${errorMessage}`,
149
+ };
150
+ console.log(JSON.stringify(response));
151
+ process.exit(1);
152
+ }
153
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const specCommand: Command;
@@ -0,0 +1,283 @@
1
+ import { Command } from 'commander';
2
+ import { getClient, closeClient } from '../db/client.js';
3
+ import { parseSpecRow } from '../db/parsers.js';
4
+ import { readStdin } from '../utils/io.js';
5
+ import { generateId } from '../utils/id.js';
6
+ /**
7
+ * Parse markdown spec format matching SKILL.md:
8
+ *
9
+ * # {spec name}
10
+ *
11
+ * ## Summary
12
+ * ...rest of spec content...
13
+ */
14
+ function parseMarkdownSpec(markdown) {
15
+ const lines = markdown.split('\n');
16
+ const result = { title: '', content: '' };
17
+ let contentStartIndex = 0;
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const trimmed = lines[i].trim();
20
+ // Parse title: # {spec name}
21
+ if (trimmed.startsWith('# ') && !trimmed.startsWith('## ')) {
22
+ result.title = trimmed.substring(2).trim();
23
+ contentStartIndex = i + 1;
24
+ break;
25
+ }
26
+ }
27
+ // Everything after the title line is content
28
+ result.content = lines.slice(contentStartIndex).join('\n').trim();
29
+ return result;
30
+ }
31
+ export const specCommand = new Command('spec')
32
+ .description('Manage specs');
33
+ // Create subcommand
34
+ specCommand
35
+ .command('create')
36
+ .description('Create a new spec from stdin')
37
+ .option('--stdin', 'Read spec markdown from stdin')
38
+ .option('--title <title>', 'Spec title')
39
+ .option('--content <content>', 'Spec content')
40
+ .action(async (options) => {
41
+ try {
42
+ let id;
43
+ let title;
44
+ let content;
45
+ if (options.stdin) {
46
+ const markdown = await readStdin();
47
+ const parsed = parseMarkdownSpec(markdown);
48
+ // Field-level validation
49
+ const missing = [];
50
+ if (!parsed.title)
51
+ missing.push('title: Missing # Title header');
52
+ if (!parsed.content)
53
+ missing.push('content: No content found after header');
54
+ if (missing.length > 0) {
55
+ const response = {
56
+ success: false,
57
+ error: missing.join('; '),
58
+ };
59
+ console.log(JSON.stringify(response));
60
+ process.exit(1);
61
+ }
62
+ id = generateId('SPEC');
63
+ title = parsed.title;
64
+ content = parsed.content;
65
+ }
66
+ else {
67
+ if (!options.title) {
68
+ const response = {
69
+ success: false,
70
+ error: 'Required: --title (or use --stdin)',
71
+ };
72
+ console.log(JSON.stringify(response));
73
+ process.exit(1);
74
+ }
75
+ id = generateId('SPEC');
76
+ title = options.title;
77
+ content = options.content || '';
78
+ }
79
+ const client = await getClient();
80
+ await client.execute({
81
+ sql: `INSERT INTO specs (id, title, content) VALUES (?, ?, ?)`,
82
+ args: [id, title, content],
83
+ });
84
+ closeClient();
85
+ const response = {
86
+ success: true,
87
+ data: { id, status: 'created' },
88
+ };
89
+ console.log(JSON.stringify(response));
90
+ }
91
+ catch (error) {
92
+ const response = {
93
+ success: false,
94
+ error: `Failed to create spec: ${error.message}`,
95
+ };
96
+ console.log(JSON.stringify(response));
97
+ process.exit(1);
98
+ }
99
+ });
100
+ // Get subcommand
101
+ specCommand
102
+ .command('get')
103
+ .description('Get a spec by ID')
104
+ .argument('<id>', 'Spec ID')
105
+ .action(async (id) => {
106
+ try {
107
+ const client = await getClient();
108
+ const result = await client.execute({
109
+ sql: 'SELECT * FROM specs WHERE id = ?',
110
+ args: [id],
111
+ });
112
+ closeClient();
113
+ if (result.rows.length === 0) {
114
+ const response = {
115
+ success: false,
116
+ error: `Spec ${id} not found`,
117
+ };
118
+ console.log(JSON.stringify(response));
119
+ process.exit(1);
120
+ }
121
+ const spec = parseSpecRow(result.rows[0]);
122
+ const response = {
123
+ success: true,
124
+ data: spec,
125
+ };
126
+ console.log(JSON.stringify(response));
127
+ }
128
+ catch (error) {
129
+ const response = {
130
+ success: false,
131
+ error: `Failed to get spec: ${error.message}`,
132
+ };
133
+ console.log(JSON.stringify(response));
134
+ process.exit(1);
135
+ }
136
+ });
137
+ // List subcommand
138
+ specCommand
139
+ .command('list')
140
+ .description('List specs')
141
+ .option('--limit <n>', 'Limit results', '20')
142
+ .action(async (options) => {
143
+ try {
144
+ const client = await getClient();
145
+ const result = await client.execute({
146
+ sql: 'SELECT * FROM specs ORDER BY created_at DESC LIMIT ?',
147
+ args: [parseInt(options.limit, 10)],
148
+ });
149
+ closeClient();
150
+ const specs = result.rows.map((row) => parseSpecRow(row));
151
+ const response = {
152
+ success: true,
153
+ data: specs,
154
+ };
155
+ console.log(JSON.stringify(response));
156
+ }
157
+ catch (error) {
158
+ const response = {
159
+ success: false,
160
+ error: `Failed to list specs: ${error.message}`,
161
+ };
162
+ console.log(JSON.stringify(response));
163
+ process.exit(1);
164
+ }
165
+ });
166
+ // Update subcommand
167
+ specCommand
168
+ .command('update')
169
+ .description('Update a spec')
170
+ .argument('<id>', 'Spec ID')
171
+ .option('--title <title>', 'New title')
172
+ .option('--content-stdin', 'Read new content from stdin')
173
+ .action(async (id, options) => {
174
+ try {
175
+ const client = await getClient();
176
+ const updates = [];
177
+ const args = [];
178
+ // Read stdin — parse as full spec markdown if it has a # title
179
+ let stdinParsed;
180
+ if (options.contentStdin) {
181
+ const raw = await readStdin();
182
+ const parsed = parseMarkdownSpec(raw);
183
+ if (parsed.title) {
184
+ stdinParsed = parsed;
185
+ }
186
+ else {
187
+ // Plain content text (no title header)
188
+ updates.push('content = ?');
189
+ args.push(raw.trim());
190
+ }
191
+ }
192
+ if (options.title) {
193
+ updates.push('title = ?');
194
+ args.push(options.title);
195
+ }
196
+ else if (stdinParsed?.title) {
197
+ updates.push('title = ?');
198
+ args.push(stdinParsed.title);
199
+ }
200
+ if (stdinParsed?.content) {
201
+ updates.push('content = ?');
202
+ args.push(stdinParsed.content);
203
+ }
204
+ if (updates.length === 0) {
205
+ closeClient();
206
+ const response = {
207
+ success: false,
208
+ error: 'No updates provided. Use --title or --content-stdin',
209
+ };
210
+ console.log(JSON.stringify(response));
211
+ process.exit(1);
212
+ }
213
+ updates.push("updated_at = datetime('now')");
214
+ args.push(id);
215
+ const result = await client.execute({
216
+ sql: `UPDATE specs SET ${updates.join(', ')} WHERE id = ?`,
217
+ args,
218
+ });
219
+ closeClient();
220
+ if (result.rowsAffected === 0) {
221
+ const response = {
222
+ success: false,
223
+ error: `Spec ${id} not found`,
224
+ };
225
+ console.log(JSON.stringify(response));
226
+ process.exit(1);
227
+ }
228
+ const response = {
229
+ success: true,
230
+ data: { id, status: 'updated' },
231
+ };
232
+ console.log(JSON.stringify(response));
233
+ }
234
+ catch (error) {
235
+ const response = {
236
+ success: false,
237
+ error: `Failed to update spec: ${error.message}`,
238
+ };
239
+ console.log(JSON.stringify(response));
240
+ process.exit(1);
241
+ }
242
+ });
243
+ // Delete subcommand
244
+ specCommand
245
+ .command('delete')
246
+ .description('Delete a spec by ID')
247
+ .argument('<id>', 'Spec ID')
248
+ .action(async (id) => {
249
+ try {
250
+ const client = await getClient();
251
+ const existing = await client.execute({
252
+ sql: 'SELECT id FROM specs WHERE id = ?',
253
+ args: [id],
254
+ });
255
+ if (existing.rows.length === 0) {
256
+ closeClient();
257
+ const response = {
258
+ success: false,
259
+ error: `Spec ${id} not found`,
260
+ };
261
+ console.log(JSON.stringify(response));
262
+ process.exit(1);
263
+ }
264
+ await client.execute({
265
+ sql: 'DELETE FROM specs WHERE id = ?',
266
+ args: [id],
267
+ });
268
+ closeClient();
269
+ const response = {
270
+ success: true,
271
+ data: { id, status: 'deleted' },
272
+ };
273
+ console.log(JSON.stringify(response));
274
+ }
275
+ catch (error) {
276
+ const response = {
277
+ success: false,
278
+ error: `Failed to delete spec: ${error.message}`,
279
+ };
280
+ console.log(JSON.stringify(response));
281
+ process.exit(1);
282
+ }
283
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const statusCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig, configExists } from '../utils/config.js';
3
+ import { getClient, closeClient } from '../db/client.js';
4
+ export const statusCommand = new Command('status')
5
+ .description('Check Turso connection status')
6
+ .action(async () => {
7
+ try {
8
+ // Check config
9
+ if (!configExists()) {
10
+ const response = {
11
+ success: false,
12
+ error: 'Not configured. Run: superintent init --url <url> --token <token>',
13
+ };
14
+ console.log(JSON.stringify(response));
15
+ process.exit(1);
16
+ }
17
+ const config = loadConfig();
18
+ // Test connection
19
+ const client = await getClient();
20
+ // Get counts
21
+ const ticketCount = await client.execute('SELECT COUNT(*) as count FROM tickets');
22
+ const knowledgeCount = await client.execute('SELECT COUNT(*) as count FROM knowledge');
23
+ closeClient();
24
+ const response = {
25
+ success: true,
26
+ data: {
27
+ connected: true,
28
+ url: config.url.replace(/\/\/.*:.*@/, '//***@'), // Hide token in URL if present
29
+ tickets: ticketCount.rows[0].count,
30
+ knowledge: knowledgeCount.rows[0].count,
31
+ },
32
+ };
33
+ console.log(JSON.stringify(response));
34
+ }
35
+ catch (error) {
36
+ const response = {
37
+ success: false,
38
+ error: `Connection failed: ${error.message}`,
39
+ };
40
+ console.log(JSON.stringify(response));
41
+ process.exit(1);
42
+ }
43
+ });
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import type { Ticket, KnowledgeInput } from '../types.js';
3
+ export declare function generateExtractProposals(ticket: Ticket, namespace: string): KnowledgeInput[];
4
+ export declare const ticketCommand: Command;