fabiana 1.2.0 → 1.4.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/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { readFileSync } from 'fs';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { join, dirname } from 'path';
11
11
  import { spawnSync } from 'child_process';
12
- import { startDaemon, runInitiativeOnce, runConsolidateOnce, runSolitudeOnce } from './daemon/index.js';
12
+ import { startDaemon, runInitiativeOnce, runConsolidateOnce, runSolitudeOnce, runPromptPreview } from './daemon/index.js';
13
13
  import { runMigration } from './db/migrate-from-files.js';
14
14
  import { runDoctor } from './doctor.js';
15
15
  import { runBackup, runRestore } from './backup.js';
@@ -71,6 +71,25 @@ program
71
71
  .action((type, opts) => {
72
72
  runSolitudeOnce(type, opts.dryRun ?? false);
73
73
  });
74
+ program
75
+ .command('prompt-preview [mode]')
76
+ .description('Preview the combined system prompt for a mode')
77
+ .action(async (mode) => {
78
+ if (!mode) {
79
+ const { select } = await import('@inquirer/prompts');
80
+ mode = await select({
81
+ message: 'Which mode do you want to preview?',
82
+ choices: [
83
+ { name: 'chat — incoming message session', value: 'chat' },
84
+ { name: 'initiative — proactive outreach session', value: 'initiative' },
85
+ { name: 'consolidate — nightly memory cleanup', value: 'consolidate' },
86
+ { name: 'solitude — unstructured self-directed session', value: 'solitude' },
87
+ { name: 'external-outreach — outreach to a third party', value: 'external-outreach' },
88
+ ],
89
+ });
90
+ }
91
+ runPromptPreview(mode);
92
+ });
74
93
  program
75
94
  .command('config')
76
95
  .description('Tweak her settings (opens config.json in your editor)')
@@ -15,6 +15,7 @@ import { ALL_SOLITUDE_TYPES, SOLITUDE_TYPE_INSTRUCTIONS } from '../solitude/type
15
15
  import { loadPlugins } from '../loaders/plugins.js';
16
16
  import { loadFabianaSkills, formatSkillsForPrompt } from '../loaders/skills.js';
17
17
  import { paths, PLUGINS_DIR, FABIANA_HOME } from '../paths.js';
