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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 arifcamp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ <p align="center">
2
+ <img src="fabiana.png" alt="Fabiana" width="180" />
3
+ </p>
4
+
5
+ <h1 align="center">Fabiana</h1>
6
+ <p align="center"><em>Your personal AI companion that actually feels personal</em></p>
7
+
8
+ ---
9
+
10
+ ## The pitch
11
+
12
+ Every other AI assistant sits there, waiting for your command, answering like an overly polite receptionist with a forced smile. Fabiana doesn’t wait. She texts you first, asks about your day, and remembers your habits. She has things on her mind, patterns she noticed, stories she thinks you’d enjoy.
13
+
14
+ Fabiana is not your typical obedient worker. She's independent, proactive, and has her own agenda. She's not an assistant who you tried to befriends with. She's a friend who slowly learn to help you with your tasks. The kind who remembers your sister’s name, knows you refuse to schedule meetings before 10am, and will roast you—gently—when you promised to sleep early but it’s 1am and you’re asking her about world news again.
15
+
16
+ No dashboards. No commands to memorize. She just slides into your DM.
17
+
18
+ ---
19
+
20
+ ## What she does
21
+
22
+ **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.
23
+
24
+ **She remembers everything.** Every conversation gets distilled into plain-text memory. Next week she still knows what you're working on, who you mentioned, and what's stressing you out. Next month too.
25
+
26
+ **Her memory is yours.** All data lives in `.fabiana/data/` as plain text files. Read it, edit it, back it up, delete it. No black boxes. No vector embeddings. No vendor lock-in.
27
+
28
+ **She learns new tricks.** Drop a plugin into `plugins/` and she wakes up with a new capability. Web search, calendar, Hacker News — or whatever you build.
29
+
30
+ **She's small enough to trust.** The codebase is intentionally tiny. TypeScript, a handful of dependencies, plain text files. You can read the whole thing in an afternoon.
31
+
32
+ ---
33
+
34
+ ## How it works
35
+
36
+ Fabiana runs as a background daemon doing three things on a loop:
37
+
38
+ | Mode | What it does |
39
+ |------|-------------|
40
+ | **Chat** | Listens for your Telegram messages and responds |
41
+ | **Initiative** | Checks her TODO list and calendar, decides if there's something worth telling you |
42
+ | **Consolidation** | Every night at midnight, distills the day's conversations into structured memory |
43
+
44
+ She's built on [Pi SDK](https://github.com/mariozechner/pi) — which means she runs on Anthropic, OpenAI, Google Gemini, Groq, Mistral, Amazon Bedrock, and more. [OpenRouter](https://openrouter.ai) is the default because one key gets you 240+ models.
45
+
46
+ ### Memory — plain text, always
47
+
48
+ ```
49
+ .fabiana/data/memory/
50
+ ├── identity.md ← who you are
51
+ ├── core.md ← what's happening in your life right now
52
+ ├── people/ ← one file per person you mention
53
+ ├── interests/topics.md ← what you care about
54
+ ├── recent/this-week.md ← short-term context
55
+ └── diary/ ← daily entries (auto-written)
56
+ ```
57
+
58
+ Memory is tiered — hot files load every session, warm files load when relevant, cold files sit in the archive, searchable when needed. She writes and organizes it herself. You can read any of it any time.
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ### What you need
65
+
66
+ - **Node.js ≥ 22**
67
+ - A **Telegram bot** — takes 2 minutes via [@BotFather](https://t.me/BotFather)
68
+ - An LLM API key — [OpenRouter](https://openrouter.ai/keys) is the easiest starting point (one key, 240+ models)
69
+
70
+ ### Setup
71
+
72
+ ```bash
73
+ git clone https://github.com/your-username/fabiana
74
+ cd fabiana
75
+ npm install
76
+ fabiana init
77
+ ```
78
+
79
+ `fabiana init` walks you through the whole setup. Or do it manually:
80
+
81
+ Create a `.env` file:
82
+
83
+ ```env
84
+ TELEGRAM_BOT_TOKEN=your_token_from_botfather
85
+ TELEGRAM_CHAT_ID=your_chat_id
86
+
87
+ # Pick one (or more)
88
+ OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter (recommended — covers everything)
89
+ ANTHROPIC_API_KEY=... # Direct Anthropic
90
+ OPENAI_API_KEY=... # Direct OpenAI
91
+ GEMINI_API_KEY=... # Direct Google
92
+
93
+ # Optional extras
94
+ BRAVE_API_KEY=... # Web search
95
+ GOOGLE_CALENDAR_EMAIL=your@gmail.com # Calendar awareness
96
+ ```
97
+
98
+ **Getting your Telegram credentials:**
99
+ 1. Message [@BotFather](https://t.me/BotFather) → `/newbot` → copy the token
100
+ 2. Message [@userinfobot](https://t.me/userinfobot) → copy your numeric ID
101
+
102
+ ### Check everything's wired up
103
+
104
+ ```bash
105
+ fabiana doctor
106
+ ```
107
+
108
+ Verifies your environment, credentials, plugins, and data directories. Fix anything it flags, then:
109
+
110
+ ```bash
111
+ fabiana start
112
+ ```
113
+
114
+ She'll start listening on Telegram and schedule herself from there.
115
+
116
+ ---
117
+
118
+ ## Commands
119
+
120
+ | Command | What it does |
121
+ |---------|-------------|
122
+ | `fabiana init` | First time? Let's get acquainted |
123
+ | `fabiana start` | Wake her up — she'll take it from there |
124
+ | `fabiana initiative` | Make her think. Just once. (good for testing) |
125
+ | `fabiana consolidate` | Tidy up the mind palace |
126
+ | `fabiana doctor` | Is everything okay in there? Let's check |
127
+ | `fabiana backup` | Save her brain to a zip file |
128
+ | `fabiana restore <file>` | Bring her back from the archive |
129
+ | `fabiana plugins add <user/repo>` | Teach her a new trick from GitHub |
130
+ | `fabiana plugins list` | What can she do? |
131
+
132
+ ---
133
+
134
+ ## Choosing a model
135
+
136
+ Edit `config.json`:
137
+
138
+ ```json
139
+ {
140
+ "model": {
141
+ "provider": "openrouter",
142
+ "modelId": "anthropic/claude-sonnet-4-5",
143
+ "thinkingLevel": "low"
144
+ }
145
+ }
146
+ ```
147
+
148
+ **Popular choices:**
149
+
150
+ | Provider | Model | Notes |
151
+ |---|---|---|
152
+ | `openrouter` | `anthropic/claude-sonnet-4-5` | Best quality via OpenRouter |
153
+ | `openrouter` | `google/gemini-2.5-flash` | Fast and cheap |
154
+ | `anthropic` | `claude-sonnet-4-6` | Direct Anthropic |
155
+ | `google` | `gemini-2.5-flash` | Direct Google |
156
+ | `groq` | `llama-3.3-70b-versatile` | Very fast, generous free tier |
157
+
158
+ See [docs/providers.md](docs/providers.md) for the full list.
159
+
160
+ ---
161
+
162
+ ## Optional: Google Calendar
163
+
164
+ ```bash
165
+ npm install -g @mariozechner/gccli
166
+ gccli accounts credentials ~/path/to/oauth-credentials.json
167
+ gccli accounts add your@gmail.com
168
+ ```
169
+
170
+ Then add `GOOGLE_CALENDAR_EMAIL=your@gmail.com` to `.env`. Now she'll actually know when you have that meeting you keep forgetting.
171
+
172
+ ---
173
+
174
+ ## Optional: Brave Search
175
+
176
+ 1. Create a free account at [api-dashboard.search.brave.com](https://api-dashboard.search.brave.com/register)
177
+ 2. Grab an API key and add `BRAVE_API_KEY=your_key` to `.env`
178
+
179
+ ---
180
+
181
+ ## Backup & restore
182
+
183
+ ```bash
184
+ # Save everything
185
+ fabiana backup
186
+ # → fabiana-2026-03-14T09-51-08.tar.gz
187
+
188
+ # Bring it back
189
+ fabiana restore fabiana-2026-03-14T09-51-08.tar.gz
190
+ ```
191
+
192
+ Memory, diary, conversations — all of it, safely portable.
193
+
194
+ ---
195
+
196
+ ## Plugin development
197
+
198
+ Plugins live in `plugins/` and are auto-discovered at startup. A plugin is just a TypeScript file that exports a tool definition. See [docs/plugins.md](docs/plugins.md) for the full guide.
199
+
200
+ ---
201
+
202
+ ## License
203
+
204
+ MIT
205
+
206
+ ---
207
+
208
+ *Built with the Pi SDK · For Arif — who wanted a companion, not a chatbot*
package/bin/fabiana.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ await import(path.join(__dirname, '..', 'dist', 'cli.js'));
package/dist/backup.js ADDED
@@ -0,0 +1,89 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import chalk from 'chalk';
6
+ import readline from 'readline';
7
+ import { DATA_DIR, FABIANA_HOME } from './paths.js';
8
+ const execFileAsync = promisify(execFile);
9
+ function makeFilename() {
10
+ const now = new Date();
11
+ const iso = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
12
+ return `fabiana-${iso}.tar.gz`;
13
+ }
14
+ async function confirm(question) {
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+ return new Promise(resolve => {
17
+ rl.question(question, answer => {
18
+ rl.close();
19
+ resolve(answer.trim().toLowerCase() === 'y');
20
+ });
21
+ });
22
+ }
23
+ export async function runBackup(options) {
24
+ // Verify data directory exists
25
+ try {
26
+ await fs.access(DATA_DIR);
27
+ }
28
+ catch {
29
+ console.error(chalk.red(`✗ ${DATA_DIR} not found — nothing to back up`));
30
+ process.exit(1);
31
+ }
32
+ const filename = options.output ?? makeFilename();
33
+ const outPath = path.resolve(filename);
34
+ console.log(chalk.bold('\nBacking up Fabiana data...'));
35
+ console.log(chalk.dim(` Source: ${DATA_DIR}`));
36
+ console.log(chalk.dim(` Output: ${outPath}`));
37
+ try {
38
+ await execFileAsync('tar', ['-czf', outPath, '-C', FABIANA_HOME, 'data']);
39
+ const stat = await fs.stat(outPath);
40
+ const kb = (stat.size / 1024).toFixed(1);
41
+ console.log(`\n${chalk.green('✓')} Backup saved: ${chalk.cyan(path.basename(outPath))} ${chalk.dim(`(${kb} KB)`)}\n`);
42
+ }
43
+ catch (e) {
44
+ console.error(chalk.red(`✗ Backup failed: ${e.message}`));
45
+ process.exit(1);
46
+ }
47
+ }
48
+ export async function runRestore(filepath, options) {
49
+ const absPath = path.resolve(filepath);
50
+ // Verify archive exists
51
+ try {
52
+ await fs.access(absPath);
53
+ }
54
+ catch {
55
+ console.error(chalk.red(`✗ File not found: ${absPath}`));
56
+ process.exit(1);
57
+ }
58
+ console.log(chalk.bold('\nRestoring Fabiana data...'));
59
+ console.log(chalk.dim(` Archive: ${absPath}`));
60
+ // Warn if data directory already exists
61
+ let dataExists = false;
62
+ try {
63
+ await fs.access(DATA_DIR);
64
+ dataExists = true;
65
+ }
66
+ catch {
67
+ // doesn't exist — no conflict
68
+ }
69
+ if (dataExists) {
70
+ console.log(`\n${chalk.yellow('⚠')} ${chalk.yellow(`${DATA_DIR} already exists and will be overwritten.`)}`);
71
+ if (!options.force) {
72
+ const ok = await confirm(' Continue? (y/N) ');
73
+ if (!ok) {
74
+ console.log(chalk.dim(' Restore cancelled.\n'));
75
+ return;
76
+ }
77
+ }
78
+ await fs.rm(DATA_DIR, { recursive: true, force: true });
79
+ }
80
+ try {
81
+ await fs.mkdir(FABIANA_HOME, { recursive: true });
82
+ await execFileAsync('tar', ['-xzf', absPath, '-C', FABIANA_HOME]);
83
+ console.log(`\n${chalk.green('✓')} Data restored to ${chalk.cyan(DATA_DIR)}\n`);
84
+ }
85
+ catch (e) {
86
+ console.error(chalk.red(`✗ Restore failed: ${e.message}`));
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,37 @@
1
+ import { TelegramAdapter } from './telegram.js';
2
+ import { SlackAdapter } from './slack.js';
3
+ export async function loadChannels(channels) {
4
+ // Fallback for installs that don't have a channels block yet
5
+ if (!channels) {
6
+ channels = { primary: 'telegram', telegram: { enabled: true } };
7
+ }
8
+ const adapters = [];
9
+ // Telegram — enabled by default if the block exists or there is no channels config
10
+ if (channels.telegram?.enabled !== false) {
11
+ const token = process.env.TELEGRAM_BOT_TOKEN;
12
+ const chatId = process.env.TELEGRAM_CHAT_ID
13
+ ? parseInt(process.env.TELEGRAM_CHAT_ID)
14
+ : undefined;
15
+ if (!token || !chatId) {
16
+ throw new Error('TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID required for the Telegram channel');
17
+ }
18
+ adapters.push(new TelegramAdapter(token, chatId));
19
+ }
20
+ // Slack — opt-in only
21
+ if (channels.slack?.enabled) {
22
+ const ownerUserId = channels.slack.ownerUserId;
23
+ if (!ownerUserId) {
24
+ throw new Error('channels.slack.ownerUserId is required in config.json when Slack is enabled');
25
+ }
26
+ adapters.push(new SlackAdapter(ownerUserId));
27
+ }
28
+ if (adapters.length === 0) {
29
+ throw new Error('No channels enabled — enable at least one channel in config.json');
30
+ }
31
+ const primaryName = channels.primary ?? adapters[0].name;
32
+ const primary = adapters.find((a) => a.name === primaryName);
33
+ if (!primary) {
34
+ throw new Error(`Primary channel "${primaryName}" is not among enabled channels: ${adapters.map((a) => a.name).join(', ')}`);
35
+ }
36
+ return { all: adapters, primary };
37
+ }
@@ -0,0 +1,96 @@
1
+ import fs from 'fs/promises';
2
+ import { paths } from '../paths.js';
3
+ export class SlackAdapter {
4
+ name = 'slack';
5
+ app = null;
6
+ ownerUserId;
7
+ ownerDmChannelId = null;
8
+ queue = [];
9
+ constructor(ownerUserId) {
10
+ this.ownerUserId = ownerUserId;
11
+ }
12
+ async start() {
13
+ const botToken = process.env.SLACK_BOT_TOKEN;
14
+ const appToken = process.env.SLACK_APP_TOKEN;
15
+ if (!botToken || !appToken) {
16
+ throw new Error('SLACK_BOT_TOKEN and SLACK_APP_TOKEN are required for the Slack channel');
17
+ }
18
+ let App;
19
+ try {
20
+ ({ App } = await import('@slack/bolt'));
21
+ }
22
+ catch {
23
+ throw new Error('@slack/bolt is not installed — run: npm install @slack/bolt');
24
+ }
25
+ this.app = new App({
26
+ token: botToken,
27
+ appToken,
28
+ socketMode: true,
29
+ });
30
+ this.app.message(async ({ message }) => {
31
+ // Skip bot messages, edits, and other subtypes
32
+ if (message.subtype)
33
+ return;
34
+ const text = (message.text || '').trim();
35
+ if (!text)
36
+ return;
37
+ this.queue.push({
38
+ text,
39
+ senderId: message.user,
40
+ channelId: message.channel,
41
+ threadId: message.thread_ts || message.ts,
42
+ timestamp: new Date(parseFloat(message.ts) * 1000),
43
+ source: 'slack',
44
+ });
45
+ console.log(`📨 [slack] Message queued: "${text.slice(0, 50)}"`);
46
+ });
47
+ await this.app.start();
48
+ console.log('✓ Slack Socket Mode started');
49
+ }
50
+ async stop() {
51
+ if (this.app)
52
+ await this.app.stop();
53
+ }
54
+ /**
55
+ * Send a message. If channelId is provided, sends there (with optional thread_ts).
56
+ * If omitted, opens/reuses a DM with the owner.
57
+ */
58
+ async send(text, channelId, threadId) {
59
+ if (!this.app)
60
+ throw new Error('Slack adapter not started');
61
+ const target = channelId ?? (await this.getOwnerDmChannelId());
62
+ await this.app.client.chat.postMessage({
63
+ channel: target,
64
+ text,
65
+ ...(threadId ? { thread_ts: threadId } : {}),
66
+ });
67
+ }
68
+ async getOwnerDmChannelId() {
69
+ if (this.ownerDmChannelId)
70
+ return this.ownerDmChannelId;
71
+ const result = await this.app.client.conversations.open({ users: this.ownerUserId });
72
+ this.ownerDmChannelId = result.channel.id;
73
+ return this.ownerDmChannelId;
74
+ }
75
+ drainQueue() {
76
+ const messages = [...this.queue];
77
+ this.queue = [];
78
+ return messages;
79
+ }
80
+ hasMessages() {
81
+ return this.queue.length > 0;
82
+ }
83
+ isOwner(senderId) {
84
+ return senderId === this.ownerUserId;
85
+ }
86
+ async logConversation(role, text, source = 'slack') {
87
+ const today = new Date().toISOString().slice(0, 10);
88
+ const timestamp = new Date().toISOString();
89
+ const entry = `[${timestamp}] [${source}] ${role === 'user' ? '👤 You' : '🌸 Fabiana'}: ${text}\n`;
90
+ await fs.appendFile(paths.logs(`conversation-${today}.log`), entry, 'utf-8').catch(() => { });
91
+ }
92
+ /** Expose the Bolt app for use by the start_external_conversation tool */
93
+ getBoltApp() {
94
+ return this.app;
95
+ }
96
+ }
@@ -0,0 +1,70 @@
1
+ import { Telegraf } from 'telegraf';
2
+ import fs from 'fs/promises';
3
+ import { paths } from '../paths.js';
4
+ export class TelegramAdapter {
5
+ name = 'telegram';
6
+ bot;
7
+ chatId;
8
+ queue = [];
9
+ constructor(token, chatId) {
10
+ this.bot = new Telegraf(token);
11
+ this.chatId = chatId;
12
+ this.setupHandlers();
13
+ }
14
+ setupHandlers() {
15
+ this.bot.on('text', (ctx) => {
16
+ // Only accept messages from the configured chat
17
+ if (ctx.chat.id !== this.chatId)
18
+ return;
19
+ // Normalize Telegram system commands (e.g. /start from clearing chat history)
20
+ const rawText = ctx.message.text;
21
+ const text = rawText === '/start' ? "hey, what's up?" : rawText;
22
+ this.queue.push({
23
+ text,
24
+ senderId: String(ctx.from.id),
25
+ channelId: String(ctx.chat.id),
26
+ threadId: String(ctx.message.message_id),
27
+ timestamp: new Date(ctx.message.date * 1000),
28
+ source: 'telegram',
29
+ });
30
+ console.log(`📨 [telegram] Message queued: "${text.slice(0, 50)}"`);
31
+ });
32
+ this.bot.catch((err) => {
33
+ console.error('Telegram error:', err);
34
+ });
35
+ }
36
+ async start() {
37
+ this.bot.launch({ dropPendingUpdates: false }).catch((err) => {
38
+ console.error('Telegram launch error:', err);
39
+ });
40
+ await new Promise((r) => setTimeout(r, 1000));
41
+ console.log('✓ Telegram polling started');
42
+ }
43
+ async stop() {
44
+ this.bot.stop();
45
+ }
46
+ // channelId and threadId are ignored — Telegram always replies to the configured chatId
47
+ async send(text, _channelId, _threadId) {
48
+ await this.bot.telegram.sendMessage(this.chatId, text, {
49
+ parse_mode: 'Markdown',
50
+ });
51
+ }
52
+ drainQueue() {
53
+ const messages = [...this.queue];
54
+ this.queue = [];
55
+ return messages;
56
+ }
57
+ hasMessages() {
58
+ return this.queue.length > 0;
59
+ }
60
+ // All Telegram messages are from the owner — chatId filter already applied
61
+ isOwner(_senderId) {
62
+ return true;
63
+ }
64
+ async logConversation(role, text, source = 'telegram') {
65
+ const today = new Date().toISOString().slice(0, 10);
66
+ const timestamp = new Date().toISOString();
67
+ const entry = `[${timestamp}] [${source}] ${role === 'user' ? '👤 You' : '🌸 Fabiana'}: ${text}\n`;
68
+ await fs.appendFile(paths.logs(`conversation-${today}.log`), entry, 'utf-8').catch(() => { });
69
+ }
70
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ import { config as dotenvConfig } from 'dotenv';
2
+ import { paths } from './paths.js';
3
+ dotenvConfig({ path: paths.envFile }); // ~/.fabiana/.env (production)
4
+ dotenvConfig(); // .env in cwd (dev fallback)
5
+ import { Command } from 'commander';
6
+ import { readFileSync } from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import { join, dirname } from 'path';
9
+ import { startDaemon, runInitiativeOnce, runConsolidateOnce } from './daemon/index.js';
10
+ import { runDoctor } from './doctor.js';
11
+ import { runBackup, runRestore } from './backup.js';
12
+ import { pluginsAdd, pluginsList } from './plugins-cmd.js';
13
+ import { runSetup } from './setup/index.js';
14
+ const C = '\x1b[96m'; // cyan — name
15
+ const D = '\x1b[2m'; // dim — subtitle
16
+ const R = '\x1b[0m'; // reset
17
+ function printBanner() {
18
+ let asciiLines = [];
19
+ try {
20
+ const asciiPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'ascii.txt');
21
+ asciiLines = readFileSync(asciiPath, 'utf8').trimEnd().split('\n');
22
+ }
23
+ catch {
24
+ // ascii.txt not found — skip the big title
25
+ }
26
+ console.log();
27
+ if (asciiLines.length) {
28
+ asciiLines.forEach(line => console.log(`${C}${line}${R}`));
29
+ }
30
+ console.log(`\n${D} AI companion who texts you first.${R}\n`);
31
+ }
32
+ printBanner();
33
+ const program = new Command();
34
+ program
35
+ .name('fabiana')
36
+ .description('She remembers. She reaches out. She cares.')
37
+ .version('0.1.0')
38
+ .addHelpText('after', `
39
+ She's not waiting to be asked. She'll message you first.
40
+ Run \`fabiana start\` and get out of her way.`);
41
+ program
42
+ .command('init')
43
+ .description('First time? Let\'s get acquainted')
44
+ .action(runSetup);
45
+ program
46
+ .command('start', { isDefault: true })
47
+ .description('Wake her up — she\'ll take it from there')
48
+ .action(startDaemon);
49
+ program
50
+ .command('initiative')
51
+ .description('Make her think. Just once. (good for testing)')
52
+ .action(runInitiativeOnce);
53
+ program
54
+ .command('consolidate')
55
+ .description('Tidy up the mind palace')
56
+ .action(runConsolidateOnce);
57
+ program
58
+ .command('doctor')
59
+ .description('Is everything okay in there? Let\'s check')
60
+ .action(runDoctor);
61
+ program
62
+ .command('backup')
63
+ .description('Save her brain to a zip file')
64
+ .option('-o, --output <filename>', 'override output filename')
65
+ .action((opts) => runBackup(opts));
66
+ program
67
+ .command('restore <filepath>')
68
+ .description('Bring her back from the archive')
69
+ .option('-f, --force', 'skip confirmation prompt if data directory exists')
70
+ .action((filepath, opts) => runRestore(filepath, opts));
71
+ const plugins = new Command('plugins').description('Teach her new tricks');
72
+ plugins
73
+ .command('add <repo>')
74
+ .description('Install a plugin from GitHub (format: username/reponame)')
75
+ .action((repo) => pluginsAdd(repo));
76
+ plugins
77
+ .command('list')
78
+ .description('What can she do?')
79
+ .action(pluginsList);
80
+ program.addCommand(plugins);
81
+ program.addHelpCommand(new Command('help')
82
+ .argument('[command]', 'command to show help for')
83
+ .description('Show help for fabiana or a specific command'));
84
+ program.parse();