docs: update state.json with pairing persistence and TUI wiring
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
# Pairing System: TUI Wiring + SQLite Persistence
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Make the DM pairing system fully functional by wiring up the TUI `/pair` command and persisting approved senders across daemon restarts.
|
||||
|
||||
**Architecture:** Add a `PairingStore` interface to decouple `PairingManager` from storage. `SessionStore` implements it via a new `pairing_approved` SQLite table. The TUI receives `PairingManager` as an optional dependency and handles `/pair` subcommands directly.
|
||||
|
||||
**Tech Stack:** TypeScript, better-sqlite3, Vitest
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add PairingStore Interface and Persistence to PairingManager
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/channels/pairing.ts` (add `PairingStore` interface, export `ApprovedSender`, accept optional store)
|
||||
- Modify: `src/channels/index.ts` (re-export `PairingStore` and `ApprovedSender`)
|
||||
- Test: `src/channels/pairing.test.ts`
|
||||
|
||||
**Step 1: Write failing tests for persistence integration**
|
||||
|
||||
Add these tests to `src/channels/pairing.test.ts` inside a new `describe('PairingManager with store')` block:
|
||||
|
||||
```typescript
|
||||
describe('PairingManager with store', () => {
|
||||
it('calls store.saveApproved when a code is validated', () => {
|
||||
const store: PairingStore = {
|
||||
loadApproved: () => [],
|
||||
saveApproved: vi.fn(),
|
||||
removeApproved: vi.fn(),
|
||||
};
|
||||
const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
|
||||
const code = mgr.generateCode();
|
||||
mgr.validateCode('telegram', '123', code);
|
||||
expect(store.saveApproved).toHaveBeenCalledWith(expect.objectContaining({
|
||||
channel: 'telegram',
|
||||
senderId: '123',
|
||||
}));
|
||||
});
|
||||
|
||||
it('calls store.removeApproved when approval is revoked', () => {
|
||||
const store: PairingStore = {
|
||||
loadApproved: () => [],
|
||||
saveApproved: vi.fn(),
|
||||
removeApproved: vi.fn(),
|
||||
};
|
||||
const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
|
||||
const code = mgr.generateCode();
|
||||
mgr.validateCode('telegram', '123', code);
|
||||
mgr.revokeApproval('telegram', '123');
|
||||
expect(store.removeApproved).toHaveBeenCalledWith('telegram', '123');
|
||||
});
|
||||
|
||||
it('loads approved senders from store on construction', () => {
|
||||
const store: PairingStore = {
|
||||
loadApproved: () => [
|
||||
{ channel: 'telegram', senderId: '111', approvedAt: Date.now(), codeUsed: 'AAAAAA' },
|
||||
{ channel: 'discord', senderId: '222', approvedAt: Date.now(), codeUsed: 'BBBBBB' },
|
||||
],
|
||||
saveApproved: vi.fn(),
|
||||
removeApproved: vi.fn(),
|
||||
};
|
||||
const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
|
||||
expect(mgr.isApproved('telegram', '111')).toBe(true);
|
||||
expect(mgr.isApproved('discord', '222')).toBe(true);
|
||||
expect(mgr.listApproved()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('works without a store (backward-compatible)', () => {
|
||||
const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 });
|
||||
const code = mgr.generateCode();
|
||||
mgr.validateCode('telegram', '123', code);
|
||||
expect(mgr.isApproved('telegram', '123')).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `pnpm test:run src/channels/pairing.test.ts`
|
||||
Expected: FAIL — `PairingStore` is not exported, constructor doesn't accept second arg
|
||||
|
||||
**Step 3: Implement PairingStore interface and update PairingManager**
|
||||
|
||||
In `src/channels/pairing.ts`:
|
||||
|
||||
1. Export the `ApprovedSender` interface (currently private — just add `export`).
|
||||
|
||||
2. Add `PairingStore` interface after the existing interfaces (around line 23):
|
||||
|
||||
```typescript
|
||||
export interface PairingStore {
|
||||
loadApproved(): ApprovedSender[];
|
||||
saveApproved(sender: ApprovedSender): void;
|
||||
removeApproved(channel: string, senderId: string): void;
|
||||
}
|
||||
```
|
||||
|
||||
3. Update the `PairingManager` constructor to accept an optional `PairingStore`:
|
||||
|
||||
```typescript
|
||||
export class PairingManager {
|
||||
private config: PairingConfig;
|
||||
private pendingCodes: Map<string, PendingCode> = new Map();
|
||||
private approvedSenders: Map<string, ApprovedSender> = new Map();
|
||||
private store?: PairingStore;
|
||||
|
||||
constructor(config: PairingConfig, store?: PairingStore) {
|
||||
this.config = config;
|
||||
this.store = store;
|
||||
if (store) {
|
||||
for (const sender of store.loadApproved()) {
|
||||
const key = `${sender.channel}:${sender.senderId}`;
|
||||
this.approvedSenders.set(key, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. In `validateCode()`, after `this.approvedSenders.set(key, ...)` (around line 84), add:
|
||||
|
||||
```typescript
|
||||
this.store?.saveApproved(this.approvedSenders.get(key)!);
|
||||
```
|
||||
|
||||
5. In `revokeApproval()`, after `this.approvedSenders.delete(key)` (around line 100), change to:
|
||||
|
||||
```typescript
|
||||
revokeApproval(channel: string, senderId: string): boolean {
|
||||
const key = `${channel}:${senderId}`;
|
||||
const deleted = this.approvedSenders.delete(key);
|
||||
if (deleted) {
|
||||
this.store?.removeApproved(channel, senderId);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
```
|
||||
|
||||
6. Update `src/channels/index.ts` to re-export:
|
||||
|
||||
```typescript
|
||||
export { PairingManager, type PairingConfig, type PairingStore, type ApprovedSender } from './pairing.js';
|
||||
```
|
||||
|
||||
**Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `pnpm test:run src/channels/pairing.test.ts`
|
||||
Expected: ALL PASS (16 existing + 4 new = 20 tests)
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/channels/pairing.ts src/channels/pairing.test.ts src/channels/index.ts
|
||||
git commit -m "feat(pairing): add PairingStore interface for persistence injection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add SQLite Persistence to SessionStore
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/session/store.ts` (add `pairing_approved` table, add methods, add `getPairingStore()`)
|
||||
- Modify: `src/session/index.ts` (re-export `PairingStore` type if needed)
|
||||
- Test: `src/session/store.test.ts` (existing test file — add pairing persistence tests)
|
||||
|
||||
**Step 1: Check existing store test file**
|
||||
|
||||
Read `src/session/store.test.ts` to understand the test setup pattern (likely uses temp DB files or `:memory:`).
|
||||
|
||||
**Step 2: Write failing tests for pairing persistence**
|
||||
|
||||
Add a new `describe('pairing persistence')` block to `src/session/store.test.ts`:
|
||||
|
||||
```typescript
|
||||
describe('pairing persistence', () => {
|
||||
it('getPairingStore returns a PairingStore', () => {
|
||||
const store = new SessionStore(':memory:');
|
||||
const pairingStore = store.getPairingStore();
|
||||
expect(pairingStore).toBeDefined();
|
||||
expect(pairingStore.loadApproved).toBeInstanceOf(Function);
|
||||
expect(pairingStore.saveApproved).toBeInstanceOf(Function);
|
||||
expect(pairingStore.removeApproved).toBeInstanceOf(Function);
|
||||
store.close();
|
||||
});
|
||||
|
||||
it('saveApproved and loadApproved round-trip', () => {
|
||||
const store = new SessionStore(':memory:');
|
||||
const ps = store.getPairingStore();
|
||||
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
|
||||
ps.saveApproved({ channel: 'discord', senderId: '456', approvedAt: 2000, codeUsed: 'CC33DD' });
|
||||
const loaded = ps.loadApproved();
|
||||
expect(loaded).toHaveLength(2);
|
||||
expect(loaded).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ channel: 'telegram', senderId: '123', codeUsed: 'AABB11' }),
|
||||
expect.objectContaining({ channel: 'discord', senderId: '456', codeUsed: 'CC33DD' }),
|
||||
]));
|
||||
store.close();
|
||||
});
|
||||
|
||||
it('removeApproved deletes a sender', () => {
|
||||
const store = new SessionStore(':memory:');
|
||||
const ps = store.getPairingStore();
|
||||
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
|
||||
ps.removeApproved('telegram', '123');
|
||||
expect(ps.loadApproved()).toHaveLength(0);
|
||||
store.close();
|
||||
});
|
||||
|
||||
it('saveApproved upserts on duplicate channel+senderId', () => {
|
||||
const store = new SessionStore(':memory:');
|
||||
const ps = store.getPairingStore();
|
||||
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'FIRST1' });
|
||||
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 2000, codeUsed: 'SECOND' });
|
||||
const loaded = ps.loadApproved();
|
||||
expect(loaded).toHaveLength(1);
|
||||
expect(loaded[0].codeUsed).toBe('SECOND');
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Run tests to verify they fail**
|
||||
|
||||
Run: `pnpm test:run src/session/store.test.ts`
|
||||
Expected: FAIL — `getPairingStore` does not exist
|
||||
|
||||
**Step 4: Implement pairing persistence in SessionStore**
|
||||
|
||||
In `src/session/store.ts`:
|
||||
|
||||
1. Import the `PairingStore` and `ApprovedSender` types at the top:
|
||||
|
||||
```typescript
|
||||
import type { PairingStore, ApprovedSender } from '../channels/pairing.js';
|
||||
```
|
||||
|
||||
2. In `init()`, add the new table after the existing `CREATE TABLE` block:
|
||||
|
||||
```typescript
|
||||
private init(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pairing_approved (
|
||||
channel TEXT NOT NULL,
|
||||
sender_id TEXT NOT NULL,
|
||||
approved_at INTEGER NOT NULL,
|
||||
code_used TEXT NOT NULL,
|
||||
PRIMARY KEY (channel, sender_id)
|
||||
);
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
||||
3. Add `getPairingStore()` method to `SessionStore`:
|
||||
|
||||
```typescript
|
||||
getPairingStore(): PairingStore {
|
||||
return {
|
||||
loadApproved: (): ApprovedSender[] => {
|
||||
const rows = this.db.prepare(
|
||||
'SELECT channel, sender_id, approved_at, code_used FROM pairing_approved'
|
||||
).all() as Array<{ channel: string; sender_id: string; approved_at: number; code_used: string }>;
|
||||
return rows.map(r => ({
|
||||
channel: r.channel,
|
||||
senderId: r.sender_id,
|
||||
approvedAt: r.approved_at,
|
||||
codeUsed: r.code_used,
|
||||
}));
|
||||
},
|
||||
saveApproved: (sender: ApprovedSender): void => {
|
||||
this.db.prepare(`
|
||||
INSERT OR REPLACE INTO pairing_approved (channel, sender_id, approved_at, code_used)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(sender.channel, sender.senderId, sender.approvedAt, sender.codeUsed);
|
||||
},
|
||||
removeApproved: (channel: string, senderId: string): void => {
|
||||
this.db.prepare(
|
||||
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?'
|
||||
).run(channel, senderId);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Run tests to verify they pass**
|
||||
|
||||
Run: `pnpm test:run src/session/store.test.ts`
|
||||
Expected: ALL PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/session/store.ts src/session/store.test.ts
|
||||
git commit -m "feat(session): add pairing_approved table and getPairingStore()"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Wire PairingStore Through Daemon Startup
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/daemon/services.ts:94-109` (update `initPairingManager` to accept store)
|
||||
- Modify: `src/daemon/index.ts:104` (create store, pass to pairing manager)
|
||||
|
||||
**Step 1: Update initPairingManager to accept PairingStore**
|
||||
|
||||
In `src/daemon/services.ts`, change the `initPairingManager` function signature:
|
||||
|
||||
```typescript
|
||||
import type { PairingStore } from '../channels/index.js';
|
||||
|
||||
export function initPairingManager(config: Config, store?: PairingStore): PairingManager | undefined {
|
||||
if (!config.pairing.enabled) return undefined;
|
||||
|
||||
const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/);
|
||||
const codeTtlMs = ttlMatch
|
||||
? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h'])
|
||||
: 5 * 60_000;
|
||||
|
||||
const manager = new PairingManager({
|
||||
enabled: true,
|
||||
codeTtl: codeTtlMs,
|
||||
codeLength: config.pairing.code_length,
|
||||
}, store);
|
||||
console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`);
|
||||
return manager;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Wire PairingStore from SessionStore in daemon/index.ts**
|
||||
|
||||
In `src/daemon/index.ts`, around line 104, change:
|
||||
|
||||
```typescript
|
||||
const pairingManager = initPairingManager(config);
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
const pairingStore = config.pairing.enabled ? sessionStore.getPairingStore() : undefined;
|
||||
const pairingManager = initPairingManager(config, pairingStore);
|
||||
```
|
||||
|
||||
**Step 3: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: No errors
|
||||
|
||||
**Step 4: Run all tests to verify nothing broke**
|
||||
|
||||
Run: `pnpm test:run`
|
||||
Expected: ALL PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/daemon/services.ts src/daemon/index.ts
|
||||
git commit -m "feat(daemon): wire PairingStore from SessionStore into PairingManager"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire TUI /pair Command Execution
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/minimal.ts:33-43,159-203` (add PairingManager to config, add case 'pair')
|
||||
- Modify: `src/cli/tui.ts:46-163` (create PairingManager, pass to MinimalTui)
|
||||
|
||||
**Step 1: Add PairingManager to MinimalTuiConfig**
|
||||
|
||||
In `src/frontends/tui/minimal.ts`, add import and config field:
|
||||
|
||||
```typescript
|
||||
import type { PairingManager } from '../../channels/pairing.js';
|
||||
```
|
||||
|
||||
Add to `MinimalTuiConfig` interface (around line 42):
|
||||
|
||||
```typescript
|
||||
pairingManager?: PairingManager;
|
||||
```
|
||||
|
||||
**Step 2: Add case 'pair' to handleCommand**
|
||||
|
||||
In `src/frontends/tui/minimal.ts`, add a new case in `handleCommand()` before the `case 'message':` line:
|
||||
|
||||
```typescript
|
||||
case 'pair':
|
||||
this.handlePairCommand(command.action, command.args);
|
||||
break;
|
||||
```
|
||||
|
||||
Add the handler method to the `MinimalTui` class:
|
||||
|
||||
```typescript
|
||||
private handlePairCommand(action?: 'generate' | 'list' | 'revoke', args?: string): void {
|
||||
const pm = this.config.pairingManager;
|
||||
if (!pm) {
|
||||
console.log(`${colors.gray}Pairing not enabled. Set pairing.enabled: true in config.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'generate': {
|
||||
const code = pm.generateCode(args);
|
||||
const pending = pm.listPendingCodes().find(p => p.code === code);
|
||||
const expiresIn = pending ? Math.round((pending.expiresAt - Date.now()) / 1000) : '?';
|
||||
console.log(`${colors.bold}Pairing code: ${code}${colors.reset}`);
|
||||
console.log(`${colors.gray}Expires in ${expiresIn}s${args ? ` (label: ${args})` : ''}${colors.reset}\n`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'revoke': {
|
||||
if (!args) {
|
||||
console.log(`${colors.gray}Usage: /pair revoke <channel> <senderId>${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
const parts = args.split(/\s+/);
|
||||
if (parts.length < 2) {
|
||||
console.log(`${colors.gray}Usage: /pair revoke <channel> <senderId>${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
const [channel, senderId] = parts;
|
||||
const revoked = pm.revokeApproval(channel, senderId);
|
||||
if (revoked) {
|
||||
console.log(`${colors.bold}Revoked approval for ${channel}:${senderId}${colors.reset}\n`);
|
||||
} else {
|
||||
console.log(`${colors.gray}No approval found for ${channel}:${senderId}${colors.reset}\n`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list':
|
||||
default: {
|
||||
const pending = pm.listPendingCodes();
|
||||
const approved = pm.listApproved();
|
||||
|
||||
if (pending.length === 0 && approved.length === 0) {
|
||||
console.log(`${colors.gray}No pending codes or approved senders.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.length > 0) {
|
||||
console.log(`${colors.bold}Pending codes:${colors.reset}`);
|
||||
for (const p of pending) {
|
||||
const expiresIn = Math.round((p.expiresAt - Date.now()) / 1000);
|
||||
console.log(` ${p.code} expires in ${expiresIn}s${p.label ? ` (${p.label})` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (approved.length > 0) {
|
||||
console.log(`${colors.bold}Approved senders:${colors.reset}`);
|
||||
for (const a of approved) {
|
||||
const date = new Date(a.approvedAt).toISOString().slice(0, 16).replace('T', ' ');
|
||||
console.log(` ${a.channel}:${a.senderId} since ${date} (code: ${a.codeUsed})`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Wire PairingManager into cli/tui.ts**
|
||||
|
||||
In `src/cli/tui.ts`, after the `modelRouter` creation (around line 63) and before the `MinimalTui` construction (line 142), add:
|
||||
|
||||
```typescript
|
||||
const { initPairingManager } = await import('../daemon/services.js');
|
||||
const pairingStore = config.pairing.enabled ? sessionStore.getPairingStore() : undefined;
|
||||
const pairingManager = initPairingManager(config, pairingStore);
|
||||
```
|
||||
|
||||
Then pass it to `MinimalTui` (add to the config object around line 148):
|
||||
|
||||
```typescript
|
||||
const tui = new MinimalTui({
|
||||
session,
|
||||
modelClient: modelRouter,
|
||||
modelRouter,
|
||||
systemPrompt,
|
||||
agent,
|
||||
pairingManager, // <-- add this
|
||||
localProviders: config.models.local_providers,
|
||||
...
|
||||
```
|
||||
|
||||
**Step 4: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: No errors
|
||||
|
||||
**Step 5: Run all tests**
|
||||
|
||||
Run: `pnpm test:run`
|
||||
Expected: ALL PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/minimal.ts src/cli/tui.ts
|
||||
git commit -m "feat(tui): wire /pair command execution with PairingManager"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add Missing Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/frontends/tui/commands.test.ts` (add /pair parsing tests)
|
||||
|
||||
**Step 1: Add /pair parsing tests**
|
||||
|
||||
Add a new `describe('/pair command')` block to `src/frontends/tui/commands.test.ts`:
|
||||
|
||||
```typescript
|
||||
describe('/pair command', () => {
|
||||
it('parses /pair as list', () => {
|
||||
expect(parseCommand('/pair')).toEqual({ type: 'pair', action: 'list' });
|
||||
});
|
||||
|
||||
it('parses /pair list', () => {
|
||||
expect(parseCommand('/pair list')).toEqual({ type: 'pair', action: 'list' });
|
||||
});
|
||||
|
||||
it('parses /pair generate without label', () => {
|
||||
expect(parseCommand('/pair generate')).toEqual({ type: 'pair', action: 'generate', args: undefined });
|
||||
});
|
||||
|
||||
it('parses /pair generate with label', () => {
|
||||
expect(parseCommand('/pair generate for alice')).toEqual({ type: 'pair', action: 'generate', args: 'for alice' });
|
||||
});
|
||||
|
||||
it('parses /pair revoke with channel and sender', () => {
|
||||
expect(parseCommand('/pair revoke telegram 12345')).toEqual({ type: 'pair', action: 'revoke', args: 'telegram 12345' });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run tests to verify they pass**
|
||||
|
||||
Run: `pnpm test:run src/frontends/tui/commands.test.ts`
|
||||
Expected: ALL PASS (parsing already works, we're just adding test coverage)
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/frontends/tui/commands.test.ts
|
||||
git commit -m "test(tui): add /pair command parsing tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update state.json and Run Final Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/state.json` (update `dm_pairing_codes` entry)
|
||||
|
||||
**Step 1: Run full test suite**
|
||||
|
||||
Run: `pnpm test:run`
|
||||
Expected: ALL PASS
|
||||
|
||||
**Step 2: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: No errors
|
||||
|
||||
**Step 3: Run build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: Clean build
|
||||
|
||||
**Step 4: Update state.json**
|
||||
|
||||
Update the `dm_pairing_codes` entry in `docs/plans/state.json`:
|
||||
|
||||
- Update `description` to mention SQLite persistence and TUI execution
|
||||
- Add `src/session/store.ts` to `files_modified`
|
||||
- Add `src/daemon/services.ts` to `files_modified` (if not already)
|
||||
- Add `src/cli/tui.ts` to `files_modified`
|
||||
- Add `src/frontends/tui/minimal.ts` to `files_modified`
|
||||
- Update `test_status` count to reflect new tests
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/state.json
|
||||
git commit -m "docs: update state.json with pairing persistence and TUI execution"
|
||||
```
|
||||
@@ -867,7 +867,7 @@
|
||||
"dm_pairing_codes": {
|
||||
"priority": "Tier4",
|
||||
"status": "completed",
|
||||
"description": "PairingManager with TTL codes, channel adapter integration (Telegram, Discord, Slack, WhatsApp), gateway pairing handlers (generate/list/revoke), TUI /pair command, daemon wiring.",
|
||||
"description": "PairingManager with TTL codes, channel adapter integration (Telegram, Discord, Slack, WhatsApp), gateway pairing handlers (generate/list/revoke), TUI /pair command execution, daemon wiring, SQLite persistence via PairingStore interface.",
|
||||
"files_created": [
|
||||
"src/channels/pairing.ts",
|
||||
"src/channels/pairing.test.ts",
|
||||
@@ -884,9 +884,14 @@
|
||||
"src/gateway/handlers/handlers.test.ts",
|
||||
"src/gateway/server.ts",
|
||||
"src/daemon/index.ts",
|
||||
"src/frontends/tui/commands.ts"
|
||||
"src/daemon/services.ts",
|
||||
"src/session/store.ts",
|
||||
"src/frontends/tui/commands.ts",
|
||||
"src/frontends/tui/commands.test.ts",
|
||||
"src/frontends/tui/minimal.ts",
|
||||
"src/cli/tui.ts"
|
||||
],
|
||||
"test_status": "22/22 passing (16 pairing + 6 handlers)"
|
||||
"test_status": "35/35 passing (20 pairing + 6 handlers + 4 store + 5 commands)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -928,7 +933,7 @@
|
||||
},
|
||||
|
||||
"overall_progress": {
|
||||
"total_test_count": 1107,
|
||||
"total_test_count": 1120,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
|
||||
Reference in New Issue
Block a user