@abarcenas/ai-workflow-template 1.34.0 → 1.38.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/.agents/instructions/knowledgebase.instructions.md +238 -0
- package/.agents/instructions/learned-knowledge.instructions.md +71 -0
- package/.husky/post-commit +10 -0
- package/.husky/post-merge +0 -1
- package/.opencode/agents/architect.agent.md +1 -0
- package/.opencode/agents/coder.agent.md +1 -0
- package/.opencode/agents/deployer.agent.md +1 -0
- package/.opencode/agents/designer.agent.md +1 -0
- package/.opencode/agents/e2e-tester.agent.md +1 -0
- package/.opencode/agents/implementer.agent.md +1 -0
- package/.opencode/agents/orchestrator/feature-pipeline.agent.md +1 -0
- package/.opencode/agents/orchestrator/orchestrator.agent.md +1 -0
- package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +1 -0
- package/.opencode/agents/researcher.agent.md +1 -0
- package/.opencode/agents/reviewer.agent.md +1 -0
- package/.opencode/agents/tracker.agent.md +2 -0
- package/.opencode/agents/unit-tester.agent.md +1 -0
- package/README.md +1 -1
- package/memory-bank/.vocabulary.json +8 -2
- package/package.json +12 -6
- package/scripts/knowledgebase-cli.js +228 -0
- package/scripts/knowledgebase-cli.test.js +576 -0
- package/scripts/knowledgebase-index.js +780 -0
- package/scripts/knowledgebase-index.test.js +759 -0
- package/scripts/knowledgebase-init.sql +193 -0
- package/scripts/mcp-knowledgebase-server.js +330 -0
- package/scripts/setup/constants.js +36 -3
- package/scripts/setup/hooks.js +2 -2
- package/scripts/setup/hooks.test.js +13 -5
- package/scripts/setup/index.js +29 -2
- package/scripts/setup/index.test.js +36 -1
- package/scripts/setup/knowledgebase.js +204 -0
- package/scripts/setup/knowledgebase.test.js +248 -0
- package/scripts/sync.js +40 -6
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
-- knowledgebase-init.sql — Manual Database Provisioning Script
|
|
3
|
+
-- ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
--
|
|
5
|
+
-- Purpose:
|
|
6
|
+
-- Creates the PostgreSQL + pgvector schema for the centralized
|
|
7
|
+
-- cross-project knowledgebase. This is the MANUAL fallback when
|
|
8
|
+
-- auto-provision via knowledgebase-index.js fails due to insufficient
|
|
9
|
+
-- superuser privileges for CREATE EXTENSION vector.
|
|
10
|
+
--
|
|
11
|
+
-- Usage:
|
|
12
|
+
-- psql $DATABASE_URL -f scripts/knowledgebase-init.sql
|
|
13
|
+
--
|
|
14
|
+
-- Auto-provision fallback:
|
|
15
|
+
-- The core engine (knowledgebase-index.js) attempts the same DDL
|
|
16
|
+
-- programmatically via pg.Pool.query(). If it fails on CREATE EXTENSION
|
|
17
|
+
-- (common on managed PostgreSQL where you lack superuser), the error
|
|
18
|
+
-- message directs you to run this script manually.
|
|
19
|
+
--
|
|
20
|
+
-- Requirements:
|
|
21
|
+
-- - PostgreSQL 16+ with pgvector extension installed
|
|
22
|
+
-- - A user with CREATEROLE or superuser for CREATE EXTENSION
|
|
23
|
+
-- - A database already created (e.g. createdb knowledgebase)
|
|
24
|
+
--
|
|
25
|
+
-- Schema:
|
|
26
|
+
-- Two tables with HNSW vector index for cosine similarity search.
|
|
27
|
+
-- See docs/adr-knowledgebase-pgvector.md §2 for full design rationale.
|
|
28
|
+
--
|
|
29
|
+
-- ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
-- ══ 1. Extension ══════════════════════════════════════════════════════════
|
|
32
|
+
-- Requires superuser privileges. On managed PostgreSQL (Supabase, Neon,
|
|
33
|
+
-- Aiven, etc.), pgvector is typically pre-installed. If you see:
|
|
34
|
+
-- ERROR: permission denied to create extension "vector"
|
|
35
|
+
-- ask your database admin to run: CREATE EXTENSION IF NOT EXISTS vector;
|
|
36
|
+
|
|
37
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
38
|
+
|
|
39
|
+
-- ══ 2. Projects table ═════════════════════════════════════════════════════
|
|
40
|
+
-- Each consumer project is registered once. The project_id is derived from
|
|
41
|
+
-- the consumer's package.json "name" field, which is stable across sessions
|
|
42
|
+
-- and unique within an organization.
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
45
|
+
id TEXT PRIMARY KEY, -- package.json "name" field (e.g. "my-consumer-app")
|
|
46
|
+
name TEXT NOT NULL, -- human-readable display name
|
|
47
|
+
first_indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
48
|
+
last_indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- ══ 3. Knowledge chunks table ════════════════════════════════════════════
|
|
52
|
+
-- Each row represents one session's "New knowledge" bullet set from a
|
|
53
|
+
-- learned-knowledge.instructions.md file. Content is embedded using
|
|
54
|
+
-- all-MiniLM-L6-v2 (384 dimensions) for cosine similarity search.
|
|
55
|
+
--
|
|
56
|
+
-- The uq_knowledge_chunk constraint ensures idempotency: re-indexing the
|
|
57
|
+
-- same project + session + content produces no duplicates.
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
|
60
|
+
id BIGSERIAL PRIMARY KEY,
|
|
61
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
62
|
+
session_date DATE NOT NULL, -- from ## Session: heading
|
|
63
|
+
session_title TEXT, -- full heading text (e.g. "Session: 2026-07-24 — Feature Pipeline")
|
|
64
|
+
pipeline TEXT, -- from **Pipeline:** field
|
|
65
|
+
coverage TEXT, -- from **Coverage:** field
|
|
66
|
+
tdd_iterations INTEGER NOT NULL DEFAULT 0, -- from **TDD Iterations:** field
|
|
67
|
+
content TEXT NOT NULL, -- the "New knowledge" bullet text
|
|
68
|
+
content_hash TEXT NOT NULL, -- SHA-256(content + session_date + pipeline)
|
|
69
|
+
embedding VECTOR(384), -- from all-MiniLM-L6-v2 (384d)
|
|
70
|
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
71
|
+
|
|
72
|
+
-- Idempotency: same project + session date + content hash → skip silently
|
|
73
|
+
CONSTRAINT uq_knowledge_chunk UNIQUE (project_id, session_date, content_hash)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- ══ 4. Indexes ═══════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
-- ── 4a. HNSW index for approximate nearest neighbor (ANN) search ─────────
|
|
79
|
+
-- Enables fast cosine similarity lookups (~1ms for 10k vectors).
|
|
80
|
+
-- Uses vector_cosine_ops operator class for the <=> (cosine distance) operator.
|
|
81
|
+
-- Parameters:
|
|
82
|
+
-- m = 16 — max connections per element (higher = more accurate but slower build)
|
|
83
|
+
-- ef_construction = 64 — dynamic candidate list size during build
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_embedding
|
|
86
|
+
ON knowledge_chunks
|
|
87
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
88
|
+
WITH (m = 16, ef_construction = 64);
|
|
89
|
+
|
|
90
|
+
-- ── 4b. B-tree indexes for structured queries and filters ────────────────
|
|
91
|
+
|
|
92
|
+
-- Filter knowledge by project (used by knowledgebase_search tool)
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_project_id
|
|
94
|
+
ON knowledge_chunks (project_id);
|
|
95
|
+
|
|
96
|
+
-- Temporal queries: "show me knowledge from the last month"
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_session_date
|
|
98
|
+
ON knowledge_chunks (session_date DESC);
|
|
99
|
+
|
|
100
|
+
-- Filter by pipeline type (e.g. "TDD Infrastructure Bootstrap", "Feature Pipeline")
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_pipeline
|
|
102
|
+
ON knowledge_chunks (pipeline);
|
|
103
|
+
|
|
104
|
+
-- Recently indexed projects (used by knowledgebase_list tool)
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_projects_last_indexed
|
|
106
|
+
ON projects (last_indexed_at DESC);
|
|
107
|
+
|
|
108
|
+
-- ══ 5. Match function ════════════════════════════════════════════════════
|
|
109
|
+
-- Convenience function for cosine similarity search with optional
|
|
110
|
+
-- project_id filtering. Returns results ordered by similarity descending.
|
|
111
|
+
--
|
|
112
|
+
-- Usage examples:
|
|
113
|
+
--
|
|
114
|
+
-- -- Find top 5 chunks similar to a query embedding
|
|
115
|
+
-- SELECT * FROM match_knowledge('[0.01, 0.02, ...]'::vector(384), 0.7, 5);
|
|
116
|
+
--
|
|
117
|
+
-- -- Filter to a specific project
|
|
118
|
+
-- SELECT * FROM match_knowledge('[0.01, 0.02, ...]'::vector(384), 0.7, 5, 'my-app');
|
|
119
|
+
--
|
|
120
|
+
-- -- Use with knowledgebase-index.js generated embeddings
|
|
121
|
+
-- -- (the core engine builds embedding vector from query text)
|
|
122
|
+
|
|
123
|
+
CREATE OR REPLACE FUNCTION match_knowledge(
|
|
124
|
+
query_embedding VECTOR(384),
|
|
125
|
+
match_threshold DOUBLE PRECISION, -- minimum similarity (0.0 to 1.0); 0.7 is a good default
|
|
126
|
+
match_count INTEGER, -- max results to return (1-20 recommended)
|
|
127
|
+
filter_project_id TEXT DEFAULT NULL -- optional: restrict to a single project
|
|
128
|
+
)
|
|
129
|
+
RETURNS TABLE(
|
|
130
|
+
id BIGINT,
|
|
131
|
+
project_id TEXT,
|
|
132
|
+
session_date DATE,
|
|
133
|
+
session_title TEXT,
|
|
134
|
+
pipeline TEXT,
|
|
135
|
+
coverage TEXT,
|
|
136
|
+
tdd_iterations INTEGER,
|
|
137
|
+
content TEXT,
|
|
138
|
+
content_hash TEXT,
|
|
139
|
+
similarity DOUBLE PRECISION,
|
|
140
|
+
indexed_at TIMESTAMPTZ
|
|
141
|
+
)
|
|
142
|
+
LANGUAGE SQL
|
|
143
|
+
STABLE
|
|
144
|
+
AS $$
|
|
145
|
+
SELECT
|
|
146
|
+
id,
|
|
147
|
+
project_id,
|
|
148
|
+
session_date,
|
|
149
|
+
session_title,
|
|
150
|
+
pipeline,
|
|
151
|
+
coverage,
|
|
152
|
+
tdd_iterations,
|
|
153
|
+
content,
|
|
154
|
+
content_hash,
|
|
155
|
+
1 - (embedding <=> query_embedding) AS similarity,
|
|
156
|
+
indexed_at
|
|
157
|
+
FROM knowledge_chunks
|
|
158
|
+
WHERE 1 - (embedding <=> query_embedding) > match_threshold
|
|
159
|
+
AND (filter_project_id IS NULL OR project_id = filter_project_id)
|
|
160
|
+
ORDER BY embedding <=> query_embedding
|
|
161
|
+
LIMIT match_count;
|
|
162
|
+
$$;
|
|
163
|
+
|
|
164
|
+
-- ══ 6. OpenAI upgrade path ═══════════════════════════════════════════════
|
|
165
|
+
-- The default embedding model is Xenova/all-MiniLM-L6-v2 (384 dimensions).
|
|
166
|
+
-- To upgrade to OpenAI text-embedding-3-small (1536 dimensions):
|
|
167
|
+
--
|
|
168
|
+
-- 1. Set OPENAI_API_KEY in your .env file
|
|
169
|
+
-- 2. Resize the embedding column (requires dropping and recreating the
|
|
170
|
+
-- HNSW index first — HNSW indexes cannot be built on expression columns)
|
|
171
|
+
-- 3. Rebuild the HNSW index for the new dimension
|
|
172
|
+
--
|
|
173
|
+
-- -- Step 1: Drop the old HNSW index (required before ALTER COLUMN TYPE)
|
|
174
|
+
-- DROP INDEX IF EXISTS idx_knowledge_chunks_embedding;
|
|
175
|
+
--
|
|
176
|
+
-- -- Step 2: Change column type to 1536 dimensions
|
|
177
|
+
-- ALTER TABLE knowledge_chunks ALTER COLUMN embedding TYPE VECTOR(1536);
|
|
178
|
+
--
|
|
179
|
+
-- -- Step 3: Rebuild the HNSW index
|
|
180
|
+
-- CREATE INDEX idx_knowledge_chunks_embedding
|
|
181
|
+
-- ON knowledge_chunks
|
|
182
|
+
-- USING hnsw (embedding vector_cosine_ops)
|
|
183
|
+
-- WITH (m = 16, ef_construction = 64);
|
|
184
|
+
--
|
|
185
|
+
-- ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
+
|
|
187
|
+
-- ══ Done ═════════════════════════════════════════════════════════════════
|
|
188
|
+
-- Verify installation by running:
|
|
189
|
+
-- \dt knowledge_chunks projects
|
|
190
|
+
-- \di idx_*
|
|
191
|
+
-- \df match_knowledge
|
|
192
|
+
-- SELECT * FROM match_knowledge('[0,0,0,0]'::vector(384), 0.0, 1);
|
|
193
|
+
-- (the last query should return 0 rows — confirming the function works)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mcp-knowledgebase-server.js — MCP server exposing knowledgebase tools to AI agents.
|
|
5
|
+
*
|
|
6
|
+
* Provides: knowledgebase_search, knowledgebase_index, knowledgebase_stats, knowledgebase_list
|
|
7
|
+
* Uses stdio transport — OpenCode spawns this as a child process.
|
|
8
|
+
*
|
|
9
|
+
* Configuration (opencode.json):
|
|
10
|
+
* "knowledgebase": {
|
|
11
|
+
* "type": "local",
|
|
12
|
+
* "command": ["node", "scripts/mcp-knowledgebase-server.js"],
|
|
13
|
+
* "enabled": true
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
18
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
+
import {
|
|
24
|
+
search,
|
|
25
|
+
upsertChunks,
|
|
26
|
+
chunkLearnedKnowledge,
|
|
27
|
+
getStats,
|
|
28
|
+
listProjects,
|
|
29
|
+
getPool,
|
|
30
|
+
closePool,
|
|
31
|
+
} from './knowledgebase-index.js';
|
|
32
|
+
import { readFileSync } from 'node:fs';
|
|
33
|
+
import { resolve } from 'node:path';
|
|
34
|
+
|
|
35
|
+
const server = new Server(
|
|
36
|
+
{
|
|
37
|
+
name: 'knowledgebase',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
capabilities: {
|
|
42
|
+
tools: {},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// ── List tools ───────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
50
|
+
tools: [
|
|
51
|
+
{
|
|
52
|
+
name: 'knowledgebase_search',
|
|
53
|
+
description:
|
|
54
|
+
'Semantic search across learned knowledge from all indexed projects. ' +
|
|
55
|
+
'Use this when planning tasks, encountering errors, or seeking patterns from past pipeline sessions.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
query: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description:
|
|
62
|
+
'Natural language query about patterns, conventions, gotchas, or agent behavior ' +
|
|
63
|
+
'(e.g. "how to handle verbose logging in child processes")',
|
|
64
|
+
},
|
|
65
|
+
projectId: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Optional: filter to a specific project by its package.json name',
|
|
68
|
+
},
|
|
69
|
+
threshold: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
default: 0.6,
|
|
72
|
+
description: 'Minimum similarity threshold (0.0–1.0). Higher values return fewer but more relevant results.',
|
|
73
|
+
},
|
|
74
|
+
limit: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
default: 5,
|
|
77
|
+
description: 'Number of results to return (1–50)',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ['query'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'knowledgebase_index',
|
|
85
|
+
description:
|
|
86
|
+
'Index or re-index a project\'s learned knowledge content. ' +
|
|
87
|
+
'Use after adding new sessions to learned-knowledge.instructions.md. ' +
|
|
88
|
+
'Idempotent — unchanged sessions are skipped.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
projectId: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Project identifier (from package.json "name")',
|
|
95
|
+
},
|
|
96
|
+
content: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description:
|
|
99
|
+
'Optional: markdown content of learned-knowledge.instructions.md to index. ' +
|
|
100
|
+
'If omitted, reads from .agents/instructions/learned-knowledge.instructions.md',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ['projectId'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'knowledgebase_stats',
|
|
108
|
+
description:
|
|
109
|
+
'Get statistics about the centralized knowledgebase, ' +
|
|
110
|
+
'including total projects, chunks, database size, and last sync timestamp.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'knowledgebase_list',
|
|
118
|
+
description:
|
|
119
|
+
'List all projects currently indexed in the knowledgebase. ' +
|
|
120
|
+
'Useful for discovering which projects have shared knowledge.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
// ── Handle tool calls ────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
132
|
+
const { name, arguments: args } = request.params;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
switch (name) {
|
|
136
|
+
case 'knowledgebase_search': {
|
|
137
|
+
const results = await search(args.query, {
|
|
138
|
+
project_id: args.projectId || undefined,
|
|
139
|
+
threshold: args.threshold || 0.6,
|
|
140
|
+
limit: args.limit || 5,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (results.length === 0) {
|
|
144
|
+
const pool = await getPool();
|
|
145
|
+
if (!pool) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: 'Knowledgebase not available: DATABASE_URL is not configured. ' +
|
|
150
|
+
'Set DATABASE_URL in your .env file to enable cross-project knowledge search.',
|
|
151
|
+
}],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
content: [{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: 'No results found. The knowledgebase may be empty.',
|
|
158
|
+
}],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const formatted = results
|
|
163
|
+
.map((r, i) =>
|
|
164
|
+
`### Result ${i + 1} (similarity: ${r.similarity})\n` +
|
|
165
|
+
`**Project:** \`${r.project_id}\`\n` +
|
|
166
|
+
`**Date:** ${r.session_date}\n` +
|
|
167
|
+
`**Pipeline:** ${r.pipeline || 'N/A'}\n\n` +
|
|
168
|
+
`${r.content}`
|
|
169
|
+
)
|
|
170
|
+
.join('\n\n---\n\n');
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: 'text', text: formatted }],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'knowledgebase_index': {
|
|
178
|
+
const projectId = args.projectId;
|
|
179
|
+
if (!projectId) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: 'Error: projectId is required for knowledgebase_index.',
|
|
184
|
+
}],
|
|
185
|
+
isError: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let content = args.content;
|
|
190
|
+
if (!content) {
|
|
191
|
+
try {
|
|
192
|
+
content = readFileSync(
|
|
193
|
+
resolve(
|
|
194
|
+
process.cwd(),
|
|
195
|
+
'.agents/instructions/learned-knowledge.instructions.md'
|
|
196
|
+
),
|
|
197
|
+
'utf8'
|
|
198
|
+
);
|
|
199
|
+
} catch {
|
|
200
|
+
return {
|
|
201
|
+
content: [{
|
|
202
|
+
type: 'text',
|
|
203
|
+
text: 'No content provided and the default file ' +
|
|
204
|
+
'.agents/instructions/learned-knowledge.instructions.md ' +
|
|
205
|
+
'could not be read. Provide the "content" parameter or ensure the file exists.',
|
|
206
|
+
}],
|
|
207
|
+
isError: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const chunks = chunkLearnedKnowledge(content, projectId);
|
|
213
|
+
if (chunks.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
content: [{
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: `No valid knowledge chunks found for project "${projectId}". ` +
|
|
218
|
+
'Ensure the content contains "## Session:" headers with ' +
|
|
219
|
+
'"**New knowledge:**" sections.',
|
|
220
|
+
}],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await upsertChunks(chunks);
|
|
225
|
+
return {
|
|
226
|
+
content: [{
|
|
227
|
+
type: 'text',
|
|
228
|
+
text:
|
|
229
|
+
`Indexed ${result.inserted} chunks, updated ${result.updated}, ` +
|
|
230
|
+
`skipped ${result.skipped} for project "${projectId}".`,
|
|
231
|
+
}],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'knowledgebase_stats': {
|
|
236
|
+
const stats = await getStats();
|
|
237
|
+
|
|
238
|
+
if (stats.total_chunks === 0 && stats.total_projects === 0) {
|
|
239
|
+
const pool = await getPool();
|
|
240
|
+
if (!pool) {
|
|
241
|
+
return {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: 'Knowledgebase not available: DATABASE_URL is not configured.',
|
|
245
|
+
}],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text:
|
|
254
|
+
`**Knowledgebase Stats**\n` +
|
|
255
|
+
`- Total projects: ${stats.total_projects}\n` +
|
|
256
|
+
`- Total chunks: ${stats.total_chunks}\n` +
|
|
257
|
+
`- Database size: ${stats.db_size}\n` +
|
|
258
|
+
`- Last sync: ${stats.last_sync || 'Never'}`,
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case 'knowledgebase_list': {
|
|
264
|
+
const projects = await listProjects();
|
|
265
|
+
|
|
266
|
+
if (projects.length === 0) {
|
|
267
|
+
const pool = await getPool();
|
|
268
|
+
if (!pool) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: 'Knowledgebase not available: DATABASE_URL is not configured.',
|
|
273
|
+
}],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: 'No projects indexed yet.',
|
|
280
|
+
}],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const formatted = projects
|
|
285
|
+
.map((p) =>
|
|
286
|
+
`- ${p.name || p.project_id} (${p.chunk_count} chunks, ` +
|
|
287
|
+
`last indexed: ${p.last_indexed || 'Never'})`
|
|
288
|
+
)
|
|
289
|
+
.join('\n');
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
content: [{
|
|
293
|
+
type: 'text',
|
|
294
|
+
text: `**Indexed Projects:**\n${formatted}`,
|
|
295
|
+
}],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
default:
|
|
300
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{
|
|
305
|
+
type: 'text',
|
|
306
|
+
text: `Error: ${err.message}`,
|
|
307
|
+
}],
|
|
308
|
+
isError: true,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── Start server ─────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
async function main() {
|
|
316
|
+
const transport = new StdioServerTransport();
|
|
317
|
+
await server.connect(transport);
|
|
318
|
+
console.error('[knowledgebase-mcp] Server started via stdio transport');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
process.on('SIGTERM', () => {
|
|
322
|
+
console.error('[knowledgebase-mcp] SIGTERM received, closing pool');
|
|
323
|
+
closePool().catch(() => {});
|
|
324
|
+
process.exit(0);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
main().catch((err) => {
|
|
328
|
+
console.error('[knowledgebase-mcp] Fatal error:', err);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
});
|
|
@@ -41,8 +41,14 @@ export const HOOK_MERGE_SEPARATOR = '# --- @abarcenas/ai-workflow-template ---'
|
|
|
41
41
|
export const TEMPLATE_HOOKS = {
|
|
42
42
|
'pre-commit': {
|
|
43
43
|
source: '.husky/pre-commit',
|
|
44
|
-
content:
|
|
45
|
-
'# Managed by @abarcenas/ai-workflow-template setup
|
|
44
|
+
content: [
|
|
45
|
+
'# Managed by @abarcenas/ai-workflow-template setup',
|
|
46
|
+
'#!/bin/sh',
|
|
47
|
+
'',
|
|
48
|
+
'node scripts/bump-version.js',
|
|
49
|
+
'node scripts/validate-memory-schema.js',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n'),
|
|
46
52
|
description:
|
|
47
53
|
'Auto-bump version + validate memory-bank schema before commits',
|
|
48
54
|
},
|
|
@@ -51,7 +57,6 @@ export const TEMPLATE_HOOKS = {
|
|
|
51
57
|
content: [
|
|
52
58
|
'# Managed by @abarcenas/ai-workflow-template setup',
|
|
53
59
|
'#!/bin/sh',
|
|
54
|
-
'. "$(dirname "$0")/_/husky.sh"',
|
|
55
60
|
'',
|
|
56
61
|
'# After git pull/merge, check if memory-bank files changed',
|
|
57
62
|
'# and update the vector index incrementally',
|
|
@@ -65,6 +70,26 @@ export const TEMPLATE_HOOKS = {
|
|
|
65
70
|
description:
|
|
66
71
|
'Auto-update memory bank vector index after pull/merge',
|
|
67
72
|
},
|
|
73
|
+
'post-commit': {
|
|
74
|
+
source: '.husky/post-commit',
|
|
75
|
+
content: [
|
|
76
|
+
'# Managed by @abarcenas/ai-workflow-template setup',
|
|
77
|
+
'#!/bin/sh',
|
|
78
|
+
'',
|
|
79
|
+
'# After commit, check if learned knowledge file changed',
|
|
80
|
+
'# and sync to the centralized knowledgebase',
|
|
81
|
+
'',
|
|
82
|
+
'CHANGED=$(git diff HEAD~1 --name-only 2>/dev/null | grep ".agents/instructions/learned-knowledge.instructions.md")',
|
|
83
|
+
'',
|
|
84
|
+
'if [ -n "$CHANGED" ]; then',
|
|
85
|
+
' echo "[knowledgebase] Learned knowledge file changed. Syncing to knowledgebase..."',
|
|
86
|
+
' node scripts/knowledgebase-cli.js sync 2>/dev/null || echo "[knowledgebase] Sync skipped (DATABASE_URL not configured or service unavailable)"',
|
|
87
|
+
'fi',
|
|
88
|
+
'',
|
|
89
|
+
].join('\n'),
|
|
90
|
+
description:
|
|
91
|
+
'Auto-sync learned knowledge to centralized knowledgebase after commits',
|
|
92
|
+
},
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
// ── CI/CD environment detection ───────────────────────────────────────────────
|
|
@@ -113,6 +138,7 @@ export const SUPPORTED_FLAGS = {
|
|
|
113
138
|
'--skip-hooks': 'skipHooks',
|
|
114
139
|
'--skip-prepare': 'skipPrepare',
|
|
115
140
|
'--skip-sync': 'skipSync',
|
|
141
|
+
'--skip-knowledgebase': 'skipKnowledgebase',
|
|
116
142
|
'--help': 'help',
|
|
117
143
|
'-h': 'help',
|
|
118
144
|
'--version': 'version',
|
|
@@ -222,5 +248,12 @@ export const MEMORY_BANK_STUBS = {
|
|
|
222
248
|
- **To Build**: {{what remains}}
|
|
223
249
|
- **Status**: {{overall project status}}
|
|
224
250
|
- **Known Issues**: {{list known issues}}
|
|
251
|
+
`,
|
|
252
|
+
'learned-knowledge.instructions.md': `# Learned Knowledge
|
|
253
|
+
|
|
254
|
+
Cross-session knowledge accumulated by agent pipelines. Each session records discoveries, patterns, gotchas, and agent tuning notes for future reference.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
225
258
|
`,
|
|
226
259
|
}
|
package/scripts/setup/hooks.js
CHANGED
|
@@ -52,8 +52,8 @@ function buildHookContent(hookName) {
|
|
|
52
52
|
const template = TEMPLATE_HOOKS[hookName]
|
|
53
53
|
// The `content` field is always a string in our constants (pre-composed).
|
|
54
54
|
const raw = template.content
|
|
55
|
-
//
|
|
56
|
-
return
|
|
55
|
+
// Template content already has the marker on its first line.
|
|
56
|
+
return raw
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ── Hook file path helper ────────────────────────────────────────────────────
|
|
@@ -66,15 +66,16 @@ function cleanupTempDirs() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
* expected file content without exporting a private function.
|
|
69
|
+
* Returns the expected hook file content as produced by `buildHookContent()`.
|
|
71
70
|
*
|
|
72
|
-
*
|
|
71
|
+
* The template content in `TEMPLATE_HOOKS` already includes `HOOK_MARKER`
|
|
72
|
+
* as its first line, so this just returns the raw content directly.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} hookName - Key from `TEMPLATE_HOOKS` ('pre-commit' | 'post-merge' | 'post-commit')
|
|
73
75
|
* @returns {string} The full hook file content as written by hooks.js.
|
|
74
76
|
*/
|
|
75
77
|
function expectedContent(hookName) {
|
|
76
|
-
|
|
77
|
-
return HOOK_MARKER + '\n' + raw
|
|
78
|
+
return TEMPLATE_HOOKS[hookName].content
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -169,6 +170,7 @@ describe('installHooks', () => {
|
|
|
169
170
|
existingHooks: {
|
|
170
171
|
'pre-commit': { exists: true, isManaged: true },
|
|
171
172
|
'post-merge': { exists: true, isManaged: true },
|
|
173
|
+
'post-commit': { exists: true, isManaged: true },
|
|
172
174
|
},
|
|
173
175
|
})
|
|
174
176
|
const huskyDir = join(ctx.consumerRoot, '.husky')
|
|
@@ -203,6 +205,7 @@ describe('installHooks', () => {
|
|
|
203
205
|
existingHooks: {
|
|
204
206
|
'pre-commit': { exists: true, isManaged: false },
|
|
205
207
|
'post-merge': { exists: true, isManaged: false },
|
|
208
|
+
'post-commit': { exists: true, isManaged: false },
|
|
206
209
|
},
|
|
207
210
|
})
|
|
208
211
|
const huskyDir = join(ctx.consumerRoot, '.husky')
|
|
@@ -240,6 +243,7 @@ describe('installHooks', () => {
|
|
|
240
243
|
existingHooks: {
|
|
241
244
|
'pre-commit': { exists: true, isManaged: false },
|
|
242
245
|
'post-merge': { exists: true, isManaged: false },
|
|
246
|
+
'post-commit': { exists: true, isManaged: false },
|
|
243
247
|
},
|
|
244
248
|
})
|
|
245
249
|
const huskyDir = join(ctx.consumerRoot, '.husky')
|
|
@@ -400,12 +404,14 @@ describe('installHooks', () => {
|
|
|
400
404
|
existingHooks: {
|
|
401
405
|
'pre-commit': { exists: true, isManaged: false },
|
|
402
406
|
'post-merge': { exists: true, isManaged: false },
|
|
407
|
+
'post-commit': { exists: true, isManaged: false },
|
|
403
408
|
},
|
|
404
409
|
})
|
|
405
410
|
const huskyDir = join(ctx.consumerRoot, '.husky')
|
|
406
411
|
mkdirSync(huskyDir)
|
|
407
412
|
writeFileSync(join(huskyDir, 'pre-commit'), '# orig\n')
|
|
408
413
|
writeFileSync(join(huskyDir, 'post-merge'), '# orig\n')
|
|
414
|
+
writeFileSync(join(huskyDir, 'post-commit'), '# orig\n')
|
|
409
415
|
|
|
410
416
|
const results = await installHooks(ctx)
|
|
411
417
|
|
|
@@ -414,6 +420,7 @@ describe('installHooks', () => {
|
|
|
414
420
|
}
|
|
415
421
|
expect(existsSync(join(huskyDir, 'pre-commit.bak'))).toBe(true)
|
|
416
422
|
expect(existsSync(join(huskyDir, 'post-merge.bak'))).toBe(true)
|
|
423
|
+
expect(existsSync(join(huskyDir, 'post-commit.bak'))).toBe(true)
|
|
417
424
|
})
|
|
418
425
|
|
|
419
426
|
// ── 12. Partial existingHooks map ─────────────────────────────────────
|
|
@@ -424,6 +431,7 @@ describe('installHooks', () => {
|
|
|
424
431
|
existingHooks: {
|
|
425
432
|
'pre-commit': { exists: true, isManaged: true },
|
|
426
433
|
// post-merge absent → treated as exists=false, isManaged=false
|
|
434
|
+
// post-commit absent → treated as exists=false, isManaged=false
|
|
427
435
|
},
|
|
428
436
|
})
|
|
429
437
|
const huskyDir = join(ctx.consumerRoot, '.husky')
|