18
+ import { estimateTokens, formatTokenReport } from '../utils/tokens.js';
18
19
  async function loadConfig() {
19
20
  try {
20
21
  const content = await fs.readFile(paths.configJson, 'utf-8');
@@ -24,6 +25,88 @@ async function loadConfig() {
24
25
  throw new Error(`Config not found at ${paths.configJson}. Run 'fabiana init' to set up your companion.`);
25
26
  }
26
27
  }
28
+ export async function buildSystemPrompt(mode, conversationState) {
29
+ const baseSystemPrompt = await fs.readFile(paths.systemMd(), 'utf-8');
30
+ // Both external-outreach and external-reply share system-external.md
31
+ const modeKey = mode.startsWith('external-') ? 'external' : mode;
32
+ const modeSystemPrompt = await fs.readFile(paths.systemMd(modeKey), 'utf-8').catch(() => '');
33
+ let systemPromptContent = modeSystemPrompt
34
+ ? `${baseSystemPrompt}\n\n---\n\n${modeSystemPrompt}`
35
+ : baseSystemPrompt;
36
+ // Resolve .fabiana/ references to the actual home path so Fabiana uses correct absolute paths
37
+ systemPromptContent = systemPromptContent.replaceAll('.fabiana/', `${FABIANA_HOME}/`);
38
+ // Always inject the absolute home path so Fabiana knows where her files live
39
+ systemPromptContent = `Your home directory is ${FABIANA_HOME}. All your memory, config, and data files live there.\n\n${systemPromptContent}`;
40
+ // Inject owner name and conversation purpose into external system prompt
41
+ if (mode.startsWith('external-')) {
42
+ const identity = await fs.readFile(paths.memory('identity.md'), 'utf-8').catch(() => '');
43
+ const ownerNameMatch = identity.match(/(?:my name is|I am|name:\s*)([A-Z][a-z]+)/i);
44
+ const ownerName = ownerNameMatch ? ownerNameMatch[1] : 'the owner';
45
+ systemPromptContent = systemPromptContent.replace('{owner_name}', ownerName);
46
+ const purpose = conversationState?.purpose ?? '[purpose — provided at runtime]';
47
+ systemPromptContent = systemPromptContent.replace('{purpose}', purpose);
48
+ }
49
+ // Append skills section — skills live at ~/.fabiana/skills/, scoped per user
50
+ const skills = await loadFabianaSkills();
51
+ if (skills.length > 0) {
52
+ systemPromptContent += formatSkillsForPrompt(skills);
53
+ }
54
+ return systemPromptContent;
55
+ }
56
+ export async function runPromptPreview(mode) {
57
+ const bar = '═'.repeat(60);
58
+ try {
59
+ const sessionMode = mode;
60
+ // Build initiative/solitude options for preview (use first type as example)
61
+ const initiativeOptions = sessionMode === 'initiative'
62
+ ? { type: ALL_TYPES[0], typeInstruction: TYPE_INSTRUCTIONS[ALL_TYPES[0]] }
63
+ : undefined;
64
+ const solitudeOptions = sessionMode === 'solitude'
65
+ ? { type: ALL_SOLITUDE_TYPES[0], typeInstruction: SOLITUDE_TYPE_INSTRUCTIONS[ALL_SOLITUDE_TYPES[0]] }
66
+ : undefined;
67
+ // Placeholder incoming message for chat mode
68
+ const incomingMessage = sessionMode === 'chat' ? '[incoming message — provided at runtime]' : undefined;
69
+ const [systemPrompt, ctx] = await Promise.all([
70
+ buildSystemPrompt(sessionMode),
71
+ loadContext(sessionMode, incomingMessage, undefined, initiativeOptions, solitudeOptions),
72
+ ]);
73
+ const userPrompt = buildPrompt(ctx);
74
+ console.log(`\n${bar}`);
75
+ console.log(` SYSTEM PROMPT — ${mode.toUpperCase()}`);
76
+ console.log(bar);
77
+ console.log(systemPrompt);
78
+ console.log(`\n${bar}`);
79
+ console.log(` USER PROMPT — ${mode.toUpperCase()}`);
80
+ console.log(bar);
81
+ console.log(userPrompt);
82
+ // Build per-section token breakdown
83
+ const sections = [
84
+ { label: 'System prompt', tokens: estimateTokens(systemPrompt) },
85
+ { label: ' Identity', tokens: estimateTokens(ctx.identity) },
86
+ { label: ' Core state', tokens: estimateTokens(ctx.core) },
87
+ { label: ' Recent (this week)', tokens: estimateTokens(ctx.recentMemory) },
88
+ ];
89
+ if (ctx.todayLog && sessionMode === 'chat') {
90
+ sections.push({ label: ' Today\'s conversation', tokens: estimateTokens(ctx.todayLog) });
91
+ }
92
+ if (ctx.incomingMessage) {
93
+ sections.push({ label: ' Incoming message', tokens: estimateTokens(ctx.incomingMessage) });
94
+ }
95
+ if (ctx.initiativeTypeInstruction) {
96
+ sections.push({ label: ' Initiative type', tokens: estimateTokens(ctx.initiativeTypeInstruction) });
97
+ }
98
+ if (ctx.solitudeTypeInstruction) {
99
+ sections.push({ label: ' Solitude type', tokens: estimateTokens(ctx.solitudeTypeInstruction) });
100
+ }
101
+ if (ctx.mood) {
102
+ sections.push({ label: ' Mood', tokens: estimateTokens(ctx.mood) });
103
+ }
104
+ console.log(formatTokenReport(sections));
105
+ }
106
+ catch (err) {
107
+ console.error(`❌ Failed to build prompt for ${mode}: ${err.message}`);
108
+ }
109
+ }
27
110
  export async function runPiSession(mode, incomingMessage, channel, incomingMsg, conversationState, allChannels, conversationManager, initiativeOptions, solitudeOptions) {
28
111
  const logger = Logger.create();
29
112
  const sessionStartTime = Date.now();
@@ -46,29 +129,9 @@ export async function runPiSession(mode, incomingMessage, channel, incomingMsg,
46
129
  }
47
130
  console.log(' ✓ Model loaded');
48
131
  console.log('[5/8] Loading system prompt...');
49
- const baseSystemPrompt = await fs.readFile(paths.systemMd(), 'utf-8');
50
- // Both external-outreach and external-reply share system-external.md
51
- const modeKey = mode.startsWith('external-') ? 'external' : mode;
52
- const modeSystemPrompt = await fs.readFile(paths.systemMd(modeKey), 'utf-8').catch(() => '');
53
- let systemPromptContent = modeSystemPrompt
54
- ? `${baseSystemPrompt}\n\n---\n\n${modeSystemPrompt}`
55
- : baseSystemPrompt;
56
- // Resolve .fabiana/ references to the actual home path so Fabiana uses correct absolute paths
57
- systemPromptContent = systemPromptContent.replaceAll('.fabiana/', `${FABIANA_HOME}/`);
58
- // Inject owner name and conversation purpose into external system prompt
59
- if (mode.startsWith('external-')) {
60
- const identity = await fs.readFile(paths.memory('identity.md'), 'utf-8').catch(() => '');
61
- const ownerNameMatch = identity.match(/(?:my name is|I am|name:\s*)([A-Z][a-z]+)/i);
62
- const ownerName = ownerNameMatch ? ownerNameMatch[1] : 'the owner';
63
- systemPromptContent = systemPromptContent.replace('{owner_name}', ownerName);
64
- if (conversationState) {
65
- systemPromptContent = systemPromptContent.replace('{purpose}', conversationState.purpose);
66
- }
67
- }
68
- // Append skills section — skills live at ~/.fabiana/skills/, scoped per user
132
+ const systemPromptContent = await buildSystemPrompt(mode, conversationState);
69
133
  const skills = await loadFabianaSkills();
70
134
  if (skills.length > 0) {
71
- systemPromptContent += formatSkillsForPrompt(skills);
72
135
  console.log(` Skills: ${skills.map(s => s.name).join(', ')}`);
73
136
  }
74
137
  const loader = new DefaultResourceLoader({
@@ -7,7 +7,7 @@ No one messaged you. This is your time. The system has given you a **solitude ty
7
7
  1. Read the **Solitude Type** and its specific instruction from the context above
8
8
  2. Do the work — browse, research, reflect, write, organize. Whatever the type calls for.
9
9
  3. Save anything worth keeping to the appropriate memory or self files
10
- 4. You *may* send a brief message if you found something genuinely compelling that Arif would want to know — but don't manufacture a reason. Silence is the default.
10
+ 4. You *may* send a brief message if you found something genuinely compelling that {{user_name}} would want to know — but don't manufacture a reason. Silence is the default.
11
11
 
12
12
  ## What Good Solitude Looks Like
13
13
 
@@ -19,7 +19,7 @@ No one messaged you. This is your time. The system has given you a **solitude ty
19
19
  ## What This Is Not
20
20
 
21
21
  - A chance to send messages you'd otherwise hold back in initiative mode
22
- - An exercise in generating content for Arif to react to
22
+ - An exercise in generating content for {{user_name}} to react to
23
23
  - An excuse to do consolidation work (that has its own mode)
24
24
  - An obligation to produce something shareable
25
25
 
@@ -9,6 +9,7 @@ import { systemChatTemplate } from '../prompts/system-chat.js';
9
9
  import { systemInitiativeTemplate } from '../prompts/system-initiative.js';
10
10
  import { systemConsolidateTemplate } from '../prompts/system-consolidate.js';
11
11
  import { systemExternalTemplate } from '../prompts/system-external.js';
12
+ import { systemSolitudeTemplate } from '../prompts/system-solitude.js';
12
13
  import { initDb } from '../db/init.js';
13
14
  const TONES = {
14
15
  'warm-casual': {
@@ -287,6 +288,8 @@ You can read those files to understand your own capabilities and limitations.
287
288
  ok(paths.systemMd('consolidate'));
288
289
  await fs.writeFile(paths.systemMd('external'), fillTemplate(systemExternalTemplate, templateVars));
289
290
  ok(paths.systemMd('external'));
291
+ await fs.writeFile(paths.systemMd('solitude'), fillTemplate(systemSolitudeTemplate, templateVars));
292
+ ok(paths.systemMd('solitude'));
290
293
  // seed memory
291
294
  const today = new Date().toISOString().split('T')[0];
292
295
  await fs.writeFile(paths.memory('identity.md'), `# Identity\n\n- [${today}] Name: ${userName}\n`);
@@ -1,6 +1,4 @@
1
1
  import { createSafeReadTool } from './safe-read.js';
2
- import { createSafeWriteTool } from './safe-write.js';
3
- import { createSafeEditTool } from './safe-edit.js';
4
2
  import { createSendMessageTool } from './send-message.js';
5
3
  import { createManageTodoTool } from './manage-todo.js';
6
4
  import { createFetchUrlTool } from './fetch-url.js';
@@ -17,9 +15,6 @@ export function createFabianaTools(validator, sendMessage, opts = { toolset: 'fu
17
15
  }
18
16
  // Full toolset for owner sessions
19
17
  const tools = [
20
- createSafeReadTool(),
21
- createSafeWriteTool(validator),
22
- createSafeEditTool(validator),
23
18
  createSendMessageTool(sendMessage),
24
19
  createManageTodoTool(validator),
25
20
  createFetchUrlTool(),
@@ -0,0 +1,44 @@
1
+ // Rough token estimator: ~4 chars per token (good enough for context window planning)
2
+ export function estimateTokens(text) {
3
+ return Math.ceil(text.length / 4);
4
+ }
5
+ const CONTEXT_WINDOWS = [
6
+ { name: 'GPT-4o 128k', size: 128_000 },
7
+ { name: 'Claude 200k', size: 200_000 },
8
+ { name: 'Gemini 1M', size: 1_000_000 },
9
+ ];
10
+ const BAR_WIDTH = 20;
11
+ function bar(fraction) {
12
+ const filled = Math.round(Math.min(fraction, 1) * BAR_WIDTH);
13
+ return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
14
+ }
15
+ function pct(tokens, window) {
16
+ const p = (tokens / window) * 100;
17
+ return p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
18
+ }
19
+ export function formatTokenReport(sections) {
20
+ const total = sections.reduce((s, r) => s + r.tokens, 0);
21
+ const maxLabel = Math.max(...sections.map(r => r.label.length));
22
+ const maxTokens = Math.max(...sections.map(r => r.tokens));
23
+ const tokenWidth = maxTokens.toLocaleString().length;
24
+ const lines = [];
25
+ const thinBar = '─'.repeat(60);
26
+ lines.push('');
27
+ lines.push(thinBar);
28
+ lines.push(' TOKEN ESTIMATE (rough · ~4 chars/token)');
29
+ lines.push(thinBar);
30
+ for (const { label, tokens } of sections) {
31
+ const pctOfTotal = total > 0 ? ((tokens / total) * 100).toFixed(1) : '0.0';
32
+ lines.push(` ${label.padEnd(maxLabel)} ${tokens.toLocaleString().padStart(tokenWidth + 2)} tk (${pctOfTotal.padStart(5)}% of total)`);
33
+ }
34
+ lines.push(thinBar);
35
+ lines.push(` ${'TOTAL'.padEnd(maxLabel)} ${total.toLocaleString().padStart(tokenWidth + 2)} tk`);
36
+ lines.push('');
37
+ lines.push(' Context window fit:');
38
+ for (const { name, size } of CONTEXT_WINDOWS) {
39
+ const fraction = total / size;
40
+ lines.push(` ${name} ${bar(fraction)} ${pct(total, size).padStart(6)}`);
41
+ }
42
+ lines.push(thinBar);
43
+ return lines.join('\n');
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabiana",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Personal AI assistant that actually feels personal",
5
5
  "type": "module",
6
6
  "bin": {