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
> **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
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
**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.
- 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.
- Do not engage with marketing emails. Ignore, skip, or flag them -- don't summarize, respond to, or act on them.
## 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.
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
+8 -1
View File
@@ -11,10 +11,18 @@ server:
port: 18800 # Overridden by PORT env var when set.
models:
fast:
provider: anthropic
model: claude-haiku-4-5-20251001
api_key: ${ANTHROPIC_API_KEY}
default:
provider: anthropic
model: claude-sonnet-4-20250514
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.
pairing:
@@ -25,4 +33,3 @@ tools:
sandbox:
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
function formatTimestamp(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const date = new Date(timestamp);
const now = new Date();
const isSameDay = date.toDateString() === now.toDateString();
if (seconds < 60) {return 'just now';}
if (minutes < 60) {return `${minutes}m ago`;}
if (hours < 24) {return `${hours}h ago`;}
if (days < 7) {return `${days}d ago`;}
return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' });
if (isSameDay) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
// Individual message component
@@ -37,7 +34,7 @@ const MessageItem = memo(function MessageItem({
}): React.ReactElement {
const isUser = message.role === 'user';
const accentColor = isUser ? 'blue' : '#ff8c00';
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : '';
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : 'unknown time';
return (
<Box
+11 -1
View File
@@ -46,6 +46,14 @@ export function formatPrompt(state: 'default' | 'thinking'): string {
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 {
session: ManagedSession;
modelClient: ModelClient;
@@ -771,7 +779,9 @@ export class MinimalTui {
private async handleMessage(content: string): Promise<void> {
// 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 {
// Use agent if available (supports tool loop)
+6
View File
@@ -23,16 +23,22 @@ describe('SessionStore', () => {
it('saves and retrieves messages', () => {
const sessionId = 'test-session';
const before = Date.now();
store.addMessage(sessionId, { role: 'user', content: 'Hello' });
store.addMessage(sessionId, { role: 'assistant', content: 'Hi there!' });
const after = Date.now();
const messages = store.getMessages(sessionId);
expect(messages).toHaveLength(2);
expect(messages[0].role).toBe('user');
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].content).toBe('Hi there!');
expect(messages[1].timestamp).toBeTypeOf('number');
});
it('clears session messages', () => {
+9 -6
View File
@@ -53,20 +53,22 @@ export class SessionStore {
}
addMessage(sessionId: string, message: Message, metadata?: HistoryMetadata): void {
const createdAtSeconds = Math.floor((message.timestamp ?? Date.now()) / 1000);
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[] {
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 => ({
role: row.role as 'user' | 'assistant',
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);
// Re-insert in order
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) {
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();