feat: implement tier 1 quick wins (tool groups, typing, pruning, verbose, think)

Five additive features with no breaking changes:

- Tool groups: group:fs, group:runtime, group:web, group:memory syntactic
  sugar for allow/deny lists in tool policy config
- Typing indicators: Discord sendTyping() and WhatsApp sendStateTyping()
  on message receipt for better UX feedback
- Session pruning: TTL-based auto-cleanup via sessions.ttl config with
  hourly daemon timer and SQLite GROUP BY pruning
- /verbose command: TUI command parser toggle for raw streaming display
- !!think prefix: per-message extended thinking mode wired through
  Anthropic (budget_tokens), OpenAI/GitHub (reasoning_effort), and
  Gemini (thinkingConfig) providers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-07 13:35:00 -08:00
parent 6bb424cddc
commit 1c2f54fae3
19 changed files with 563 additions and 20 deletions
+30
View File
@@ -1,6 +1,15 @@
import Database from 'better-sqlite3';
import type { Message } from '../models/types.js';
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
export function parseDuration(s: string): number | null {
if (s === '0' || s === 'false') return null;
const match = s.match(/^(\d+)(h|d)$/);
if (!match) return null;
const [, n, unit] = match;
return unit === 'h' ? Number(n) * 3600_000 : Number(n) * 86_400_000;
}
export class SessionStore {
private db: Database.Database;
@@ -71,6 +80,27 @@ export class SessionStore {
return rows.map(row => row.session_id);
}
/** Delete all messages for sessions with no activity since the given timestamp. Returns pruned session IDs. */
pruneStale(beforeTimestamp: number): string[] {
const stale = this.db.prepare(`
SELECT session_id FROM messages
GROUP BY session_id
HAVING MAX(created_at) < ?
`).all(beforeTimestamp) as Array<{ session_id: string }>;
if (stale.length === 0) return [];
const deleteStmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
const transaction = this.db.transaction(() => {
for (const { session_id } of stale) {
deleteStmt.run(session_id);
}
});
transaction();
return stale.map(r => r.session_id);
}
close(): void {
this.db.close();
}