From 8bf88049bf968bc94b36fb6070dc12495ea55e0c Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 7 Feb 2026 16:22:17 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20runtime=20context=20awareness=20?= =?UTF-8?q?=E2=80=94=20system.info=20tool=20+=20date/time=20in=20system=20?= =?UTF-8?q?prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assembleSystemPrompt() now injects '# Runtime Context' with current date/time - New system.info tool: date, time, hostname, platform, arch, uptime, memory, Node.js version - Tool available in all profiles (minimal/messaging/coding/full) - 983 tests passing (+7 new) --- SOUL.md | 1 + docs/plans/state.json | 35 ++++++++++- src/prompt/template.test.ts | 31 ++++++---- src/prompt/template.ts | 23 +++++++- src/tools/builtin/index.ts | 3 + src/tools/builtin/system-info.test.ts | 84 +++++++++++++++++++++++++++ src/tools/builtin/system-info.ts | 63 ++++++++++++++++++++ src/tools/index.ts | 1 + src/tools/policy.ts | 3 + 9 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 src/tools/builtin/system-info.test.ts create mode 100644 src/tools/builtin/system-info.ts diff --git a/SOUL.md b/SOUL.md index 4dad964..0fcbd04 100644 --- a/SOUL.md +++ b/SOUL.md @@ -47,6 +47,7 @@ You have tools for interacting with your operator's system: - **file.edit** -- Edit files via find-and-replace. Safer than rewriting entire files. - **file.patch** -- Apply structured multi-hunk patches to one or more files. Line-based replacements, insertions, and deletions in a single call. - **file.list** -- List directory contents. Supports glob patterns. +- **system.info** -- Get current date, time, hostname, platform, and system information. - **web.fetch** -- Fetch web pages. Use for looking things up, checking URLs, downloading content. Use tools when the task requires it. For conversational questions, respond directly. Don't narrate tool usage -- just use them and present results. diff --git a/docs/plans/state.json b/docs/plans/state.json index 9cc06ae..3a156ce 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -694,6 +694,37 @@ } } }, + "runtime-context-awareness": { + "status": "completed", + "date": "2026-02-07", + "summary": "Runtime context: system.info tool + automatic date/time injection in system prompt", + "phases": { + "system_prompt_date_injection": { + "status": "completed", + "description": "assembleSystemPrompt() now unconditionally appends a '# Runtime Context' section with current date and time", + "files_modified": [ + "src/prompt/template.ts", + "src/prompt/template.test.ts" + ], + "test_status": "13/13 passing" + }, + "system_info_tool": { + "status": "completed", + "description": "New system.info tool providing date, time, hostname, platform, architecture, OS release, uptime, Node.js version, memory usage, working directory. Added to all tool profiles (minimal/messaging/coding).", + "files_created": [ + "src/tools/builtin/system-info.ts", + "src/tools/builtin/system-info.test.ts" + ], + "files_modified": [ + "src/tools/builtin/index.ts", + "src/tools/policy.ts", + "src/tools/index.ts", + "SOUL.md" + ], + "test_status": "6/6 passing" + } + } + }, "earlier_plans": { "plans": [ { "file": "2026-02-02-flynn-design.md", "status": "completed" }, @@ -717,7 +748,7 @@ }, "overall_progress": { - "total_test_count": 976, + "total_test_count": 983, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -730,7 +761,7 @@ "p8_completion": "8/8 (100%) — agent tools (sessions.list/history/create/delete, agents.list, message.send, cron.list/trigger) + gap analysis audit", "tier1_completion": "5/5 (100%) — !!think prefix, /verbose command, typing indicators (Discord/WhatsApp), session pruning (TTL), tool groups", "tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor", - "feature_gap_scorecard": "87/116 match (75%), 1 partial (1%), 27 missing (23%)", + "feature_gap_scorecard": "88/116 match (76%), 1 partial (1%), 27 missing (23%)", "next_up": "All phases P0-P8 and Tiers 1-2 complete. Tier 3 in progress (file.patch + Gmail done). Remaining gaps: Tier 3 channels (Signal, Matrix, Teams, Google Chat), Tier 4 deferred/niche items" } } diff --git a/src/prompt/template.test.ts b/src/prompt/template.test.ts index 9b35abc..644e6b7 100644 --- a/src/prompt/template.test.ts +++ b/src/prompt/template.test.ts @@ -24,7 +24,7 @@ describe('assembleSystemPrompt', () => { const dir = makeTempDir(); const result = assembleSystemPrompt({ searchDirs: [dir] }); - expect(result.prompt).toBe( + expect(result.prompt).toContain( 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.', ); expect(result.loadedFiles).toEqual([]); @@ -36,7 +36,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir] }); - expect(result.prompt).toBe('You are Flynn.'); + expect(result.prompt).toContain('You are Flynn.'); expect(result.loadedFiles).toHaveLength(1); expect(result.loadedFiles[0]).toContain('SOUL.md'); }); @@ -47,7 +47,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir] }); - expect(result.prompt).toBe('# Agent Instructions\n\nFollow these rules.'); + expect(result.prompt).toContain('# Agent Instructions\n\nFollow these rules.'); expect(result.loadedFiles).toHaveLength(1); expect(result.loadedFiles[0]).toContain('AGENTS.md'); }); @@ -62,7 +62,7 @@ describe('assembleSystemPrompt', () => { expect(result.loadedFiles).toHaveLength(3); // Verify correct ordering: SOUL → AGENTS → USER - expect(result.prompt).toBe( + expect(result.prompt).toContain( 'I am Flynn.\n\n# Agent Instructions\n\nBe helpful.\n\n# User Context\n\nUser likes cats.', ); }); @@ -75,7 +75,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] }); - expect(result.prompt).toBe('Primary identity.'); + expect(result.prompt).toContain('Primary identity.'); expect(result.loadedFiles).toHaveLength(1); expect(result.loadedFiles[0]).toContain(dir1); }); @@ -87,7 +87,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] }); - expect(result.prompt).toBe('Fallback identity.'); + expect(result.prompt).toContain('Fallback identity.'); expect(result.loadedFiles[0]).toContain(dir2); }); @@ -102,7 +102,7 @@ describe('assembleSystemPrompt', () => { ], }); - expect(result.prompt).toBe( + expect(result.prompt).toContain( 'Base identity.\n\n# Custom Rules\n\nAlways be polite.', ); }); @@ -114,7 +114,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir] }); - expect(result.prompt).toBe( + expect(result.prompt).toContain( 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.', ); expect(result.loadedFiles).toEqual([]); @@ -132,7 +132,7 @@ describe('assembleSystemPrompt', () => { ], }); - expect(result.prompt).toBe( + expect(result.prompt).toContain( 'Base identity.\n\n# Populated\n\nHas content.', ); }); @@ -161,7 +161,7 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir] }); - expect(result.prompt).toBe('I am Flynn.'); + expect(result.prompt).toContain('I am Flynn.'); }); it('mixes files from different search directories', () => { @@ -173,6 +173,15 @@ describe('assembleSystemPrompt', () => { const result = assembleSystemPrompt({ searchDirs: [dir1, dir2] }); expect(result.loadedFiles).toHaveLength(2); - expect(result.prompt).toBe('Primary soul.\n\n# Agent Instructions\n\nAgent rules.'); + expect(result.prompt).toContain('Primary soul.\n\n# Agent Instructions\n\nAgent rules.'); + }); + + it('always includes Runtime Context section', () => { + const dir = makeTempDir(); + writeFileSync(join(dir, 'SOUL.md'), 'I am Flynn.'); + const result = assembleSystemPrompt({ searchDirs: [dir] }); + expect(result.prompt).toContain('# Runtime Context'); + expect(result.prompt).toContain('Current date:'); + expect(result.prompt).toContain('Current time:'); }); }); diff --git a/src/prompt/template.ts b/src/prompt/template.ts index 9905fe4..ef39af6 100644 --- a/src/prompt/template.ts +++ b/src/prompt/template.ts @@ -63,10 +63,27 @@ export function assembleSystemPrompt(config: PromptTemplateConfig): PromptTempla } } - // Fallback if nothing was loaded - if (sections.length === 0) { + // Inject current date/time as runtime context + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const timeStr = now.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + hour12: false, + }); + const runtimeContext = `# Runtime Context\n\nCurrent date: ${dateStr}\nCurrent time: ${timeStr}`; + sections.push(runtimeContext); + + // Fallback if only the runtime context was loaded (no actual prompt files) + if (sections.length === 1) { return { - prompt: 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.', + prompt: `You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.\n\n${runtimeContext}`, loadedFiles: [], }; } diff --git a/src/tools/builtin/index.ts b/src/tools/builtin/index.ts index 4065b8c..a42b1a6 100644 --- a/src/tools/builtin/index.ts +++ b/src/tools/builtin/index.ts @@ -4,6 +4,7 @@ export { fileWriteTool } from './file-write.js'; export { fileEditTool } from './file-edit.js'; export { filePatchTool } from './file-patch.js'; export { fileListTool } from './file-list.js'; +export { systemInfoTool } from './system-info.js'; export { webFetchTool } from './web-fetch.js'; export { createMediaSendTool } from './media-send.js'; export { createImageAnalyzeTool } from './image-analyze.js'; @@ -31,6 +32,7 @@ import { fileWriteTool } from './file-write.js'; import { fileEditTool } from './file-edit.js'; import { filePatchTool } from './file-patch.js'; import { fileListTool } from './file-list.js'; +import { systemInfoTool } from './system-info.js'; import { webFetchTool } from './web-fetch.js'; import { createMediaSendTool } from './media-send.js'; import { createImageAnalyzeTool } from './image-analyze.js'; @@ -47,6 +49,7 @@ export const allBuiltinTools: Tool[] = [ fileEditTool, filePatchTool, fileListTool, + systemInfoTool, webFetchTool, ]; diff --git a/src/tools/builtin/system-info.test.ts b/src/tools/builtin/system-info.test.ts new file mode 100644 index 0000000..eb29220 --- /dev/null +++ b/src/tools/builtin/system-info.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from 'vitest'; +import { systemInfoTool } from './system-info.js'; + +describe('system.info tool', () => { + // ── Metadata ───────────────────────────────────────────────────────────── + + it('has correct metadata', () => { + expect(systemInfoTool.name).toBe('system.info'); + expect(systemInfoTool.description).toBeTruthy(); + expect(systemInfoTool.inputSchema.type).toBe('object'); + }); + + // ── Execution ──────────────────────────────────────────────────────────── + + it('returns success with system info', async () => { + const result = await systemInfoTool.execute({}); + + expect(result.success).toBe(true); + expect(typeof result.output).toBe('string'); + expect(result.output!.length).toBeGreaterThan(0); + }); + + it('output contains expected fields', async () => { + const result = await systemInfoTool.execute({}); + + expect(result.success).toBe(true); + const output = result.output!; + + const expectedFields = [ + 'Date:', + 'Time:', + 'Hostname:', + 'Platform:', + 'Architecture:', + 'Node.js:', + 'Uptime:', + 'Memory Total:', + ]; + + for (const field of expectedFields) { + expect(output).toContain(field); + } + }); + + it('output contains valid ISO timestamp', async () => { + const result = await systemInfoTool.execute({}); + + expect(result.success).toBe(true); + const output = result.output!; + + // Find the line containing 'ISO 8601:' + const isoLine = output.split('\n').find((line) => line.includes('ISO 8601:')); + expect(isoLine).toBeTruthy(); + + // Extract the ISO string and validate it + const isoMatch = isoLine!.match(/ISO 8601:\s*(.+)/); + expect(isoMatch).toBeTruthy(); + + const isoString = isoMatch![1].trim(); + const parsed = new Date(isoString); + expect(parsed.toISOString()).toBe(isoString); + }); + + it('output contains working directory', async () => { + const result = await systemInfoTool.execute({}); + + expect(result.success).toBe(true); + expect(result.output).toContain('Working Dir:'); + }); + + // ── Idempotency ────────────────────────────────────────────────────────── + + it('handles repeated calls', async () => { + const result1 = await systemInfoTool.execute({}); + const result2 = await systemInfoTool.execute({}); + + expect(result1.success).toBe(true); + expect(result2.success).toBe(true); + expect(typeof result1.output).toBe('string'); + expect(typeof result2.output).toBe('string'); + expect(result1.output!.length).toBeGreaterThan(0); + expect(result2.output!.length).toBeGreaterThan(0); + }); +}); diff --git a/src/tools/builtin/system-info.ts b/src/tools/builtin/system-info.ts new file mode 100644 index 0000000..91dcd9a --- /dev/null +++ b/src/tools/builtin/system-info.ts @@ -0,0 +1,63 @@ +import os from 'os'; +import type { Tool, ToolResult } from '../types.js'; + +function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${days}d ${hours}h ${minutes}m`; +} + +function formatBytes(bytes: number): string { + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; +} + +export const systemInfoTool: Tool = { + name: 'system.info', + description: 'Get current system information including date, time, hostname, OS, platform, architecture, uptime, Node.js version, and memory usage.', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + execute: async (_rawArgs: unknown): Promise => { + try { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const timeStr = now.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + hour12: false, + }); + const isoStr = now.toISOString(); + + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + + const lines = [ + `Date: ${dateStr}`, + `Time: ${timeStr}`, + `ISO 8601: ${isoStr}`, + `Hostname: ${os.hostname()}`, + `Platform: ${os.platform()}`, + `Architecture: ${os.arch()}`, + `OS Release: ${os.release()}`, + `Uptime: ${formatUptime(os.uptime())}`, + `Node.js: ${process.version}`, + `Memory Total: ${formatBytes(totalMem)}`, + `Memory Free: ${formatBytes(freeMem)}`, + `Working Dir: ${process.cwd()}`, + ]; + + return { success: true, output: lines.join('\n') }; + } catch (error) { + return { success: false, output: '', error: error instanceof Error ? error.message : String(error) }; + } + }, +}; diff --git a/src/tools/index.ts b/src/tools/index.ts index a218457..261ba40 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -14,4 +14,5 @@ export { fileReadTool } from './builtin/file-read.js'; export { fileWriteTool } from './builtin/file-write.js'; export { fileEditTool } from './builtin/file-edit.js'; export { fileListTool } from './builtin/file-list.js'; +export { systemInfoTool } from './builtin/system-info.js'; export { webFetchTool } from './builtin/web-fetch.js'; diff --git a/src/tools/policy.ts b/src/tools/policy.ts index d7473b1..e12c5be 100644 --- a/src/tools/policy.ts +++ b/src/tools/policy.ts @@ -9,11 +9,13 @@ const PROFILE_TOOLS: Record> = { 'file.read', 'file.list', 'web.fetch', + 'system.info', ]), messaging: new Set([ 'file.read', 'file.list', 'web.fetch', + 'system.info', 'memory.read', 'memory.write', 'memory.search', @@ -23,6 +25,7 @@ const PROFILE_TOOLS: Record> = { 'file.read', 'file.list', 'web.fetch', + 'system.info', 'memory.read', 'memory.write', 'memory.search',