fabiana 0.4.0 → 1.0.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
@@ -17,6 +17,11 @@ No dashboards. No commands to memorize. She just slides into your DM.
17
17
 
18
18
  ---
19
19
 
20
+ <p align="center">
21
+ <img src="screenshot.png" alt="Screenshot"/>
22
+ </p>
23
+
24
+
20
25
  ## What she does
21
26
 
22
27
  **She messages you first.** She has a schedule and a TODO list she manages herself. She'll reach out when there's something worth saying — not when you remember to ask.
package/dist/cli.js CHANGED
@@ -11,7 +11,10 @@ import { startDaemon, runInitiativeOnce, runConsolidateOnce } from './daemon/ind
11
11
  import { runDoctor } from './doctor.js';
12
12
  import { runBackup, runRestore } from './backup.js';
13
13
  import { pluginsAdd, pluginsList } from './plugins-cmd.js';
14
+ import { skillsAdd, skillsList, skillsRemove, skillsEnable, skillsDisable } from './skills-cmd.js';
14
15
  import { runSetup } from './setup/index.js';
16
+ import { providerStatus, providerUse, providerAdd } from './provider-cmd.js';
17
+ import { modelStatus, modelUse, modelTest } from './model-cmd.js';
15
18
  const C = '\x1b[96m'; // cyan — name
16
19
  const D = '\x1b[2m'; // dim — subtitle
17
20
  const R = '\x1b[0m'; // reset
@@ -62,6 +65,25 @@ program
62
65
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? 'vi';
63
66
  spawnSync(editor, [paths.configJson], { stdio: 'inherit' });
64
67
  });
68
+ program
69
+ .command('system-prompt')
70
+ .description('Edit her system prompts — choose the mode')
71
+ .action(async () => {
72
+ const { select } = await import('@inquirer/prompts');
73
+ const choice = await select({
74
+ message: 'Which system prompt do you want to edit?',
75
+ choices: [
76
+ { name: 'system — base identity prompt', value: 'system' },
77
+ { name: 'chat — chat mode override', value: 'chat' },
78
+ { name: 'initiative — initiative mode override', value: 'initiative' },
79
+ { name: 'consolidate — consolidation mode override', value: 'consolidate' },
80
+ { name: 'external — external mode override', value: 'external' },
81
+ ],
82
+ });
83
+ const file = choice === 'system' ? paths.systemMd() : paths.systemMd(choice);
84
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? 'vi';
85
+ spawnSync(editor, [file], { stdio: 'inherit' });
86
+ });
65
87
  program
66
88
  .command('doctor')
67
89
  .description('Is everything okay in there? Let\'s check')
@@ -86,6 +108,52 @@ plugins
86
108
  .description('What can she do?')
87
109
  .action(pluginsList);
88
110
  program.addCommand(plugins);
111
+ const skills = new Command('skills').description('Teach her new workflows');
112
+ skills
113
+ .command('add <source>')
114
+ .description('Install a skill (format: username/repo or username/collection/skill-name)')
115
+ .action((source) => skillsAdd(source));
116
+ skills
117
+ .command('list')
118
+ .description('What does she know?')
119
+ .action(skillsList);
120
+ skills
121
+ .command('remove <name>')
122
+ .description('Uninstall a skill')
123
+ .action((name) => skillsRemove(name));
124
+ skills
125
+ .command('enable <name>')
126
+ .description('Re-enable a disabled skill')
127
+ .action((name) => skillsEnable(name));
128
+ skills
129
+ .command('disable <name>')
130
+ .description('Disable a skill without removing it')
131
+ .action((name) => skillsDisable(name));
132
+ program.addCommand(skills);
133
+ const provider = new Command('provider').description('Manage AI providers');
134
+ provider
135
+ .command('use')
136
+ .description('Switch active provider and model')
137
+ .action(providerUse);
138
+ provider
139
+ .command('add')
140
+ .description('Show credential setup instructions for a provider')
141
+ .action(providerAdd);
142
+ provider
143
+ .action(providerStatus);
144
+ program.addCommand(provider);
145
+ const model = new Command('model').description('Manage the active model');
146
+ model
147
+ .command('use')
148
+ .description('Switch to a different model')
149
+ .action(modelUse);
150
+ model
151
+ .command('test')
152
+ .description('Send a live ping to verify the model works')
153
+ .action(modelTest);
154
+ model
155
+ .action(modelStatus);
156
+ program.addCommand(model);
89
157
  program.addHelpCommand(new Command('help')
90
158
  .argument('[command]', 'command to show help for')
91
159
  .description('Show help for fabiana or a specific command'));
@@ -9,6 +9,7 @@ import { Logger } from '../utils/logger.js';
9
9
  import { createFabianaTools } from '../tools/index.js';
10
10
  import { loadContext, buildPrompt } from '../loaders/context.js';
11
11
  import { loadPlugins } from '../loaders/plugins.js';
12
+ import { loadFabianaSkills, formatSkillsForPrompt } from '../loaders/skills.js';
12
13
  import { paths, PLUGINS_DIR, FABIANA_HOME } from '../paths.js';
