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 +20 -1
- package/dist/daemon/index.js +84 -21
- package/dist/prompts/system-solitude.js +2 -2
- package/dist/setup/index.js +3 -0
- package/dist/tools/index.js +0 -5
- package/dist/utils/tokens.js +44 -0
- package/package.json +1 -1
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)')
|
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/dist/setup/index.js
CHANGED
|
@@ -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`);
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
+
}
|