fabiana 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/bin/fabiana.js +6 -0
  4. package/dist/backup.js +89 -0
  5. package/dist/channels/index.js +37 -0
  6. package/dist/channels/slack.js +96 -0
  7. package/dist/channels/telegram.js +70 -0
  8. package/dist/channels/types.js +1 -0
  9. package/dist/cli.js +84 -0
  10. package/dist/conversations/manager.js +144 -0
  11. package/dist/conversations/types.js +1 -0
  12. package/dist/daemon/index.js +419 -0
  13. package/dist/data/providers.js +134 -0
  14. package/dist/doctor.js +323 -0
  15. package/dist/loaders/context.js +72 -0
  16. package/dist/loaders/plugins.js +102 -0
  17. package/dist/paths.js +28 -0
  18. package/dist/plugins/brave-search/index.js +2692 -0
  19. package/dist/plugins/brave-search/package.json +9 -0
  20. package/dist/plugins/brave-search/plugin.json +11 -0
  21. package/dist/plugins/calendar/index.js +2720 -0
  22. package/dist/plugins/calendar/package.json +9 -0
  23. package/dist/plugins/calendar/plugin.json +13 -0
  24. package/dist/plugins/hackernews/index.js +2701 -0
  25. package/dist/plugins/hackernews/package.json +9 -0
  26. package/dist/plugins/hackernews/plugin.json +9 -0
  27. package/dist/plugins-cmd.js +269 -0
  28. package/dist/prompts/system-chat.js +26 -0
  29. package/dist/prompts/system-consolidate.js +49 -0
  30. package/dist/prompts/system-external.js +21 -0
  31. package/dist/prompts/system-initiative.js +34 -0
  32. package/dist/prompts/system.js +129 -0
  33. package/dist/setup/index.js +368 -0
  34. package/dist/telegram/poller.js +71 -0
  35. package/dist/tools/fetch-url.js +85 -0
  36. package/dist/tools/index.js +31 -0
  37. package/dist/tools/manage-todo.js +105 -0
  38. package/dist/tools/safe-edit.js +50 -0
  39. package/dist/tools/safe-read.js +35 -0
  40. package/dist/tools/safe-write.js +42 -0
  41. package/dist/tools/send-message.js +27 -0
  42. package/dist/tools/send-telegram.js +27 -0
  43. package/dist/tools/start-external-conversation.js +86 -0
  44. package/dist/utils/logger.js +34 -0
  45. package/dist/utils/permissions.js +68 -0
  46. package/package.json +55 -0