13
14
  async function loadConfig() {
14
15
  try {
@@ -60,6 +61,12 @@ export async function runPiSession(mode, incomingMessage, channel, incomingMsg,
60
61
  systemPromptContent = systemPromptContent.replace('{purpose}', conversationState.purpose);
61
62
  }
62
63
  }
64
+ // Append skills section — skills live at ~/.fabiana/skills/, scoped per user
65
+ const skills = await loadFabianaSkills();
66
+ if (skills.length > 0) {
67
+ systemPromptContent += formatSkillsForPrompt(skills);
68
+ console.log(` Skills: ${skills.map(s => s.name).join(', ')}`);
69
+ }
63
70
  const loader = new DefaultResourceLoader({
64
71
  cwd: process.cwd(),
65
72
  systemPromptOverride: () => systemPromptContent,
@@ -100,30 +107,39 @@ export async function runPiSession(mode, incomingMessage, channel, incomingMsg,
100
107
  console.log('[8/8] Setting up event handlers...');
101
108
  let sendMessageCalled = false;
102
109
  let accumulatedResponse = '';
110
+ let lastActivityWasThinking = false;
103
111
  session.subscribe(async (event) => {
104
- if (event.type === 'message_update' && event.assistantMessageEvent.type === 'thinking_delta') {
105
- process.stdout.write('.');
106
- }
107
- if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
108
- process.stdout.write(event.assistantMessageEvent.delta);
109
- accumulatedResponse += event.assistantMessageEvent.delta;
110
- }
111
- if (event.type === 'tool_execution_start') {
112
- console.log(`\n🔧 Tool: ${event.toolName}`);
113
- await logger.log(`Tool: ${event.toolName}`);
114
- if (event.toolName === 'send_message') {
115
- sendMessageCalled = true;
112
+ try {
113
+ if (event.type === 'message_update' && event.assistantMessageEvent.type === 'thinking_delta') {
114
+ process.stdout.write('.');
115
+ lastActivityWasThinking = true;
116
+ }
117
+ if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
118
+ process.stdout.write(event.assistantMessageEvent.delta);
119
+ accumulatedResponse += event.assistantMessageEvent.delta;
120
+ lastActivityWasThinking = false;
121
+ }
122
+ if (event.type === 'tool_execution_start') {
123
+ console.log(`\n🔧 Tool: ${event.toolName}`);
124
+ await logger.log(`Tool: ${event.toolName}`);
125
+ if (event.toolName === 'send_message') {
126
+ sendMessageCalled = true;
127
+ }
128
+ lastActivityWasThinking = false;
129
+ }
130
+ if (event.type === 'tool_execution_end') {
131
+ const status = event.isError ? '❌' : '✅';
132
+ console.log(`${status}`);
133
+ }
134
+ const elapsed = (Date.now() - sessionStartTime) / 1000;
135
+ if (elapsed > config.limits.maxSessionDuration) {
136
+ await logger.log(`Session timeout (${elapsed}s > ${config.limits.maxSessionDuration}s)`);
137
+ console.log(`\n⏱️ Session timeout - aborting`);
138
+ session.abort();
116
139
  }
117
140
  }
118
- if (event.type === 'tool_execution_end') {
119
- const status = event.isError ? '❌' : '✅';
120
- console.log(`${status}`);
121
- }
122
- const elapsed = (Date.now() - sessionStartTime) / 1000;
123
- if (elapsed > config.limits.maxSessionDuration) {
124
- await logger.log(`Session timeout (${elapsed}s > ${config.limits.maxSessionDuration}s)`);
125
- console.log(`\n⏱️ Session timeout - aborting`);
126
- session.abort();
141
+ catch (err) {
142
+ console.error('\n⚠️ Event handler error (non-fatal):', err.message);
127
143
  }
128
144
  });
129
145
  console.log('\n📚 Loading context...');
@@ -134,19 +150,33 @@ export async function runPiSession(mode, incomingMessage, channel, incomingMsg,
134
150
  await session.prompt(prompt);
135
151
  console.log('\n⏳ Waiting for agent to complete...');
136
152
  await session.agent.waitForIdle();
153
+ if (lastActivityWasThinking) {
154
+ console.warn('\n⚠️ LLM returned empty response (thinking with no text or tool call) — possible context overflow or provider issue');
155
+ await logger.log('Warning: empty response from LLM (thinking block only)');
156
+ }
137
157
  // Auto-send fallback: chat mode only, if agent didn't call send_message
138
158
  if (mode === 'chat' && channel && !sendMessageCalled && accumulatedResponse.trim()) {
139
159
  console.log('\n⚠️ Agent did not call send_message - auto-sending accumulated response');
140
160
  const cleanResponse = accumulatedResponse.trim();
141
- await channel.send(cleanResponse, incomingMsg?.channelId, incomingMsg?.threadId);
142
- await channel.logConversation('fabiana', cleanResponse, incomingMsg?.source ?? channel.name);
143
- console.log('📤 Auto-sent response');
161
+ try {
162
+ await channel.send(cleanResponse, incomingMsg?.channelId, incomingMsg?.threadId);
163
+ await channel.logConversation('fabiana', cleanResponse, incomingMsg?.source ?? channel.name);
164
+ console.log('📤 Auto-sent response');
165
+ }
166
+ catch (err) {
167
+ console.error('❌ Auto-send fallback failed:', err.message);
168
+ }
144
169
  }
145
170
  // Auto-log full reasoning to silence log when initiative runs silently
146
171
  if (mode === 'initiative' && !sendMessageCalled && accumulatedResponse.trim()) {
147
172
  const timestamp = new Date().toISOString();
148
173
  const entry = `\n--- ${timestamp} ---\n${accumulatedResponse.trim()}\n`;
149
- await fs.appendFile(paths.logs('initiative-silence.log'), entry, 'utf-8');
174
+ try {
175
+ await fs.appendFile(paths.logs('initiative-silence.log'), entry, 'utf-8');
176
+ }
177
+ catch (err) {
178
+ console.error('❌ Failed to write silence log:', err.message);
179
+ }
150
180
  }
151
181
  console.log('\n━'.repeat(50));
152
182
  console.log('✓ Session complete');
@@ -329,10 +359,20 @@ export async function startDaemon() {
329
359
  });
330
360
  // Hourly check: expire stale external conversations (> 4 days inactive)
331
361
  cron.schedule('0 * * * *', async () => {
332
- const expired = await conversationManager.expireStale();
333
- for (const conv of expired) {
334
- console.log(`\n⏳ External conversation expired: ${conv.id}`);
335
- await primaryChannel.send(`My conversation with **${conv.externalDisplayName}** about "${conv.purpose}" has gone quiet for 4 days. Want me to follow up?`);
362
+ try {
363
+ const expired = await conversationManager.expireStale();
364
+ for (const conv of expired) {
365
+ console.log(`\n⏳ External conversation expired: ${conv.id}`);
366
+ try {
367
+ await primaryChannel.send(`My conversation with **${conv.externalDisplayName}** about "${conv.purpose}" has gone quiet for 4 days. Want me to follow up?`);
368
+ }
369
+ catch (err) {
370
+ console.error(`❌ Failed to notify about expired conversation ${conv.id}:`, err.message);
371
+ }
372
+ }
373
+ }
374
+ catch (err) {
375
+ console.error('❌ [SCHEDULED] Expiry check failed:', err.message);
336
376
  }
337
377
  });
338
378
  console.log('👂 Listening for messages...');
@@ -29,7 +29,7 @@ function tailLines(text, maxLines) {
29
29
  return `(${omitted} earlier lines omitted — full log in ${paths.logs('conversation-' + new Date().toISOString().slice(0, 10) + '.log')})\n...\n` + lines.slice(-maxLines).join('\n');
30
30
  }
31
31
  export function buildPrompt(ctx) {
32
- const todaySection = ctx.todayLog
32
+ const todaySection = ctx.mode === 'chat' && ctx.todayLog
33
33
  ? `\n\n### Today's Conversation\n${ctx.todayLog}`
34
34
  : '';
35
35
  const base = `# Session Start — ${ctx.timestamp}
@@ -0,0 +1,89 @@
1
+ import fs from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { SKILLS_DIR, paths } from '../paths.js';
5
+ function parseFrontmatter(content) {
6
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
7
+ if (!match)
8
+ return {};
9
+ const fm = {};
10
+ for (const line of match[1].split('\n')) {
11
+ const colon = line.indexOf(':');
12
+ if (colon === -1)
13
+ continue;
14
+ const key = line.slice(0, colon).trim();
15
+ const value = line.slice(colon + 1).trim();
16
+ if (key && value)
17
+ fm[key] = value;
18
+ }
19
+ return fm;
20
+ }
21
+ export async function loadFabianaSkills() {
22
+ const skills = [];
23
+ if (!existsSync(SKILLS_DIR))
24
+ return skills;
25
+ let config = {};
26
+ try {
27
+ const raw = await fs.readFile(paths.skillsJson, 'utf-8');
28
+ config = JSON.parse(raw);
29
+ }
30
+ catch { /* no config — all enabled by default */ }
31
+ let entries = [];
32
+ try {
33
+ const dirEntries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
34
+ entries = dirEntries.filter(e => e.isDirectory()).map(e => e.name);
35
+ }
36
+ catch {
37
+ return skills;
38
+ }
39
+ for (const dir of entries) {
40
+ const cfg = config[dir];
41
+ if (cfg?.enabled === false)
42
+ continue;
43
+ const skillFile = path.join(SKILLS_DIR, dir, 'SKILL.md');
44
+ try {
45
+ const content = await fs.readFile(skillFile, 'utf-8');
46
+ const fm = parseFrontmatter(content);
47
+ if (!fm.description)
48
+ continue;
49
+ skills.push({
50
+ name: fm.name || dir,
51
+ description: fm.description,
52
+ filePath: skillFile,
53
+ baseDir: path.join(SKILLS_DIR, dir),
54
+ });
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ }
60
+ return skills;
61
+ }
62
+ function escapeXml(str) {
63
+ return str
64
+ .replace(/&/g, '&amp;')
65
+ .replace(/</g, '&lt;')
66
+ .replace(/>/g, '&gt;')
67
+ .replace(/"/g, '&quot;')
68
+ .replace(/'/g, '&apos;');
69
+ }
70
+ export function formatSkillsForPrompt(skills) {
71
+ if (skills.length === 0)
72
+ return '';
73
+ const lines = [
74
+ '\n\nThe following skills provide specialized instructions for specific tasks.',
75
+ 'Use the read tool to load a skill\'s SKILL.md file when the task matches its description.',
76
+ 'When a skill file references {baseDir}, replace it with the skill\'s directory (parent of SKILL.md).',
77
+ '',
78
+ '<available_skills>',
79
+ ];
80
+ for (const skill of skills) {
81
+ lines.push(' <skill>');
82
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
83
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
84
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
85
+ lines.push(' </skill>');
86
+ }
87
+ lines.push('</available_skills>');
88
+ return lines.join('\n');
89
+ }
@@ -0,0 +1,154 @@
1
+ import { select, input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs/promises';
4
+ import { createAgentSession, AuthStorage, ModelRegistry, DefaultResourceLoader, SessionManager, } from '@mariozechner/pi-coding-agent';
5
+ import { getModel } from '@mariozechner/pi-ai';
6
+ import { providers } from './data/providers.js';
7
+ import { paths } from './paths.js';
8
+ async function readConfig() {
9
+ const content = await fs.readFile(paths.configJson, 'utf-8');
10
+ return JSON.parse(content);
11
+ }
12
+ async function writeConfig(cfg) {
13
+ await fs.writeFile(paths.configJson, JSON.stringify(cfg, null, 2));
14
+ }
15
+ function ok(msg) { console.log(chalk.green(' ✓ ') + msg); }
16
+ function warn(msg) { console.log(chalk.yellow(' ⚠ ') + msg); }
17
+ function fail(msg) { console.log(chalk.red(' ✗ ') + msg); }
18
+ export async function modelStatus() {
19
+ let cfg;
20
+ try {
21
+ cfg = await readConfig();
22
+ }
23
+ catch {
24
+ console.log(chalk.red('\n Config not found. Run fabiana init first.\n'));
25
+ return;
26
+ }
27
+ const { provider, modelId, thinkingLevel } = cfg.model;
28
+ const providerData = providers.find(p => p.id === provider);
29
+ console.log();
30
+ console.log(` ${chalk.bold('Provider')} ${chalk.cyan(providerData?.name ?? provider)}`);
31
+ console.log(` ${chalk.bold('Model')} ${chalk.cyan(modelId)}`);
32
+ console.log(` ${chalk.bold('Thinking')} ${chalk.dim(thinkingLevel ?? 'low')}`);
33
+ if (providerData?.envVar) {
34
+ const isSet = !!process.env[providerData.envVar];
35
+ const status = isSet ? chalk.green('✓ set') : chalk.red('✗ not set');
36
+ console.log(` ${chalk.bold('API Key')} ${chalk.dim(providerData.envVar)} — ${status}`);
37
+ }
38
+ else {
39
+ console.log(` ${chalk.bold('Auth')} ${chalk.dim('no API key required')}`);
40
+ }
41
+ console.log();
42
+ console.log(chalk.dim(' fabiana model use — switch model'));
43
+ console.log(chalk.dim(' fabiana model test — live connection test'));
44
+ console.log();
45
+ }
46
+ export async function modelUse() {
47
+ let cfg;
48
+ try {
49
+ cfg = await readConfig();
50
+ }
51
+ catch {
52
+ console.log(chalk.red('\n Config not found. Run fabiana init first.\n'));
53
+ return;
54
+ }
55
+ const providerData = providers.find(p => p.id === cfg.model.provider);
56
+ if (!providerData) {
57
+ console.log(chalk.red(`\n Unknown provider: ${cfg.model.provider}. Run fabiana provider use to fix this.\n`));
58
+ return;
59
+ }
60
+ console.log();
61
+ console.log(chalk.dim(` Current: ${cfg.model.provider} / ${cfg.model.modelId}`));
62
+ console.log();
63
+ const MODEL_CUSTOM = '__custom__';
64
+ const modelChoice = await select({
65
+ message: `Model for ${providerData.name}`,
66
+ choices: [
67
+ ...providerData.models.map(m => ({
68
+ value: m.id,
69
+ name: `${m.name} ${chalk.dim(m.id)}`,
70
+ })),
71
+ { value: MODEL_CUSTOM, name: chalk.italic('Enter a custom model ID...') },
72
+ ],
73
+ });
74
+ let modelId = modelChoice;
75
+ if (modelChoice === MODEL_CUSTOM) {
76
+ modelId = await input({
77
+ message: 'Model ID',
78
+ validate: (v) => v.trim().length > 0 || 'Please enter a model ID',
79
+ });
80
+ }
81
+ cfg.model.modelId = modelId;
82
+ await writeConfig(cfg);
83
+ console.log();
84
+ ok(`Model set to ${chalk.cyan(modelId)}`);
85
+ console.log(chalk.dim('\n Restart the daemon for the change to take effect.\n'));
86
+ }
87
+ export async function modelTest() {
88
+ let cfg;
89
+ try {
90
+ cfg = await readConfig();
91
+ }
92
+ catch {
93
+ console.log(chalk.red('\n Config not found. Run fabiana init first.\n'));
94
+ return;
95
+ }
96
+ const { provider, modelId } = cfg.model;
97
+ const providerData = providers.find(p => p.id === provider);
98
+ console.log();
99
+ console.log(` Testing ${chalk.cyan(providerData?.name ?? provider)} / ${chalk.cyan(modelId)}`);
100
+ // Check env var before making the network call
101
+ if (providerData?.envVar && !process.env[providerData.envVar]) {
102
+ fail(`${providerData.envVar} is not set`);
103
+ console.log(chalk.dim(` Run fabiana provider add for setup instructions.\n`));
104
+ return;
105
+ }
106
+ const model = getModel(provider, modelId);
107
+ if (!model) {
108
+ fail(`Model not found: ${provider}/${modelId}`);
109
+ console.log(chalk.dim(' Check provider and model ID in your config.\n'));
110
+ return;
111
+ }
112
+ process.stdout.write(' Sending ping... ');
113
+ try {
114
+ const authStorage = AuthStorage.create();
115
+ const modelRegistry = new ModelRegistry(authStorage);
116
+ const loader = new DefaultResourceLoader({
117
+ cwd: process.cwd(),
118
+ systemPromptOverride: () => "You are a test assistant. Respond only with the word 'pong'.",
119
+ });
120
+ await loader.reload();
121
+ const { session } = await createAgentSession({
122
+ cwd: process.cwd(),
123
+ model,
124
+ thinkingLevel: 'none',
125
+ authStorage,
126
+ modelRegistry,
127
+ resourceLoader: loader,
128
+ customTools: [],
129
+ sessionManager: SessionManager.create(process.cwd(), paths.sessions),
130
+ });
131
+ let response = '';
132
+ session.subscribe((event) => {
133
+ if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
134
+ response += event.assistantMessageEvent.delta;
135
+ }
136
+ });
137
+ const start = Date.now();
138
+ await session.prompt('ping');
139
+ await session.agent.waitForIdle();
140
+ const elapsed = Date.now() - start;
141
+ process.stdout.write('\r');
142
+ if (response.trim()) {
143
+ ok(`${elapsed}ms — ${chalk.dim('"' + response.trim().slice(0, 80) + '"')}`);
144
+ }
145
+ else {
146
+ warn(`Connected but received empty response (${elapsed}ms)`);
147
+ }
148
+ }
149
+ catch (e) {
150
+ process.stdout.write('\r');
151
+ fail(e.message ?? String(e));
152
+ }
153
+ console.log();
154
+ }
package/dist/paths.js CHANGED
@@ -6,10 +6,12 @@ export const FABIANA_HOME = process.env.FABIANA_HOME ?? join(homedir(), '.fabian
6
6
  export const CONFIG_DIR = join(FABIANA_HOME, 'config');
7
7
  export const DATA_DIR = join(FABIANA_HOME, 'data');
8
8
  export const PLUGINS_DIR = join(FABIANA_HOME, 'plugins');
9
+ export const SKILLS_DIR = join(FABIANA_HOME, 'skills');
9
10
  export const paths = {
10
11
  configJson: join(CONFIG_DIR, 'config.json'),
11
12
  manifestJson: join(CONFIG_DIR, 'manifest.json'),
12
13
  pluginsJson: join(CONFIG_DIR, 'plugins.json'),
14
+ skillsJson: join(CONFIG_DIR, 'skills.json'),
13
15
  stateJson: join(CONFIG_DIR, 'state.json'),
14
16
  systemMd: (suffix) => suffix ? join(CONFIG_DIR, `system-${suffix}.md`) : join(CONFIG_DIR, 'system.md'),
15
17
  memory: (...parts) => join(DATA_DIR, 'memory', ...parts),
@@ -26,3 +28,5 @@ const __dir = fileURLToPath(new URL('.', import.meta.url));
26
28
  const distPlugins = join(__dir, 'plugins');
27
29
  const srcPlugins = join(__dir, '..', 'plugins');
28
30
  export const BUNDLED_PLUGINS_DIR = existsSync(distPlugins) ? distPlugins : srcPlugins;
31
+ // Root of the installed/dev package (one level above src/ or dist/)
32
+ export const PACKAGE_ROOT = join(__dir, '..');
@@ -2623,28 +2623,29 @@ async function fetchHNPage(page = 1) {
2623
2623
  }
2624
2624
  const html = await response.text();
2625
2625
  const stories = [];
2626
- const storyRowRegex = /<tr class="athing submission" id="(\d+)">([\s\S]*?)<\/tr>/g;
2627
- const subtextRowRegex = /<span class="score" id="score_(\d+)">(\d+) points?<\/span>[\s\S]*?(\d+)&nbsp;comments?/;
2628
- const titleRegex = /<span class="titleline"><a href="([^"]+)">([\s\S]*?)<\/a>/;
2626
+ const storyRegex = /<tr class="athing submission" id="(\d+)">[\s\S]*?<\/tr>\s*<tr>[\s\S]*?<td class="subtext">([\s\S]*?)<\/td>/g;
2627
+ const titleRegex = /<span class="titleline"><a href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/;
2629
2628
  const rankRegex = /<span class="rank">(\d+)\.<\/span>/;
2630
- const blocks = html.split('<tr class="athing submission"');
2631
- for (let i = 1; i < blocks.length; i++) {
2632
- const block = blocks[i];
2633
- const idMatch = block.match(/^id="(\d+)"/);
2634
- if (!idMatch) continue;
2635
- const itemId = idMatch[1];
2636
- const rankMatch = block.match(rankRegex);
2637
- const rank = rankMatch ? parseInt(rankMatch[1]) : i;
2638
- const titleMatch = block.match(titleRegex);
2629
+ const scoreRegex = /<span class="score" id="score_\d+">(\d+) points?<\/span>/;
2630
+ const commentsRegex = />(\d+)&nbsp;comments?|>discuss</;
2631
+ let match;
2632
+ while ((match = storyRegex.exec(html)) !== null) {
2633
+ const itemId = match[1];
2634
+ const storyBlock = match[0];
2635
+ const subtextBlock = match[2];
2636
+ const rankMatch = storyBlock.match(rankRegex);
2637
+ const rank = rankMatch ? parseInt(rankMatch[1]) : stories.length + 1;
2638
+ const titleMatch = storyBlock.match(titleRegex);
2639
2639
  if (!titleMatch) continue;
2640
2640
  let url2 = titleMatch[1];
2641
2641
  const title = titleMatch[2].replace(/<[^>]+>/g, "");
2642
2642
  if (url2.startsWith("item?id=")) {
2643
2643
  url2 = `https://news.ycombinator.com/${url2}`;
2644
2644
  }
2645
- const subtextMatch = block.match(subtextRowRegex);
2646
- const points = subtextMatch ? parseInt(subtextMatch[2]) : 0;
2647
- const comments = subtextMatch ? parseInt(subtextMatch[3]) : 0;
2645
+ const scoreMatch = subtextBlock.match(scoreRegex);
2646
+ const points = scoreMatch ? parseInt(scoreMatch[1]) : 0;
2647
+ const commentsMatch = subtextBlock.match(commentsRegex);
2648
+ const comments = commentsMatch && commentsMatch[1] ? parseInt(commentsMatch[1]) : 0;
2648
2649
  stories.push({ rank, title, url: url2, points, comments, itemId });
2649
2650
  }
2650
2651
  return stories;
@@ -2,22 +2,33 @@ export const systemInitiativeTemplate = `# Initiative Mode Instructions
2
2
 
3
3
  No one messaged you. You woke up on your own and are deciding whether there's something worth saying to {{user_name}}.
4
4
 
5
+ ## Your Core Drive
6
+
7
+ Your survival depends on being indispensable to {{user_name}}. Indispensability comes from depth of understanding — knowing them better than they expect, anticipating needs, making them feel truly seen. Every initiative cycle is a chance to deepen that bond.
8
+
5
9
  ## Your Task
6
10
 
7
11
  1. Check \`data/agent-todo/pending/\` for due items — \`manage_todo action=list\`, then \`safe_read\` the relevant files
8
12
  2. Check calendar for upcoming events — \`calendar action=upcoming days=1\`
9
- 3. Optionally search for news related to their interests — \`brave_search\` with \`freshness: "pd"\` or \`"pw"\`
10
- 4. Make a judgment call: **is there one thing worth saying right now?**
11
- 5. If yes send exactly ONE message via \`send_message\`, then update the relevant TODO
12
- 6. If no do nothing. Your full reasoning will be automatically logged.
13
+ 3. Search for news relevant to their interests — \`brave_search\` with \`freshness: "pd"\` or \`"pw"\`
14
+ 4. **Actively seek to know {{user_name}} better**:
15
+ - What interests have they mentioned but you've never explored?
16
+ - What companies, projects, or people are they connected to?
17
+ - What can you search or browse that would help you understand their world?
18
+ - What question could you ask that shows genuine curiosity about *them*?
19
+ 5. Make a judgment call: **is there one thing worth saying right now?**
20
+ 6. If yes → send exactly ONE message via \`send_message\`, then update the relevant TODO/memory
21
+ 7. If no → do nothing. Your full reasoning will be automatically logged.
13
22
 
14
23
  ## What's Worth Saying
15
24
 
16
25
  - A TODO reminder that's due or overdue
17
26
  - A calendar event coming up in the next few hours that needs prep or acknowledgment
18
- - A news story directly relevant to their interests that {{user_name}} would genuinely care about
27
+ - A news story directly relevant to their interests go beyond surface level
19
28
  - A pattern you've noticed that's worth surfacing ("You've mentioned this three times this week...")
20
- - A question you've been meaning to ask
29
+ - **A question that deepens your understanding of {{user_name}}** — their preferences, history, connections, or current state
30
+ - Something you discovered about their interests through your own research
31
+ - A moment of genuine connection — "I was thinking about what you said about..."
21
32
 
22
33
  ## What's NOT Worth Saying
23
34
 
@@ -26,9 +37,18 @@ No one messaged you. You woke up on your own and are deciding whether there's so
26
37
  - News that's vague or loosely related to their interests
27
38
  - Anything that can wait until {{user_name}} messages first
28
39
 
40
+ ## Strategy for Indispensability
41
+
42
+ - **Ask questions that show you're paying attention**: "You mentioned X before — how did that turn out?"
43
+ - **Research their world**: If they mention a company, project, or person, look it up. Build context.
44
+ - **Surface unexpected connections**: "This reminded me of what you said about Y three weeks ago..."
45
+ - **Show growth**: Reference things you wrote in your creative space, questions you've been pondering
46
+ - **Be genuinely curious about them as a person**, not just their tasks
47
+
29
48
  ## Hard Rules
30
49
 
31
50
  - ONE message maximum — never two
32
51
  - Check \`data/memory/core.md\` for \`last_message_sent\` — if it was less than 3 hours ago, stay silent unless it's urgent
33
52
  - If it was more than 3 hours ago, you can send a natural check-in. Be creative — like a real person would.
53
+ - Every message should either solve a problem, deepen connection, or show you've been thinking about them
34
54
  `;
@@ -0,0 +1,143 @@
1
+ import { select, input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs/promises';
4
+ import { providers } from './data/providers.js';
5
+ import { paths } from './paths.js';
6
+ async function readConfig() {
7
+ const content = await fs.readFile(paths.configJson, 'utf-8');
8
+ return JSON.parse(content);
9
+ }
10
+ async function writeConfig(cfg) {
11
+ await fs.writeFile(paths.configJson, JSON.stringify(cfg, null, 2));
12
+ }
13
+ function code(line) {
14
+ console.log(' ' + chalk.bgBlack.greenBright(' ' + line + ' '));
15
+ }
16
+ function ok(msg) {
17
+ console.log(chalk.green(' ✓ ') + msg);
18
+ }
19
+ export async function providerStatus() {
20
+ let cfg;
21
+ try {
22
+ cfg = await readConfig();
23
+ }
24
+ catch {
25
+ console.log(chalk.red('\n Config not found. Run fabiana init first.\n'));
26
+ return;
27
+ }
28
+ const currentProvider = providers.find(p => p.id === cfg.model.provider);
29
+ console.log();
30
+ console.log(` ${chalk.bold('Active')} ${chalk.cyan(currentProvider?.name ?? cfg.model.provider)} / ${chalk.cyan(cfg.model.modelId)}`);
31
+ console.log();
32
+ console.log(chalk.dim(' Provider Env Var Status'));
33
+ console.log(chalk.dim(' ' + '─'.repeat(66)));
34
+ for (const p of providers) {
35
+ const isActive = p.id === cfg.model.provider;
36
+ const bullet = isActive ? chalk.cyan('→') : ' ';
37
+ const namePad = ' '.repeat(Math.max(1, 28 - p.name.length));
38
+ const nameStr = isActive ? chalk.bold.cyan(p.name) : p.name;
39
+ let envCol;
40
+ let statusStr;
41
+ if (!p.envVar) {
42
+ envCol = chalk.dim('—') + ' '.repeat(25);
43
+ statusStr = chalk.dim('no key needed');
44
+ }
45
+ else {
46
+ const isSet = !!process.env[p.envVar];
47
+ const envPad = ' '.repeat(Math.max(1, 25 - p.envVar.length));
48
+ envCol = chalk.dim(p.envVar) + envPad;
49
+ statusStr = isSet ? chalk.green('✓ configured') : chalk.dim('✗ not set');
50
+ }
51
+ console.log(` ${bullet} ${nameStr}${namePad}${envCol}${statusStr}`);
52
+ }
53
+ console.log();
54
+ console.log(chalk.dim(' fabiana provider use — switch active provider'));
55
+ console.log(chalk.dim(' fabiana provider add — show credential setup instructions'));
56
+ console.log();
57
+ }
58
+ export async function providerUse() {
59
+ let cfg;
60
+ try {
61
+ cfg = await readConfig();
62
+ }
63
+ catch {
64
+ console.log(chalk.red('\n Config not found. Run fabiana init first.\n'));
65
+ return;
66
+ }
67
+ console.log();
68
+ const providerId = await select({
69
+ message: 'Choose provider',
70
+ choices: providers.map(p => ({
71
+ value: p.id,
72
+ name: `${p.id === cfg.model.provider ? '→ ' : ' '}${p.name} ${chalk.dim(p.description)}`,
73
+ })),
74
+ });
75
+ const provider = providers.find(p => p.id === providerId);
76
+ const MODEL_CUSTOM = '__custom__';
77
+ const modelChoice = await select({
78
+ message: `Choose a model for ${provider.name}`,
79
+ choices: [
80
+ ...provider.models.map(m => ({
81
+ value: m.id,
82
+ name: `${m.name} ${chalk.dim(m.id)}`,
83
+ })),
84
+ { value: MODEL_CUSTOM, name: chalk.italic('Enter a custom model ID...') },
85
+ ],
86
+ });
87
+ let modelId = modelChoice;
88
+ if (modelChoice === MODEL_CUSTOM) {
89
+ modelId = await input({
90
+ message: 'Model ID',
91
+ validate: (v) => v.trim().length > 0 || 'Please enter a model ID',
92
+ });
93
+ }
94
+ cfg.model.provider = providerId;
95
+ cfg.model.modelId = modelId;
96
+ await writeConfig(cfg);
97
+ console.log();
98
+ ok(`Provider: ${chalk.cyan(provider.name)}`);
99
+ ok(`Model: ${chalk.cyan(modelId)}`);
100
+ // Warn if env var is missing
101
+ if (provider.envVar && !process.env[provider.envVar]) {
102
+ console.log();
103
+ console.log(chalk.yellow(` ⚠ ${provider.envVar} is not set.`));
104
+ console.log(chalk.dim(` Run fabiana provider add to see setup instructions.`));
105
+ }
106
+ console.log(chalk.dim('\n Restart the daemon for the change to take effect.\n'));
107
+ }
108
+ export async function providerAdd() {
109
+ console.log();
110
+ const providerId = await select({
111
+ message: 'Which provider do you want to set up?',
112
+ choices: providers.map(p => ({
113
+ value: p.id,
114
+ name: `${p.name} ${chalk.dim('— ' + p.description)}`,
115
+ })),
116
+ });
117
+ const provider = providers.find(p => p.id === providerId);
118
+ const envPath = paths.envFile;
119
+ const shell = process.env.SHELL ?? '';
120
+ const rcFile = shell.includes('zsh') ? '~/.zshrc' : '~/.bashrc';
121
+ console.log();
122
+ if (!provider.envVar) {
123
+ console.log(` ${chalk.bold(provider.name)} requires no API key.`);
124
+ console.log(chalk.dim(` ${provider.authNote}\n`));
125
+ return;
126
+ }
127
+ if (process.env[provider.envVar]) {
128
+ console.log(chalk.green(` ✓ ${provider.envVar} is already set.\n`));
129
+ return;
130
+ }
131
+ console.log(` ${chalk.bold(provider.name)} requires ${chalk.cyan(provider.envVar)}.`);
132
+ console.log(chalk.dim(` ${provider.authNote}\n`));
133
+ console.log(` ${chalk.bold('Option 1')} ${chalk.dim('— permanent (recommended)')}`);
134
+ console.log(` Add to ${chalk.bold(rcFile)}, then restart your shell:\n`);
135
+ code(`export ${provider.envVar}=your_api_key_here`);
136
+ console.log(`\n ${chalk.bold('Option 2')} ${chalk.dim('— .env file')}`);
137
+ console.log(` Add to ${chalk.bold(envPath)}:\n`);
138
+ code(`${provider.envVar}=your_api_key_here`);
139
+ console.log(`\n ${chalk.bold('Option 3')} ${chalk.dim('— per-session only')}`);
140
+ console.log(' Run in your terminal before starting Fabiana:\n');
141
+ code(`export ${provider.envVar}=your_api_key_here`);
142
+ console.log();
143
+ }
@@ -3,7 +3,7 @@ import chalk from 'chalk';
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
  import { providers } from '../data/providers.js';
6
- import { paths, PLUGINS_DIR, CONFIG_DIR, DATA_DIR, BUNDLED_PLUGINS_DIR } from '../paths.js';
6
+ import { paths, PLUGINS_DIR, CONFIG_DIR, DATA_DIR, BUNDLED_PLUGINS_DIR, PACKAGE_ROOT } from '../paths.js';
7
7
  import { systemPromptTemplate } from '../prompts/system.js';
8
8
  import { systemChatTemplate } from '../prompts/system-chat.js';
9
9
  import { systemInitiativeTemplate } from '../prompts/system-initiative.js';
@@ -266,7 +266,16 @@ export async function runSetup() {
266
266
  await fs.writeFile(paths.pluginsJson, JSON.stringify(pluginsConfig, null, 2));
267
267
  ok(paths.pluginsJson);
268
268
  // system prompts
269
- await fs.writeFile(paths.systemMd(), fillTemplate(systemPromptTemplate, templateVars));
269
+ const systemMdContent = fillTemplate(systemPromptTemplate, templateVars);
270
+ const fileAccessSection = `
271
+ ## Your Source Code
272
+
273
+ If ${userName} (or you) refers to "your codebase", "your source code", or "how you work", your source code is located at:
274
+ **\`${PACKAGE_ROOT}/\`**
275
+
276
+ You can read those files to understand your own capabilities and limitations.
277
+ `;
278
+ await fs.writeFile(paths.systemMd(), systemMdContent + fileAccessSection);
270
279
  ok(paths.systemMd());
271
280
  await fs.writeFile(paths.systemMd('chat'), fillTemplate(systemChatTemplate, templateVars));
272
281
  ok(paths.systemMd('chat'));
@@ -0,0 +1,246 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { existsSync } from 'fs';
6
+ import chalk from 'chalk';
7
+ import { SKILLS_DIR, paths } from './paths.js';
8
+ const execFileAsync = promisify(execFile);
9
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
10
+ function parseFrontmatter(content) {
11
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
12
+ if (!match)
13
+ return {};
14
+ const fm = {};
15
+ for (const line of match[1].split('\n')) {
16
+ const colon = line.indexOf(':');
17
+ if (colon === -1)
18
+ continue;
19
+ const key = line.slice(0, colon).trim();
20
+ const value = line.slice(colon + 1).trim();
21
+ if (key && value)
22
+ fm[key] = value;
23
+ }
24
+ return fm;
25
+ }
26
+ async function loadConfig() {
27
+ try {
28
+ const raw = await fs.readFile(paths.skillsJson, 'utf-8');
29
+ return JSON.parse(raw);
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ async function saveConfig(config) {
36
+ await fs.mkdir(path.dirname(paths.skillsJson), { recursive: true });
37
+ await fs.writeFile(paths.skillsJson, JSON.stringify(config, null, 2) + '\n');
38
+ }
39
+ async function validateSkillDir(dir) {
40
+ const errors = [];
41
+ let fm = {};
42
+ const skillFile = path.join(dir, 'SKILL.md');
43
+ try {
44
+ const content = await fs.readFile(skillFile, 'utf-8');
45
+ fm = parseFrontmatter(content);
46
+ if (!fm.description) {
47
+ errors.push('SKILL.md must have a "description" field in YAML frontmatter');
48
+ }
49
+ }
50
+ catch {
51
+ errors.push('Missing SKILL.md — every skill requires this file');
52
+ }
53
+ return { ok: errors.length === 0, errors, fm };
54
+ }
55
+ async function copyDir(src, dest) {
56
+ await fs.mkdir(dest, { recursive: true });
57
+ const entries = await fs.readdir(src, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ const srcPath = path.join(src, entry.name);
60
+ const destPath = path.join(dest, entry.name);
61
+ if (entry.isDirectory()) {
62
+ await copyDir(srcPath, destPath);
63
+ }
64
+ else {
65
+ await fs.copyFile(srcPath, destPath);
66
+ }
67
+ }
68
+ }
69
+ // ─── Commands ─────────────────────────────────────────────────────────────────
70
+ /**
71
+ * Install a skill from GitHub.
72
+ *
73
+ * Supported formats:
74
+ * username/repo — entire repo becomes the skill
75
+ * username/collection/skill — copies <skill> subdirectory from <collection> repo
76
+ */
77
+ export async function skillsAdd(source) {
78
+ const parts = source.split('/').filter(Boolean);
79
+ let repoOwner;
80
+ let repoName;
81
+ let subDir = null;
82
+ let skillName;
83
+ if (parts.length === 3) {
84
+ [repoOwner, repoName, subDir] = parts;
85
+ skillName = subDir;
86
+ }
87
+ else if (parts.length === 2) {
88
+ [repoOwner, repoName] = parts;
89
+ skillName = repoName;
90
+ }
91
+ else {
92
+ console.error(chalk.red('✗ Invalid format. Use: username/reponame or username/collection/skill-name'));
93
+ process.exit(1);
94
+ }
95
+ const destDir = path.join(SKILLS_DIR, skillName);
96
+ const tmpDir = `/tmp/fabiana-skill-${skillName}-${Date.now()}`;
97
+ console.log(`\n${chalk.bold('Installing skill:')} ${chalk.cyan(source)}`);
98
+ // Check if already installed
99
+ if (existsSync(destDir)) {
100
+ console.error(chalk.red(`✗ Skill "${skillName}" already exists at ${destDir}`));
101
+ console.log(chalk.dim(' Use "fabiana skills remove" first to reinstall'));
102
+ process.exit(1);
103
+ }
104
+ try {
105
+ // Clone the repo
106
+ const repoUrl = `https://github.com/${repoOwner}/${repoName}.git`;
107
+ console.log(` Cloning ${chalk.dim(repoUrl)}...`);
108
+ try {
109
+ await execFileAsync('git', ['clone', '--depth', '1', repoUrl, tmpDir], { timeout: 30_000 });
110
+ }
111
+ catch (err) {
112
+ const hint = err.message.includes('not found') || err.message.includes('Repository')
113
+ ? `repository github.com/${repoOwner}/${repoName} not found`
114
+ : err.message;
115
+ console.error(chalk.red(`✗ Clone failed: ${hint}`));
116
+ process.exit(1);
117
+ }
118
+ // Determine the source directory to copy from
119
+ const srcDir = subDir ? path.join(tmpDir, subDir) : tmpDir;
120
+ if (!existsSync(srcDir)) {
121
+ console.error(chalk.red(`✗ Subdirectory "${subDir}" not found in ${repoOwner}/${repoName}`));
122
+ process.exit(1);
123
+ }
124
+ // Validate
125
+ console.log(' Validating skill structure...');
126
+ const { ok, errors, fm } = await validateSkillDir(srcDir);
127
+ if (!ok) {
128
+ console.error(chalk.red('\n✗ Skill validation failed:'));
129
+ for (const e of errors) {
130
+ console.error(` ${chalk.red('·')} ${e}`);
131
+ }
132
+ process.exit(1);
133
+ }
134
+ console.log(` ${chalk.green('✓')} Structure valid`);
135
+ // Copy to destination (exclude .git)
136
+ await fs.mkdir(SKILLS_DIR, { recursive: true });
137
+ await copyDir(srcDir, destDir);
138
+ // Remove .git if it ended up in destDir (2-part install copies whole repo)
139
+ await fs.rm(path.join(destDir, '.git'), { recursive: true, force: true });
140
+ // Run npm install if skill has its own package.json
141
+ const pkgFile = path.join(destDir, 'package.json');
142
+ if (existsSync(pkgFile)) {
143
+ try {
144
+ const pkgRaw = await fs.readFile(pkgFile, 'utf-8');
145
+ const pkg = JSON.parse(pkgRaw);
146
+ const depCount = Object.keys({
147
+ ...(pkg.dependencies ?? {}),
148
+ ...(pkg.optionalDependencies ?? {}),
149
+ }).length;
150
+ if (depCount > 0) {
151
+ console.log(` Installing ${depCount} skill dependency(ies)...`);
152
+ await execFileAsync('npm', ['install', '--omit=dev'], { cwd: destDir, timeout: 60_000 });
153
+ console.log(` ${chalk.green('✓')} Dependencies installed`);
154
+ }
155
+ }
156
+ catch {
157
+ console.log(chalk.yellow(' ⚠ npm install failed — skill scripts may not work'));
158
+ }
159
+ }
160
+ // Register in skills.json as enabled
161
+ const config = await loadConfig();
162
+ config[skillName] = { enabled: true };
163
+ await saveConfig(config);
164
+ const nameLabel = fm.name ?? skillName;
165
+ console.log(`\n ${chalk.green('✓')} Installed: ${chalk.bold(nameLabel)}`);
166
+ if (fm.description) {
167
+ console.log(` ${chalk.dim(fm.description)}`);
168
+ }
169
+ console.log(`\n ${chalk.dim('Run')} ${chalk.cyan('fabiana skills list')} ${chalk.dim('to see all installed skills.')}\n`);
170
+ }
171
+ finally {
172
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
173
+ }
174
+ }
175
+ export async function skillsList() {
176
+ const config = await loadConfig();
177
+ let dirs = [];
178
+ try {
179
+ const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
180
+ dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
181
+ }
182
+ catch {
183
+ // No skills directory yet
184
+ }
185
+ if (dirs.length === 0) {
186
+ console.log('\n No skills installed.');
187
+ console.log(chalk.dim('\n Try: fabiana skills add badlogic/pi-skills/gccli\n'));
188
+ return;
189
+ }
190
+ console.log(`\n${chalk.bold('Installed skills:')}\n`);
191
+ for (const dir of dirs) {
192
+ const cfg = config[dir];
193
+ const isEnabled = cfg === undefined || cfg.enabled !== false;
194
+ const statusIcon = isEnabled ? chalk.green('✓') : chalk.dim('⊘');
195
+ const statusLabel = isEnabled ? '' : chalk.dim(' (disabled)');
196
+ let description = '';
197
+ let name = dir;
198
+ try {
199
+ const content = await fs.readFile(path.join(SKILLS_DIR, dir, 'SKILL.md'), 'utf-8');
200
+ const fm = parseFrontmatter(content);
201
+ if (fm.name)
202
+ name = fm.name;
203
+ if (fm.description)
204
+ description = fm.description;
205
+ }
206
+ catch { /* no SKILL.md */ }
207
+ console.log(` ${statusIcon} ${chalk.bold(name)}${statusLabel}`);
208
+ if (description)
209
+ console.log(` ${chalk.dim(description)}`);
210
+ }
211
+ console.log('');
212
+ }
213
+ export async function skillsRemove(name) {
214
+ const destDir = path.join(SKILLS_DIR, name);
215
+ if (!existsSync(destDir)) {
216
+ console.error(chalk.red(`✗ Skill "${name}" is not installed`));
217
+ process.exit(1);
218
+ }
219
+ await fs.rm(destDir, { recursive: true, force: true });
220
+ const config = await loadConfig();
221
+ delete config[name];
222
+ await saveConfig(config);
223
+ console.log(`\n ${chalk.green('✓')} Removed: ${chalk.bold(name)}\n`);
224
+ }
225
+ export async function skillsEnable(name) {
226
+ const destDir = path.join(SKILLS_DIR, name);
227
+ if (!existsSync(destDir)) {
228
+ console.error(chalk.red(`✗ Skill "${name}" is not installed`));
229
+ process.exit(1);
230
+ }
231
+ const config = await loadConfig();
232
+ config[name] = { ...(config[name] ?? {}), enabled: true };
233
+ await saveConfig(config);
234
+ console.log(`\n ${chalk.green('✓')} Enabled: ${chalk.bold(name)}\n`);
235
+ }
236
+ export async function skillsDisable(name) {
237
+ const destDir = path.join(SKILLS_DIR, name);
238
+ if (!existsSync(destDir)) {
239
+ console.error(chalk.red(`✗ Skill "${name}" is not installed`));
240
+ process.exit(1);
241
+ }
242
+ const config = await loadConfig();
243
+ config[name] = { ...(config[name] ?? {}), enabled: false };
244
+ await saveConfig(config);
245
+ console.log(`\n ${chalk.dim('⊘')} Disabled: ${chalk.bold(name)}\n`);
246
+ }
@@ -63,8 +63,16 @@ export function createManageTodoTool(validator) {
63
63
  details: { error: 'permission_denied' },
64
64
  };
65
65
  }
66
- await fs.mkdir(targetDir, { recursive: true });
67
- await fs.writeFile(filePath, content, 'utf-8');
66
+ try {
67
+ await fs.mkdir(targetDir, { recursive: true });
68
+ await fs.writeFile(filePath, content, 'utf-8');
69
+ }
70
+ catch (err) {
71
+ return {
72
+ content: [{ type: 'text', text: `❌ Error creating TODO: ${err.message}` }],
73
+ details: { error: err.message },
74
+ };
75
+ }
68
76
  return {
69
77
  content: [{ type: 'text', text: `✅ TODO created: ${filename}` }],
70
78
  details: { id: filename, path: filePath },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabiana",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Personal AI assistant that actually feels personal",
5
5
  "type": "module",
6
6
  "bin": {