thatgfsj-code 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +10 -5
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.js +1 -1
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +7 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/skills/brainstorming.d.ts +3 -0
- package/dist/skills/brainstorming.d.ts.map +1 -0
- package/dist/skills/brainstorming.js +42 -0
- package/dist/skills/brainstorming.js.map +1 -0
- package/dist/skills/code-review.d.ts +3 -0
- package/dist/skills/code-review.d.ts.map +1 -0
- package/dist/skills/code-review.js +44 -0
- package/dist/skills/code-review.js.map +1 -0
- package/dist/skills/executing-plans.d.ts +3 -0
- package/dist/skills/executing-plans.d.ts.map +1 -0
- package/dist/skills/executing-plans.js +26 -0
- package/dist/skills/executing-plans.js.map +1 -0
- package/dist/skills/frontend-design.d.ts +3 -0
- package/dist/skills/frontend-design.d.ts.map +1 -0
- package/dist/skills/frontend-design.js +34 -0
- package/dist/skills/frontend-design.js.map +1 -0
- package/dist/skills/git-workflow.d.ts +3 -0
- package/dist/skills/git-workflow.d.ts.map +1 -0
- package/dist/skills/git-workflow.js +35 -0
- package/dist/skills/git-workflow.js.map +1 -0
- package/dist/skills/improve-architecture.d.ts +3 -0
- package/dist/skills/improve-architecture.d.ts.map +1 -0
- package/dist/skills/improve-architecture.js +37 -0
- package/dist/skills/improve-architecture.js.map +1 -0
- package/dist/skills/index.d.ts +50 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +98 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/neuroweave.d.ts +3 -0
- package/dist/skills/neuroweave.d.ts.map +1 -0
- package/dist/skills/neuroweave.js +46 -0
- package/dist/skills/neuroweave.js.map +1 -0
- package/dist/skills/playwright.d.ts +3 -0
- package/dist/skills/playwright.d.ts.map +1 -0
- package/dist/skills/playwright.js +71 -0
- package/dist/skills/playwright.js.map +1 -0
- package/dist/skills/prototype.d.ts +3 -0
- package/dist/skills/prototype.d.ts.map +1 -0
- package/dist/skills/prototype.js +29 -0
- package/dist/skills/prototype.js.map +1 -0
- package/dist/skills/subagent.d.ts +3 -0
- package/dist/skills/subagent.d.ts.map +1 -0
- package/dist/skills/subagent.js +27 -0
- package/dist/skills/subagent.js.map +1 -0
- package/dist/skills/supabase.d.ts +3 -0
- package/dist/skills/supabase.d.ts.map +1 -0
- package/dist/skills/supabase.js +43 -0
- package/dist/skills/supabase.js.map +1 -0
- package/dist/skills/systematic-debugging.d.ts +3 -0
- package/dist/skills/systematic-debugging.d.ts.map +1 -0
- package/dist/skills/systematic-debugging.js +43 -0
- package/dist/skills/systematic-debugging.js.map +1 -0
- package/dist/skills/tdd.d.ts +3 -0
- package/dist/skills/tdd.d.ts.map +1 -0
- package/dist/skills/tdd.js +38 -0
- package/dist/skills/tdd.js.map +1 -0
- package/dist/skills/triage.d.ts +3 -0
- package/dist/skills/triage.d.ts.map +1 -0
- package/dist/skills/triage.js +34 -0
- package/dist/skills/triage.js.map +1 -0
- package/dist/skills/verification.d.ts +3 -0
- package/dist/skills/verification.d.ts.map +1 -0
- package/dist/skills/verification.js +30 -0
- package/dist/skills/verification.js.map +1 -0
- package/dist/skills/writing-plans.d.ts +3 -0
- package/dist/skills/writing-plans.d.ts.map +1 -0
- package/dist/skills/writing-plans.js +41 -0
- package/dist/skills/writing-plans.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/nwt.d.ts +70 -0
- package/dist/tools/nwt.d.ts.map +1 -0
- package/dist/tools/nwt.js +289 -0
- package/dist/tools/nwt.js.map +1 -0
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +6 -6
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/ChatList.d.ts +4 -5
- package/dist/tui/components/ChatList.d.ts.map +1 -1
- package/dist/tui/components/ChatList.js +3 -3
- package/dist/tui/components/ChatList.js.map +1 -1
- package/dist/tui/components/ChatMessage.d.ts +2 -1
- package/dist/tui/components/ChatMessage.d.ts.map +1 -1
- package/dist/tui/components/ChatMessage.js +7 -7
- package/dist/tui/components/ChatMessage.js.map +1 -1
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +1 -1
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/StatusBar.d.ts +9 -0
- package/dist/tui/components/StatusBar.d.ts.map +1 -0
- package/dist/tui/components/StatusBar.js +8 -0
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/dist/tui/components/Thinking.js +1 -1
- package/dist/tui/components/Thinking.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts +2 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +29 -24
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +16 -22
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +28 -1
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/package.json +1 -1
- package/src/app/index.ts +12 -6
- package/src/cmd/index.tsx +1 -1
- package/src/prompts/index.ts +8 -0
- package/src/skills/brainstorming.ts +43 -0
- package/src/skills/code-review.ts +45 -0
- package/src/skills/executing-plans.ts +27 -0
- package/src/skills/frontend-design.ts +35 -0
- package/src/skills/git-workflow.ts +36 -0
- package/src/skills/improve-architecture.ts +38 -0
- package/src/skills/index.ts +136 -0
- package/src/skills/neuroweave.ts +47 -0
- package/src/skills/playwright.ts +72 -0
- package/src/skills/prototype.ts +30 -0
- package/src/skills/subagent.ts +28 -0
- package/src/skills/supabase.ts +44 -0
- package/src/skills/systematic-debugging.ts +44 -0
- package/src/skills/tdd.ts +39 -0
- package/src/skills/triage.ts +35 -0
- package/src/skills/verification.ts +31 -0
- package/src/skills/writing-plans.ts +42 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/nwt.ts +362 -0
- package/src/tui/app.tsx +9 -8
- package/src/tui/components/ChatList.tsx +6 -3
- package/src/tui/components/ChatMessage.tsx +15 -12
- package/src/tui/components/Header.tsx +13 -7
- package/src/tui/components/StatusBar.tsx +30 -0
- package/src/tui/components/Thinking.tsx +3 -3
- package/src/tui/components/ToolCall.tsx +34 -32
- package/src/tui/components/UserInput.tsx +22 -40
- package/src/tui/hooks/useCommands.ts +29 -1
package/src/tools/nwt.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NeuroWeave Timeline (NWT) Tool - Project evolution memory
|
|
3
|
+
* Built-in port of https://github.com/Thatgfsj/neuroweave-timeline
|
|
4
|
+
*
|
|
5
|
+
* Stores meaningful actions (decisions, refactors, bug fixes) as timeline events
|
|
6
|
+
* in .nwt/ directory. Supports monthly archives (max 1 month per archive).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Tool, ToolResult } from './types.js';
|
|
10
|
+
import {
|
|
11
|
+
readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
12
|
+
readdirSync, renameSync
|
|
13
|
+
} from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
|
|
16
|
+
interface TimelineEvent {
|
|
17
|
+
id: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
task: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
files: string[];
|
|
23
|
+
tags: string[];
|
|
24
|
+
parent?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const NWT_DIR = '.nwt';
|
|
28
|
+
const EVENTS_DIR = 'events';
|
|
29
|
+
const ARCHIVE_DIR = 'archives';
|
|
30
|
+
const MAX_EVENTS = 500;
|
|
31
|
+
const ARCHIVE_AFTER_DAYS = 30;
|
|
32
|
+
|
|
33
|
+
export class NwtTool implements Tool {
|
|
34
|
+
name = 'nwt';
|
|
35
|
+
description = 'NeuroWeave Timeline - Project evolution memory. Log decisions, track changes, search history. Actions: init, log, history, search, story, explain, archive';
|
|
36
|
+
|
|
37
|
+
inputSchema = {
|
|
38
|
+
type: 'object' as const,
|
|
39
|
+
properties: {
|
|
40
|
+
action: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'Action: init, log, history, search, story, explain, archive',
|
|
43
|
+
},
|
|
44
|
+
task: { type: 'string', description: 'Short imperative title (for log)' },
|
|
45
|
+
summary: { type: 'string', description: 'What was done (for log)' },
|
|
46
|
+
reason: { type: 'string', description: 'Why it was done (for log)' },
|
|
47
|
+
files: { type: 'string', description: 'Comma-separated file paths (for log)' },
|
|
48
|
+
tags: { type: 'string', description: 'Comma-separated tags (for log)' },
|
|
49
|
+
query: { type: 'string', description: 'Search query (for search/explain)' },
|
|
50
|
+
limit: { type: 'number', description: 'Max results (for history/search)' },
|
|
51
|
+
},
|
|
52
|
+
required: ['action'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
metadata = {
|
|
56
|
+
permissions: ['read', 'write'] as ('read' | 'write' | 'execute' | 'network')[],
|
|
57
|
+
tags: ['nwt', 'timeline', 'memory', 'history'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
parameters = [
|
|
61
|
+
{ name: 'action', type: 'string', description: 'Action to perform', required: true },
|
|
62
|
+
{ name: 'task', type: 'string', description: 'Short title', required: false },
|
|
63
|
+
{ name: 'summary', type: 'string', description: 'What was done', required: false },
|
|
64
|
+
{ name: 'reason', type: 'string', description: 'Why it was done', required: false },
|
|
65
|
+
{ name: 'files', type: 'string', description: 'Comma-separated file paths', required: false },
|
|
66
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated tags', required: false },
|
|
67
|
+
{ name: 'query', type: 'string', description: 'Search query', required: false },
|
|
68
|
+
{ name: 'limit', type: 'number', description: 'Max results', required: false },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
async execute(params: Record<string, any>, ctx?: any): Promise<ToolResult> {
|
|
72
|
+
const cwd = ctx?.workingDirectory || process.cwd();
|
|
73
|
+
const action = params.action;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
switch (action) {
|
|
77
|
+
case 'init':
|
|
78
|
+
return this.init(cwd);
|
|
79
|
+
case 'log':
|
|
80
|
+
return this.log(cwd, params);
|
|
81
|
+
case 'history':
|
|
82
|
+
return this.history(cwd, params.limit || 20);
|
|
83
|
+
case 'search':
|
|
84
|
+
return this.search(cwd, params.query || '', params.limit || 20);
|
|
85
|
+
case 'story':
|
|
86
|
+
return this.story(cwd);
|
|
87
|
+
case 'explain':
|
|
88
|
+
return this.explain(cwd, params.query || '');
|
|
89
|
+
case 'archive':
|
|
90
|
+
return this.archive(cwd);
|
|
91
|
+
default:
|
|
92
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
93
|
+
}
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
return { success: false, error: error.message };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Init ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
private init(cwd: string): ToolResult {
|
|
102
|
+
const nwtDir = join(cwd, NWT_DIR);
|
|
103
|
+
const eventsDir = join(nwtDir, EVENTS_DIR);
|
|
104
|
+
const archiveDir = join(nwtDir, ARCHIVE_DIR);
|
|
105
|
+
|
|
106
|
+
if (!existsSync(nwtDir)) mkdirSync(nwtDir, { recursive: true });
|
|
107
|
+
if (!existsSync(eventsDir)) mkdirSync(eventsDir, { recursive: true });
|
|
108
|
+
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
// Write metadata
|
|
111
|
+
const meta = {
|
|
112
|
+
version: '1.0.0',
|
|
113
|
+
created: new Date().toISOString(),
|
|
114
|
+
description: 'NeuroWeave Timeline - Project evolution memory',
|
|
115
|
+
};
|
|
116
|
+
writeFileSync(join(nwtDir, 'meta.json'), JSON.stringify(meta, null, 2));
|
|
117
|
+
|
|
118
|
+
return { success: true, output: 'NWT initialized in .nwt/' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Log Event ─────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
private log(cwd: string, params: Record<string, any>): ToolResult {
|
|
124
|
+
const nwtDir = join(cwd, NWT_DIR);
|
|
125
|
+
if (!existsSync(nwtDir)) {
|
|
126
|
+
this.init(cwd);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { task, summary, reason, files, tags } = params;
|
|
130
|
+
if (!task || !summary) {
|
|
131
|
+
return { success: false, error: 'task and summary are required' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get next ID
|
|
135
|
+
const eventsDir = join(nwtDir, EVENTS_DIR);
|
|
136
|
+
const existing = existsSync(eventsDir)
|
|
137
|
+
? readdirSync(eventsDir).filter(f => f.endsWith('.json'))
|
|
138
|
+
: [];
|
|
139
|
+
const nextId = (existing.length + 1).toString().padStart(6, '0');
|
|
140
|
+
|
|
141
|
+
// Get parent (last event)
|
|
142
|
+
const parent = existing.length > 0
|
|
143
|
+
? existing.sort().pop()?.replace('.json', '')
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
const event: TimelineEvent = {
|
|
147
|
+
id: nextId,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
task: task.trim(),
|
|
150
|
+
summary: summary.trim(),
|
|
151
|
+
reason: reason?.trim() || undefined,
|
|
152
|
+
files: files ? files.split(',').map((f: string) => f.trim()).filter(Boolean) : [],
|
|
153
|
+
tags: tags ? tags.split(',').map((t: string) => t.trim().toLowerCase()).filter(Boolean) : [],
|
|
154
|
+
parent: parent || undefined,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
writeFileSync(
|
|
158
|
+
join(eventsDir, `${nextId}.json`),
|
|
159
|
+
JSON.stringify(event, null, 2)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Auto-archive if too many events
|
|
163
|
+
if (existing.length >= MAX_EVENTS) {
|
|
164
|
+
this.archive(cwd);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
output: `Event [${nextId}] logged: ${event.task}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── History ───────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
private history(cwd: string, limit: number): ToolResult {
|
|
176
|
+
const events = this.loadEvents(cwd);
|
|
177
|
+
if (events.length === 0) {
|
|
178
|
+
return { success: true, output: 'No events yet. Use nwt log to start tracking.' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const recent = events.slice(-limit);
|
|
182
|
+
const lines = recent.map(e => {
|
|
183
|
+
const time = e.timestamp.split('T')[0];
|
|
184
|
+
const files = e.files.length > 0 ? ` [${e.files.join(', ')}]` : '';
|
|
185
|
+
const tags = e.tags.length > 0 ? ` {${e.tags.join(', ')}}` : '';
|
|
186
|
+
return `[${e.id}] ${time} ${e.task}${files}${tags}`;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return { success: true, output: lines.join('\n') };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Search ────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
private search(cwd: string, query: string, limit: number): ToolResult {
|
|
195
|
+
if (!query) return { success: false, error: 'query is required' };
|
|
196
|
+
|
|
197
|
+
const events = this.loadEvents(cwd);
|
|
198
|
+
const q = query.toLowerCase();
|
|
199
|
+
const matches = events.filter(e =>
|
|
200
|
+
e.task.toLowerCase().includes(q) ||
|
|
201
|
+
e.summary.toLowerCase().includes(q) ||
|
|
202
|
+
(e.reason || '').toLowerCase().includes(q) ||
|
|
203
|
+
e.files.some(f => f.toLowerCase().includes(q)) ||
|
|
204
|
+
e.tags.some(t => t.includes(q))
|
|
205
|
+
).slice(-limit);
|
|
206
|
+
|
|
207
|
+
if (matches.length === 0) {
|
|
208
|
+
return { success: true, output: `No events matching "${query}"` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const lines = matches.map(e => {
|
|
212
|
+
const time = e.timestamp.split('T')[0];
|
|
213
|
+
return `[${e.id}] ${time} ${e.task}\n ${e.summary}`;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return { success: true, output: lines.join('\n\n') };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Story ─────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
private story(cwd: string): ToolResult {
|
|
222
|
+
const events = this.loadEvents(cwd);
|
|
223
|
+
if (events.length === 0) {
|
|
224
|
+
return { success: true, output: 'No events yet.' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Group by tags and find milestones
|
|
228
|
+
const milestones = events.filter(e => e.tags.includes('milestone'));
|
|
229
|
+
const decisions = events.filter(e => e.tags.includes('decision'));
|
|
230
|
+
const totalFiles = new Set(events.flatMap(e => e.files)).size;
|
|
231
|
+
|
|
232
|
+
const lines = [
|
|
233
|
+
`Project Story: ${events.length} events, ${totalFiles} files touched`,
|
|
234
|
+
'',
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
if (milestones.length > 0) {
|
|
238
|
+
lines.push('Milestones:');
|
|
239
|
+
for (const m of milestones.slice(-5)) {
|
|
240
|
+
lines.push(` [${m.id}] ${m.task} - ${m.summary}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (decisions.length > 0) {
|
|
246
|
+
lines.push('Key Decisions:');
|
|
247
|
+
for (const d of decisions.slice(-5)) {
|
|
248
|
+
lines.push(` [${d.id}] ${d.task}${d.reason ? ` - ${d.reason}` : ''}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push('');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push('Recent Activity:');
|
|
254
|
+
for (const e of events.slice(-5)) {
|
|
255
|
+
const time = e.timestamp.split('T')[0];
|
|
256
|
+
lines.push(` ${time} ${e.task}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { success: true, output: lines.join('\n') };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Explain File ──────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
private explain(cwd: string, filePath: string): ToolResult {
|
|
265
|
+
if (!filePath) return { success: false, error: 'file path is required' };
|
|
266
|
+
|
|
267
|
+
const events = this.loadEvents(cwd);
|
|
268
|
+
const matches = events.filter(e =>
|
|
269
|
+
e.files.some(f => f === filePath || f.endsWith(filePath))
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (matches.length === 0) {
|
|
273
|
+
return { success: true, output: `No history for "${filePath}"` };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lines = matches.map(e => {
|
|
277
|
+
const time = e.timestamp.split('T')[0];
|
|
278
|
+
return `[${e.id}] ${time} ${e.task}\n ${e.summary}${e.reason ? `\n Reason: ${e.reason}` : ''}`;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return { success: true, output: `History of ${filePath}:\n\n${lines.join('\n\n')}` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Archive ───────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
private archive(cwd: string): ToolResult {
|
|
287
|
+
const nwtDir = join(cwd, NWT_DIR);
|
|
288
|
+
const eventsDir = join(nwtDir, EVENTS_DIR);
|
|
289
|
+
const archiveDir = join(nwtDir, ARCHIVE_DIR);
|
|
290
|
+
|
|
291
|
+
if (!existsSync(eventsDir)) {
|
|
292
|
+
return { success: true, output: 'No events to archive.' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const cutoff = new Date();
|
|
296
|
+
cutoff.setDate(cutoff.getDate() - ARCHIVE_AFTER_DAYS);
|
|
297
|
+
|
|
298
|
+
const files = readdirSync(eventsDir).filter(f => f.endsWith('.json')).sort();
|
|
299
|
+
const toArchive: string[] = [];
|
|
300
|
+
const toKeep: string[] = [];
|
|
301
|
+
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
try {
|
|
304
|
+
const event = JSON.parse(readFileSync(join(eventsDir, file), 'utf-8'));
|
|
305
|
+
const eventDate = new Date(event.timestamp);
|
|
306
|
+
if (eventDate < cutoff) {
|
|
307
|
+
toArchive.push(file);
|
|
308
|
+
} else {
|
|
309
|
+
toKeep.push(file);
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
toKeep.push(file);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (toArchive.length === 0) {
|
|
317
|
+
return { success: true, output: 'No events old enough to archive.' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create archive file
|
|
321
|
+
const archiveName = `archive-${new Date().toISOString().split('T')[0]}.json`;
|
|
322
|
+
const archiveEvents = toArchive.map(f => {
|
|
323
|
+
return JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
writeFileSync(
|
|
327
|
+
join(archiveDir, archiveName),
|
|
328
|
+
JSON.stringify(archiveEvents, null, 2)
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Remove archived events from events dir
|
|
332
|
+
for (const file of toArchive) {
|
|
333
|
+
const src = join(eventsDir, file);
|
|
334
|
+
try { renameSync(src, join(archiveDir, file)); } catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
output: `Archived ${toArchive.length} events to archives/${archiveName}. ${toKeep.length} events remain.`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Helpers ───────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
private loadEvents(cwd: string): TimelineEvent[] {
|
|
346
|
+
const eventsDir = join(cwd, NWT_DIR, EVENTS_DIR);
|
|
347
|
+
if (!existsSync(eventsDir)) return [];
|
|
348
|
+
|
|
349
|
+
const files = readdirSync(eventsDir).filter(f => f.endsWith('.json')).sort();
|
|
350
|
+
const events: TimelineEvent[] = [];
|
|
351
|
+
|
|
352
|
+
for (const file of files) {
|
|
353
|
+
try {
|
|
354
|
+
events.push(JSON.parse(readFileSync(join(eventsDir, file), 'utf-8')));
|
|
355
|
+
} catch {
|
|
356
|
+
// Skip corrupted files
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return events;
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState, useCallback
|
|
3
|
-
import { Box, Text,
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useStdout } from 'ink';
|
|
4
4
|
import { Header } from './components/Header.js';
|
|
5
5
|
import { ChatList } from './components/ChatList.js';
|
|
6
6
|
import { Thinking } from './components/Thinking.js';
|
|
7
7
|
import { UserInput } from './components/UserInput.js';
|
|
8
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
8
9
|
import { useChat } from './hooks/useChat.js';
|
|
9
10
|
import { useCommands } from './hooks/useCommands.js';
|
|
10
11
|
import type { App } from '../app/index.js';
|
|
@@ -19,10 +20,10 @@ export function TuiApp({ app }: Props) {
|
|
|
19
20
|
const { messages, isThinking, streaming, streamingToolCalls, sendMessage } = useChat(app);
|
|
20
21
|
const { handleCommand } = useCommands(app);
|
|
21
22
|
const [systemMessages, setSystemMessages] = useState<MessageData[]>([]);
|
|
22
|
-
const {
|
|
23
|
+
const { stdout } = useStdout();
|
|
24
|
+
const terminalWidth = stdout?.columns || 80;
|
|
23
25
|
|
|
24
26
|
const onSubmit = useCallback(async (input: string) => {
|
|
25
|
-
// Check for built-in commands first
|
|
26
27
|
const result = handleCommand(input);
|
|
27
28
|
if (result.handled) {
|
|
28
29
|
if (result.output) {
|
|
@@ -32,29 +33,29 @@ export function TuiApp({ app }: Props) {
|
|
|
32
33
|
{ role: 'assistant', content: result.output! },
|
|
33
34
|
]);
|
|
34
35
|
}
|
|
35
|
-
// Handle clear
|
|
36
36
|
if (input.toLowerCase().trim() === 'clear') {
|
|
37
37
|
setSystemMessages([]);
|
|
38
38
|
}
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
// Send to AI
|
|
43
41
|
await sendMessage(input);
|
|
44
42
|
}, [handleCommand, sendMessage]);
|
|
45
43
|
|
|
46
44
|
const allMessages = [...systemMessages, ...messages];
|
|
45
|
+
const activeSkills = app.skills.listActive().map(s => s.id);
|
|
47
46
|
|
|
48
47
|
return (
|
|
49
|
-
<Box flexDirection="column"
|
|
48
|
+
<Box flexDirection="column" paddingX={1}>
|
|
50
49
|
<Header provider={config.provider} model={config.model} />
|
|
51
50
|
<ChatList
|
|
52
51
|
messages={allMessages}
|
|
53
52
|
streaming={streaming}
|
|
54
53
|
streamingToolCalls={streamingToolCalls}
|
|
54
|
+
width={terminalWidth - 4}
|
|
55
55
|
/>
|
|
56
56
|
{isThinking && <Thinking />}
|
|
57
57
|
<UserInput onSubmit={onSubmit} disabled={isThinking} />
|
|
58
|
+
<StatusBar messageCount={allMessages.length} skills={activeSkills} />
|
|
58
59
|
</Box>
|
|
59
60
|
);
|
|
60
61
|
}
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box } from 'ink';
|
|
4
4
|
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
|
+
import type { ToolCallData } from './ToolCall.js';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
messages: MessageData[];
|
|
8
9
|
streaming?: string;
|
|
9
|
-
streamingToolCalls?:
|
|
10
|
+
streamingToolCalls?: ToolCallData[];
|
|
11
|
+
width?: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
|
|
14
|
+
export function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
|
|
13
15
|
return (
|
|
14
16
|
<Box flexDirection="column" flexGrow={1}>
|
|
15
17
|
{messages.map((msg, i) => (
|
|
16
|
-
<ChatMessage key={i} message={msg} />
|
|
18
|
+
<ChatMessage key={i} message={msg} width={width} />
|
|
17
19
|
))}
|
|
18
20
|
{(streaming || streamingToolCalls) && (
|
|
19
21
|
<ChatMessage
|
|
@@ -22,6 +24,7 @@ export function ChatList({ messages, streaming, streamingToolCalls }: Props) {
|
|
|
22
24
|
content: streaming || '',
|
|
23
25
|
toolCalls: streamingToolCalls,
|
|
24
26
|
}}
|
|
27
|
+
width={width}
|
|
25
28
|
/>
|
|
26
29
|
)}
|
|
27
30
|
</Box>
|
|
@@ -12,40 +12,43 @@ interface MessageData {
|
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
message: MessageData;
|
|
15
|
+
width?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function UserMessage({ content }: { content: string }) {
|
|
18
|
+
function UserMessage({ content, width }: { content: string; width?: number }) {
|
|
18
19
|
return (
|
|
19
|
-
<Box
|
|
20
|
-
<
|
|
21
|
-
|
|
20
|
+
<Box borderStyle="round" borderLeft borderColor="#06B6D4" paddingLeft={1} marginBottom={1}>
|
|
21
|
+
<Box flexDirection="column" width={width ? width - 4 : undefined}>
|
|
22
|
+
<Text bold color="#06B6D4">You</Text>
|
|
22
23
|
<Text>{content}</Text>
|
|
23
24
|
</Box>
|
|
24
25
|
</Box>
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
|
|
29
|
+
function AssistantMessage({ content, toolCalls, width }: { content: string; toolCalls?: ToolCallData[]; width?: number }) {
|
|
29
30
|
return (
|
|
30
31
|
<Box flexDirection="column" marginBottom={1}>
|
|
31
|
-
<Text bold color="cyan">AI</Text>
|
|
32
32
|
{toolCalls && toolCalls.map((tc, i) => (
|
|
33
|
-
<ToolCall key={i} tool={tc} />
|
|
33
|
+
<ToolCall key={i} tool={tc} width={width} />
|
|
34
34
|
))}
|
|
35
35
|
{content && (
|
|
36
|
-
<Box
|
|
37
|
-
<
|
|
36
|
+
<Box borderStyle="round" borderLeft borderColor="#22D3EE" paddingLeft={1}>
|
|
37
|
+
<Box flexDirection="column" width={width ? width - 4 : undefined}>
|
|
38
|
+
<Text bold color="#22D3EE">AI</Text>
|
|
39
|
+
<Markdown content={content} />
|
|
40
|
+
</Box>
|
|
38
41
|
</Box>
|
|
39
42
|
)}
|
|
40
43
|
</Box>
|
|
41
44
|
);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
export function ChatMessage({ message }: Props) {
|
|
47
|
+
export function ChatMessage({ message, width }: Props) {
|
|
45
48
|
if (message.role === 'user') {
|
|
46
|
-
return <UserMessage content={message.content} />;
|
|
49
|
+
return <UserMessage content={message.content} width={width} />;
|
|
47
50
|
}
|
|
48
|
-
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
|
|
51
|
+
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} width={width} />;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export type { MessageData };
|
|
@@ -9,14 +9,20 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
export function Header({ provider, model }: Props) {
|
|
11
11
|
return (
|
|
12
|
-
<Box flexDirection="column" marginBottom={
|
|
13
|
-
<Box>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
<Box flexDirection="column" marginBottom={0}>
|
|
13
|
+
<Box justifyContent="space-between" width="100%">
|
|
14
|
+
<Box>
|
|
15
|
+
<Text color="#06B6D4" bold> ⚡ </Text>
|
|
16
|
+
<Text color="#22D3EE" bold>THATGFSJ CODE</Text>
|
|
17
|
+
<Text dimColor> v0.7.0</Text>
|
|
18
|
+
</Box>
|
|
19
|
+
<Box>
|
|
20
|
+
<Text color="#06B6D4" bold> {provider} </Text>
|
|
21
|
+
<Text dimColor>/</Text>
|
|
22
|
+
<Text color="#22D3EE"> {model} </Text>
|
|
23
|
+
</Box>
|
|
18
24
|
</Box>
|
|
19
|
-
<Text
|
|
25
|
+
<Text color="#374151">{'─'.repeat(80)}</Text>
|
|
20
26
|
</Box>
|
|
21
27
|
);
|
|
22
28
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
messageCount: number;
|
|
7
|
+
skills: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function StatusBar({ messageCount, skills }: Props) {
|
|
11
|
+
const activeSkills = skills.slice(0, 3).join(', ');
|
|
12
|
+
const moreSkills = skills.length > 3 ? ` +${skills.length - 3}` : '';
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box flexDirection="column" marginTop={0}>
|
|
16
|
+
<Text color="#374151">{'─'.repeat(80)}</Text>
|
|
17
|
+
<Box justifyContent="space-between" width="100%">
|
|
18
|
+
<Box>
|
|
19
|
+
<Text backgroundColor="#06B6D4" color="#0F172A" bold> ⚡ THATGFSJ CODE </Text>
|
|
20
|
+
<Text dimColor> │ {messageCount} messages</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
<Box>
|
|
23
|
+
{skills.length > 0 && (
|
|
24
|
+
<Text dimColor> skills: {activeSkills}{moreSkills} </Text>
|
|
25
|
+
)}
|
|
26
|
+
</Box>
|
|
27
|
+
</Box>
|
|
28
|
+
</Box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -9,11 +9,11 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
export function Thinking({ message = 'Thinking' }: Props) {
|
|
11
11
|
return (
|
|
12
|
-
<Box>
|
|
13
|
-
<Text color="
|
|
12
|
+
<Box paddingLeft={1}>
|
|
13
|
+
<Text color="#06B6D4">
|
|
14
14
|
<Spinner type="dots" />
|
|
15
15
|
</Text>
|
|
16
|
-
<Text
|
|
16
|
+
<Text color="#06B6D4"> {message}</Text>
|
|
17
17
|
</Box>
|
|
18
18
|
);
|
|
19
19
|
}
|
|
@@ -11,58 +11,60 @@ interface ToolCallData {
|
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
tool: ToolCallData;
|
|
14
|
+
width?: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
+
function formatToolParams(name: string, args: string): string {
|
|
17
18
|
try {
|
|
18
19
|
const obj = JSON.parse(args);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
switch (name) {
|
|
21
|
+
case 'file': {
|
|
22
|
+
const action = obj.action || '';
|
|
23
|
+
const path = (obj.path || '').replace(/.*[/\\]/, '');
|
|
24
|
+
return `${action} ${path || obj.path || ''}`;
|
|
25
|
+
}
|
|
26
|
+
case 'shell': return obj.command || '';
|
|
27
|
+
case 'git': return obj.command || '';
|
|
28
|
+
case 'search': return obj.query || obj.pattern || '';
|
|
29
|
+
case 'nwt': return obj.action || '';
|
|
30
|
+
default: {
|
|
31
|
+
const first = Object.entries(obj)[0];
|
|
32
|
+
if (!first) return '';
|
|
33
|
+
const val = typeof first[1] === 'string' && first[1].length > 40
|
|
34
|
+
? first[1].slice(0, 40) + '...'
|
|
35
|
+
: JSON.stringify(first[1]);
|
|
36
|
+
return `${first[0]}=${val}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
29
39
|
} catch {
|
|
30
|
-
return args.length >
|
|
40
|
+
return args.length > 50 ? args.slice(0, 50) + '...' : args;
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
function formatResult(output: string, maxLines =
|
|
44
|
+
function formatResult(output: string, maxLines = 10): { text: string; truncated: boolean } {
|
|
35
45
|
const lines = output.split('\n');
|
|
36
|
-
if (lines.length <= maxLines) {
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
text: lines.slice(0, maxLines).join('\n'),
|
|
41
|
-
truncated: true,
|
|
42
|
-
};
|
|
46
|
+
if (lines.length <= maxLines) return { text: output, truncated: false };
|
|
47
|
+
return { text: lines.slice(0, maxLines).join('\n'), truncated: true };
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
export function ToolCall({ tool }: Props) {
|
|
46
|
-
const
|
|
50
|
+
export function ToolCall({ tool, width }: Props) {
|
|
51
|
+
const params = formatToolParams(tool.name, tool.args);
|
|
47
52
|
const result = tool.result ? formatResult(tool.result) : null;
|
|
48
53
|
|
|
49
54
|
return (
|
|
50
|
-
<Box flexDirection="column"
|
|
55
|
+
<Box borderStyle="round" borderLeft borderColor="#06B6D4" paddingLeft={1} flexDirection="column" marginBottom={0}>
|
|
51
56
|
<Box>
|
|
52
|
-
<Text color="
|
|
53
|
-
<Text
|
|
54
|
-
{argsStr && <Text dimColor> {argsStr}</Text>}
|
|
57
|
+
<Text color="#06B6D4" bold>⚙ {tool.name}: </Text>
|
|
58
|
+
<Text color="#94A3B8">{params}</Text>
|
|
55
59
|
</Box>
|
|
56
60
|
{result && (
|
|
57
|
-
<Box paddingLeft={
|
|
61
|
+
<Box paddingLeft={1} flexDirection="column">
|
|
58
62
|
{result.text.split('\n').map((line, i) => (
|
|
59
|
-
<Text key={i}
|
|
60
|
-
{
|
|
63
|
+
<Text key={i} color={tool.isError ? '#EF4444' : '#64748B'}>
|
|
64
|
+
{line}
|
|
61
65
|
</Text>
|
|
62
66
|
))}
|
|
63
|
-
{result.truncated &&
|
|
64
|
-
<Text dimColor>│ ... (truncated)</Text>
|
|
65
|
-
)}
|
|
67
|
+
{result.truncated && <Text dimColor>...</Text>}
|
|
66
68
|
</Box>
|
|
67
69
|
)}
|
|
68
70
|
</Box>
|