@@ -0,0 +1,134 @@
1
+ export const providers = [
2
+ {
3
+ id: 'openrouter',
4
+ name: 'OpenRouter',
5
+ description: 'One API key, 240+ models (easiest starting point)',
6
+ envVar: 'OPENROUTER_API_KEY',
7
+ authNote: 'Get one at openrouter.ai/keys',
8
+ models: [
9
+ { id: 'anthropic/claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
10
+ { id: 'moonshotai/kimi-k2.5', name: 'Kimi K2.5' },
11
+ { id: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
12
+ { id: 'meta-llama/llama-4-maverick', name: 'Llama 4 Maverick' },
13
+ ],
14
+ },
15
+ {
16
+ id: 'anthropic',
17
+ name: 'Anthropic (direct)',
18
+ description: 'Direct Claude access — usually cheaper and faster than OpenRouter for Claude',
19
+ envVar: 'ANTHROPIC_API_KEY',
20
+ authNote: 'Get one at console.anthropic.com',
21
+ models: [
22
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
23
+ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
24
+ { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5 (fast & cheap)' },
25
+ ],
26
+ },
27
+ {
28
+ id: 'openai',
29
+ name: 'OpenAI (direct)',
30
+ description: 'Direct GPT and o-series model access',
31
+ envVar: 'OPENAI_API_KEY',
32
+ authNote: 'Get one at platform.openai.com/api-keys',
33
+ models: [
34
+ { id: 'gpt-4o', name: 'GPT-4o' },
35
+ { id: 'gpt-4.1', name: 'GPT-4.1' },
36
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini (fast & cheap)' },
37
+ ],
38
+ },
39
+ {
40
+ id: 'google',
41
+ name: 'Google Gemini (direct)',
42
+ description: 'Fast and cost-effective, especially Gemini Flash',
43
+ envVar: 'GEMINI_API_KEY',
44
+ authNote: 'Get one at aistudio.google.com/apikey',
45
+ models: [
46
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash (fast & cheap)' },
47
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
48
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash' },
49
+ ],
50
+ },
51
+ {
52
+ id: 'google-gemini-cli',
53
+ name: 'Google Gemini CLI (OAuth)',
54
+ description: 'Uses existing Gemini CLI login — no API key needed',
55
+ envVar: null,
56
+ authNote: "Run 'gemini' or 'gcloud auth login' first — no API key required",
57
+ models: [{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' }],
58
+ },
59
+ {
60
+ id: 'google-vertex',
61
+ name: 'Google Vertex AI',
62
+ description: 'Enterprise Google Cloud access — requires GCP project with Vertex AI enabled',
63
+ envVar: 'GOOGLE_CLOUD_PROJECT',
64
+ authNote: "Run 'gcloud auth application-default login' first",
65
+ models: [
66
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
67
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6 (via Vertex)' },
68
+ ],
69
+ },
70
+ {
71
+ id: 'amazon-bedrock',
72
+ name: 'Amazon Bedrock',
73
+ description: 'AWS-hosted models — Claude, Mistral, Amazon Nova, and more',
74
+ envVar: 'AWS_ACCESS_KEY_ID',
75
+ authNote: 'Use AWS_PROFILE, AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, or IAM roles',
76
+ models: [
77
+ { id: 'anthropic.claude-sonnet-4-6', name: 'Claude Sonnet 4.6 (via Bedrock)' },
78
+ { id: 'amazon.nova-premier-v1:0', name: 'Amazon Nova Premier' },
79
+ ],
80
+ },
81
+ {
82
+ id: 'mistral',
83
+ name: 'Mistral (direct)',
84
+ description: 'Direct Mistral models, including Codestral and Magistral',
85
+ envVar: 'MISTRAL_API_KEY',
86
+ authNote: 'Get one at console.mistral.ai',
87
+ models: [
88
+ { id: 'magistral-medium-latest', name: 'Magistral Medium' },
89
+ { id: 'mistral-large-latest', name: 'Mistral Large' },
90
+ ],
91
+ },
92
+ {
93
+ id: 'groq',
94
+ name: 'Groq',
95
+ description: 'Extremely fast inference, often free for common open models',
96
+ envVar: 'GROQ_API_KEY',
97
+ authNote: 'Get one at console.groq.com',
98
+ models: [
99
+ { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B' },
100
+ { id: 'moonshotai/kimi-k2-instruct', name: 'Kimi K2 Instruct' },
101
+ { id: 'qwen-qwq-32b', name: 'Qwen QwQ 32B' },
102
+ ],
103
+ },
104
+ {
105
+ id: 'xai',
106
+ name: 'xAI (Grok)',
107
+ description: 'Grok models with vision capabilities',
108
+ envVar: 'XAI_API_KEY',
109
+ authNote: 'Get one at console.x.ai',
110
+ models: [
111
+ { id: 'grok-3', name: 'Grok 3' },
112
+ { id: 'grok-3-fast', name: 'Grok 3 Fast' },
113
+ ],
114
+ },
115
+ {
116
+ id: 'github-copilot',
117
+ name: 'GitHub Copilot',
118
+ description: 'Uses your existing GitHub Copilot subscription — no separate API key',
119
+ envVar: 'GH_TOKEN',
120
+ authNote: 'Use GH_TOKEN, GITHUB_TOKEN, or COPILOT_GITHUB_TOKEN',
121
+ models: [
122
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
123
+ { id: 'gpt-4o', name: 'GPT-4o' },
124
+ ],
125
+ },
126
+ {
127
+ id: 'azure-openai-responses',
128
+ name: 'Azure OpenAI',
129
+ description: 'OpenAI models deployed on Azure — requires an Azure OpenAI resource',
130
+ envVar: 'AZURE_OPENAI_API_KEY',
131
+ authNote: 'Requires an Azure OpenAI resource and deployment',
132
+ models: [{ id: 'gpt-4o', name: 'GPT-4o (via Azure)' }],
133
+ },
134
+ ];
package/dist/doctor.js ADDED
@@ -0,0 +1,323 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { paths, PLUGINS_DIR, DATA_DIR } from './paths.js';
4
+ import { execFile } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import chalk from 'chalk';
7
+ import { getModel } from '@mariozechner/pi-ai';
8
+ import { AuthStorage, ModelRegistry } from '@mariozechner/pi-coding-agent';
9
+ const execFileAsync = promisify(execFile);
10
+ // ─── Output helpers ───────────────────────────────────────────────────────────
11
+ let totalFails = 0;
12
+ let totalWarns = 0;
13
+ function pass(label, note = '') {
14
+ const text = note ? `${label} ${chalk.dim(`(${note})`)}` : label;
15
+ console.log(` ${chalk.green('✓')} ${text}`);
16
+ }
17
+ function advisory(label, hint = '') {
18
+ const text = hint ? `${label} ${chalk.dim(`— ${hint}`)}` : label;
19
+ console.log(` ${chalk.yellow('⚠')} ${text}`);
20
+ totalWarns++;
21
+ }
22
+ function error(label, hint = '') {
23
+ const text = hint ? `${label} ${chalk.dim(`— ${hint}`)}` : label;
24
+ console.log(` ${chalk.red('✗')} ${text}`);
25
+ totalFails++;
26
+ }
27
+ function section(title) {
28
+ console.log(`\n${chalk.bold(title)}`);
29
+ }
30
+ // ─── Checks ──────────────────────────────────────────────────────────────────
31
+ async function checkEnvironment() {
32
+ section('Environment');
33
+ const [major] = process.versions.node.split('.').map(Number);
34
+ if (major >= 22) {
35
+ pass(`Node ${process.versions.node}`);
36
+ }
37
+ else {
38
+ error(`Node ${process.versions.node}`, 'requires ≥22');
39
+ }
40
+ try {
41
+ await fs.access(paths.envFile);
42
+ pass('.env file found');
43
+ }
44
+ catch {
45
+ advisory('.env file not found', 'env vars must be set via export or shell profile');
46
+ }
47
+ // Read config to know which env vars are actually required
48
+ let providerEnvVar = null;
49
+ let telegramEnabled = true;
50
+ let slackEnabled = false;
51
+ try {
52
+ const raw = await fs.readFile(paths.configJson, 'utf-8');
53
+ const cfg = JSON.parse(raw);
54
+ telegramEnabled = cfg.channels?.telegram?.enabled ?? true;
55
+ slackEnabled = cfg.channels?.slack?.enabled ?? false;
56
+ // Look up env var for this provider
57
+ const { providers } = await import('./data/providers.js');
58
+ const p = providers.find((p) => p.id === cfg.model?.provider);
59
+ providerEnvVar = p?.envVar ?? null;
60
+ }
61
+ catch {
62
+ // Config not found — fall through, checkConfig() will report it
63
+ }
64
+ if (providerEnvVar) {
65
+ if (process.env[providerEnvVar]) {
66
+ pass(providerEnvVar);
67
+ }
68
+ else {
69
+ error(providerEnvVar, 'not set');
70
+ }
71
+ }
72
+ if (telegramEnabled) {
73
+ for (const key of ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID']) {
74
+ if (process.env[key]) {
75
+ pass(key);
76
+ }
77
+ else {
78
+ error(key, 'not set');
79
+ }
80
+ }
81
+ }
82
+ if (slackEnabled) {
83
+ if (process.env['SLACK_BOT_TOKEN']) {
84
+ pass('SLACK_BOT_TOKEN');
85
+ }
86
+ else {
87
+ error('SLACK_BOT_TOKEN', 'not set');
88
+ }
89
+ }
90
+ }
91
+ async function checkConfig() {
92
+ section('Configuration');
93
+ try {
94
+ const raw = await fs.readFile(paths.configJson, 'utf-8');
95
+ const cfg = JSON.parse(raw);
96
+ pass(paths.configJson, `model: ${cfg.model?.provider}/${cfg.model?.modelId}`);
97
+ }
98
+ catch (e) {
99
+ error(paths.configJson, 'not found — run `fabiana init` to set up');
100
+ }
101
+ try {
102
+ const raw = await fs.readFile(paths.manifestJson, 'utf-8');
103
+ JSON.parse(raw);
104
+ pass(paths.manifestJson);
105
+ }
106
+ catch (e) {
107
+ error(paths.manifestJson, e.message);
108
+ }
109
+ for (const file of [
110
+ paths.systemMd(),
111
+ paths.systemMd('chat'),
112
+ paths.systemMd('initiative'),
113
+ paths.systemMd('consolidate'),
114
+ ]) {
115
+ try {
116
+ const stat = await fs.stat(file);
117
+ pass(path.basename(file), `${stat.size} bytes`);
118
+ }
119
+ catch {
120
+ error(file, 'not found');
121
+ }
122
+ }
123
+ }
124
+ async function checkPiSdk() {
125
+ section('Pi SDK');
126
+ let modelId = 'unknown';
127
+ try {
128
+ const raw = await fs.readFile(paths.configJson, 'utf-8');
129
+ const cfg = JSON.parse(raw);
130
+ modelId = `${cfg.model?.provider}/${cfg.model?.modelId}`;
131
+ const model = getModel(cfg.model?.provider, cfg.model?.modelId);
132
+ if (!model) {
133
+ error(`Model not found: ${modelId}`);
134
+ return;
135
+ }
136
+ pass(`Model resolved: ${modelId}`);
137
+ }
138
+ catch (e) {
139
+ error(`Model check failed (${modelId})`, e.message);
140
+ return;
141
+ }
142
+ try {
143
+ AuthStorage.create();
144
+ new ModelRegistry(AuthStorage.create());
145
+ pass('AuthStorage + ModelRegistry initialized');
146
+ }
147
+ catch (e) {
148
+ error('Pi SDK initialization failed', e.message);
149
+ }
150
+ }
151
+ async function checkTelegram() {
152
+ section('Telegram');
153
+ const token = process.env.TELEGRAM_BOT_TOKEN;
154
+ if (!token) {
155
+ advisory('Skipped', 'TELEGRAM_BOT_TOKEN not set');
156
+ return;
157
+ }
158
+ try {
159
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`);
160
+ const data = await res.json();
161
+ if (data.ok) {
162
+ pass(`Connected as @${data.result.username}`, data.result.first_name);
163
+ }
164
+ else {
165
+ error('Bot API error', data.description ?? 'unknown error');
166
+ }
167
+ }
168
+ catch (e) {
169
+ error('Telegram unreachable', e.message);
170
+ }
171
+ }
172
+ async function checkExternalTools() {
173
+ section('External Tools');
174
+ try {
175
+ const { stdout } = await execFileAsync('gccli', ['--version'], { timeout: 5000 });
176
+ pass(`gccli ${stdout.trim()}`);
177
+ }
178
+ catch (e) {
179
+ if (e.code === 'ENOENT') {
180
+ advisory('gccli not installed', 'calendar plugin will not work — npm install -g @mariozechner/gccli');
181
+ }
182
+ else {
183
+ pass('gccli found');
184
+ }
185
+ }
186
+ }
187
+ async function checkPlugins() {
188
+ section('Plugins');
189
+ const pluginsConfigPath = paths.pluginsJson;
190
+ let pluginsConfig = {};
191
+ let hasConfig = false;
192
+ try {
193
+ const raw = await fs.readFile(pluginsConfigPath, 'utf-8');
194
+ pluginsConfig = JSON.parse(raw);
195
+ hasConfig = true;
196
+ }
197
+ catch {
198
+ // No plugins.json — all plugins enabled by default
199
+ }
200
+ try {
201
+ await fs.access(PLUGINS_DIR);
202
+ }
203
+ catch {
204
+ advisory('No plugins/ directory found');
205
+ return;
206
+ }
207
+ const entries = await fs.readdir(PLUGINS_DIR, { withFileTypes: true });
208
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
209
+ if (dirs.length === 0) {
210
+ advisory('No plugins found');
211
+ return;
212
+ }
213
+ for (const dir of dirs) {
214
+ const pluginCfg = pluginsConfig[dir];
215
+ const isEnabled = !hasConfig || (pluginCfg !== undefined && pluginCfg.enabled !== false);
216
+ const tsPath = path.join(PLUGINS_DIR, dir, 'index.ts');
217
+ const jsPath = path.join(PLUGINS_DIR, dir, 'index.js');
218
+ let entryPath = null;
219
+ for (const p of [tsPath, jsPath]) {
220
+ try {
221
+ await fs.access(p);
222
+ entryPath = p;
223
+ break;
224
+ }
225
+ catch { /* try next */ }
226
+ }
227
+ if (!entryPath) {
228
+ error(dir, 'no index.ts or index.js found');
229
+ continue;
230
+ }
231
+ try {
232
+ const mod = await import(path.resolve(entryPath));
233
+ const toolName = mod.tool?.name ?? dir;
234
+ const version = mod.metadata?.version ? `v${mod.metadata.version}` : '';
235
+ const label = `${toolName} ${version}`.trim();
236
+ if (!mod.tool) {
237
+ error(dir, 'missing `export const tool`');
238
+ continue;
239
+ }
240
+ else if (!mod.tool.execute) {
241
+ error(dir, 'tool.execute not defined');
242
+ continue;
243
+ }
244
+ else if (!isEnabled) {
245
+ advisory(label, `disabled in ${chalk.cyan(pluginsConfigPath)}`);
246
+ continue;
247
+ }
248
+ else {
249
+ pass(label);
250
+ }
251
+ // Check env vars declared in plugin.json
252
+ const manifestPath = path.join(PLUGINS_DIR, dir, 'plugin.json');
253
+ try {
254
+ const raw = await fs.readFile(manifestPath, 'utf-8');
255
+ const manifest = JSON.parse(raw);
256
+ for (const envVar of (manifest.env ?? [])) {
257
+ if (process.env[envVar.key]) {
258
+ pass(` ${envVar.key}`);
259
+ }
260
+ else if (envVar.required) {
261
+ error(` ${envVar.key}`, envVar.description);
262
+ }
263
+ else {
264
+ advisory(` ${envVar.key} not set`, envVar.description);
265
+ }
266
+ }
267
+ }
268
+ catch {
269
+ // No plugin.json — skip env checks
270
+ }
271
+ }
272
+ catch (e) {
273
+ error(dir, e.message);
274
+ }
275
+ }
276
+ }
277
+ async function checkDataDirectories() {
278
+ section('Data Directories');
279
+ const required = [
280
+ path.join(DATA_DIR, 'memory'),
281
+ path.join(DATA_DIR, 'agent-todo', 'pending'),
282
+ path.join(DATA_DIR, 'agent-todo', 'scheduled'),
283
+ path.join(DATA_DIR, 'agent-todo', 'completed'),
284
+ path.join(DATA_DIR, 'logs'),
285
+ ];
286
+ for (const dir of required) {
287
+ try {
288
+ await fs.access(dir);
289
+ pass(dir);
290
+ }
291
+ catch {
292
+ advisory(dir, 'missing — will be created on first run');
293
+ }
294
+ }
295
+ }
296
+ // ─── Entry point ─────────────────────────────────────────────────────────────
297
+ export async function runDoctor() {
298
+ console.log(`\n${chalk.magenta('🌸 Fabiana Doctor')}`);
299
+ console.log(chalk.dim('━'.repeat(50)));
300
+ await checkEnvironment();
301
+ await checkConfig();
302
+ await checkPiSdk();
303
+ await checkTelegram();
304
+ await checkExternalTools();
305
+ await checkPlugins();
306
+ await checkDataDirectories();
307
+ console.log(`\n${chalk.dim('━'.repeat(50))}`);
308
+ if (totalFails > 0) {
309
+ const errPart = chalk.red(`${totalFails} error(s)`);
310
+ const warnPart = totalWarns > 0 ? `, ${chalk.yellow(`${totalWarns} warning(s)`)}` : '';
311
+ console.log(`${chalk.red('✗')} ${errPart}${warnPart} — fix the errors above before starting Fabiana`);
312
+ process.exit(1);
313
+ }
314
+ else if (totalWarns > 0) {
315
+ console.log(`${chalk.yellow('⚠')} ${chalk.yellow(`${totalWarns} warning(s)`)} — Fabiana will start but some features may not work`);
316
+ console.log(`\n ${chalk.bold('Ready to go:')} fabiana start`);
317
+ }
318
+ else {
319
+ console.log(`${chalk.green('✓')} ${chalk.green('All checks passed')} — Fabiana is ready!`);
320
+ console.log(`\n ${chalk.bold('Start her up:')} ${chalk.cyan('fabiana start')}`);
321
+ }
322
+ console.log('');
323
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'fs/promises';
2
+ import { paths } from '../paths.js';
3
+ export async function loadContext(mode, incomingMessage, conversationState) {
4
+ const timestamp = new Date().toISOString();
5
+ const identity = await readFile(paths.memory('identity.md'), '(No identity file yet)');
6
+ const core = await readFile(paths.memory('core.md'), '(No core memory yet)');
7
+ const recentMemory = await readFile(paths.memory('recent', 'this-week.md'), '(No recent memory yet)');
8
+ return { mode, identity, core, recentMemory, incomingMessage, timestamp, conversationState };
9
+ }
10
+ async function readFile(filePath, fallback) {
11
+ try {
12
+ return await fs.readFile(filePath, 'utf-8');
13
+ }
14
+ catch {
15
+ return fallback;
16
+ }
17
+ }
18
+ export function buildPrompt(ctx) {
19
+ const base = `# Session Start — ${ctx.timestamp}
20
+ **Mode**: ${ctx.mode}
21
+
22
+ ---
23
+
24
+ ## 🧠 Memory
25
+
26
+ ### Identity
27
+ ${ctx.identity}
28
+
29
+ ### Core State
30
+ ${ctx.core}
31
+
32
+ ### Recent (This Week)
33
+ ${ctx.recentMemory}`;
34
+ if (ctx.mode === 'chat' && ctx.incomingMessage) {
35
+ return `${base}
36
+
37
+ ---
38
+
39
+ ## 💬 Incoming Message
40
+
41
+ > ${ctx.incomingMessage}`;
42
+ }
43
+ if (ctx.mode === 'external-reply' && ctx.conversationState && ctx.incomingMessage) {
44
+ return `${base}
45
+
46
+ ---
47
+
48
+ ## 🗣️ External Conversation
49
+
50
+ **ID**: ${ctx.conversationState.id}
51
+ **With**: ${ctx.conversationState.externalDisplayName} (${ctx.conversationState.externalUserId})
52
+ **Purpose**: ${ctx.conversationState.purpose}
53
+ **Channel**: ${ctx.conversationState.channel}
54
+
55
+ ### Incoming Message
56
+
57
+ > ${ctx.incomingMessage}`;
58
+ }
59
+ if (ctx.mode === 'external-outreach' && ctx.conversationState) {
60
+ return `${base}
61
+
62
+ ---
63
+
64
+ ## 🗣️ External Conversation Context
65
+
66
+ **ID**: ${ctx.conversationState.id}
67
+ **With**: ${ctx.conversationState.externalDisplayName} (${ctx.conversationState.externalUserId})
68
+ **Purpose**: ${ctx.conversationState.purpose}
69
+ **Channel**: ${ctx.conversationState.channel}`;
70
+ }
71
+ return base;
72
+ }
@@ -0,0 +1,102 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { PLUGINS_DIR, paths } from '../paths.js';
4
+ export class PluginLoader {
5
+ pluginsDir;
6
+ enabledPlugins = null;
7
+ constructor(pluginsDir = PLUGINS_DIR) {
8
+ this.pluginsDir = path.resolve(pluginsDir);
9
+ }
10
+ async loadAll() {
11
+ const tools = [];
12
+ try {
13
+ await fs.access(this.pluginsDir);
14
+ }
15
+ catch {
16
+ console.log(`[PLUGINS] No plugins directory found at ${this.pluginsDir}`);
17
+ return tools;
18
+ }
19
+ const entries = await fs.readdir(this.pluginsDir, { withFileTypes: true });
20
+ const pluginDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
21
+ if (pluginDirs.length === 0) {
22
+ console.log('[PLUGINS] No plugins found');
23
+ return tools;
24
+ }
25
+ console.log(`[PLUGINS] Scanning ${pluginDirs.length} plugin(s)...`);
26
+ for (const dir of pluginDirs) {
27
+ if (this.enabledPlugins && !this.enabledPlugins.has(dir)) {
28
+ console.log(`[PLUGINS] ⊘ ${dir} (disabled)`);
29
+ continue;
30
+ }
31
+ try {
32
+ const tsPath = path.join(this.pluginsDir, dir, 'index.ts');
33
+ const jsPath = path.join(this.pluginsDir, dir, 'index.js');
34
+ let pluginPath;
35
+ try {
36
+ await fs.access(tsPath);
37
+ pluginPath = tsPath;
38
+ }
39
+ catch {
40
+ try {
41
+ await fs.access(jsPath);
42
+ pluginPath = jsPath;
43
+ }
44
+ catch {
45
+ console.log(`[PLUGINS] ⚠️ ${dir} (no index.ts or index.js found)`);
46
+ continue;
47
+ }
48
+ }
49
+ const module = await import(pluginPath);
50
+ if (!module.tool) {
51
+ console.log(`[PLUGINS] ⚠️ ${dir} (no tool export found)`);
52
+ continue;
53
+ }
54
+ const tool = module.tool;
55
+ const metadata = module.metadata;
56
+ if (!tool.name || !tool.description || !tool.execute) {
57
+ console.log(`[PLUGINS] ⚠️ ${dir} (invalid tool definition)`);
58
+ continue;
59
+ }
60
+ tools.push(tool);
61
+ const version = metadata?.version ? `v${metadata.version}` : '';
62
+ console.log(`[PLUGINS] ✓ ${tool.name} ${version}`.trim());
63
+ }
64
+ catch (err) {
65
+ console.log(`[PLUGINS] ✗ ${dir} (${err.message})`);
66
+ }
67
+ }
68
+ return tools;
69
+ }
70
+ async loadPluginConfig(configPath = paths.pluginsJson) {
71
+ try {
72
+ const content = await fs.readFile(configPath, 'utf-8');
73
+ const config = JSON.parse(content);
74
+ const enabled = Object.entries(config)
75
+ .filter(([, pluginCfg]) => pluginCfg.enabled !== false)
76
+ .map(([name]) => name);
77
+ this.enabledPlugins = new Set(enabled);
78
+ console.log(`[PLUGINS] Loaded config: ${enabled.length} enabled`);
79
+ }
80
+ catch {
81
+ // No config file or invalid — load all plugins
82
+ }
83
+ }
84
+ }
85
+ export async function loadPlugins(pluginsDir) {
86
+ const loader = new PluginLoader(pluginsDir);
87
+ await loader.loadPluginConfig();
88
+ return loader.loadAll();
89
+ }
90
+ export async function loadPluginsConfig(configPath = paths.pluginsJson) {
91
+ try {
92
+ const content = await fs.readFile(configPath, 'utf-8');
93
+ return JSON.parse(content);
94
+ }
95
+ catch {
96
+ return {};
97
+ }
98
+ }
99
+ export async function savePluginsConfig(config, configPath = paths.pluginsJson) {
100
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
101
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n');
102
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,28 @@
1
+ import { join } from 'path';
2
+ import { homedir } from 'os';
3
+ import { fileURLToPath } from 'url';
4
+ import { existsSync } from 'fs';
5
+ export const FABIANA_HOME = process.env.FABIANA_HOME ?? join(homedir(), '.fabiana');
6
+ export const CONFIG_DIR = join(FABIANA_HOME, 'config');
7
+ export const DATA_DIR = join(FABIANA_HOME, 'data');
8
+ export const PLUGINS_DIR = join(FABIANA_HOME, 'plugins');
9
+ export const paths = {
10
+ configJson: join(CONFIG_DIR, 'config.json'),
11
+ manifestJson: join(CONFIG_DIR, 'manifest.json'),
12
+ pluginsJson: join(CONFIG_DIR, 'plugins.json'),
13
+ stateJson: join(CONFIG_DIR, 'state.json'),
14
+ systemMd: (suffix) => suffix ? join(CONFIG_DIR, `system-${suffix}.md`) : join(CONFIG_DIR, 'system.md'),
15
+ memory: (...parts) => join(DATA_DIR, 'memory', ...parts),
16
+ logs: (filename) => join(DATA_DIR, 'logs', filename),
17
+ sessions: join(DATA_DIR, 'sessions'),
18
+ agentTodo: join(DATA_DIR, 'agent-todo'),
19
+ conversations: join(DATA_DIR, 'conversations'),
20
+ envFile: join(FABIANA_HOME, '.env'),
21
+ };
22
+ // Default plugins bundled with the package.
23
+ // Dev: src/paths.ts → __dir = src/ → no src/plugins/ → falls back to ../plugins (root)
24
+ // Prod: dist/paths.js → __dir = dist/ → dist/plugins/ exists → uses dist/plugins/
25
+ const __dir = fileURLToPath(new URL('.', import.meta.url));
26
+ const distPlugins = join(__dir, 'plugins');
27
+ const srcPlugins = join(__dir, '..', 'plugins');
28
+ export const BUNDLED_PLUGINS_DIR = existsSync(distPlugins) ? distPlugins : srcPlugins;