gemkit-cli 0.2.3 → 0.3.1
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/README.md +141 -7
- package/dist/commands/agent/index.d.ts +9 -0
- package/dist/commands/agent/index.js +1329 -0
- package/dist/commands/cache/index.d.ts +5 -0
- package/dist/commands/cache/index.js +43 -0
- package/dist/commands/catalog/index.d.ts +2 -0
- package/dist/commands/catalog/index.js +57 -0
- package/dist/commands/config/index.d.ts +7 -0
- package/dist/commands/config/index.js +122 -0
- package/dist/commands/convert/index.d.ts +8 -0
- package/dist/commands/convert/index.js +391 -0
- package/dist/commands/doctor/index.d.ts +2 -0
- package/dist/commands/doctor/index.js +243 -0
- package/dist/commands/extension/index.d.ts +5 -0
- package/dist/commands/extension/index.js +52 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +37 -0
- package/dist/commands/init/index.d.ts +6 -0
- package/dist/commands/init/index.js +345 -0
- package/dist/commands/new/index.d.ts +5 -0
- package/dist/commands/new/index.js +49 -0
- package/dist/commands/office/index.d.ts +5 -0
- package/dist/commands/office/index.js +283 -0
- package/dist/commands/paste/index.d.ts +10 -0
- package/dist/commands/paste/index.js +533 -0
- package/dist/commands/plan/index.d.ts +8 -0
- package/dist/commands/plan/index.js +247 -0
- package/dist/commands/session/index.d.ts +8 -0
- package/dist/commands/session/index.js +289 -0
- package/dist/commands/tokens/index.d.ts +6 -0
- package/dist/commands/tokens/index.js +148 -0
- package/dist/commands/update/index.d.ts +26 -0
- package/dist/commands/update/index.js +199 -0
- package/dist/commands/versions/index.d.ts +5 -0
- package/dist/commands/versions/index.js +39 -0
- package/dist/domains/agent/index.d.ts +8 -0
- package/dist/domains/agent/index.js +8 -0
- package/dist/domains/agent/mappings.d.ts +32 -0
- package/dist/domains/agent/mappings.js +164 -0
- package/dist/domains/agent/profile.d.ts +26 -0
- package/dist/domains/agent/profile.js +225 -0
- package/dist/domains/agent/pty-context.d.ts +11 -0
- package/dist/domains/agent/pty-context.js +83 -0
- package/dist/domains/agent/pty-providers.d.ts +18 -0
- package/dist/domains/agent/pty-providers.js +66 -0
- package/dist/domains/agent/pty-session.d.ts +33 -0
- package/dist/domains/agent/pty-session.js +82 -0
- package/dist/domains/agent/pty-types.d.ts +127 -0
- package/dist/domains/agent/pty-types.js +4 -0
- package/dist/domains/agent/search.d.ts +45 -0
- package/dist/domains/agent/search.js +614 -0
- package/dist/domains/agent/types.d.ts +78 -0
- package/dist/domains/agent/types.js +5 -0
- package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
- package/dist/domains/agent-office/documents-scanner.js +143 -0
- package/dist/domains/agent-office/event-emitter.d.ts +43 -0
- package/dist/domains/agent-office/event-emitter.js +86 -0
- package/dist/domains/agent-office/file-watcher.d.ts +40 -0
- package/dist/domains/agent-office/file-watcher.js +173 -0
- package/dist/domains/agent-office/icons.d.ts +11 -0
- package/dist/domains/agent-office/icons.js +36 -0
- package/dist/domains/agent-office/index.d.ts +12 -0
- package/dist/domains/agent-office/index.js +20 -0
- package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
- package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
- package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
- package/dist/domains/agent-office/renderer/web/server.js +228 -0
- package/dist/domains/agent-office/renderer/web.d.ts +30 -0
- package/dist/domains/agent-office/renderer/web.js +111 -0
- package/dist/domains/agent-office/session-bridge.d.ts +23 -0
- package/dist/domains/agent-office/session-bridge.js +171 -0
- package/dist/domains/agent-office/state-machine.d.ts +5 -0
- package/dist/domains/agent-office/state-machine.js +82 -0
- package/dist/domains/agent-office/types.d.ts +91 -0
- package/dist/domains/agent-office/types.js +4 -0
- package/dist/domains/cache/index.d.ts +1 -0
- package/dist/domains/cache/index.js +1 -0
- package/dist/domains/cache/manager.d.ts +22 -0
- package/dist/domains/cache/manager.js +84 -0
- package/dist/domains/config/index.d.ts +5 -0
- package/dist/domains/config/index.js +5 -0
- package/dist/domains/config/manager.d.ts +24 -0
- package/dist/domains/config/manager.js +85 -0
- package/dist/domains/config/schema.d.ts +17 -0
- package/dist/domains/config/schema.js +96 -0
- package/dist/domains/convert/converter.d.ts +78 -0
- package/dist/domains/convert/converter.js +471 -0
- package/dist/domains/convert/index.d.ts +5 -0
- package/dist/domains/convert/index.js +5 -0
- package/dist/domains/convert/types.d.ts +88 -0
- package/dist/domains/convert/types.js +18 -0
- package/dist/domains/github/download.d.ts +12 -0
- package/dist/domains/github/download.js +51 -0
- package/dist/domains/github/index.d.ts +2 -0
- package/dist/domains/github/index.js +2 -0
- package/dist/domains/github/releases.d.ts +16 -0
- package/dist/domains/github/releases.js +68 -0
- package/dist/domains/installation/conflict.d.ts +13 -0
- package/dist/domains/installation/conflict.js +38 -0
- package/dist/domains/installation/file-sync.d.ts +16 -0
- package/dist/domains/installation/file-sync.js +77 -0
- package/dist/domains/installation/index.d.ts +3 -0
- package/dist/domains/installation/index.js +3 -0
- package/dist/domains/installation/metadata.d.ts +20 -0
- package/dist/domains/installation/metadata.js +52 -0
- package/dist/domains/plan/index.d.ts +2 -0
- package/dist/domains/plan/index.js +2 -0
- package/dist/domains/plan/resolver.d.ts +24 -0
- package/dist/domains/plan/resolver.js +164 -0
- package/dist/domains/plan/types.d.ts +13 -0
- package/dist/domains/plan/types.js +4 -0
- package/dist/domains/session/env.d.ts +51 -0
- package/dist/domains/session/env.js +118 -0
- package/dist/domains/session/index.d.ts +8 -0
- package/dist/domains/session/index.js +8 -0
- package/dist/domains/session/manager.d.ts +56 -0
- package/dist/domains/session/manager.js +205 -0
- package/dist/domains/session/paths.d.ts +6 -0
- package/dist/domains/session/paths.js +6 -0
- package/dist/domains/session/types.d.ts +121 -0
- package/dist/domains/session/types.js +5 -0
- package/dist/domains/session/writer.d.ts +82 -0
- package/dist/domains/session/writer.js +431 -0
- package/dist/domains/tokens/index.d.ts +5 -0
- package/dist/domains/tokens/index.js +5 -0
- package/dist/domains/tokens/pricing.d.ts +38 -0
- package/dist/domains/tokens/pricing.js +129 -0
- package/dist/domains/tokens/scanner.d.ts +42 -0
- package/dist/domains/tokens/scanner.js +168 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +90 -59
- package/dist/services/aipty.d.ts +76 -0
- package/dist/services/aipty.js +276 -0
- package/dist/services/archive.d.ts +22 -0
- package/dist/services/archive.js +53 -0
- package/dist/services/auto-update.d.ts +26 -0
- package/dist/services/auto-update.js +117 -0
- package/dist/services/hash.d.ts +36 -0
- package/dist/services/hash.js +63 -0
- package/dist/services/logger.d.ts +28 -0
- package/dist/services/logger.js +102 -0
- package/dist/services/music.d.ts +67 -0
- package/dist/services/music.js +290 -0
- package/dist/services/npm.d.ts +22 -0
- package/dist/services/npm.js +65 -0
- package/dist/services/pty-client.d.ts +66 -0
- package/dist/services/pty-client.js +154 -0
- package/dist/services/pty-server.d.ts +102 -0
- package/dist/services/pty-server.js +613 -0
- package/dist/types/index.d.ts +155 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/colors.d.ts +43 -0
- package/dist/utils/colors.js +98 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.js +56 -0
- package/dist/utils/paths.d.ts +46 -0
- package/dist/utils/paths.js +89 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +31 -0
- package/package.json +55 -54
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OfficeEventEmitter } from '../../event-emitter.js';
|
|
2
|
+
export interface WebServerOptions {
|
|
3
|
+
port: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
autoOpen?: boolean;
|
|
6
|
+
emitter: OfficeEventEmitter;
|
|
7
|
+
}
|
|
8
|
+
export declare class OfficeWebServer {
|
|
9
|
+
private server;
|
|
10
|
+
private wss;
|
|
11
|
+
private options;
|
|
12
|
+
private clients;
|
|
13
|
+
constructor(options: WebServerOptions);
|
|
14
|
+
/**
|
|
15
|
+
* Start the server
|
|
16
|
+
*/
|
|
17
|
+
start(): Promise<number>;
|
|
18
|
+
/**
|
|
19
|
+
* Stop the server
|
|
20
|
+
*/
|
|
21
|
+
stop(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Handle HTTP requests
|
|
24
|
+
*/
|
|
25
|
+
private handleRequest;
|
|
26
|
+
/**
|
|
27
|
+
* Setup WebSocket handlers
|
|
28
|
+
*/
|
|
29
|
+
private setupWebSocket;
|
|
30
|
+
/**
|
|
31
|
+
* Broadcast state to all clients
|
|
32
|
+
*/
|
|
33
|
+
private broadcastState;
|
|
34
|
+
/**
|
|
35
|
+
* Broadcast event to all clients
|
|
36
|
+
*/
|
|
37
|
+
private broadcastEvent;
|
|
38
|
+
/**
|
|
39
|
+
* Serialize state for JSON (convert Map to object)
|
|
40
|
+
*/
|
|
41
|
+
private serializeState;
|
|
42
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
import { getIndexHtml } from './assets.js';
|
|
7
|
+
export class OfficeWebServer {
|
|
8
|
+
server = null;
|
|
9
|
+
wss = null;
|
|
10
|
+
options;
|
|
11
|
+
clients = new Set();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Start the server
|
|
17
|
+
*/
|
|
18
|
+
async start() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let port = this.options.port;
|
|
21
|
+
let attempts = 0;
|
|
22
|
+
const maxAttempts = 10;
|
|
23
|
+
const tryStart = () => {
|
|
24
|
+
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
25
|
+
this.server.on('error', (err) => {
|
|
26
|
+
if (err.code === 'EADDRINUSE' && attempts < maxAttempts) {
|
|
27
|
+
attempts++;
|
|
28
|
+
port++;
|
|
29
|
+
tryStart();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
reject(err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.server.listen(port, this.options.host || 'localhost', () => {
|
|
36
|
+
// Setup WebSocket server
|
|
37
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
38
|
+
this.setupWebSocket();
|
|
39
|
+
// Subscribe to state changes
|
|
40
|
+
this.options.emitter.onStateChange((state) => {
|
|
41
|
+
this.broadcastState(state);
|
|
42
|
+
});
|
|
43
|
+
this.options.emitter.onEvent((event) => {
|
|
44
|
+
this.broadcastEvent(event);
|
|
45
|
+
});
|
|
46
|
+
resolve(port);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
tryStart();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Stop the server
|
|
54
|
+
*/
|
|
55
|
+
stop() {
|
|
56
|
+
if (this.wss) {
|
|
57
|
+
for (const client of this.clients) {
|
|
58
|
+
client.close();
|
|
59
|
+
}
|
|
60
|
+
this.wss.close();
|
|
61
|
+
this.wss = null;
|
|
62
|
+
}
|
|
63
|
+
if (this.server) {
|
|
64
|
+
this.server.close();
|
|
65
|
+
this.server = null;
|
|
66
|
+
}
|
|
67
|
+
this.clients.clear();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Handle HTTP requests
|
|
71
|
+
*/
|
|
72
|
+
handleRequest(req, res) {
|
|
73
|
+
const url = req.url || '/';
|
|
74
|
+
// API endpoints
|
|
75
|
+
if (url === '/api/state') {
|
|
76
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
77
|
+
res.end(JSON.stringify(this.serializeState(this.options.emitter.getState())));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (url === '/api/history') {
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify(this.options.emitter.getHistory()));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Open document API
|
|
86
|
+
if (url.startsWith('/api/open-doc')) {
|
|
87
|
+
const urlObj = new URL(url, `http://${req.headers.host}`);
|
|
88
|
+
const docPath = urlObj.searchParams.get('path');
|
|
89
|
+
if (!docPath) {
|
|
90
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
91
|
+
res.end(JSON.stringify({ success: false, error: 'No path provided' }));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Path could be absolute or relative - try both
|
|
95
|
+
let fullPath = docPath;
|
|
96
|
+
if (!existsSync(fullPath)) {
|
|
97
|
+
fullPath = resolve(process.cwd(), docPath);
|
|
98
|
+
}
|
|
99
|
+
if (!existsSync(fullPath)) {
|
|
100
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ success: false, error: `File not found: ${docPath}` }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Detect IDE from multiple sources
|
|
105
|
+
// Priority: EDITOR env var > session appName (if valid) > code (fallback)
|
|
106
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
107
|
+
const state = this.options.emitter.getState();
|
|
108
|
+
const sessionAppName = state.appName?.toLowerCase() || '';
|
|
109
|
+
// Valid IDE commands whitelist
|
|
110
|
+
const validIdes = ['code', 'cursor', 'windsurf', 'zed', 'vim', 'nvim', 'nano', 'subl', 'atom', 'idea', 'webstorm', 'notepad++', 'antigravity'];
|
|
111
|
+
const isValidIde = validIdes.some(ide => sessionAppName.includes(ide));
|
|
112
|
+
let command;
|
|
113
|
+
if (editor) {
|
|
114
|
+
// User explicitly set EDITOR env var - use that
|
|
115
|
+
command = `"${editor}" "${fullPath}"`;
|
|
116
|
+
console.log(`[Agent Office] Using IDE from EDITOR env: ${editor}`);
|
|
117
|
+
}
|
|
118
|
+
else if (sessionAppName && isValidIde) {
|
|
119
|
+
// Use appName from session only if it's a valid IDE
|
|
120
|
+
command = `${sessionAppName} "${fullPath}"`;
|
|
121
|
+
console.log(`[Agent Office] Using IDE from session: ${sessionAppName}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Fallback to VS Code
|
|
125
|
+
command = `code "${fullPath}"`;
|
|
126
|
+
console.log('[Agent Office] No IDE detected, defaulting to VS Code');
|
|
127
|
+
}
|
|
128
|
+
exec(command, (error) => {
|
|
129
|
+
if (error) {
|
|
130
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
131
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
135
|
+
res.end(JSON.stringify({ success: true }));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Serve embedded index.html for root path
|
|
141
|
+
const urlPath = url.split('?')[0].split('#')[0];
|
|
142
|
+
if (urlPath === '/' || urlPath === '/index.html') {
|
|
143
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
144
|
+
res.end(getIndexHtml());
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// All other paths return 404 (CSS/JS are inlined in HTML)
|
|
148
|
+
res.writeHead(404);
|
|
149
|
+
res.end('Not Found');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Setup WebSocket handlers
|
|
153
|
+
*/
|
|
154
|
+
setupWebSocket() {
|
|
155
|
+
if (!this.wss)
|
|
156
|
+
return;
|
|
157
|
+
this.wss.on('connection', (ws) => {
|
|
158
|
+
this.clients.add(ws);
|
|
159
|
+
// Send current state immediately
|
|
160
|
+
const state = this.options.emitter.getState();
|
|
161
|
+
try {
|
|
162
|
+
ws.send(JSON.stringify({ type: 'state', data: this.serializeState(state) }));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
// Ignore send errors on initial connect
|
|
166
|
+
}
|
|
167
|
+
ws.on('message', (data) => {
|
|
168
|
+
try {
|
|
169
|
+
const msg = JSON.parse(data.toString());
|
|
170
|
+
if (msg.type === 'ping') {
|
|
171
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
172
|
+
}
|
|
173
|
+
if (msg.type === 'replay') {
|
|
174
|
+
const events = this.options.emitter.replay(msg.fromTimestamp);
|
|
175
|
+
ws.send(JSON.stringify({ type: 'replay', data: events }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
// Ignore invalid messages
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
ws.on('close', () => {
|
|
183
|
+
this.clients.delete(ws);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Broadcast state to all clients
|
|
189
|
+
*/
|
|
190
|
+
broadcastState(state) {
|
|
191
|
+
const message = JSON.stringify({ type: 'state', data: this.serializeState(state) });
|
|
192
|
+
for (const client of this.clients) {
|
|
193
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
194
|
+
try {
|
|
195
|
+
client.send(message);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
// Ignore send errors
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Broadcast event to all clients
|
|
205
|
+
*/
|
|
206
|
+
broadcastEvent(event) {
|
|
207
|
+
const message = JSON.stringify({ type: 'event', data: event });
|
|
208
|
+
for (const client of this.clients) {
|
|
209
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
210
|
+
try {
|
|
211
|
+
client.send(message);
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
// Ignore send errors
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Serialize state for JSON (convert Map to object)
|
|
221
|
+
*/
|
|
222
|
+
serializeState(state) {
|
|
223
|
+
return {
|
|
224
|
+
...state,
|
|
225
|
+
agents: Object.fromEntries(state.agents),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface WebDashboardOptions {
|
|
2
|
+
port?: number;
|
|
3
|
+
host?: string;
|
|
4
|
+
autoOpen?: boolean;
|
|
5
|
+
onReady?: (port: number) => void;
|
|
6
|
+
onError?: (error: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class WebDashboard {
|
|
9
|
+
private server;
|
|
10
|
+
private emitter;
|
|
11
|
+
private watcher;
|
|
12
|
+
private options;
|
|
13
|
+
constructor(options?: WebDashboardOptions);
|
|
14
|
+
/**
|
|
15
|
+
* Start the web dashboard
|
|
16
|
+
*/
|
|
17
|
+
start(): Promise<number>;
|
|
18
|
+
/**
|
|
19
|
+
* Stop the dashboard
|
|
20
|
+
*/
|
|
21
|
+
stop(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Open browser to URL
|
|
24
|
+
*/
|
|
25
|
+
private openBrowser;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start web dashboard (convenience function)
|
|
29
|
+
*/
|
|
30
|
+
export declare function startWebDashboard(options?: WebDashboardOptions): Promise<WebDashboard>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { OfficeEventEmitter } from '../event-emitter.js';
|
|
4
|
+
import { SessionFileWatcher } from '../file-watcher.js';
|
|
5
|
+
import { sessionToOfficeState } from '../session-bridge.js';
|
|
6
|
+
import { scanPlanDocuments } from '../documents-scanner.js';
|
|
7
|
+
import { OfficeWebServer } from './web/server.js';
|
|
8
|
+
import { createInitialState } from '../state-machine.js';
|
|
9
|
+
import { getPlansDir } from '../../../utils/paths.js';
|
|
10
|
+
export class WebDashboard {
|
|
11
|
+
server = null;
|
|
12
|
+
emitter;
|
|
13
|
+
watcher;
|
|
14
|
+
options;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = {
|
|
17
|
+
port: 3847,
|
|
18
|
+
host: 'localhost',
|
|
19
|
+
autoOpen: true,
|
|
20
|
+
...options,
|
|
21
|
+
};
|
|
22
|
+
// Initialize event emitter
|
|
23
|
+
this.emitter = new OfficeEventEmitter(createInitialState());
|
|
24
|
+
// Initialize file watcher
|
|
25
|
+
this.watcher = new SessionFileWatcher({
|
|
26
|
+
onSessionChange: (session) => {
|
|
27
|
+
const state = sessionToOfficeState(session);
|
|
28
|
+
// Scan documents if plan set
|
|
29
|
+
if (state.activePlan) {
|
|
30
|
+
try {
|
|
31
|
+
const plansDir = getPlansDir();
|
|
32
|
+
const planPath = join(plansDir, state.activePlan);
|
|
33
|
+
state.documents = scanPlanDocuments(planPath);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// Ignore doc scan errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.emitter.setState(state);
|
|
40
|
+
},
|
|
41
|
+
onEvent: (event) => {
|
|
42
|
+
this.emitter.emit(event);
|
|
43
|
+
},
|
|
44
|
+
onError: (error) => {
|
|
45
|
+
if (this.options.onError) {
|
|
46
|
+
this.options.onError(error);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Start the web dashboard
|
|
53
|
+
*/
|
|
54
|
+
async start() {
|
|
55
|
+
// Start file watcher
|
|
56
|
+
const watcherStarted = this.watcher.start();
|
|
57
|
+
if (!watcherStarted) {
|
|
58
|
+
throw new Error('No active session found');
|
|
59
|
+
}
|
|
60
|
+
// Create and start server
|
|
61
|
+
this.server = new OfficeWebServer({
|
|
62
|
+
port: this.options.port,
|
|
63
|
+
host: this.options.host,
|
|
64
|
+
emitter: this.emitter,
|
|
65
|
+
});
|
|
66
|
+
const actualPort = await this.server.start();
|
|
67
|
+
// Auto-open browser
|
|
68
|
+
if (this.options.autoOpen) {
|
|
69
|
+
this.openBrowser(`http://${this.options.host}:${actualPort}`);
|
|
70
|
+
}
|
|
71
|
+
if (this.options.onReady) {
|
|
72
|
+
this.options.onReady(actualPort);
|
|
73
|
+
}
|
|
74
|
+
return actualPort;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Stop the dashboard
|
|
78
|
+
*/
|
|
79
|
+
stop() {
|
|
80
|
+
if (this.server) {
|
|
81
|
+
this.server.stop();
|
|
82
|
+
this.server = null;
|
|
83
|
+
}
|
|
84
|
+
this.watcher.stop();
|
|
85
|
+
this.emitter.dispose();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Open browser to URL
|
|
89
|
+
*/
|
|
90
|
+
openBrowser(url) {
|
|
91
|
+
const command = process.platform === 'win32'
|
|
92
|
+
? `start ${url}`
|
|
93
|
+
: process.platform === 'darwin'
|
|
94
|
+
? `open ${url}`
|
|
95
|
+
: `xdg-open ${url}`;
|
|
96
|
+
exec(command, (error) => {
|
|
97
|
+
if (error) {
|
|
98
|
+
// Log to console if browser opening fails
|
|
99
|
+
console.log(`Open ${url} in your browser`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Start web dashboard (convenience function)
|
|
106
|
+
*/
|
|
107
|
+
export async function startWebDashboard(options = {}) {
|
|
108
|
+
const dashboard = new WebDashboard(options);
|
|
109
|
+
await dashboard.start();
|
|
110
|
+
return dashboard;
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GkSession, GkAgent } from '../session/types.js';
|
|
2
|
+
import { OfficeState, OfficeAgent, OrchestratorAgent, InboxItem, CharacterType } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Map agent role to character type
|
|
5
|
+
* Must match JS role detection: research, code/executor, plan, test, design/ui/ux
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCharacterType(role: string): CharacterType;
|
|
8
|
+
/**
|
|
9
|
+
* Check if agent is the orchestrator (Main Agent)
|
|
10
|
+
*/
|
|
11
|
+
export declare function isOrchestrator(agent: GkAgent, session: GkSession): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Convert single GkAgent to OfficeAgent
|
|
14
|
+
*/
|
|
15
|
+
export declare function agentToOfficeAgent(agent: GkAgent, session: GkSession): OfficeAgent | OrchestratorAgent;
|
|
16
|
+
/**
|
|
17
|
+
* Generate inbox item from completed agent
|
|
18
|
+
*/
|
|
19
|
+
export declare function agentToInboxItem(agent: GkAgent): InboxItem;
|
|
20
|
+
/**
|
|
21
|
+
* Convert full GkSession to OfficeState
|
|
22
|
+
*/
|
|
23
|
+
export declare function sessionToOfficeState(session: GkSession | null): OfficeState;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createInitialState } from './state-machine.js';
|
|
2
|
+
import { getIconForRole, formatDisplayName } from './icons.js';
|
|
3
|
+
/**
|
|
4
|
+
* Map agent role to character type
|
|
5
|
+
* Must match JS role detection: research, code/executor, plan, test, design/ui/ux
|
|
6
|
+
*/
|
|
7
|
+
export function getCharacterType(role) {
|
|
8
|
+
const r = role.toLowerCase();
|
|
9
|
+
// Only main agent is orchestrator
|
|
10
|
+
if (r.includes('main'))
|
|
11
|
+
return 'orchestrator';
|
|
12
|
+
// Match roles to JS detection logic
|
|
13
|
+
if (r.includes('research') || r.includes('scout'))
|
|
14
|
+
return 'researcher';
|
|
15
|
+
if (r.includes('code') || r.includes('executor') || r.includes('debug'))
|
|
16
|
+
return 'coder';
|
|
17
|
+
if (r.includes('plan'))
|
|
18
|
+
return 'planner';
|
|
19
|
+
if (r.includes('test'))
|
|
20
|
+
return 'tester';
|
|
21
|
+
if (r.includes('design') || r.includes('ui') || r.includes('ux') || r.includes('artist'))
|
|
22
|
+
return 'designer';
|
|
23
|
+
// Legacy mappings
|
|
24
|
+
if (r.includes('doc') || r.includes('writer'))
|
|
25
|
+
return 'writer';
|
|
26
|
+
if (r.includes('manager') || r.includes('git'))
|
|
27
|
+
return 'manager';
|
|
28
|
+
// Everything else goes to other
|
|
29
|
+
return 'other';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if agent is the orchestrator (Main Agent)
|
|
33
|
+
*/
|
|
34
|
+
export function isOrchestrator(agent, session) {
|
|
35
|
+
return (agent.agentType === 'Main Agent' ||
|
|
36
|
+
agent.parentGkSessionId === null ||
|
|
37
|
+
agent.gkSessionId === session.gkSessionId);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map GkAgent status to OfficeAgent state
|
|
41
|
+
*/
|
|
42
|
+
function mapStatusToState(status, hasActiveSkill) {
|
|
43
|
+
if (status === 'active') {
|
|
44
|
+
return hasActiveSkill ? 'working' : 'working';
|
|
45
|
+
}
|
|
46
|
+
if (status === 'completed')
|
|
47
|
+
return 'idle';
|
|
48
|
+
if (status === 'failed')
|
|
49
|
+
return 'idle';
|
|
50
|
+
return 'idle';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate progress from agent timing
|
|
54
|
+
*/
|
|
55
|
+
function calculateProgress(agent) {
|
|
56
|
+
if (agent.status === 'completed')
|
|
57
|
+
return 100;
|
|
58
|
+
if (agent.status === 'failed')
|
|
59
|
+
return 0;
|
|
60
|
+
if (!agent.startTime)
|
|
61
|
+
return 0;
|
|
62
|
+
const start = new Date(agent.startTime).getTime();
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const elapsed = now - start;
|
|
65
|
+
// Estimate based on typical task duration (2 min default)
|
|
66
|
+
const estimatedMs = 120000;
|
|
67
|
+
return Math.min(100, Math.round((elapsed / estimatedMs) * 100));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate speech bubble based on agent state
|
|
71
|
+
*/
|
|
72
|
+
function generateSpeechBubble(agent) {
|
|
73
|
+
if (agent.status === 'completed')
|
|
74
|
+
return 'Task complete!';
|
|
75
|
+
if (agent.status === 'failed')
|
|
76
|
+
return 'Task failed';
|
|
77
|
+
if (agent.injected?.skills?.length) {
|
|
78
|
+
return `Working on ${agent.injected.skills[0]}...`;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Convert single GkAgent to OfficeAgent
|
|
84
|
+
*/
|
|
85
|
+
export function agentToOfficeAgent(agent, session) {
|
|
86
|
+
const isOrch = isOrchestrator(agent, session);
|
|
87
|
+
const hasActiveSkill = !!(agent.injected?.skills?.length && agent.status === 'active');
|
|
88
|
+
const role = agent.agentRole || 'unknown';
|
|
89
|
+
const base = {
|
|
90
|
+
id: agent.gkSessionId,
|
|
91
|
+
agentType: isOrch ? 'orchestrator' : 'sub-agent',
|
|
92
|
+
role,
|
|
93
|
+
characterType: getCharacterType(role),
|
|
94
|
+
icon: getIconForRole(role, isOrch),
|
|
95
|
+
state: mapStatusToState(agent.status, hasActiveSkill),
|
|
96
|
+
activeSkill: agent.injected?.skills?.[0] || null,
|
|
97
|
+
progress: calculateProgress(agent),
|
|
98
|
+
speechBubble: generateSpeechBubble(agent),
|
|
99
|
+
hasFireEffect: hasActiveSkill,
|
|
100
|
+
gkSessionId: agent.gkSessionId,
|
|
101
|
+
parentSessionId: agent.parentGkSessionId,
|
|
102
|
+
};
|
|
103
|
+
if (isOrch) {
|
|
104
|
+
const subAgents = session.agents.filter(a => !isOrchestrator(a, session));
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
agentType: 'orchestrator',
|
|
108
|
+
delegatedTo: subAgents.filter(a => a.status === 'active').map(a => a.gkSessionId),
|
|
109
|
+
totalSubAgents: subAgents.length,
|
|
110
|
+
completedSubAgents: subAgents.filter(a => a.status === 'completed').length,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return base;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generate inbox item from completed agent
|
|
117
|
+
*/
|
|
118
|
+
export function agentToInboxItem(agent) {
|
|
119
|
+
const duration = agent.endTime && agent.startTime
|
|
120
|
+
? new Date(agent.endTime).getTime() - new Date(agent.startTime).getTime()
|
|
121
|
+
: 0;
|
|
122
|
+
return {
|
|
123
|
+
id: `inbox-${agent.gkSessionId}`,
|
|
124
|
+
agentId: agent.gkSessionId,
|
|
125
|
+
agentRole: agent.agentRole || 'unknown',
|
|
126
|
+
agentIcon: getIconForRole(agent.agentRole || ''),
|
|
127
|
+
timestamp: agent.endTime ? new Date(agent.endTime).getTime() : Date.now(),
|
|
128
|
+
status: 'unread',
|
|
129
|
+
title: `${formatDisplayName(agent.agentRole || 'Agent')} completed`,
|
|
130
|
+
preview: agent.prompt ? agent.prompt.slice(0, 100) : 'Task completed',
|
|
131
|
+
fullContent: null,
|
|
132
|
+
tokenUsage: agent.tokenUsage
|
|
133
|
+
? { input: agent.tokenUsage.input, output: agent.tokenUsage.output }
|
|
134
|
+
: null,
|
|
135
|
+
duration,
|
|
136
|
+
skillsUsed: agent.injected?.skills || [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Convert full GkSession to OfficeState
|
|
141
|
+
*/
|
|
142
|
+
export function sessionToOfficeState(session) {
|
|
143
|
+
const state = createInitialState();
|
|
144
|
+
if (!session)
|
|
145
|
+
return state;
|
|
146
|
+
state.sessionId = session.gkSessionId;
|
|
147
|
+
state.projectDir = session.projectDir;
|
|
148
|
+
state.activePlan = session.activePlan;
|
|
149
|
+
state.appName = session.appName || null; // IDE name from gk session init
|
|
150
|
+
state.isActive = session.agents.some(a => a.status === 'active');
|
|
151
|
+
// Process agents
|
|
152
|
+
for (const agent of session.agents) {
|
|
153
|
+
const officeAgent = agentToOfficeAgent(agent, session);
|
|
154
|
+
if (officeAgent.agentType === 'orchestrator') {
|
|
155
|
+
state.orchestrator = officeAgent;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
state.agents.set(officeAgent.id, officeAgent);
|
|
159
|
+
}
|
|
160
|
+
// Add completed agents to inbox
|
|
161
|
+
if (agent.status === 'completed') {
|
|
162
|
+
const existingItem = state.inbox.find(i => i.agentId === agent.gkSessionId);
|
|
163
|
+
if (!existingItem) {
|
|
164
|
+
state.inbox.push(agentToInboxItem(agent));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Sort inbox by timestamp descending
|
|
169
|
+
state.inbox.sort((a, b) => b.timestamp - a.timestamp);
|
|
170
|
+
return state;
|
|
171
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AgentState, OfficeState, OfficeAgent, OfficeEvent } from './types.js';
|
|
2
|
+
export declare function isValidTransition(from: AgentState, to: AgentState): boolean;
|
|
3
|
+
export declare function createInitialState(): OfficeState;
|
|
4
|
+
export declare function transitionAgent(agent: OfficeAgent, event: OfficeEvent): OfficeAgent;
|
|
5
|
+
export declare function processEvent(state: OfficeState, event: OfficeEvent): OfficeState;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Valid state transitions
|
|
2
|
+
const VALID_TRANSITIONS = {
|
|
3
|
+
idle: ['working', 'walking', 'receiving'],
|
|
4
|
+
working: ['idle', 'delivering', 'walking'],
|
|
5
|
+
walking: ['idle', 'working', 'delivering'],
|
|
6
|
+
delivering: ['idle', 'walking'],
|
|
7
|
+
receiving: ['working', 'idle'],
|
|
8
|
+
};
|
|
9
|
+
export function isValidTransition(from, to) {
|
|
10
|
+
return VALID_TRANSITIONS[from]?.includes(to) ?? false;
|
|
11
|
+
}
|
|
12
|
+
export function createInitialState() {
|
|
13
|
+
return {
|
|
14
|
+
orchestrator: null,
|
|
15
|
+
agents: new Map(),
|
|
16
|
+
sessionId: null,
|
|
17
|
+
projectDir: null,
|
|
18
|
+
activePlan: null,
|
|
19
|
+
appName: null,
|
|
20
|
+
currentNotification: null,
|
|
21
|
+
inbox: [],
|
|
22
|
+
documents: [],
|
|
23
|
+
isActive: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function transitionAgent(agent, event) {
|
|
27
|
+
// Map event type to new state
|
|
28
|
+
const stateMap = {
|
|
29
|
+
agent_idle: 'idle',
|
|
30
|
+
agent_working: 'working',
|
|
31
|
+
skill_activated: 'working',
|
|
32
|
+
handoff_start: 'walking',
|
|
33
|
+
received_work: 'receiving',
|
|
34
|
+
delivering: 'delivering',
|
|
35
|
+
task_complete: 'idle',
|
|
36
|
+
};
|
|
37
|
+
const newState = stateMap[event.type];
|
|
38
|
+
if (!newState)
|
|
39
|
+
return agent;
|
|
40
|
+
if (!isValidTransition(agent.state, newState)) {
|
|
41
|
+
return agent; // Invalid transition, no change
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...agent,
|
|
45
|
+
state: newState,
|
|
46
|
+
activeSkill: event.skill ?? agent.activeSkill,
|
|
47
|
+
hasFireEffect: event.type === 'skill_activated',
|
|
48
|
+
speechBubble: event.message || null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function processEvent(state, event) {
|
|
52
|
+
// Update agent state
|
|
53
|
+
const agent = state.agents.get(event.agentId);
|
|
54
|
+
if (agent) {
|
|
55
|
+
state.agents.set(event.agentId, transitionAgent(agent, event));
|
|
56
|
+
}
|
|
57
|
+
else if (state.orchestrator && state.orchestrator.id === event.agentId) {
|
|
58
|
+
state.orchestrator = transitionAgent(state.orchestrator, event); // Cast as any to handle type overlap
|
|
59
|
+
}
|
|
60
|
+
// Generate notification if needed
|
|
61
|
+
const notification = generateNotification(event);
|
|
62
|
+
if (notification) {
|
|
63
|
+
state.currentNotification = notification;
|
|
64
|
+
}
|
|
65
|
+
return { ...state };
|
|
66
|
+
}
|
|
67
|
+
function generateNotification(event) {
|
|
68
|
+
const notificationMap = {
|
|
69
|
+
skill_activated: { type: 'skill', template: `Skill activated: ${event.skill}` },
|
|
70
|
+
handoff_start: { type: 'handoff', template: 'Agent delivering information...' },
|
|
71
|
+
handoff_complete: { type: 'handoff', template: 'Handoff complete!' },
|
|
72
|
+
task_complete: { type: 'success', template: 'Task completed! Check your inbox' },
|
|
73
|
+
};
|
|
74
|
+
const config = notificationMap[event.type];
|
|
75
|
+
if (!config)
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
message: config.template,
|
|
79
|
+
type: config.type,
|
|
80
|
+
timestamp: event.timestamp,
|
|
81
|
+
};
|
|
82
|
+
}
|