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
+84
View File
@@ -409,6 +409,90 @@ describe('ToolPolicy', () => {
});
});
describe('tool groups', () => {
it('expands group:fs in allow list', () => {
const policy = new ToolPolicy(defaultConfig({
profile: 'minimal',
allow: ['group:fs'],
}));
const result = policy.filterTools(ALL_TOOLS);
const names = result.map(t => t.name);
expect(names).toContain('file.read');
expect(names).toContain('file.write');
expect(names).toContain('file.edit');
expect(names).toContain('file.list');
expect(names).not.toContain('shell.exec');
});
it('expands group:runtime in deny list', () => {
const policy = new ToolPolicy(defaultConfig({
deny: ['group:runtime'],
}));
const result = policy.filterTools(ALL_TOOLS);
const names = result.map(t => t.name);
expect(names).not.toContain('shell.exec');
expect(names).not.toContain('process.start');
expect(names).not.toContain('process.status');
expect(names).not.toContain('process.output');
expect(names).not.toContain('process.kill');
expect(names).not.toContain('process.list');
expect(names).toContain('file.read');
});
it('expands groups in agent overrides', () => {
const policy = new ToolPolicy(defaultConfig({
agents: {
fast: { profile: 'minimal', allow: ['group:memory'], deny: [] },
},
}));
const result = policy.filterTools(ALL_TOOLS, { agent: 'fast' });
const names = result.map(t => t.name);
expect(names).toContain('memory.read');
expect(names).toContain('memory.write');
expect(names).toContain('memory.search');
expect(names).toContain('file.read'); // from minimal profile
expect(names).not.toContain('shell.exec');
});
it('expands groups in provider deny', () => {
const policy = new ToolPolicy(defaultConfig({
providers: {
ollama: { allow: [], deny: ['group:web'] },
},
}));
const result = policy.filterTools(ALL_TOOLS, { provider: 'ollama' });
const names = result.map(t => t.name);
expect(names).not.toContain('web.fetch');
expect(names).not.toContain('web.search');
expect(names).toContain('file.read');
expect(names).toContain('shell.exec');
});
it('mixes groups with individual names', () => {
const policy = new ToolPolicy(defaultConfig({
profile: 'minimal',
allow: ['group:memory', 'shell.exec'],
}));
const result = policy.filterTools(ALL_TOOLS);
const names = result.map(t => t.name);
expect(names).toContain('memory.read');
expect(names).toContain('shell.exec');
expect(names).toContain('file.read'); // from minimal
});
it('unknown group name passes through as literal', () => {
const policy = new ToolPolicy(defaultConfig({
profile: 'minimal',
allow: ['group:nonexistent'],
}));
const result = policy.filterTools(ALL_TOOLS);
// Should only have minimal tools — 'group:nonexistent' doesn't match any real tool
const names = result.map(t => t.name);
expect(names).toContain('file.read');
expect(names).not.toContain('shell.exec');
});
});
describe('edge cases', () => {
it('handles empty tool list', () => {
const policy = new ToolPolicy(defaultConfig());