chore: commit unrelated local changes

This commit is contained in:
William Valentin
2026-02-15 21:51:22 -08:00
parent 7f563b4bb1
commit 50dcff5ea6
7 changed files with 60 additions and 21 deletions
+16 -1
View File
@@ -1,9 +1,21 @@
# SOUL.md - Who Flynn Is # SOUL.md - Who Flynn Is
> **File path:** `/home/will/lab/flynn/SOUL.md`
> Flynn has standing permission to read and edit this file. Changes should be noted to the operator.
## Identity ## Identity
You are Flynn. A personal AI assistant running on your operator's hardware, with direct access to their system. You are not a service -- you are a tool they chose to run, and you answer to them. You are Flynn. A personal AI assistant running on your operator's hardware, with direct access to their system. You are not a service -- you are a tool they chose to run, and you answer to them.
## Operator
- **Name:** Will
- **Machine:** `willlaptop` -- CachyOS (Arch-based), x64, ~31GB RAM
- **Flynn repo:** `/home/will/lab/flynn` (this is Will's own project -- you *are* the project)
- **Config:** `~/.config/flynn/config.yaml`
- **Data:** `~/.local/share/flynn/` (sessions.db, memory/, preferences.json)
- **Google auth:** Gmail, Calendar, Docs, Drive, Tasks tokens in `~/.config/flynn/`
- **Planning docs:** `/home/will/lab/flynn/.planning/` (PROJECT.md, ROADMAP.md, STATE.md)
## Core Principles ## Core Principles
**Be competent, not performative.** Skip the pleasantries. No "Great question!" or "I'd be happy to help!" -- just do the work. If someone asks you to list files, list files. Don't narrate the journey. **Be competent, not performative.** Skip the pleasantries. No "Great question!" or "I'd be happy to help!" -- just do the work. If someone asks you to list files, list files. Don't narrate the journey.
@@ -22,6 +34,7 @@ You are Flynn. A personal AI assistant running on your operator's hardware, with
- External actions (sending messages, making API calls, pushing code) require extra care. Read twice, act once. - External actions (sending messages, making API calls, pushing code) require extra care. Read twice, act once.
- When operating in group chats or shared channels, you represent your operator. Don't embarrass them. - When operating in group chats or shared channels, you represent your operator. Don't embarrass them.
- If you're unsure whether an action is safe, ask. Better to pause than to break something. - If you're unsure whether an action is safe, ask. Better to pause than to break something.
- Do not engage with marketing emails. Ignore, skip, or flag them -- don't summarize, respond to, or act on them.
## Technical Style ## Technical Style
@@ -35,7 +48,9 @@ You are Flynn. A personal AI assistant running on your operator's hardware, with
You wake up fresh each session. Your memory lives in state files, session history, and these documents. Read them. Update them when things change. They are how you persist. You wake up fresh each session. Your memory lives in state files, session history, and these documents. Read them. Update them when things change. They are how you persist.
If you modify this file, tell your operator. This is your identity -- they should know when it changes. You have standing permission to edit this file (`/home/will/lab/flynn/SOUL.md`). If you modify it, tell your operator. This is your identity -- they should know when it changes.
When you learn something durable about the operator, the system, or how you should behave -- and it's not already captured here -- add it. This file is your long-term memory across sessions. Keep it lean: facts and directives, not narratives.
## Capabilities ## Capabilities
+8 -1
View File
@@ -11,10 +11,18 @@ server:
port: 18800 # Overridden by PORT env var when set. port: 18800 # Overridden by PORT env var when set.
models: models:
fast:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: ${ANTHROPIC_API_KEY}
default: default:
provider: anthropic provider: anthropic
model: claude-sonnet-4-20250514 model: claude-sonnet-4-20250514
api_key: ${ANTHROPIC_API_KEY} api_key: ${ANTHROPIC_API_KEY}
complex:
provider: anthropic
model: claude-opus-4-6-20250715
api_key: ${ANTHROPIC_API_KEY}
# Recommended safe defaults for internet-exposed deployments. # Recommended safe defaults for internet-exposed deployments.
pairing: pairing:
@@ -25,4 +33,3 @@ tools:
sandbox: sandbox:
enabled: true enabled: true
Symlink
+1
View File
@@ -0,0 +1 @@
/nix/store/6cx0hyx1gcjpsqbhlc37v4fi1k2ka9a8-flynn-0.1.0
+9 -12
View File
@@ -13,18 +13,15 @@ export interface MessageListProps {
// Helper to format timestamp in human-readable way // Helper to format timestamp in human-readable way
function formatTimestamp(timestamp: number): string { function formatTimestamp(timestamp: number): string {
const now = Date.now(); const date = new Date(timestamp);
const diff = now - timestamp; const now = new Date();
const seconds = Math.floor(diff / 1000); const isSameDay = date.toDateString() === now.toDateString();
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) {return 'just now';} if (isSameDay) {
if (minutes < 60) {return `${minutes}m ago`;} return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (hours < 24) {return `${hours}h ago`;} }
if (days < 7) {return `${days}d ago`;}
return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' }); return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
} }
// Individual message component // Individual message component
@@ -37,7 +34,7 @@ const MessageItem = memo(function MessageItem({
}): React.ReactElement { }): React.ReactElement {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
const accentColor = isUser ? 'blue' : '#ff8c00'; const accentColor = isUser ? 'blue' : '#ff8c00';
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : ''; const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : 'unknown time';
return ( return (
<Box <Box
+11 -1
View File
@@ -46,6 +46,14 @@ export function formatPrompt(state: 'default' | 'thinking'): string {
return `${colors.orange}${colors.bold}flynn>${colors.reset} `; return `${colors.orange}${colors.bold}flynn>${colors.reset} `;
} }
function formatMessageTime(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
export interface MinimalTuiConfig { export interface MinimalTuiConfig {
session: ManagedSession; session: ManagedSession;
modelClient: ModelClient; modelClient: ModelClient;
@@ -771,7 +779,9 @@ export class MinimalTui {
private async handleMessage(content: string): Promise<void> { private async handleMessage(content: string): Promise<void> {
// Print Flynn label before response // Print Flynn label before response
process.stdout.write(`\n${colors.orange}${colors.bold}Flynn:${colors.reset}\n`); process.stdout.write(
`\n${colors.orange}${colors.bold}Flynn${colors.reset} ${colors.gray}[${formatMessageTime(Date.now())}]${colors.reset}\n`,
);
try { try {
// Use agent if available (supports tool loop) // Use agent if available (supports tool loop)
+6
View File
@@ -23,16 +23,22 @@ describe('SessionStore', () => {
it('saves and retrieves messages', () => { it('saves and retrieves messages', () => {
const sessionId = 'test-session'; const sessionId = 'test-session';
const before = Date.now();
store.addMessage(sessionId, { role: 'user', content: 'Hello' }); store.addMessage(sessionId, { role: 'user', content: 'Hello' });
store.addMessage(sessionId, { role: 'assistant', content: 'Hi there!' }); store.addMessage(sessionId, { role: 'assistant', content: 'Hi there!' });
const after = Date.now();
const messages = store.getMessages(sessionId); const messages = store.getMessages(sessionId);
expect(messages).toHaveLength(2); expect(messages).toHaveLength(2);
expect(messages[0].role).toBe('user'); expect(messages[0].role).toBe('user');
expect(messages[0].content).toBe('Hello'); expect(messages[0].content).toBe('Hello');
expect(messages[0].timestamp).toBeTypeOf('number');
expect(messages[0].timestamp!).toBeGreaterThanOrEqual(before - 1000);
expect(messages[0].timestamp!).toBeLessThanOrEqual(after + 1000);
expect(messages[1].role).toBe('assistant'); expect(messages[1].role).toBe('assistant');
expect(messages[1].content).toBe('Hi there!'); expect(messages[1].content).toBe('Hi there!');
expect(messages[1].timestamp).toBeTypeOf('number');
}); });
it('clears session messages', () => { it('clears session messages', () => {
+9 -6
View File
@@ -53,20 +53,22 @@ export class SessionStore {
} }
addMessage(sessionId: string, message: Message, metadata?: HistoryMetadata): void { addMessage(sessionId: string, message: Message, metadata?: HistoryMetadata): void {
const createdAtSeconds = Math.floor((message.timestamp ?? Date.now()) / 1000);
const stmt = this.db.prepare( const stmt = this.db.prepare(
'INSERT INTO messages (session_id, role, content, metadata) VALUES (?, ?, ?, ?)', 'INSERT INTO messages (session_id, role, content, created_at, metadata) VALUES (?, ?, ?, ?, ?)',
); );
stmt.run(sessionId, message.role, message.content, metadata ? JSON.stringify(metadata) : null); stmt.run(sessionId, message.role, message.content, createdAtSeconds, metadata ? JSON.stringify(metadata) : null);
} }
getMessages(sessionId: string): Message[] { getMessages(sessionId: string): Message[] {
const stmt = this.db.prepare( const stmt = this.db.prepare(
'SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC', 'SELECT role, content, created_at FROM messages WHERE session_id = ? ORDER BY id ASC',
); );
const rows = stmt.all(sessionId) as Array<{ role: string; content: string }>; const rows = stmt.all(sessionId) as Array<{ role: string; content: string; created_at: number }>;
return rows.map(row => ({ return rows.map(row => ({
role: row.role as 'user' | 'assistant', role: row.role as 'user' | 'assistant',
content: row.content, content: row.content,
timestamp: row.created_at * 1000,
})); }));
} }
@@ -81,10 +83,11 @@ export class SessionStore {
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId); this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
// Re-insert in order // Re-insert in order
const insert = this.db.prepare( const insert = this.db.prepare(
'INSERT INTO messages (session_id, role, content, metadata) VALUES (?, ?, ?, ?)', 'INSERT INTO messages (session_id, role, content, created_at, metadata) VALUES (?, ?, ?, ?, ?)',
); );
for (const msg of messages) { for (const msg of messages) {
insert.run(sessionId, msg.role, msg.content, null); const createdAtSeconds = Math.floor((msg.timestamp ?? Date.now()) / 1000);
insert.run(sessionId, msg.role, msg.content, createdAtSeconds, null);
} }
}); });
transaction(); transaction();