gru-ai 0.1.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/.claude/skills/brainstorm/SKILL.md +340 -0
- package/.claude/skills/code-review-excellence/SKILL.md +198 -0
- package/.claude/skills/directive/SKILL.md +121 -0
- package/.claude/skills/directive/docs/pipeline/00-delegation-and-triage.md +181 -0
- package/.claude/skills/directive/docs/pipeline/01-checkpoint.md +34 -0
- package/.claude/skills/directive/docs/pipeline/02-read-directive.md +38 -0
- package/.claude/skills/directive/docs/pipeline/03-read-context.md +15 -0
- package/.claude/skills/directive/docs/pipeline/04-challenge.md +38 -0
- package/.claude/skills/directive/docs/pipeline/05-planning.md +64 -0
- package/.claude/skills/directive/docs/pipeline/06-technical-audit.md +88 -0
- package/.claude/skills/directive/docs/pipeline/07-plan-approval.md +145 -0
- package/.claude/skills/directive/docs/pipeline/07b-project-brainstorm.md +85 -0
- package/.claude/skills/directive/docs/pipeline/08-worktree-and-state.md +50 -0
- package/.claude/skills/directive/docs/pipeline/09-execute-projects.md +709 -0
- package/.claude/skills/directive/docs/pipeline/10-wrapup.md +242 -0
- package/.claude/skills/directive/docs/pipeline/11-completion-gate.md +75 -0
- package/.claude/skills/directive/docs/reference/rules/casting-rules.md +78 -0
- package/.claude/skills/directive/docs/reference/rules/failure-handling.md +20 -0
- package/.claude/skills/directive/docs/reference/rules/phase-definitions.md +42 -0
- package/.claude/skills/directive/docs/reference/rules/scope-and-dod.md +30 -0
- package/.claude/skills/directive/docs/reference/schemas/audit-output.md +44 -0
- package/.claude/skills/directive/docs/reference/schemas/brainstorm-output.md +52 -0
- package/.claude/skills/directive/docs/reference/schemas/challenger-output.md +13 -0
- package/.claude/skills/directive/docs/reference/schemas/checkpoint.md +18 -0
- package/.claude/skills/directive/docs/reference/schemas/current-json.md +5 -0
- package/.claude/skills/directive/docs/reference/schemas/directive-json.md +143 -0
- package/.claude/skills/directive/docs/reference/schemas/investigation-output.md +37 -0
- package/.claude/skills/directive/docs/reference/schemas/plan-schema.md +103 -0
- package/.claude/skills/directive/docs/reference/templates/architect-prompt.md +66 -0
- package/.claude/skills/directive/docs/reference/templates/auditor-prompt.md +53 -0
- package/.claude/skills/directive/docs/reference/templates/brainstorm-prompt.md +68 -0
- package/.claude/skills/directive/docs/reference/templates/challenger-prompt.md +35 -0
- package/.claude/skills/directive/docs/reference/templates/digest.md +134 -0
- package/.claude/skills/directive/docs/reference/templates/investigator-prompt.md +51 -0
- package/.claude/skills/directive/docs/reference/templates/planner-prompt.md +130 -0
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/.claude/skills/gruai-agents/SKILL.md +161 -0
- package/.claude/skills/gruai-config/SKILL.md +61 -0
- package/.claude/skills/healthcheck/SKILL.md +216 -0
- package/.claude/skills/report/SKILL.md +380 -0
- package/.claude/skills/scout/SKILL.md +452 -0
- package/.claude/skills/seo-audit/SKILL.md +107 -0
- package/.claude/skills/walkthrough/SKILL.md +274 -0
- package/.claude/skills/webapp-testing/SKILL.md +96 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/cli/templates/CLAUDE.md.template +57 -0
- package/cli/templates/agent-roles/backend.md +47 -0
- package/cli/templates/agent-roles/cmo.md +52 -0
- package/cli/templates/agent-roles/content.md +48 -0
- package/cli/templates/agent-roles/coo.md +66 -0
- package/cli/templates/agent-roles/cpo.md +52 -0
- package/cli/templates/agent-roles/cto.md +63 -0
- package/cli/templates/agent-roles/data.md +46 -0
- package/cli/templates/agent-roles/design.md +46 -0
- package/cli/templates/agent-roles/frontend.md +47 -0
- package/cli/templates/agent-roles/fullstack.md +47 -0
- package/cli/templates/agent-roles/qa.md +46 -0
- package/cli/templates/backlog.json.template +3 -0
- package/cli/templates/directive.json.template +9 -0
- package/cli/templates/directive.md.template +23 -0
- package/cli/templates/goals-index.md +21 -0
- package/cli/templates/gruai.config.json.template +12 -0
- package/cli/templates/lessons.md +16 -0
- package/cli/templates/vision.md +35 -0
- package/cli/templates/welcome-directive/directive.json +9 -0
- package/cli/templates/welcome-directive/directive.md +53 -0
- package/dist/assets/GamePage-C5XQQOQH.js +49 -0
- package/dist/assets/README.md +17 -0
- package/dist/assets/characters/char_0.png +0 -0
- package/dist/assets/characters/char_1.png +0 -0
- package/dist/assets/characters/char_10.png +0 -0
- package/dist/assets/characters/char_11.png +0 -0
- package/dist/assets/characters/char_2.png +0 -0
- package/dist/assets/characters/char_3.png +0 -0
- package/dist/assets/characters/char_4.png +0 -0
- package/dist/assets/characters/char_5.png +0 -0
- package/dist/assets/characters/char_6.png +0 -0
- package/dist/assets/characters/char_7.png +0 -0
- package/dist/assets/characters/char_8.png +0 -0
- package/dist/assets/characters/char_9.png +0 -0
- package/dist/assets/index-CnTPDqpP.js +12 -0
- package/dist/assets/index-gR5q7ikB.css +1 -0
- package/dist/assets/office/furniture.png +0 -0
- package/dist/assets/office/room-builder.png +0 -0
- package/dist/index.html +16 -0
- package/dist-server/scripts/intelligence-trends.d.ts +100 -0
- package/dist-server/scripts/intelligence-trends.js +365 -0
- package/dist-server/server/actions/cleanup.d.ts +4 -0
- package/dist-server/server/actions/cleanup.js +30 -0
- package/dist-server/server/actions/send-input.d.ts +6 -0
- package/dist-server/server/actions/send-input.js +147 -0
- package/dist-server/server/actions/terminal.d.ts +4 -0
- package/dist-server/server/actions/terminal.js +427 -0
- package/dist-server/server/config.d.ts +9 -0
- package/dist-server/server/config.js +217 -0
- package/dist-server/server/db.d.ts +7 -0
- package/dist-server/server/db.js +79 -0
- package/dist-server/server/hooks/event-receiver.d.ts +11 -0
- package/dist-server/server/hooks/event-receiver.js +36 -0
- package/dist-server/server/index.d.ts +1 -0
- package/dist-server/server/index.js +552 -0
- package/dist-server/server/notifications/macos.d.ts +5 -0
- package/dist-server/server/notifications/macos.js +22 -0
- package/dist-server/server/notifications/notifier.d.ts +17 -0
- package/dist-server/server/notifications/notifier.js +110 -0
- package/dist-server/server/parsers/process-discovery.d.ts +39 -0
- package/dist-server/server/parsers/process-discovery.js +776 -0
- package/dist-server/server/parsers/session-scanner.d.ts +56 -0
- package/dist-server/server/parsers/session-scanner.js +390 -0
- package/dist-server/server/parsers/session-state.d.ts +68 -0
- package/dist-server/server/parsers/session-state.js +696 -0
- package/dist-server/server/parsers/session-state.test.d.ts +1 -0
- package/dist-server/server/parsers/session-state.test.js +950 -0
- package/dist-server/server/parsers/task-parser.d.ts +10 -0
- package/dist-server/server/parsers/task-parser.js +97 -0
- package/dist-server/server/parsers/team-parser.d.ts +3 -0
- package/dist-server/server/parsers/team-parser.js +67 -0
- package/dist-server/server/platform/__tests__/claude-code.test.d.ts +1 -0
- package/dist-server/server/platform/__tests__/claude-code.test.js +311 -0
- package/dist-server/server/platform/claude-code.d.ts +34 -0
- package/dist-server/server/platform/claude-code.js +94 -0
- package/dist-server/server/platform/index.d.ts +5 -0
- package/dist-server/server/platform/index.js +1 -0
- package/dist-server/server/platform/types.d.ts +190 -0
- package/dist-server/server/platform/types.js +9 -0
- package/dist-server/server/state/aggregator.d.ts +42 -0
- package/dist-server/server/state/aggregator.js +1080 -0
- package/dist-server/server/state/work-item-types.d.ts +555 -0
- package/dist-server/server/state/work-item-types.js +168 -0
- package/dist-server/server/types.d.ts +237 -0
- package/dist-server/server/types.js +1 -0
- package/dist-server/server/watchers/claude-watcher.d.ts +17 -0
- package/dist-server/server/watchers/claude-watcher.js +130 -0
- package/dist-server/server/watchers/context-watcher.d.ts +22 -0
- package/dist-server/server/watchers/context-watcher.js +125 -0
- package/dist-server/server/watchers/directive-watcher.d.ts +46 -0
- package/dist-server/server/watchers/directive-watcher.js +497 -0
- package/dist-server/server/watchers/session-watcher.d.ts +18 -0
- package/dist-server/server/watchers/session-watcher.js +126 -0
- package/dist-server/server/watchers/state-watcher.d.ts +36 -0
- package/dist-server/server/watchers/state-watcher.js +369 -0
- package/package.json +68 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { getDb, closeDb } from './db.js';
|
|
7
|
+
import { Aggregator } from './state/aggregator.js';
|
|
8
|
+
import { ClaudeCodeAdapter } from './platform/claude-code.js';
|
|
9
|
+
import { DirectiveWatcher } from './watchers/directive-watcher.js';
|
|
10
|
+
import { StateWatcher } from './watchers/state-watcher.js';
|
|
11
|
+
import { processEvent } from './hooks/event-receiver.js';
|
|
12
|
+
import { focusPane } from './actions/terminal.js';
|
|
13
|
+
import { sendInput } from './actions/send-input.js';
|
|
14
|
+
import { Notifier } from './notifications/notifier.js';
|
|
15
|
+
// --- Load config and initialize ---
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const PORT = config.server.port;
|
|
18
|
+
// Initialize DB (creates table if needed)
|
|
19
|
+
getDb();
|
|
20
|
+
// Create platform adapter and aggregator
|
|
21
|
+
const adapter = new ClaudeCodeAdapter(config.claudeHome);
|
|
22
|
+
const aggregator = new Aggregator(config, adapter);
|
|
23
|
+
aggregator.initialize();
|
|
24
|
+
// Create and start notifier
|
|
25
|
+
const notifier = new Notifier(aggregator, config.notifications ?? { macOS: true, browser: true });
|
|
26
|
+
notifier.start();
|
|
27
|
+
// Broadcast notification_fired events to all WebSocket clients
|
|
28
|
+
notifier.on('notification_fired', (payload) => {
|
|
29
|
+
const message = {
|
|
30
|
+
version: 1,
|
|
31
|
+
type: 'notification_fired',
|
|
32
|
+
payload,
|
|
33
|
+
};
|
|
34
|
+
const data = JSON.stringify(message);
|
|
35
|
+
for (const client of wss.clients) {
|
|
36
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
37
|
+
client.send(data);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Start file watchers (created via adapter factory methods)
|
|
42
|
+
const claudeWatcher = adapter.createMetadataWatcher(aggregator);
|
|
43
|
+
claudeWatcher.start();
|
|
44
|
+
const sessionWatcher = adapter.createSessionWatcher(aggregator, aggregator.projectFilter);
|
|
45
|
+
sessionWatcher.start();
|
|
46
|
+
const directiveWatcher = new DirectiveWatcher(aggregator, config.claudeHome);
|
|
47
|
+
directiveWatcher.start();
|
|
48
|
+
const stateWatcher = new StateWatcher(aggregator, config);
|
|
49
|
+
stateWatcher.start();
|
|
50
|
+
// ContextWatcher removed — StateWatcher now reads .context/ directly
|
|
51
|
+
// Track last event timestamp for health endpoint
|
|
52
|
+
let lastEventTimestamp = null;
|
|
53
|
+
const serverStartTime = new Date().toISOString();
|
|
54
|
+
// --- HTTP Server ---
|
|
55
|
+
const server = http.createServer((req, res) => {
|
|
56
|
+
// CORS headers for dev mode
|
|
57
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
58
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
59
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
60
|
+
if (req.method === 'OPTIONS') {
|
|
61
|
+
res.writeHead(204);
|
|
62
|
+
res.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const url = new URL(req.url ?? '/', `http://localhost:${PORT}`);
|
|
66
|
+
// --- API Routes ---
|
|
67
|
+
if (url.pathname === '/api/state' && req.method === 'GET') {
|
|
68
|
+
handleGetState(res);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (url.pathname === '/api/events' && req.method === 'POST') {
|
|
72
|
+
handlePostEvent(req, res);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (url.pathname === '/api/events' && req.method === 'GET') {
|
|
76
|
+
handleGetEvents(res);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (url.pathname === '/api/health' && req.method === 'GET') {
|
|
80
|
+
handleHealth(res);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (url.pathname === '/api/directive' && req.method === 'GET') {
|
|
84
|
+
handleGetDirective(res);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// --- Work state API routes ---
|
|
88
|
+
if (url.pathname === '/api/state/features' && req.method === 'GET') {
|
|
89
|
+
handleStateFeatures(url, res);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === '/api/state/backlogs' && req.method === 'GET') {
|
|
93
|
+
handleStateBacklogs(url, res);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (url.pathname === '/api/state/conductor' && req.method === 'GET') {
|
|
97
|
+
handleStateConductor(res);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (url.pathname === '/api/state/artifact-content' && req.method === 'GET') {
|
|
101
|
+
handleArtifactContent(url, res);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (url.pathname === '/api/actions/focus-session' && req.method === 'POST') {
|
|
105
|
+
handleFocusSession(req, res);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (url.pathname === '/api/actions/send-input' && req.method === 'POST') {
|
|
109
|
+
handleSendInput(req, res);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (url.pathname === '/api/actions/directive-complete' && req.method === 'POST') {
|
|
113
|
+
handleDirectiveComplete(req, res);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// --- Static file serving for production ---
|
|
117
|
+
// In prod (dist-server/server/index.js): up 2 to repo root. In dev: up 1.
|
|
118
|
+
const distDirProd = path.join(import.meta.dirname, '..', '..', 'dist');
|
|
119
|
+
const distDirDev = path.join(import.meta.dirname, '..', 'dist');
|
|
120
|
+
const distDir = fs.existsSync(distDirProd) ? distDirProd : distDirDev;
|
|
121
|
+
if (fs.existsSync(distDir)) {
|
|
122
|
+
serveStatic(url.pathname, distDir, res);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// 404
|
|
126
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
128
|
+
});
|
|
129
|
+
// --- WebSocket Server ---
|
|
130
|
+
const wss = new WebSocketServer({ server });
|
|
131
|
+
wss.on('connection', (ws) => {
|
|
132
|
+
console.log(`[ws] Client connected (total: ${wss.clients.size})`);
|
|
133
|
+
// Send full state snapshot on connect
|
|
134
|
+
const message = {
|
|
135
|
+
version: 1,
|
|
136
|
+
type: 'full_state',
|
|
137
|
+
payload: aggregator.getState(),
|
|
138
|
+
};
|
|
139
|
+
ws.send(JSON.stringify(message));
|
|
140
|
+
ws.on('close', () => {
|
|
141
|
+
console.log(`[ws] Client disconnected (total: ${wss.clients.size})`);
|
|
142
|
+
});
|
|
143
|
+
ws.on('error', (err) => {
|
|
144
|
+
console.error(`[ws] Client error:`, err);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// --- Broadcast state changes to all clients ---
|
|
148
|
+
aggregator.on('change', (type) => {
|
|
149
|
+
const state = aggregator.getState();
|
|
150
|
+
let payload;
|
|
151
|
+
switch (type) {
|
|
152
|
+
case 'sessions_updated':
|
|
153
|
+
payload = { sessions: state.sessions };
|
|
154
|
+
break;
|
|
155
|
+
case 'projects_updated':
|
|
156
|
+
payload = { projects: state.projects };
|
|
157
|
+
break;
|
|
158
|
+
case 'teams_updated':
|
|
159
|
+
payload = { teams: state.teams };
|
|
160
|
+
break;
|
|
161
|
+
case 'tasks_updated':
|
|
162
|
+
payload = { tasksByTeam: state.tasksByTeam, tasksBySession: state.tasksBySession };
|
|
163
|
+
break;
|
|
164
|
+
case 'event_added':
|
|
165
|
+
payload = { events: state.events.slice(0, 1) }; // Just the newest event
|
|
166
|
+
break;
|
|
167
|
+
case 'events_updated':
|
|
168
|
+
payload = { events: state.events };
|
|
169
|
+
break;
|
|
170
|
+
case 'session_activities_updated':
|
|
171
|
+
payload = { sessionActivities: state.sessionActivities };
|
|
172
|
+
break;
|
|
173
|
+
case 'directive_updated':
|
|
174
|
+
payload = { directiveState: state.directiveState, directiveHistory: state.directiveHistory, activeDirectives: state.activeDirectives };
|
|
175
|
+
break;
|
|
176
|
+
case 'state_updated':
|
|
177
|
+
payload = { workState: aggregator.getWorkState() };
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
payload = state;
|
|
181
|
+
}
|
|
182
|
+
const message = {
|
|
183
|
+
version: 1,
|
|
184
|
+
type,
|
|
185
|
+
payload,
|
|
186
|
+
};
|
|
187
|
+
const data = JSON.stringify(message);
|
|
188
|
+
for (const client of wss.clients) {
|
|
189
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
190
|
+
client.send(data);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// --- Route Handlers ---
|
|
195
|
+
function handleGetState(res) {
|
|
196
|
+
const state = aggregator.getState();
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
198
|
+
res.end(JSON.stringify(state));
|
|
199
|
+
}
|
|
200
|
+
function handlePostEvent(req, res) {
|
|
201
|
+
let body = '';
|
|
202
|
+
req.on('data', (chunk) => {
|
|
203
|
+
body += chunk;
|
|
204
|
+
});
|
|
205
|
+
req.on('end', () => {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(body);
|
|
208
|
+
const event = processEvent(parsed);
|
|
209
|
+
aggregator.addEvent(event);
|
|
210
|
+
lastEventTimestamp = event.timestamp;
|
|
211
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
212
|
+
res.end(JSON.stringify({ ok: true, eventId: event.id }));
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error(`[api] Error processing event:`, err);
|
|
216
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
217
|
+
res.end(JSON.stringify({ error: 'Invalid event data' }));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
req.on('error', (err) => {
|
|
221
|
+
console.error(`[api] Request error:`, err);
|
|
222
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
223
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function handleGetEvents(res) {
|
|
227
|
+
const state = aggregator.getState();
|
|
228
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
229
|
+
res.end(JSON.stringify(state.events));
|
|
230
|
+
}
|
|
231
|
+
function handleHealth(res) {
|
|
232
|
+
const health = {
|
|
233
|
+
status: 'ok',
|
|
234
|
+
uptime: process.uptime(),
|
|
235
|
+
startedAt: serverStartTime,
|
|
236
|
+
watchers: {
|
|
237
|
+
claude: claudeWatcher.ready,
|
|
238
|
+
session: sessionWatcher.ready,
|
|
239
|
+
directive: directiveWatcher.ready,
|
|
240
|
+
state: stateWatcher.ready,
|
|
241
|
+
},
|
|
242
|
+
connectedClients: wss.clients.size,
|
|
243
|
+
lastEventTimestamp,
|
|
244
|
+
projects: config.projects.map((p) => ({
|
|
245
|
+
name: p.name,
|
|
246
|
+
path: p.path,
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
250
|
+
res.end(JSON.stringify(health));
|
|
251
|
+
}
|
|
252
|
+
function handleGetDirective(res) {
|
|
253
|
+
const state = directiveWatcher.readCurrentState();
|
|
254
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
255
|
+
res.end(JSON.stringify(state));
|
|
256
|
+
}
|
|
257
|
+
// --- Work State Handlers ---
|
|
258
|
+
function handleStateFeatures(url, res) {
|
|
259
|
+
const ws = aggregator.getWorkState();
|
|
260
|
+
const category = url.searchParams.get('category');
|
|
261
|
+
let features = ws.features?.features ?? [];
|
|
262
|
+
if (category) {
|
|
263
|
+
features = features.filter(f => f.category === category);
|
|
264
|
+
}
|
|
265
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify({ generated: ws.features?.generated ?? null, features }));
|
|
267
|
+
}
|
|
268
|
+
function handleStateBacklogs(url, res) {
|
|
269
|
+
const ws = aggregator.getWorkState();
|
|
270
|
+
const category = url.searchParams.get('category');
|
|
271
|
+
let items = ws.backlogs?.items ?? [];
|
|
272
|
+
if (category) {
|
|
273
|
+
items = items.filter(b => b.category === category);
|
|
274
|
+
}
|
|
275
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
276
|
+
res.end(JSON.stringify({ generated: ws.backlogs?.generated ?? null, items }));
|
|
277
|
+
}
|
|
278
|
+
function handleStateConductor(res) {
|
|
279
|
+
const ws = aggregator.getWorkState();
|
|
280
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
281
|
+
res.end(JSON.stringify(ws.conductor));
|
|
282
|
+
}
|
|
283
|
+
function handleArtifactContent(url, res) {
|
|
284
|
+
const filePath = url.searchParams.get('path') ?? '';
|
|
285
|
+
if (!filePath) {
|
|
286
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
287
|
+
res.end(JSON.stringify({ error: 'Missing path parameter' }));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Resolve relative path against project — try both direct and .context/ prefix
|
|
291
|
+
for (const project of config.projects) {
|
|
292
|
+
const candidates = [
|
|
293
|
+
path.join(project.path, filePath),
|
|
294
|
+
path.join(project.path, '.context', filePath),
|
|
295
|
+
];
|
|
296
|
+
for (const fullPath of candidates) {
|
|
297
|
+
const resolved = path.resolve(fullPath);
|
|
298
|
+
// Security: ensure the resolved path is within the project
|
|
299
|
+
if (!resolved.startsWith(path.resolve(project.path)))
|
|
300
|
+
continue;
|
|
301
|
+
try {
|
|
302
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
303
|
+
res.writeHead(200, { 'Content-Type': 'text/markdown' });
|
|
304
|
+
res.end(content);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
313
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
314
|
+
}
|
|
315
|
+
function handleFocusSession(req, res) {
|
|
316
|
+
let body = '';
|
|
317
|
+
req.on('data', (chunk) => {
|
|
318
|
+
body += chunk;
|
|
319
|
+
});
|
|
320
|
+
req.on('end', () => {
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(body);
|
|
323
|
+
if (!parsed.paneId || typeof parsed.paneId !== 'string') {
|
|
324
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
325
|
+
res.end(JSON.stringify({ error: 'Missing paneId' }));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
focusPane(parsed.paneId).then((result) => {
|
|
329
|
+
const status = result.ok ? 200 : 400;
|
|
330
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
331
|
+
res.end(JSON.stringify(result));
|
|
332
|
+
}).catch((err) => {
|
|
333
|
+
console.error(`[api] Focus pane error:`, err);
|
|
334
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
335
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
req.on('error', (err) => {
|
|
344
|
+
console.error(`[api] Request error:`, err);
|
|
345
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
346
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function handleSendInput(req, res) {
|
|
350
|
+
let body = '';
|
|
351
|
+
req.on('data', (chunk) => {
|
|
352
|
+
body += chunk;
|
|
353
|
+
});
|
|
354
|
+
req.on('end', () => {
|
|
355
|
+
try {
|
|
356
|
+
const parsed = JSON.parse(body);
|
|
357
|
+
if (!parsed.paneId || typeof parsed.paneId !== 'string') {
|
|
358
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
359
|
+
res.end(JSON.stringify({ error: 'Missing paneId' }));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!parsed.type || !['approve', 'reject', 'abort', 'text'].includes(parsed.type)) {
|
|
363
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ error: 'Missing or invalid type' }));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (parsed.input === undefined || parsed.input === null || typeof parsed.input !== 'string') {
|
|
368
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
369
|
+
res.end(JSON.stringify({ error: 'Missing input' }));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const request = {
|
|
373
|
+
paneId: parsed.paneId,
|
|
374
|
+
input: parsed.input,
|
|
375
|
+
type: parsed.type,
|
|
376
|
+
};
|
|
377
|
+
sendInput(request, aggregator).then((result) => {
|
|
378
|
+
const status = result.ok ? 200 : 400;
|
|
379
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
380
|
+
res.end(JSON.stringify(result));
|
|
381
|
+
}).catch((err) => {
|
|
382
|
+
console.error(`[api] Send input error:`, err);
|
|
383
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
389
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
req.on('error', (err) => {
|
|
393
|
+
console.error(`[api] Request error:`, err);
|
|
394
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
395
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function handleDirectiveComplete(req, res) {
|
|
399
|
+
let body = '';
|
|
400
|
+
req.on('data', (chunk) => {
|
|
401
|
+
body += chunk;
|
|
402
|
+
});
|
|
403
|
+
req.on('end', () => {
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(body);
|
|
406
|
+
if (!parsed.action || !['approve', 'reject'].includes(parsed.action)) {
|
|
407
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
408
|
+
res.end(JSON.stringify({ error: 'Missing or invalid action (approve|reject)' }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
// If directiveName is provided, use it directly; otherwise fall back to readCurrentState()
|
|
412
|
+
let targetDirectiveName;
|
|
413
|
+
if (parsed.directiveName) {
|
|
414
|
+
// Sanitize: reject path traversal attempts
|
|
415
|
+
if (parsed.directiveName.includes('/') || parsed.directiveName.includes('\\') || parsed.directiveName.includes('..')) {
|
|
416
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
417
|
+
res.end(JSON.stringify({ error: 'Invalid directive name' }));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
targetDirectiveName = parsed.directiveName;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const state = directiveWatcher.readCurrentState();
|
|
424
|
+
if (!state) {
|
|
425
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
426
|
+
res.end(JSON.stringify({ error: 'No active directive' }));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
targetDirectiveName = state.directiveName;
|
|
430
|
+
}
|
|
431
|
+
// Update the directive.json status
|
|
432
|
+
const directiveJsonPath = path.join(process.cwd(), '.context', 'directives', targetDirectiveName, 'directive.json');
|
|
433
|
+
try {
|
|
434
|
+
const raw = fs.readFileSync(directiveJsonPath, 'utf-8');
|
|
435
|
+
const directive = JSON.parse(raw);
|
|
436
|
+
if (parsed.action === 'approve') {
|
|
437
|
+
directive.status = 'completed';
|
|
438
|
+
directive.completed = new Date().toISOString().split('T')[0];
|
|
439
|
+
if (directive.pipeline?.completion) {
|
|
440
|
+
directive.pipeline.completion.status = 'completed';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
// Reject: keep in_progress, add feedback
|
|
445
|
+
directive.status = 'in_progress';
|
|
446
|
+
if (directive.pipeline?.completion) {
|
|
447
|
+
directive.pipeline.completion.status = 'pending';
|
|
448
|
+
}
|
|
449
|
+
if (parsed.feedback) {
|
|
450
|
+
directive.pipeline = directive.pipeline ?? {};
|
|
451
|
+
directive.pipeline.completion = directive.pipeline.completion ?? {};
|
|
452
|
+
directive.pipeline.completion.feedback = parsed.feedback;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
directive.updated_at = new Date().toISOString();
|
|
456
|
+
fs.writeFileSync(directiveJsonPath, JSON.stringify(directive, null, 2) + '\n');
|
|
457
|
+
// Watcher picks up directive.json change directly — no current.json needed
|
|
458
|
+
console.log(`[api] Directive ${targetDirectiveName} ${parsed.action === 'approve' ? 'approved' : 'rejected'}${parsed.feedback ? ` (feedback: ${parsed.feedback})` : ''}`);
|
|
459
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
460
|
+
res.end(JSON.stringify({ ok: true, action: parsed.action, directive: targetDirectiveName }));
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
console.error(`[api] Failed to update directive:`, err);
|
|
464
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
465
|
+
res.end(JSON.stringify({ error: 'Failed to update directive file' }));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
470
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
req.on('error', (err) => {
|
|
474
|
+
console.error(`[api] Request error:`, err);
|
|
475
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
476
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// --- Static file serving ---
|
|
480
|
+
const MIME_TYPES = {
|
|
481
|
+
'.html': 'text/html',
|
|
482
|
+
'.js': 'application/javascript',
|
|
483
|
+
'.css': 'text/css',
|
|
484
|
+
'.json': 'application/json',
|
|
485
|
+
'.png': 'image/png',
|
|
486
|
+
'.jpg': 'image/jpeg',
|
|
487
|
+
'.svg': 'image/svg+xml',
|
|
488
|
+
'.ico': 'image/x-icon',
|
|
489
|
+
'.woff': 'font/woff',
|
|
490
|
+
'.woff2': 'font/woff2',
|
|
491
|
+
};
|
|
492
|
+
function serveStatic(pathname, distDir, res) {
|
|
493
|
+
let filePath = path.join(distDir, pathname);
|
|
494
|
+
// Default to index.html for SPA routing
|
|
495
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
496
|
+
filePath = path.join(distDir, 'index.html');
|
|
497
|
+
}
|
|
498
|
+
if (!fs.existsSync(filePath)) {
|
|
499
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
500
|
+
res.end('Not found');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const ext = path.extname(filePath);
|
|
504
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
505
|
+
const content = fs.readFileSync(filePath);
|
|
506
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
507
|
+
res.end(content);
|
|
508
|
+
}
|
|
509
|
+
// --- Start Server ---
|
|
510
|
+
server.listen(PORT, () => {
|
|
511
|
+
console.log(`\n Conductor server running at http://localhost:${PORT}`);
|
|
512
|
+
console.log(` WebSocket available at ws://localhost:${PORT}`);
|
|
513
|
+
console.log(` Health check: http://localhost:${PORT}/api/health`);
|
|
514
|
+
console.log(` Dashboard state: http://localhost:${PORT}/api/state`);
|
|
515
|
+
if (config.projects.length > 0) {
|
|
516
|
+
console.log(` Watching ${config.projects.length} project(s):`);
|
|
517
|
+
for (const p of config.projects) {
|
|
518
|
+
console.log(` - ${p.name}: ${p.path}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
console.log('');
|
|
522
|
+
});
|
|
523
|
+
// --- Graceful shutdown ---
|
|
524
|
+
function shutdown() {
|
|
525
|
+
console.log('\n[shutdown] Shutting down...');
|
|
526
|
+
// Stop notifier
|
|
527
|
+
notifier.stop();
|
|
528
|
+
// Close watchers
|
|
529
|
+
claudeWatcher.stop().catch(console.error);
|
|
530
|
+
sessionWatcher.stop().catch(console.error);
|
|
531
|
+
directiveWatcher.stop().catch(console.error);
|
|
532
|
+
stateWatcher.stop().catch(console.error);
|
|
533
|
+
// Destroy aggregator (cleans up timers)
|
|
534
|
+
aggregator.destroy();
|
|
535
|
+
// Close WebSocket connections
|
|
536
|
+
for (const client of wss.clients) {
|
|
537
|
+
client.close();
|
|
538
|
+
}
|
|
539
|
+
// Close HTTP server
|
|
540
|
+
server.close(() => {
|
|
541
|
+
closeDb();
|
|
542
|
+
console.log('[shutdown] Server closed');
|
|
543
|
+
process.exit(0);
|
|
544
|
+
});
|
|
545
|
+
// Force exit after 5s
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
console.error('[shutdown] Forced exit after timeout');
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}, 5000);
|
|
550
|
+
}
|
|
551
|
+
process.on('SIGINT', shutdown);
|
|
552
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Escape a string for use inside an AppleScript double-quoted string.
|
|
4
|
+
* Backslashes must be escaped first, then double quotes.
|
|
5
|
+
*/
|
|
6
|
+
function escapeAppleScript(str) {
|
|
7
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Send a macOS notification via osascript.
|
|
11
|
+
* Fire-and-forget: does not await, logs errors silently.
|
|
12
|
+
*/
|
|
13
|
+
export function sendMacNotification(title, body) {
|
|
14
|
+
const escapedTitle = escapeAppleScript(title);
|
|
15
|
+
const escapedBody = escapeAppleScript(body);
|
|
16
|
+
const script = `display notification "${escapedBody}" with title "${escapedTitle}"`;
|
|
17
|
+
execFile('osascript', ['-e', script], (err) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
console.error('[notifications] osascript error:', err.message);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Aggregator } from '../state/aggregator.js';
|
|
3
|
+
import type { NotificationConfig } from '../types.js';
|
|
4
|
+
export declare class Notifier extends EventEmitter {
|
|
5
|
+
private dedup;
|
|
6
|
+
private config;
|
|
7
|
+
private aggregator;
|
|
8
|
+
private prevSessions;
|
|
9
|
+
private cleanupInterval;
|
|
10
|
+
constructor(aggregator: Aggregator, config: NotificationConfig);
|
|
11
|
+
start(): void;
|
|
12
|
+
private handleChange;
|
|
13
|
+
private getStaleTeamSessionIds;
|
|
14
|
+
private notify;
|
|
15
|
+
updateConfig(config: NotificationConfig): void;
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|