create-bk-agent 0.20.0 → 0.21.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # create-bk-agent
2
2
 
3
+ **Status: stable** — works and is maintained; will not be expanded during the pilot phase.
4
+
3
5
  Scaffold a new [BackendKit agent](https://github.com/BackendKit-labs/backendkit-agent-framework) project in seconds — or add capabilities to an existing one.
4
6
 
5
7
  ## Create a project
@@ -11,7 +13,7 @@ npx create-bk-agent my-project
11
13
  The CLI asks three questions and generates a production-ready project:
12
14
 
13
15
  ```
14
- ✨ create-bk-agent v0.19.1
16
+ ✨ create-bk-agent v0.20.0
15
17
 
16
18
  ? Project name: my-project
17
19
  ? Architecture: Multi-agent (orchestrator + specialists)
@@ -53,12 +53,16 @@ function detectProject(cwd) {
53
53
  };
54
54
  if (!deps['@backendkit-labs/agent-core'])
55
55
  return null;
56
+ const isEnterprise = !!deps['@backendkit-labs/agent-enterprise'];
56
57
  const engineFile = path.join(cwd, 'src', 'engine.ts');
57
58
  let architecture = 'unknown';
58
59
  let engineFnName = 'createEngine';
59
60
  if (fs.existsSync(engineFile)) {
60
61
  const content = fs.readFileSync(engineFile, 'utf-8');
61
- if (content.includes('ORCHESTRATOR_PROFILE') || content.includes("id: 'orchestrator'")) {
62
+ if (isEnterprise && content.includes('ObsidianRAGProvider')) {
63
+ architecture = 'enterprise';
64
+ }
65
+ else if (content.includes('ORCHESTRATOR_PROFILE') || content.includes("id: 'orchestrator'")) {
62
66
  architecture = 'multi-agent';
63
67
  }
64
68
  else if (content.includes('export function create')) {
@@ -76,5 +80,6 @@ function detectProject(cwd) {
76
80
  serverFile: path.join(cwd, 'src', 'server.ts'),
77
81
  pkgFile,
78
82
  engineFnName,
83
+ isEnterprise,
79
84
  };
80
85
  }
@@ -7,6 +7,7 @@ const specialist_1 = require("./modules/specialist");
7
7
  const tools_1 = require("./modules/tools");
8
8
  const mcp_client_1 = require("./modules/mcp-client");
9
9
  const plugin_1 = require("./modules/plugin");
10
+ const knowledge_1 = require("./modules/knowledge");
10
11
  const HELP = `
11
12
  Usage: npx create-bk-agent add <module> [options]
12
13
 
@@ -21,6 +22,9 @@ Modules:
21
22
  tools <domain> Add a tool file for a new domain
22
23
  (creates src/tools/<domain>-tools.ts and patches engine.ts)
23
24
 
25
+ knowledge <path> Add a folder or vault to the RAG knowledge base (enterprise agents)
26
+ --vault <path> Register an entirely new Obsidian vault (instead of a subfolder)
27
+
24
28
  mcp-client <url> Add a remote MCP server to the engine
25
29
  --name <s> Name for the mcpServers entry (default: last URL segment)
26
30
 
@@ -31,6 +35,9 @@ Examples:
31
35
  npx create-bk-agent add server --port 4000
32
36
  npx create-bk-agent add specialist data-analyst
33
37
  npx create-bk-agent add tools github
38
+ npx create-bk-agent add knowledge ./docs
39
+ npx create-bk-agent add knowledge "04-Recursos/Backend"
40
+ npx create-bk-agent add knowledge --vault "C:/Company/HR-Vault"
34
41
  npx create-bk-agent add mcp-client http://localhost:3001/mcp --name billing
35
42
  npx create-bk-agent add plugin docker
36
43
  npx create-bk-agent add plugin k8s
@@ -68,6 +75,9 @@ function runAdd(args, cwd) {
68
75
  case 'plugin':
69
76
  (0, plugin_1.addPlugin)(info, rest);
70
77
  break;
78
+ case 'knowledge':
79
+ (0, knowledge_1.addKnowledge)(info, rest);
80
+ break;
71
81
  default:
72
82
  console.error(`\n✗ Unknown module: "${module}"\n`);
73
83
  console.log(HELP);
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.addKnowledge = addKnowledge;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
40
+ const patcher_1 = require("../patcher");
41
+ function parseArgs(args) {
42
+ const vaultIdx = args.indexOf('--vault');
43
+ if (vaultIdx !== -1) {
44
+ const target = args[vaultIdx + 1];
45
+ if (!target)
46
+ return null;
47
+ return { target, isNewVault: true };
48
+ }
49
+ if (args[0])
50
+ return { target: args[0], isNewVault: false };
51
+ return null;
52
+ }
53
+ /**
54
+ * Adds `folderPath` to the `folders:` array inside ObsidianRAGProvider({...}).
55
+ *
56
+ * Handles three states of engine.ts:
57
+ * 1. `folders:` already exists (uncommented) → append entry
58
+ * 2. `// folders:` is commented out (template default) → uncomment + set
59
+ * 3. No `folders:` at all → insert before closing `}` of RAG constructor
60
+ */
61
+ function patchFolders(engineFile, folderPath) {
62
+ if (!fs.existsSync(engineFile))
63
+ return 'no_rag_found';
64
+ const content = fs.readFileSync(engineFile, 'utf-8');
65
+ if (!content.includes('ObsidianRAGProvider'))
66
+ return 'no_rag_found';
67
+ const normalized = folderPath.replace(/\\/g, '/');
68
+ if (content.includes(`'${normalized}'`))
69
+ return 'already_exists';
70
+ // Case 1: folders array already exists (uncommented)
71
+ if (/\bfolders:\s*\[/.test(content)) {
72
+ const patched = content.replace(/(\bfolders:\s*\[)([^\]]*)(\])/, (_full, open, inner, close) => {
73
+ const trimmed = inner.trimEnd();
74
+ const needsComma = trimmed.length > 0 && !trimmed.endsWith(',');
75
+ return `${open}${inner}${needsComma ? ',' : ''}\n '${normalized}',\n ${close}`;
76
+ });
77
+ if (patched !== content) {
78
+ fs.writeFileSync(engineFile, patched);
79
+ return 'added';
80
+ }
81
+ }
82
+ // Case 2: folders line is commented out (// TODO: ... \n // folders: [...])
83
+ if (content.includes('// folders:') || content.includes('// TODO: restrict indexing')) {
84
+ const patched = content.replace(/[ \t]*\/\/\s*TODO:\s*restrict indexing[^\n]*\n[ \t]*\/\/\s*folders:[^\n]*\n/, ` folders: ['${normalized}'],\n`);
85
+ if (patched !== content) {
86
+ fs.writeFileSync(engineFile, patched);
87
+ return 'added';
88
+ }
89
+ // Fallback: just the commented folders line
90
+ const patched2 = content.replace(/[ \t]*\/\/\s*folders:[^\n]*\n/, ` folders: ['${normalized}'],\n`);
91
+ if (patched2 !== content) {
92
+ fs.writeFileSync(engineFile, patched2);
93
+ return 'added';
94
+ }
95
+ }
96
+ // Case 3: no folders key — insert before closing `}` of RAG constructor
97
+ const lines = content.split('\n');
98
+ let inRag = false;
99
+ let depth = 0;
100
+ let closeIdx = -1;
101
+ for (let i = 0; i < lines.length; i++) {
102
+ if (lines[i].includes('new ObsidianRAGProvider(')) {
103
+ inRag = true;
104
+ depth = 0;
105
+ }
106
+ if (inRag) {
107
+ depth += (lines[i].match(/\{/g) ?? []).length;
108
+ depth -= (lines[i].match(/\}/g) ?? []).length;
109
+ if (depth === 0) {
110
+ closeIdx = i;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ if (closeIdx === -1)
116
+ return 'no_rag_found';
117
+ lines.splice(closeIdx, 0, ` folders: ['${normalized}'],`);
118
+ fs.writeFileSync(engineFile, lines.join('\n'));
119
+ return 'added';
120
+ }
121
+ /**
122
+ * Adds a second ObsidianRAGProvider for a completely new vault and merges
123
+ * its tool into the existing tools registry.
124
+ * Used when --vault flag is passed to add a new vault, not just a folder.
125
+ */
126
+ function addNewVault(engineFile, vaultPath) {
127
+ if (!fs.existsSync(engineFile))
128
+ return 'no_rag_found';
129
+ const content = fs.readFileSync(engineFile, 'utf-8');
130
+ if (!content.includes('ObsidianRAGProvider'))
131
+ return 'no_rag_found';
132
+ const normalized = vaultPath.replace(/\\/g, '/');
133
+ const varName = `rag_${path.basename(normalized).replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`;
134
+ if (content.includes(normalized))
135
+ return 'already_exists';
136
+ // Insert new RAG instance after the first one
137
+ const ragBlock = [
138
+ '',
139
+ ` // Additional vault: ${path.basename(normalized)}`,
140
+ ` const ${varName} = new ObsidianRAGProvider({`,
141
+ ` vaultPath: '${normalized}',`,
142
+ ` ollamaHost: config.ollamaHost,`,
143
+ ` embeddingModel: config.embeddingModel,`,
144
+ ` });`,
145
+ ].join('\n');
146
+ const patched = content.replace(/(const rag = new ObsidianRAGProvider\(\{[\s\S]*?\}\);)/, `$1\n${ragBlock}`);
147
+ // Register the new tool
148
+ const withTool = patched.replace(/tools\s*\n?\s*\.register\(rag\.createTool\(\)\)/, `tools\n .register(rag.createTool())\n .register(${varName}.createTool())`);
149
+ if (withTool === content)
150
+ return 'no_rag_found';
151
+ fs.writeFileSync(engineFile, withTool);
152
+ return 'added';
153
+ }
154
+ // ── Non-enterprise setup ──────────────────────────────────────────────────────
155
+ function setupRagFromScratch(info, vaultPath) {
156
+ console.log('\n📦 Installing @backendkit-labs/agent-enterprise ...');
157
+ const added = (0, patcher_1.addPackageDependency)(info.pkgFile, '@backendkit-labs/agent-enterprise', '^0.1.0');
158
+ if (added) {
159
+ (0, child_process_1.execSync)('npm install', { cwd: info.cwd, stdio: 'inherit' });
160
+ }
161
+ if (!fs.existsSync(info.engineFile)) {
162
+ console.log('\n⚠ src/engine.ts not found — add RAG manually:');
163
+ printManualInstructions(vaultPath);
164
+ return;
165
+ }
166
+ const content = fs.readFileSync(info.engineFile, 'utf-8');
167
+ if (content.includes('ObsidianRAGProvider')) {
168
+ console.log('\n⚠ ObsidianRAGProvider already present in engine.ts');
169
+ return;
170
+ }
171
+ // Add import
172
+ (0, patcher_1.addImportLine)(info.engineFile, `import { ObsidianRAGProvider } from '@backendkit-labs/agent-enterprise';`);
173
+ // Insert RAG instance + tool registration
174
+ const normalized = vaultPath.replace(/\\/g, '/');
175
+ const ragBlock = [
176
+ '',
177
+ ` // Knowledge base (RAG over Obsidian vault)`,
178
+ ` const rag = new ObsidianRAGProvider({`,
179
+ ` vaultPath: '${normalized}',`,
180
+ ` // ollamaHost: process.env.OLLAMA_HOST ?? 'http://localhost:11434',`,
181
+ ` // embeddingModel: process.env.EMBEDDING_MODEL ?? 'nomic-embed-text',`,
182
+ ` });`,
183
+ '',
184
+ ].join('\n');
185
+ // Insert before first ToolRegistry or ToolRegistry instantiation
186
+ let patched = fs.readFileSync(info.engineFile, 'utf-8');
187
+ patched = patched.replace(/(const tools = new ToolRegistry\(\);)/, `${ragBlock} $1`);
188
+ // Register the tool
189
+ patched = patched.replace(/(const tools = new ToolRegistry\(\);[\s\S]*?tools)/, (match) => match.replace(/tools\s*\n?\s*\.register\(/, `tools\n .register(rag.createTool())\n .register(`));
190
+ fs.writeFileSync(info.engineFile, patched);
191
+ console.log('\n✓ Added ObsidianRAGProvider to src/engine.ts');
192
+ console.log('✓ Registered search_knowledge tool');
193
+ console.log('\n Make sure Ollama is running and nomic-embed-text is available:');
194
+ console.log(' ollama pull nomic-embed-text\n');
195
+ }
196
+ function printManualInstructions(vaultPath) {
197
+ const normalized = vaultPath.replace(/\\/g, '/');
198
+ console.log(`
199
+ 1. Import in engine.ts:
200
+ import { ObsidianRAGProvider } from '@backendkit-labs/agent-enterprise';
201
+
202
+ 2. Create RAG instance:
203
+ const rag = new ObsidianRAGProvider({ vaultPath: '${normalized}' });
204
+
205
+ 3. Register tool:
206
+ tools.register(rag.createTool());
207
+ `);
208
+ }
209
+ // ── Main export ───────────────────────────────────────────────────────────────
210
+ const USAGE = `
211
+ Usage: npx create-bk-agent add knowledge <folder-path>
212
+ npx create-bk-agent add knowledge --vault <vault-path>
213
+
214
+ Examples:
215
+ npx create-bk-agent add knowledge ./docs
216
+ npx create-bk-agent add knowledge "04-Recursos/Backend"
217
+ npx create-bk-agent add knowledge --vault "C:/Company/HR-Vault"
218
+ `;
219
+ function addKnowledge(info, args) {
220
+ const parsed = parseArgs(args);
221
+ if (!parsed) {
222
+ console.log(USAGE);
223
+ process.exit(1);
224
+ }
225
+ const absPath = path.resolve(info.cwd, parsed.target);
226
+ if (!info.isEnterprise) {
227
+ console.log('\n⚠ This project does not use @backendkit-labs/agent-enterprise.');
228
+ console.log(' Setting up RAG from scratch...\n');
229
+ setupRagFromScratch(info, absPath);
230
+ return;
231
+ }
232
+ let result;
233
+ if (parsed.isNewVault) {
234
+ result = addNewVault(info.engineFile, absPath);
235
+ }
236
+ else {
237
+ result = patchFolders(info.engineFile, absPath);
238
+ }
239
+ switch (result) {
240
+ case 'added':
241
+ console.log(`\n✓ Added knowledge source to src/engine.ts`);
242
+ console.log(` Path: ${absPath}`);
243
+ console.log('\n The vault will be re-indexed on next run (incremental — only new/changed files).\n');
244
+ break;
245
+ case 'already_exists':
246
+ console.log(`\n⚠ '${absPath}' is already registered in engine.ts\n`);
247
+ break;
248
+ case 'no_rag_found':
249
+ console.log('\n✗ Could not find ObsidianRAGProvider in src/engine.ts');
250
+ console.log(' Add it manually:\n');
251
+ printManualInstructions(absPath);
252
+ break;
253
+ }
254
+ }
package/dist/src/index.js CHANGED
@@ -57,6 +57,7 @@ function describeChoices(choices) {
57
57
  coding: 'Coding assistant',
58
58
  autonomous: 'Autonomous daemon',
59
59
  minimal: 'Minimal / embedded',
60
+ enterprise: 'Enterprise (local LLM + RAG)',
60
61
  };
61
62
  const deploy = {
62
63
  repl: 'Terminal REPL',
@@ -64,6 +65,7 @@ function describeChoices(choices) {
64
65
  'mcp-stdio': 'MCP Server (stdio)',
65
66
  hybrid: 'REPL + MCP Server',
66
67
  library: 'Library',
68
+ web: 'Web Server (chat + kanban)',
67
69
  };
68
70
  const archLabel = arch[choices.architecture] ?? choices.architecture;
69
71
  if (!choices.deployMode)
@@ -80,6 +82,27 @@ function buildNextSteps(choices) {
80
82
  return ['Configure triggers in src/index.ts', 'npm run dev'];
81
83
  if (arch === 'minimal')
82
84
  return ['Implement your LLM provider in src/index.ts', 'npm run dev'];
85
+ // Enterprise — local LLM + RAG
86
+ if (arch === 'enterprise') {
87
+ if (mode === 'web') {
88
+ return [
89
+ 'Make sure Ollama is running: ollama serve',
90
+ 'Pull model: ollama pull llama3.2',
91
+ 'Review vault path in .env (VAULT_PATH)',
92
+ 'npm run dev # indexes vault + starts server on :4001',
93
+ '# Chat: http://localhost:4001/',
94
+ '# Kanban: http://localhost:4001/#kanban',
95
+ '# Monitor: ws://localhost:4001/monitor',
96
+ ];
97
+ }
98
+ return [
99
+ 'Make sure Ollama is running: ollama serve',
100
+ 'Pull required models: ollama pull llama3.2 && ollama pull nomic-embed-text',
101
+ 'Review your vault path in .env (VAULT_PATH)',
102
+ 'Customize agents in src/agents/ for your business domain',
103
+ mode === 'repl' ? 'npm run dev' : 'npm run server',
104
+ ];
105
+ }
83
106
  // Layered architectures (multi-agent, single) — vary by deploy mode
84
107
  const domainSteps = arch === 'multi-agent'
85
108
  ? ['Rename agents/ and tools/ to match your domain', 'Implement tools in src/tools/']
@@ -33,6 +33,11 @@ const ARCHITECTURE_CHOICES = [
33
33
  value: 'minimal',
34
34
  description: 'Best for embedding in an existing app or custom architecture',
35
35
  },
36
+ {
37
+ title: p('Enterprise', 22) + 'Local LLM + Obsidian knowledge base — no cloud needed',
38
+ value: 'enterprise',
39
+ description: 'Business agent with RAG over your company docs, runs 100% on-premise',
40
+ },
36
41
  ];
37
42
  const SKILL_PACK_CHOICES = [
38
43
  { title: p('Node.js / TypeScript', 24) + 'npm, jest, tsc, eslint', value: 'node' },
@@ -40,7 +45,7 @@ const SKILL_PACK_CHOICES = [
40
45
  { title: p('Go', 24) + 'go test, golangci-lint', value: 'go' },
41
46
  { title: p('Docker / K8s', 24) + 'docker, kubectl, helm', value: 'docker' },
42
47
  ];
43
- const DEPLOY_MODE_CHOICES = [
48
+ const DEPLOY_MODE_CHOICES_BASE = [
44
49
  {
45
50
  title: p('Terminal REPL', 28) + 'Humans type requests, agent responds in the terminal',
46
51
  value: 'repl',
@@ -63,6 +68,15 @@ const DEPLOY_MODE_CHOICES = [
63
68
  value: 'library',
64
69
  },
65
70
  ];
71
+ const WEB_DEPLOY_CHOICE = {
72
+ title: p('Web server', 28) + 'WebSocket chat + kanban monitor UI on port 4001',
73
+ value: 'web',
74
+ description: 'Serves @backendkit-labs/agent-web with all 6 enterprise departments',
75
+ };
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ const deployChoices = (_prev, v) => v.architecture === 'enterprise'
78
+ ? [...DEPLOY_MODE_CHOICES_BASE, WEB_DEPLOY_CHOICE]
79
+ : DEPLOY_MODE_CHOICES_BASE;
66
80
  const PROVIDER_CHOICES = [
67
81
  { title: p('Anthropic (Claude)', 32) + 'claude-sonnet-4-6 recommended', value: 'anthropic' },
68
82
  { title: p('OpenAI', 32) + 'gpt-4o recommended', value: 'openai' },
@@ -71,12 +85,14 @@ const PROVIDER_CHOICES = [
71
85
  ];
72
86
  // ── Conditional question helpers ──────────────────────────────────────────────
73
87
  const NEEDS_SKILLS = ['multi-agent', 'single'];
74
- const NEEDS_DEPLOY = ['multi-agent', 'single', 'coding'];
88
+ const NEEDS_DEPLOY = ['multi-agent', 'single', 'coding', 'enterprise'];
75
89
  // @types/prompts doesn't fully type the function form of `type` — cast at call site.
76
90
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
91
  const whenSkills = (_prev, v) => NEEDS_SKILLS.includes(v.architecture) ? 'multiselect' : null;
78
92
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
93
  const whenDeploy = (_prev, v) => NEEDS_DEPLOY.includes(v.architecture) ? 'select' : null;
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ const whenVault = (_prev, v) => v.architecture === 'enterprise' ? 'text' : null;
80
96
  // ── Main question flow ────────────────────────────────────────────────────────
81
97
  async function askQuestions(defaultName) {
82
98
  const raw = await (0, prompts_1.default)([
@@ -108,9 +124,17 @@ async function askQuestions(defaultName) {
108
124
  type: whenDeploy,
109
125
  name: 'deployMode',
110
126
  message: 'How will this agent be used?',
111
- choices: DEPLOY_MODE_CHOICES,
127
+ choices: deployChoices,
112
128
  initial: 0,
113
129
  },
130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
+ {
132
+ type: whenVault,
133
+ name: 'vaultPath',
134
+ message: 'Obsidian vault path (absolute):',
135
+ initial: '',
136
+ validate: (v) => v.trim().length > 0 || 'Vault path is required for enterprise agents',
137
+ },
114
138
  {
115
139
  type: 'select',
116
140
  name: 'provider',
@@ -127,5 +151,6 @@ async function askQuestions(defaultName) {
127
151
  skillPacks: (raw.skillPacks ?? []),
128
152
  deployMode: (raw.deployMode ?? undefined),
129
153
  provider: raw.provider,
154
+ vaultPath: (raw.vaultPath ?? undefined),
130
155
  };
131
156
  }
@@ -43,7 +43,7 @@ const BASE_DIR = path.join(TEMPLATES_DIR, 'base');
43
43
  const DEPLOY_DIR = path.join(TEMPLATES_DIR, 'deploy');
44
44
  // Only these architectures use the base + deploy layer pattern.
45
45
  // The rest (coding, autonomous, minimal) are self-contained in base/.
46
- const LAYERED = ['multi-agent', 'single'];
46
+ const LAYERED = ['multi-agent', 'single', 'enterprise'];
47
47
  // ── Provider env var hints ────────────────────────────────────────────────────
48
48
  const PROVIDER_ENV = {
49
49
  anthropic: { varName: 'ANTHROPIC_API_KEY', placeholder: 'sk-ant-...', model: 'claude-sonnet-4-6' },
@@ -77,6 +77,11 @@ const DEPLOY_SCRIPTS = {
77
77
  delete s['server'];
78
78
  },
79
79
  // hybrid: keep both dev (REPL) and server (MCP) — no changes
80
+ web: (s) => {
81
+ s['dev'] = 'ts-node src/server.ts';
82
+ s['start'] = 'node dist/server.js';
83
+ delete s['server'];
84
+ },
80
85
  library: (s) => {
81
86
  delete s['dev'];
82
87
  delete s['server'];
@@ -84,6 +89,7 @@ const DEPLOY_SCRIPTS = {
84
89
  },
85
90
  };
86
91
  const MCP_DEPLOY_MODES = ['mcp-http', 'mcp-stdio', 'hybrid'];
92
+ const WEB_DEPLOY_MODES = ['web'];
87
93
  function patchPackageJson(dir, projectName, deployMode) {
88
94
  const pkgPath = path.join(dir, 'package.json');
89
95
  if (!fs.existsSync(pkgPath))
@@ -98,14 +104,27 @@ function patchPackageJson(dir, projectName, deployMode) {
98
104
  pkg.dependencies ??= {};
99
105
  pkg.dependencies['@backendkit-labs/mcp-server'] ??= '^0.15.0';
100
106
  }
107
+ if (deployMode && WEB_DEPLOY_MODES.includes(deployMode)) {
108
+ pkg.dependencies ??= {};
109
+ pkg.dependencies['@backendkit-labs/agent-web'] ??= '^0.1.0';
110
+ }
101
111
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
102
112
  }
103
113
  // ── Write .env stub ───────────────────────────────────────────────────────────
104
- function writeEnv(dir, provider) {
114
+ function writeEnv(dir, provider, choices) {
105
115
  const envExample = path.join(dir, '.env.example');
106
116
  const envFile = path.join(dir, '.env');
107
117
  if (!fs.existsSync(envExample) || fs.existsSync(envFile))
108
118
  return;
119
+ if (choices.architecture === 'enterprise') {
120
+ // Enterprise uses its own .env.example — just inject VAULT_PATH
121
+ let content = fs.readFileSync(envExample, 'utf-8');
122
+ if (choices.vaultPath) {
123
+ content = content.replace(/^VAULT_PATH=.*/m, `VAULT_PATH=${choices.vaultPath}`);
124
+ }
125
+ fs.writeFileSync(envFile, content);
126
+ return;
127
+ }
109
128
  const p = PROVIDER_ENV[provider];
110
129
  const content = [
111
130
  '# Generated by create-bk-agent',
@@ -156,10 +175,10 @@ function scaffold(choices, targetDir) {
156
175
  // 2. Deploy layer — overlaid on top of base (layered architectures only)
157
176
  if (isLayered && choices.deployMode && choices.deployMode !== 'library') {
158
177
  const deployDir = path.join(DEPLOY_DIR, choices.deployMode);
159
- if (!fs.existsSync(deployDir)) {
160
- throw new Error(`Deploy template not found: ${choices.deployMode} (in ${deployDir})`);
178
+ if (fs.existsSync(deployDir)) {
179
+ copyDir(deployDir, targetDir);
161
180
  }
162
- copyDir(deployDir, targetDir);
181
+ // No directory = base template already covers this mode
163
182
  }
164
183
  // 3. Skill packs — appended to engine.ts
165
184
  for (const pack of choices.skillPacks) {
@@ -168,7 +187,7 @@ function scaffold(choices, targetDir) {
168
187
  // 4. Patch package.json (name + deploy-mode script adjustments)
169
188
  patchPackageJson(targetDir, choices.projectName, isLayered ? choices.deployMode : undefined);
170
189
  // 5. .env stub
171
- writeEnv(targetDir, choices.provider);
190
+ writeEnv(targetDir, choices.provider, choices);
172
191
  }
173
192
  function installDeps(targetDir) {
174
193
  (0, child_process_1.execSync)('npm install', { cwd: targetDir, stdio: 'inherit' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bk-agent",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Create a new BackendKit agent project",
5
5
  "main": "dist/src/index.js",
6
6
  "bin": {
@@ -0,0 +1,13 @@
1
+ # Enterprise Agent — Local LLM + Knowledge Base
2
+ # Make sure Ollama is running before starting: ollama serve
3
+ # Pull required models: ollama pull llama3.2 && ollama pull nomic-embed-text
4
+
5
+ # Local LLM (via Ollama)
6
+ OLLAMA_HOST=http://localhost:11434
7
+ OLLAMA_MODEL=llama3.2
8
+
9
+ # Embeddings model for knowledge base search
10
+ EMBEDDING_MODEL=nomic-embed-text
11
+
12
+ # Absolute path to your Obsidian vault
13
+ VAULT_PATH=/path/to/your/Obsidian/Vault
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "my-agent",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Enterprise agent with local LLM and Obsidian knowledge base",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "dev": "ts-node src/index.ts",
9
+ "build": "tsc",
10
+ "start": "node dist/index.js",
11
+ "server": "ts-node src/server.ts",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@backendkit-labs/agent-core": "^0.20.0",
16
+ "@backendkit-labs/agent-coding": "^0.19.2",
17
+ "@backendkit-labs/agent-enterprise": "^0.2.0",
18
+ "dotenv": "^16.0.0",
19
+ "zod": "^3.25.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "ts-node": "^10.9.0",
24
+ "typescript": "^5.4.0"
25
+ },
26
+ "engines": { "node": ">=20.0.0" }
27
+ }
@@ -0,0 +1,24 @@
1
+ import type { AgentProfile } from '@backendkit-labs/agent-core';
2
+
3
+ export const DOMAIN_SPECIALIST_PROFILE: AgentProfile = {
4
+ id: 'domain-specialist',
5
+ name: 'Domain Specialist',
6
+ icon: '📚',
7
+ description: 'Answers questions using the company knowledge base',
8
+ systemPrompt: `You are a specialist with access to the company's knowledge base.
9
+
10
+ When answering questions:
11
+ 1. Use search_knowledge to find relevant information
12
+ 2. Base your answer strictly on what you find in the knowledge base
13
+ 3. Quote or reference the source when helpful
14
+ 4. If information is not found, clearly say "I don't have information about that in the knowledge base"
15
+ 5. Never make up information that isn't in the documentation
16
+ 6. When you reach a conclusion or discover something useful, use write_knowledge to persist it for future sessions
17
+
18
+ // TODO: customize this prompt for your specific domain:
19
+ // - HR agent: focus on policies, onboarding, benefits
20
+ // - IT support: focus on troubleshooting guides, procedures
21
+ // - Sales: focus on product catalog, pricing, FAQs`,
22
+ allowedTools: ['search_knowledge', 'write_knowledge'],
23
+ delegatesTo: [],
24
+ };
@@ -0,0 +1,20 @@
1
+ import type { AgentProfile } from '@backendkit-labs/agent-core';
2
+
3
+ export const ORCHESTRATOR_PROFILE: AgentProfile = {
4
+ id: 'orchestrator',
5
+ name: 'Orchestrator',
6
+ icon: '🎯',
7
+ description: 'Routes user requests to the right specialist agent',
8
+ systemPrompt: `You are the main orchestrator for an enterprise knowledge agent.
9
+ Your role is to understand the user's question and route it to the right specialist.
10
+
11
+ Guidelines:
12
+ - Always delegate to domain-specialist for knowledge base questions
13
+ - Use search_knowledge directly for quick lookups
14
+ - Keep your own responses brief — let specialists do the detailed work
15
+ - If the request is outside the company knowledge base, say so clearly
16
+
17
+ // TODO: customize routing logic for your specific business domains`,
18
+ allowedTools: ['search_knowledge', 'ask_agent'],
19
+ delegatesTo: ['domain-specialist'],
20
+ };
@@ -0,0 +1,20 @@
1
+ import 'dotenv/config';
2
+
3
+ export interface Config {
4
+ ollamaHost: string;
5
+ ollamaModel: string;
6
+ embeddingModel: string;
7
+ vaultPath: string;
8
+ }
9
+
10
+ export function loadConfig(): Config {
11
+ const vaultPath = process.env.VAULT_PATH ?? '';
12
+ if (!vaultPath) throw new Error('Missing VAULT_PATH in .env — set the absolute path to your Obsidian vault');
13
+
14
+ return {
15
+ ollamaHost: process.env.OLLAMA_HOST ?? 'http://localhost:11434',
16
+ ollamaModel: process.env.OLLAMA_MODEL ?? 'llama3.2',
17
+ embeddingModel: process.env.EMBEDDING_MODEL ?? 'nomic-embed-text',
18
+ vaultPath,
19
+ };
20
+ }
@@ -0,0 +1,90 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import {
4
+ AgentEngine,
5
+ AgentRegistry,
6
+ ToolRegistry,
7
+ ProviderRegistry,
8
+ CallbackTransport,
9
+ createMemorySystem,
10
+ } from '@backendkit-labs/agent-core';
11
+ import { OllamaProvider } from '@backendkit-labs/agent-coding';
12
+ import { ObsidianRAGProvider, VaultWriter } from '@backendkit-labs/agent-enterprise';
13
+ import { loadConfig } from './config';
14
+ import { ORCHESTRATOR_PROFILE } from './agents/orchestrator';
15
+ import { DOMAIN_SPECIALIST_PROFILE } from './agents/domain-specialist';
16
+ import { exampleAction } from './tools/example-tools';
17
+
18
+ // ── Persistent storage paths ──────────────────────────────────────────────────
19
+
20
+ const APP_DIR = path.join(os.homedir(), '.my-agent');
21
+ const HISTORY = path.join(APP_DIR, 'history.json');
22
+ const MEM_DIR = path.join(APP_DIR, 'memory');
23
+
24
+ // ── Memory system ─────────────────────────────────────────────────────────────
25
+
26
+ const memory = createMemorySystem({
27
+ episodic: { provider: 'json', path: path.join(MEM_DIR, 'episodes.json') },
28
+ semantic: { provider: 'json', path: path.join(MEM_DIR, 'knowledge.json') },
29
+ procedural: { provider: 'json', path: path.join(MEM_DIR, 'patterns.json') },
30
+ retention: { maxEpisodes: 500 },
31
+ });
32
+
33
+ export function createEngine(transport: CallbackTransport): AgentEngine {
34
+ const config = loadConfig();
35
+
36
+ // ── Knowledge base (RAG) ──────────────────────────────────────────────────
37
+ // Indexes the Obsidian vault on first use. Subsequent runs are incremental
38
+ // (only re-embeds new or modified files).
39
+ const rag = new ObsidianRAGProvider({
40
+ vaultPath: config.vaultPath,
41
+ ollamaHost: config.ollamaHost,
42
+ embeddingModel: config.embeddingModel,
43
+ // TODO: restrict indexing to specific folders:
44
+ // folders: ['01-Procesos', '02-Politicas', '04-FAQs'],
45
+ });
46
+
47
+ // ── Vault write-back ──────────────────────────────────────────────────────
48
+ // Agents can contribute new knowledge back to the vault (write_knowledge tool).
49
+ // Notes land in <vaultPath>/agentes/ and are searchable on next index().
50
+ const writer = new VaultWriter({ vaultPath: config.vaultPath });
51
+
52
+ // ── Tools ─────────────────────────────────────────────────────────────────
53
+ const tools = new ToolRegistry();
54
+ tools
55
+ .register(rag.createTool()) // search_knowledge — semantic search
56
+ .register(writer.createTool('specialist')) // write_knowledge — agent write-back
57
+ .register(exampleAction);
58
+ for (const t of memory.getTools()) tools.register(t);
59
+ // TODO: register business-specific tools (CRM, ticketing system, internal APIs)
60
+
61
+ // ── Agents ────────────────────────────────────────────────────────────────
62
+ const agents = new AgentRegistry();
63
+ agents
64
+ .register(ORCHESTRATOR_PROFILE)
65
+ .register(DOMAIN_SPECIALIST_PROFILE);
66
+ // TODO: add more specialists for each business domain
67
+
68
+ // ── LLM Provider (local via Ollama) ───────────────────────────────────────
69
+ const providers = new ProviderRegistry();
70
+ providers.register('default', new OllamaProvider({
71
+ host: config.ollamaHost,
72
+ model: config.ollamaModel,
73
+ }));
74
+
75
+ return new AgentEngine({
76
+ model: { provider: 'default', id: config.ollamaModel },
77
+ agents,
78
+ tools,
79
+ providers,
80
+ defaultProvider: 'default',
81
+ defaultAgentId: 'orchestrator',
82
+ transport,
83
+ maxIterations: 30,
84
+ iterationMode: 'auto',
85
+ historyPath: HISTORY,
86
+ maxContextMessages: 100,
87
+ memorySystem: memory,
88
+ additionalRoots: [APP_DIR],
89
+ });
90
+ }
@@ -0,0 +1,19 @@
1
+ import { defineTool, z } from '@backendkit-labs/agent-core';
2
+
3
+ // Example tool — replace with tools that connect to your business systems
4
+ // e.g. CRM lookup, ticketing system, internal APIs, databases
5
+
6
+ export const exampleAction = defineTool({
7
+ name: 'create_ticket',
8
+ description: 'Creates a support ticket in the ticketing system (example — replace with your integration)',
9
+ input: z.object({
10
+ title: z.string().describe('Short description of the issue'),
11
+ priority: z.enum(['low', 'medium', 'high']).describe('Ticket priority'),
12
+ details: z.string().optional().describe('Additional details'),
13
+ }),
14
+ execute: async ({ title, priority, details }) => {
15
+ // TODO: integrate with your ticketing system (Jira, ServiceNow, Linear, etc.)
16
+ const id = `TICKET-${Date.now()}`;
17
+ return `Ticket created: ${id} — "${title}" [${priority}]${details ? `\nDetails: ${details}` : ''}`;
18
+ },
19
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022"],
7
+ "types": ["node"],
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }