hanzi-browse 2.2.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/README.md +182 -0
- package/dist/agent/loop.d.ts +63 -0
- package/dist/agent/loop.js +186 -0
- package/dist/agent/system-prompt.d.ts +7 -0
- package/dist/agent/system-prompt.js +41 -0
- package/dist/agent/tools.d.ts +9 -0
- package/dist/agent/tools.js +154 -0
- package/dist/cli/detect-credentials.d.ts +31 -0
- package/dist/cli/detect-credentials.js +44 -0
- package/dist/cli/import-credentials-handler.d.ts +14 -0
- package/dist/cli/import-credentials-handler.js +22 -0
- package/dist/cli/session-files.d.ts +28 -0
- package/dist/cli/session-files.js +118 -0
- package/dist/cli/setup.d.ts +10 -0
- package/dist/cli/setup.js +915 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +506 -0
- package/dist/dashboard/assets/index-CEFyesbT.js +46 -0
- package/dist/dashboard/assets/index-Dnht2kLU.css +1 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1116 -0
- package/dist/ipc/index.d.ts +8 -0
- package/dist/ipc/index.js +8 -0
- package/dist/ipc/native-host.d.ts +96 -0
- package/dist/ipc/native-host.js +223 -0
- package/dist/ipc/websocket-client.d.ts +73 -0
- package/dist/ipc/websocket-client.js +199 -0
- package/dist/license/manager.d.ts +20 -0
- package/dist/license/manager.js +15 -0
- package/dist/llm/client.d.ts +72 -0
- package/dist/llm/client.js +227 -0
- package/dist/llm/credentials.d.ts +61 -0
- package/dist/llm/credentials.js +200 -0
- package/dist/llm/vertex.d.ts +22 -0
- package/dist/llm/vertex.js +335 -0
- package/dist/managed/api-http.test.d.ts +7 -0
- package/dist/managed/api-http.test.js +623 -0
- package/dist/managed/api.d.ts +51 -0
- package/dist/managed/api.js +1448 -0
- package/dist/managed/api.test.d.ts +10 -0
- package/dist/managed/api.test.js +146 -0
- package/dist/managed/auth.d.ts +38 -0
- package/dist/managed/auth.js +192 -0
- package/dist/managed/billing.d.ts +70 -0
- package/dist/managed/billing.js +227 -0
- package/dist/managed/deploy.d.ts +17 -0
- package/dist/managed/deploy.js +385 -0
- package/dist/managed/e2e.test.d.ts +15 -0
- package/dist/managed/e2e.test.js +151 -0
- package/dist/managed/hardening.test.d.ts +14 -0
- package/dist/managed/hardening.test.js +346 -0
- package/dist/managed/integration.test.d.ts +8 -0
- package/dist/managed/integration.test.js +274 -0
- package/dist/managed/log.d.ts +18 -0
- package/dist/managed/log.js +31 -0
- package/dist/managed/server.d.ts +12 -0
- package/dist/managed/server.js +69 -0
- package/dist/managed/store-pg.d.ts +191 -0
- package/dist/managed/store-pg.js +479 -0
- package/dist/managed/store.d.ts +188 -0
- package/dist/managed/store.js +379 -0
- package/dist/relay/auto-start.d.ts +19 -0
- package/dist/relay/auto-start.js +71 -0
- package/dist/relay/server.d.ts +17 -0
- package/dist/relay/server.js +403 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +4 -0
- package/dist/types/session.d.ts +134 -0
- package/dist/types/session.js +16 -0
- package/package.json +61 -0
- package/skills/README.md +48 -0
- package/skills/a11y-auditor/SKILL.md +42 -0
- package/skills/e2e-tester/SKILL.md +154 -0
- package/skills/hanzi-browse/SKILL.md +182 -0
- package/skills/linkedin-prospector/SKILL.md +149 -0
- package/skills/social-poster/SKILL.md +146 -0
- package/skills/x-marketer/SKILL.md +479 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed Platform Store
|
|
3
|
+
*
|
|
4
|
+
* File-based persistence for MVP. Swap for Postgres/SQLite later.
|
|
5
|
+
* Stores: API keys, task runs, usage events, browser sessions.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { randomUUID, randomBytes, createHash } from "crypto";
|
|
11
|
+
/** SHA-256 hash for storing tokens/keys at rest */
|
|
12
|
+
function hashSecret(secret) {
|
|
13
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
const DATA_DIR = join(homedir(), ".hanzi-browse", "managed");
|
|
16
|
+
let data = {
|
|
17
|
+
workspaces: {},
|
|
18
|
+
apiKeys: {},
|
|
19
|
+
taskRuns: {},
|
|
20
|
+
browserSessions: {},
|
|
21
|
+
pairingTokens: {},
|
|
22
|
+
usageEvents: [],
|
|
23
|
+
};
|
|
24
|
+
function dataPath() {
|
|
25
|
+
return join(DATA_DIR, "store.json");
|
|
26
|
+
}
|
|
27
|
+
function load() {
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(dataPath())) {
|
|
30
|
+
const loaded = JSON.parse(readFileSync(dataPath(), "utf8"));
|
|
31
|
+
// Merge with defaults to handle new fields added after initial creation
|
|
32
|
+
data = {
|
|
33
|
+
workspaces: loaded.workspaces || {},
|
|
34
|
+
apiKeys: loaded.apiKeys || {},
|
|
35
|
+
taskRuns: loaded.taskRuns || {},
|
|
36
|
+
browserSessions: loaded.browserSessions || {},
|
|
37
|
+
pairingTokens: loaded.pairingTokens || {},
|
|
38
|
+
usageEvents: loaded.usageEvents || [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Store file missing or corrupt — start fresh (this is expected on first run)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function save() {
|
|
47
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
48
|
+
// Atomic write: write to temp file then rename.
|
|
49
|
+
// Prevents corruption if process crashes mid-write.
|
|
50
|
+
const tmpPath = dataPath() + ".tmp";
|
|
51
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
52
|
+
renameSync(tmpPath, dataPath());
|
|
53
|
+
}
|
|
54
|
+
// Init on import
|
|
55
|
+
load();
|
|
56
|
+
// --- Workspace ---
|
|
57
|
+
export function createWorkspace(name) {
|
|
58
|
+
const ws = {
|
|
59
|
+
id: randomUUID(),
|
|
60
|
+
name,
|
|
61
|
+
createdAt: Date.now(),
|
|
62
|
+
plan: "free",
|
|
63
|
+
creditBalance: 0,
|
|
64
|
+
freeTasksThisMonth: 0,
|
|
65
|
+
freeTasksResetAt: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
data.workspaces[ws.id] = ws;
|
|
68
|
+
save();
|
|
69
|
+
return ws;
|
|
70
|
+
}
|
|
71
|
+
export function getWorkspace(id) {
|
|
72
|
+
return data.workspaces[id] || null;
|
|
73
|
+
}
|
|
74
|
+
// --- Credits (file store — no enforcement, always allow) ---
|
|
75
|
+
export function checkTaskAllowance(_workspaceId) {
|
|
76
|
+
return { allowed: true, source: "free", freeRemaining: 999, creditBalance: 0 };
|
|
77
|
+
}
|
|
78
|
+
export function deductTaskCredit(_workspaceId) {
|
|
79
|
+
return "free";
|
|
80
|
+
}
|
|
81
|
+
export function addCredits(_workspaceId, _amount) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
export function updateWorkspaceBilling(id, fields) {
|
|
85
|
+
const ws = data.workspaces[id];
|
|
86
|
+
if (!ws)
|
|
87
|
+
return null;
|
|
88
|
+
if (fields.stripeCustomerId !== undefined)
|
|
89
|
+
ws.stripeCustomerId = fields.stripeCustomerId;
|
|
90
|
+
if (fields.plan !== undefined)
|
|
91
|
+
ws.plan = fields.plan;
|
|
92
|
+
if (fields.subscriptionId !== undefined)
|
|
93
|
+
ws.subscriptionId = fields.subscriptionId;
|
|
94
|
+
if (fields.subscriptionStatus !== undefined)
|
|
95
|
+
ws.subscriptionStatus = fields.subscriptionStatus;
|
|
96
|
+
save();
|
|
97
|
+
return ws;
|
|
98
|
+
}
|
|
99
|
+
// --- API Keys ---
|
|
100
|
+
export function createApiKey(workspaceId, name) {
|
|
101
|
+
const key = `hic_live_${randomBytes(24).toString("hex")}`;
|
|
102
|
+
const keyHash = hashSecret(key);
|
|
103
|
+
const keyPrefix = key.slice(0, 20);
|
|
104
|
+
const apiKey = {
|
|
105
|
+
id: randomUUID(),
|
|
106
|
+
key: keyHash, // Store hash, not plaintext
|
|
107
|
+
keyPrefix,
|
|
108
|
+
name,
|
|
109
|
+
workspaceId,
|
|
110
|
+
createdAt: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
data.apiKeys[keyHash] = apiKey;
|
|
113
|
+
save();
|
|
114
|
+
// Return with the plaintext key — caller shows it once, then it's gone
|
|
115
|
+
return { ...apiKey, key };
|
|
116
|
+
}
|
|
117
|
+
export function validateApiKey(key) {
|
|
118
|
+
const keyHash = hashSecret(key);
|
|
119
|
+
const apiKey = data.apiKeys[keyHash];
|
|
120
|
+
if (!apiKey)
|
|
121
|
+
return null;
|
|
122
|
+
apiKey.lastUsedAt = Date.now();
|
|
123
|
+
return apiKey;
|
|
124
|
+
}
|
|
125
|
+
export function listApiKeys(workspaceId) {
|
|
126
|
+
return Object.values(data.apiKeys)
|
|
127
|
+
.filter((k) => k.workspaceId === workspaceId)
|
|
128
|
+
.map((k) => ({
|
|
129
|
+
...k,
|
|
130
|
+
// Normalize: old keys created before keyPrefix was added have no prefix.
|
|
131
|
+
// Use a truncated "hic_..." placeholder so the API never exposes raw hashes.
|
|
132
|
+
keyPrefix: k.keyPrefix || "hic_live_***",
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
export function deleteApiKey(id, workspaceId) {
|
|
136
|
+
for (const [hash, key] of Object.entries(data.apiKeys)) {
|
|
137
|
+
if (key.id === id && key.workspaceId === workspaceId) {
|
|
138
|
+
delete data.apiKeys[hash];
|
|
139
|
+
save();
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// --- Task Runs ---
|
|
146
|
+
export function createTaskRun(params) {
|
|
147
|
+
const run = {
|
|
148
|
+
id: randomUUID(),
|
|
149
|
+
...params,
|
|
150
|
+
status: "running",
|
|
151
|
+
steps: 0,
|
|
152
|
+
usage: { inputTokens: 0, outputTokens: 0, apiCalls: 0 },
|
|
153
|
+
createdAt: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
data.taskRuns[run.id] = run;
|
|
156
|
+
save();
|
|
157
|
+
return run;
|
|
158
|
+
}
|
|
159
|
+
export function updateTaskRun(id, updates) {
|
|
160
|
+
const run = data.taskRuns[id];
|
|
161
|
+
if (!run)
|
|
162
|
+
return null;
|
|
163
|
+
Object.assign(run, updates);
|
|
164
|
+
save();
|
|
165
|
+
return run;
|
|
166
|
+
}
|
|
167
|
+
export function getTaskRun(id) {
|
|
168
|
+
return data.taskRuns[id] || null;
|
|
169
|
+
}
|
|
170
|
+
export function listStuckTasks(maxAgeMs) {
|
|
171
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
172
|
+
return Object.values(data.taskRuns)
|
|
173
|
+
.filter((t) => t.status === "running" && t.createdAt < cutoff);
|
|
174
|
+
}
|
|
175
|
+
export function listTaskRuns(workspaceId, limit = 50) {
|
|
176
|
+
return Object.values(data.taskRuns)
|
|
177
|
+
.filter((r) => r.workspaceId === workspaceId)
|
|
178
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
179
|
+
.slice(0, limit);
|
|
180
|
+
}
|
|
181
|
+
// --- Pairing Tokens ---
|
|
182
|
+
const PAIRING_TOKEN_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
183
|
+
/**
|
|
184
|
+
* Create a short-lived pairing token. The developer (via API key) requests this,
|
|
185
|
+
* then gives it to the browser user. The extension exchanges it for a session token.
|
|
186
|
+
* The workspace binding comes from the API key, NOT from the extension.
|
|
187
|
+
*/
|
|
188
|
+
export function createPairingToken(workspaceId, apiKeyId, metadata) {
|
|
189
|
+
const plainToken = `hic_pair_${randomBytes(32).toString("hex")}`;
|
|
190
|
+
const tokenHash = hashSecret(plainToken);
|
|
191
|
+
const token = {
|
|
192
|
+
token: tokenHash, // Store hash
|
|
193
|
+
workspaceId,
|
|
194
|
+
createdBy: apiKeyId,
|
|
195
|
+
createdAt: Date.now(),
|
|
196
|
+
expiresAt: Date.now() + PAIRING_TOKEN_TTL_MS,
|
|
197
|
+
consumed: false,
|
|
198
|
+
label: metadata?.label,
|
|
199
|
+
externalUserId: metadata?.externalUserId,
|
|
200
|
+
};
|
|
201
|
+
data.pairingTokens[tokenHash] = token;
|
|
202
|
+
save();
|
|
203
|
+
return { ...token, _plainToken: plainToken };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Consume a pairing token and create a browser session.
|
|
207
|
+
* Returns null if the token is invalid, expired, or already consumed.
|
|
208
|
+
* The workspace is inherited from the pairing token — the extension cannot choose it.
|
|
209
|
+
*/
|
|
210
|
+
export function consumePairingToken(pairingTokenStr) {
|
|
211
|
+
const tokenHash = hashSecret(pairingTokenStr);
|
|
212
|
+
const pt = data.pairingTokens[tokenHash];
|
|
213
|
+
if (!pt)
|
|
214
|
+
return null;
|
|
215
|
+
if (pt.consumed)
|
|
216
|
+
return null;
|
|
217
|
+
if (Date.now() > pt.expiresAt)
|
|
218
|
+
return null;
|
|
219
|
+
// Mark consumed
|
|
220
|
+
pt.consumed = true;
|
|
221
|
+
// Create session with backend-issued credentials (30-day expiry)
|
|
222
|
+
// Metadata (label, externalUserId) is inherited from the pairing token.
|
|
223
|
+
const plainSessionToken = `hic_sess_${randomBytes(32).toString("hex")}`;
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const session = {
|
|
226
|
+
id: randomUUID(),
|
|
227
|
+
workspaceId: pt.workspaceId, // Bound by backend, not extension
|
|
228
|
+
sessionToken: hashSecret(plainSessionToken), // Store hash
|
|
229
|
+
status: "connected",
|
|
230
|
+
connectedAt: now,
|
|
231
|
+
lastHeartbeat: now,
|
|
232
|
+
expiresAt: now + 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
233
|
+
revoked: false,
|
|
234
|
+
label: pt.label,
|
|
235
|
+
externalUserId: pt.externalUserId,
|
|
236
|
+
};
|
|
237
|
+
data.browserSessions[session.id] = session;
|
|
238
|
+
save();
|
|
239
|
+
// Return with plaintext session token — shown once to the extension
|
|
240
|
+
return { ...session, sessionToken: plainSessionToken };
|
|
241
|
+
}
|
|
242
|
+
// --- Browser Sessions ---
|
|
243
|
+
/**
|
|
244
|
+
* Validate a session token. Returns the session if valid, null otherwise.
|
|
245
|
+
* This is how the relay authenticates extension connections.
|
|
246
|
+
*/
|
|
247
|
+
export function validateSessionToken(sessionToken) {
|
|
248
|
+
const tokenHash = hashSecret(sessionToken);
|
|
249
|
+
for (const session of Object.values(data.browserSessions)) {
|
|
250
|
+
if (session.sessionToken === tokenHash) {
|
|
251
|
+
// Check expiry and revocation
|
|
252
|
+
if (session.revoked)
|
|
253
|
+
return null;
|
|
254
|
+
if (session.expiresAt && Date.now() > session.expiresAt)
|
|
255
|
+
return null;
|
|
256
|
+
return session;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
export function heartbeatSession(id) {
|
|
262
|
+
const session = data.browserSessions[id];
|
|
263
|
+
if (!session)
|
|
264
|
+
return false;
|
|
265
|
+
// Reject heartbeat for revoked or expired sessions
|
|
266
|
+
if (session.revoked)
|
|
267
|
+
return false;
|
|
268
|
+
if (session.expiresAt && Date.now() > session.expiresAt)
|
|
269
|
+
return false;
|
|
270
|
+
session.lastHeartbeat = Date.now();
|
|
271
|
+
session.status = "connected";
|
|
272
|
+
// Don't save on every heartbeat — batch to avoid disk thrashing
|
|
273
|
+
// Save every 30 seconds via periodic flush instead
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Rotate a session's token. Returns the new plaintext token, or null if session is invalid.
|
|
278
|
+
* The old token is immediately invalidated (replaced by the new hash).
|
|
279
|
+
* Call this periodically (e.g., on heartbeat from relay) to limit token exposure window.
|
|
280
|
+
*/
|
|
281
|
+
export function rotateSessionToken(id) {
|
|
282
|
+
const session = data.browserSessions[id];
|
|
283
|
+
if (!session)
|
|
284
|
+
return null;
|
|
285
|
+
if (session.revoked)
|
|
286
|
+
return null;
|
|
287
|
+
if (session.expiresAt && Date.now() > session.expiresAt)
|
|
288
|
+
return null;
|
|
289
|
+
const newPlainToken = `hic_sess_${randomBytes(32).toString("hex")}`;
|
|
290
|
+
session.sessionToken = hashSecret(newPlainToken);
|
|
291
|
+
save();
|
|
292
|
+
return newPlainToken;
|
|
293
|
+
}
|
|
294
|
+
// Periodic flush for heartbeat updates
|
|
295
|
+
let _heartbeatFlushTimer = null;
|
|
296
|
+
export function startHeartbeatFlush() {
|
|
297
|
+
if (_heartbeatFlushTimer)
|
|
298
|
+
return;
|
|
299
|
+
_heartbeatFlushTimer = setInterval(() => save(), 30000);
|
|
300
|
+
}
|
|
301
|
+
export function disconnectSession(id) {
|
|
302
|
+
const session = data.browserSessions[id];
|
|
303
|
+
if (session) {
|
|
304
|
+
session.status = "disconnected";
|
|
305
|
+
save();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export function updateSessionContext(id, tabId, windowId) {
|
|
309
|
+
const session = data.browserSessions[id];
|
|
310
|
+
if (session) {
|
|
311
|
+
session.tabId = tabId;
|
|
312
|
+
if (windowId !== undefined)
|
|
313
|
+
session.windowId = windowId;
|
|
314
|
+
save();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export function getBrowserSession(id) {
|
|
318
|
+
return data.browserSessions[id] || null;
|
|
319
|
+
}
|
|
320
|
+
export function getBrowserSessionByToken(sessionToken) {
|
|
321
|
+
return validateSessionToken(sessionToken);
|
|
322
|
+
}
|
|
323
|
+
export function listBrowserSessions(workspaceId) {
|
|
324
|
+
const sessions = Object.values(data.browserSessions);
|
|
325
|
+
if (workspaceId) {
|
|
326
|
+
return sessions.filter((s) => s.workspaceId === workspaceId);
|
|
327
|
+
}
|
|
328
|
+
return sessions;
|
|
329
|
+
}
|
|
330
|
+
export function deleteBrowserSession(id, workspaceId) {
|
|
331
|
+
const session = data.browserSessions[id];
|
|
332
|
+
if (!session || session.workspaceId !== workspaceId)
|
|
333
|
+
return false;
|
|
334
|
+
delete data.browserSessions[id];
|
|
335
|
+
save();
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
// --- Task Steps (no-op for file store — only persisted in Postgres) ---
|
|
339
|
+
export async function insertTaskStep(_params) { }
|
|
340
|
+
export async function getTaskSteps(_taskRunId) { return []; }
|
|
341
|
+
export async function getTaskStepScreenshot(_taskRunId, _step) { return null; }
|
|
342
|
+
// --- Usage Events ---
|
|
343
|
+
export function recordUsage(params) {
|
|
344
|
+
// Gemini 2.5 Flash pricing
|
|
345
|
+
const inputCost = (params.inputTokens / 1_000_000) * 0.30;
|
|
346
|
+
const outputCost = (params.outputTokens / 1_000_000) * 2.50;
|
|
347
|
+
const event = {
|
|
348
|
+
id: randomUUID(),
|
|
349
|
+
...params,
|
|
350
|
+
costUsd: inputCost + outputCost,
|
|
351
|
+
createdAt: Date.now(),
|
|
352
|
+
};
|
|
353
|
+
data.usageEvents.push(event);
|
|
354
|
+
save();
|
|
355
|
+
return event;
|
|
356
|
+
}
|
|
357
|
+
export function getUsageSummary(workspaceId, since) {
|
|
358
|
+
const events = data.usageEvents.filter((e) => e.workspaceId === workspaceId && (!since || e.createdAt >= since));
|
|
359
|
+
return {
|
|
360
|
+
totalInputTokens: events.reduce((s, e) => s + e.inputTokens, 0),
|
|
361
|
+
totalOutputTokens: events.reduce((s, e) => s + e.outputTokens, 0),
|
|
362
|
+
totalApiCalls: events.reduce((s, e) => s + e.apiCalls, 0),
|
|
363
|
+
totalCostUsd: events.reduce((s, e) => s + e.costUsd, 0),
|
|
364
|
+
taskCount: new Set(events.map((e) => e.taskRunId)).size,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// --- Bootstrap: ensure a default workspace + key exist ---
|
|
368
|
+
export function ensureDefaultWorkspace() {
|
|
369
|
+
const existing = Object.values(data.workspaces)[0];
|
|
370
|
+
if (existing) {
|
|
371
|
+
const key = Object.values(data.apiKeys).find((k) => k.workspaceId === existing.id);
|
|
372
|
+
if (key)
|
|
373
|
+
return { workspace: existing, apiKey: key };
|
|
374
|
+
return { workspace: existing, apiKey: createApiKey(existing.id, "default") };
|
|
375
|
+
}
|
|
376
|
+
const workspace = createWorkspace("Default");
|
|
377
|
+
const apiKey = createApiKey(workspace.id, "default");
|
|
378
|
+
return { workspace, apiKey };
|
|
379
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-start helper for the WebSocket relay server.
|
|
3
|
+
*
|
|
4
|
+
* Used by MCP server and CLI to ensure the relay is running
|
|
5
|
+
* before attempting to connect.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get the relay URL (configurable via WS_RELAY_PORT env var)
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRelayUrl(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Check if the relay server is already running
|
|
13
|
+
*/
|
|
14
|
+
export declare function isRelayRunning(url?: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Start the relay server as a detached background process.
|
|
17
|
+
* Returns once the relay is accepting connections.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureRelayRunning(url?: string): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-start helper for the WebSocket relay server.
|
|
3
|
+
*
|
|
4
|
+
* Used by MCP server and CLI to ensure the relay is running
|
|
5
|
+
* before attempting to connect.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import WebSocket from 'ws';
|
|
11
|
+
const DEFAULT_PORT = 7862;
|
|
12
|
+
/**
|
|
13
|
+
* Get the relay URL (configurable via WS_RELAY_PORT env var)
|
|
14
|
+
*/
|
|
15
|
+
export function getRelayUrl() {
|
|
16
|
+
const port = process.env.WS_RELAY_PORT || String(DEFAULT_PORT);
|
|
17
|
+
return `ws://localhost:${port}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if the relay server is already running
|
|
21
|
+
*/
|
|
22
|
+
export async function isRelayRunning(url) {
|
|
23
|
+
const relayUrl = url || getRelayUrl();
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const ws = new WebSocket(relayUrl);
|
|
26
|
+
const timeout = setTimeout(() => {
|
|
27
|
+
ws.terminate();
|
|
28
|
+
resolve(false);
|
|
29
|
+
}, 1000);
|
|
30
|
+
ws.on('open', () => {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
ws.close();
|
|
33
|
+
resolve(true);
|
|
34
|
+
});
|
|
35
|
+
ws.on('error', () => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
resolve(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Start the relay server as a detached background process.
|
|
43
|
+
* Returns once the relay is accepting connections.
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureRelayRunning(url) {
|
|
46
|
+
const relayUrl = url || getRelayUrl();
|
|
47
|
+
// Already running?
|
|
48
|
+
if (await isRelayRunning(relayUrl)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Find the relay server script (compiled JS)
|
|
52
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
const relayScript = join(__dirname, 'server.js');
|
|
54
|
+
console.error(`[Relay] Starting relay server: ${relayScript}`);
|
|
55
|
+
// Spawn as detached process that outlives the parent
|
|
56
|
+
const child = spawn(process.execPath, [relayScript], {
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: 'ignore',
|
|
59
|
+
env: { ...process.env },
|
|
60
|
+
});
|
|
61
|
+
child.unref();
|
|
62
|
+
// Wait for it to be ready (up to 3 seconds)
|
|
63
|
+
for (let i = 0; i < 30; i++) {
|
|
64
|
+
await new Promise(r => setTimeout(r, 100));
|
|
65
|
+
if (await isRelayRunning(relayUrl)) {
|
|
66
|
+
console.error('[Relay] Server is ready');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error('Relay server failed to start within 3 seconds');
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket Relay Server
|
|
4
|
+
*
|
|
5
|
+
* Stateless message router between extension, MCP server, and CLI.
|
|
6
|
+
* Replaces file-based IPC with real-time WebSocket communication.
|
|
7
|
+
*
|
|
8
|
+
* Roles:
|
|
9
|
+
* - extension: Chrome extension service worker (one at a time)
|
|
10
|
+
* - mcp: MCP server (can have multiple)
|
|
11
|
+
* - cli: CLI clients (can have multiple)
|
|
12
|
+
*
|
|
13
|
+
* Routing:
|
|
14
|
+
* - extension → originating mcp/cli client when tagged, otherwise broadcast
|
|
15
|
+
* - mcp/cli → send to extension
|
|
16
|
+
*/
|
|
17
|
+
export {};
|