@devness/useai 0.3.8 → 0.4.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/dist/index.js +1598 -598
- package/package.json +13 -12
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -9,25 +9,1127 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ../shared/dist/types/chain.js
|
|
13
|
+
var init_chain = __esm({
|
|
14
|
+
"../shared/dist/types/chain.js"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ../shared/dist/types/config.js
|
|
20
|
+
var init_config = __esm({
|
|
21
|
+
"../shared/dist/types/config.js"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ../shared/dist/types/keystore.js
|
|
27
|
+
var init_keystore = __esm({
|
|
28
|
+
"../shared/dist/types/keystore.js"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ../shared/dist/types/milestone.js
|
|
34
|
+
var init_milestone = __esm({
|
|
35
|
+
"../shared/dist/types/milestone.js"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ../shared/dist/types/session.js
|
|
41
|
+
var init_session = __esm({
|
|
42
|
+
"../shared/dist/types/session.js"() {
|
|
43
|
+
"use strict";
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ../shared/dist/types/api.js
|
|
48
|
+
var init_api = __esm({
|
|
49
|
+
"../shared/dist/types/api.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ../shared/dist/types/user.js
|
|
55
|
+
var init_user = __esm({
|
|
56
|
+
"../shared/dist/types/user.js"() {
|
|
57
|
+
"use strict";
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ../shared/dist/types/leaderboard.js
|
|
62
|
+
var init_leaderboard = __esm({
|
|
63
|
+
"../shared/dist/types/leaderboard.js"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ../shared/dist/types/index.js
|
|
69
|
+
var init_types = __esm({
|
|
70
|
+
"../shared/dist/types/index.js"() {
|
|
71
|
+
"use strict";
|
|
72
|
+
init_chain();
|
|
73
|
+
init_config();
|
|
74
|
+
init_keystore();
|
|
75
|
+
init_milestone();
|
|
76
|
+
init_session();
|
|
77
|
+
init_api();
|
|
78
|
+
init_user();
|
|
79
|
+
init_leaderboard();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ../shared/dist/constants/paths.js
|
|
84
|
+
import { join } from "path";
|
|
85
|
+
import { homedir } from "os";
|
|
86
|
+
var USEAI_DIR, DATA_DIR, ACTIVE_DIR, SEALED_DIR, KEYSTORE_FILE, CONFIG_FILE, SESSIONS_FILE, MILESTONES_FILE, DAEMON_PID_FILE, DAEMON_PORT, DAEMON_LOG_FILE, DAEMON_MCP_URL, DAEMON_HEALTH_URL, LAUNCHD_PLIST_PATH, SYSTEMD_SERVICE_PATH, WINDOWS_STARTUP_SCRIPT_PATH;
|
|
87
|
+
var init_paths = __esm({
|
|
88
|
+
"../shared/dist/constants/paths.js"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
USEAI_DIR = join(homedir(), ".useai");
|
|
91
|
+
DATA_DIR = join(USEAI_DIR, "data");
|
|
92
|
+
ACTIVE_DIR = join(DATA_DIR, "active");
|
|
93
|
+
SEALED_DIR = join(DATA_DIR, "sealed");
|
|
94
|
+
KEYSTORE_FILE = join(USEAI_DIR, "keystore.json");
|
|
95
|
+
CONFIG_FILE = join(USEAI_DIR, "config.json");
|
|
96
|
+
SESSIONS_FILE = join(DATA_DIR, "sessions.json");
|
|
97
|
+
MILESTONES_FILE = join(DATA_DIR, "milestones.json");
|
|
98
|
+
DAEMON_PID_FILE = join(USEAI_DIR, "daemon.pid");
|
|
99
|
+
DAEMON_PORT = 19200;
|
|
100
|
+
DAEMON_LOG_FILE = join(USEAI_DIR, "daemon.log");
|
|
101
|
+
DAEMON_MCP_URL = `http://127.0.0.1:${DAEMON_PORT}/mcp`;
|
|
102
|
+
DAEMON_HEALTH_URL = `http://127.0.0.1:${DAEMON_PORT}/health`;
|
|
103
|
+
LAUNCHD_PLIST_PATH = join(homedir(), "Library", "LaunchAgents", "dev.useai.daemon.plist");
|
|
104
|
+
SYSTEMD_SERVICE_PATH = join(homedir(), ".config", "systemd", "user", "useai-daemon.service");
|
|
105
|
+
WINDOWS_STARTUP_SCRIPT_PATH = join(process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "useai-daemon.vbs");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ../shared/dist/constants/version.js
|
|
110
|
+
var VERSION;
|
|
111
|
+
var init_version = __esm({
|
|
112
|
+
"../shared/dist/constants/version.js"() {
|
|
113
|
+
"use strict";
|
|
114
|
+
VERSION = "0.4.1";
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ../shared/dist/constants/clients.js
|
|
119
|
+
var AI_CLIENT_ENV_VARS;
|
|
120
|
+
var init_clients = __esm({
|
|
121
|
+
"../shared/dist/constants/clients.js"() {
|
|
122
|
+
"use strict";
|
|
123
|
+
AI_CLIENT_ENV_VARS = {
|
|
124
|
+
CURSOR_EDITOR: "cursor",
|
|
125
|
+
WINDSURF_EDITOR: "windsurf",
|
|
126
|
+
CLAUDE_CODE: "claude-code",
|
|
127
|
+
VSCODE_PID: "vscode",
|
|
128
|
+
CODEX_CLI: "codex",
|
|
129
|
+
GEMINI_CLI: "gemini-cli",
|
|
130
|
+
JETBRAINS_IDE: "jetbrains",
|
|
131
|
+
ZED_EDITOR: "zed"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ../shared/dist/constants/defaults.js
|
|
137
|
+
var GENESIS_HASH;
|
|
138
|
+
var init_defaults = __esm({
|
|
139
|
+
"../shared/dist/constants/defaults.js"() {
|
|
140
|
+
"use strict";
|
|
141
|
+
GENESIS_HASH = "GENESIS";
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ../shared/dist/constants/index.js
|
|
146
|
+
var init_constants = __esm({
|
|
147
|
+
"../shared/dist/constants/index.js"() {
|
|
148
|
+
"use strict";
|
|
149
|
+
init_paths();
|
|
150
|
+
init_version();
|
|
151
|
+
init_clients();
|
|
152
|
+
init_defaults();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ../shared/dist/crypto/derive.js
|
|
157
|
+
import { pbkdf2Sync } from "crypto";
|
|
158
|
+
import { hostname, userInfo } from "os";
|
|
159
|
+
function deriveEncryptionKey(salt) {
|
|
160
|
+
const machineData = `${hostname()}:${userInfo().username}:useai-keystore`;
|
|
161
|
+
return pbkdf2Sync(machineData, salt, 1e5, 32, "sha256");
|
|
162
|
+
}
|
|
163
|
+
var init_derive = __esm({
|
|
164
|
+
"../shared/dist/crypto/derive.js"() {
|
|
165
|
+
"use strict";
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ../shared/dist/crypto/keystore.js
|
|
170
|
+
import { generateKeyPairSync, randomBytes, createCipheriv, createDecipheriv, createPrivateKey } from "crypto";
|
|
171
|
+
function decryptKeystore(ks) {
|
|
172
|
+
const salt = Buffer.from(ks.salt, "hex");
|
|
173
|
+
const iv = Buffer.from(ks.iv, "hex");
|
|
174
|
+
const tag = Buffer.from(ks.tag, "hex");
|
|
175
|
+
const encrypted = Buffer.from(ks.encrypted_private_key, "hex");
|
|
176
|
+
const encKey = deriveEncryptionKey(salt);
|
|
177
|
+
const decipher = createDecipheriv("aes-256-gcm", encKey, iv);
|
|
178
|
+
decipher.setAuthTag(tag);
|
|
179
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
180
|
+
return createPrivateKey(decrypted.toString("utf-8"));
|
|
181
|
+
}
|
|
182
|
+
function generateKeystore() {
|
|
183
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
184
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
185
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
186
|
+
const salt = randomBytes(32);
|
|
187
|
+
const encKey = deriveEncryptionKey(salt);
|
|
188
|
+
const iv = randomBytes(12);
|
|
189
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, iv);
|
|
190
|
+
const encrypted = Buffer.concat([cipher.update(privateKeyPem, "utf-8"), cipher.final()]);
|
|
191
|
+
const tag = cipher.getAuthTag();
|
|
192
|
+
const keystore = {
|
|
193
|
+
public_key_pem: publicKeyPem,
|
|
194
|
+
encrypted_private_key: encrypted.toString("hex"),
|
|
195
|
+
iv: iv.toString("hex"),
|
|
196
|
+
tag: tag.toString("hex"),
|
|
197
|
+
salt: salt.toString("hex"),
|
|
198
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
199
|
+
};
|
|
200
|
+
return { keystore, signingKey: privateKey };
|
|
201
|
+
}
|
|
202
|
+
var init_keystore2 = __esm({
|
|
203
|
+
"../shared/dist/crypto/keystore.js"() {
|
|
204
|
+
"use strict";
|
|
205
|
+
init_derive();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ../shared/dist/crypto/chain.js
|
|
210
|
+
import { createHash, sign as cryptoSign, randomUUID } from "crypto";
|
|
211
|
+
function computeHash(recordJson, prevHash) {
|
|
212
|
+
return createHash("sha256").update(recordJson + prevHash).digest("hex");
|
|
213
|
+
}
|
|
214
|
+
function signHash(hash, signingKey) {
|
|
215
|
+
if (!signingKey)
|
|
216
|
+
return "unsigned";
|
|
217
|
+
try {
|
|
218
|
+
return cryptoSign(null, Buffer.from(hash), signingKey).toString("hex");
|
|
219
|
+
} catch {
|
|
220
|
+
return "unsigned";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function buildChainRecord(type, sessionId, data, prevHash, signingKey) {
|
|
224
|
+
const id = `r_${randomUUID().slice(0, 12)}`;
|
|
225
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
226
|
+
const recordCore = JSON.stringify({ id, type, session_id: sessionId, timestamp, data });
|
|
227
|
+
const hash = computeHash(recordCore, prevHash);
|
|
228
|
+
const signature = signHash(hash, signingKey);
|
|
229
|
+
return {
|
|
230
|
+
id,
|
|
231
|
+
type,
|
|
232
|
+
session_id: sessionId,
|
|
233
|
+
timestamp,
|
|
234
|
+
data,
|
|
235
|
+
prev_hash: prevHash,
|
|
236
|
+
hash,
|
|
237
|
+
signature
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
var init_chain2 = __esm({
|
|
241
|
+
"../shared/dist/crypto/chain.js"() {
|
|
242
|
+
"use strict";
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ../shared/dist/crypto/verify.js
|
|
247
|
+
import { createHash as createHash2, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
248
|
+
var init_verify = __esm({
|
|
249
|
+
"../shared/dist/crypto/verify.js"() {
|
|
250
|
+
"use strict";
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ../shared/dist/crypto/index.js
|
|
255
|
+
var init_crypto = __esm({
|
|
256
|
+
"../shared/dist/crypto/index.js"() {
|
|
257
|
+
"use strict";
|
|
258
|
+
init_keystore2();
|
|
259
|
+
init_chain2();
|
|
260
|
+
init_verify();
|
|
261
|
+
init_derive();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ../shared/dist/validation/schemas.js
|
|
266
|
+
import { z } from "zod";
|
|
267
|
+
var taskTypeSchema, milestoneCategorySchema, complexitySchema, milestoneInputSchema, syncPayloadSchema, publishPayloadSchema;
|
|
268
|
+
var init_schemas = __esm({
|
|
269
|
+
"../shared/dist/validation/schemas.js"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
taskTypeSchema = z.enum([
|
|
272
|
+
"coding",
|
|
273
|
+
"debugging",
|
|
274
|
+
"testing",
|
|
275
|
+
"planning",
|
|
276
|
+
"reviewing",
|
|
277
|
+
"documenting",
|
|
278
|
+
"learning",
|
|
279
|
+
"other"
|
|
280
|
+
]);
|
|
281
|
+
milestoneCategorySchema = z.enum([
|
|
282
|
+
"feature",
|
|
283
|
+
"bugfix",
|
|
284
|
+
"refactor",
|
|
285
|
+
"test",
|
|
286
|
+
"docs",
|
|
287
|
+
"setup",
|
|
288
|
+
"deployment",
|
|
289
|
+
"other"
|
|
290
|
+
]);
|
|
291
|
+
complexitySchema = z.enum(["simple", "medium", "complex"]);
|
|
292
|
+
milestoneInputSchema = z.object({
|
|
293
|
+
title: z.string().min(1).max(500),
|
|
294
|
+
category: milestoneCategorySchema,
|
|
295
|
+
complexity: complexitySchema.optional()
|
|
296
|
+
});
|
|
297
|
+
syncPayloadSchema = z.object({
|
|
298
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
299
|
+
total_seconds: z.number().int().nonnegative(),
|
|
300
|
+
clients: z.record(z.string(), z.number().int().nonnegative()),
|
|
301
|
+
task_types: z.record(z.string(), z.number().int().nonnegative()),
|
|
302
|
+
languages: z.record(z.string(), z.number().int().nonnegative()),
|
|
303
|
+
sessions: z.array(z.object({
|
|
304
|
+
session_id: z.string(),
|
|
305
|
+
client: z.string(),
|
|
306
|
+
task_type: z.string(),
|
|
307
|
+
languages: z.array(z.string()),
|
|
308
|
+
files_touched: z.number().int().nonnegative(),
|
|
309
|
+
started_at: z.string(),
|
|
310
|
+
ended_at: z.string(),
|
|
311
|
+
duration_seconds: z.number().int().nonnegative(),
|
|
312
|
+
heartbeat_count: z.number().int().nonnegative(),
|
|
313
|
+
record_count: z.number().int().nonnegative(),
|
|
314
|
+
chain_start_hash: z.string(),
|
|
315
|
+
chain_end_hash: z.string(),
|
|
316
|
+
seal_signature: z.string()
|
|
317
|
+
})),
|
|
318
|
+
sync_signature: z.string()
|
|
319
|
+
});
|
|
320
|
+
publishPayloadSchema = z.object({
|
|
321
|
+
milestones: z.array(z.object({
|
|
322
|
+
id: z.string(),
|
|
323
|
+
title: z.string().min(1).max(500),
|
|
324
|
+
category: milestoneCategorySchema,
|
|
325
|
+
complexity: complexitySchema,
|
|
326
|
+
duration_minutes: z.number().int().nonnegative(),
|
|
327
|
+
languages: z.array(z.string()),
|
|
328
|
+
client: z.string(),
|
|
329
|
+
chain_hash: z.string()
|
|
330
|
+
}))
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ../shared/dist/validation/guards.js
|
|
336
|
+
var init_guards = __esm({
|
|
337
|
+
"../shared/dist/validation/guards.js"() {
|
|
338
|
+
"use strict";
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ../shared/dist/validation/index.js
|
|
343
|
+
var init_validation = __esm({
|
|
344
|
+
"../shared/dist/validation/index.js"() {
|
|
345
|
+
"use strict";
|
|
346
|
+
init_schemas();
|
|
347
|
+
init_guards();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ../shared/dist/utils/fs.js
|
|
352
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
353
|
+
function ensureDir() {
|
|
354
|
+
for (const dir of [USEAI_DIR, DATA_DIR, ACTIVE_DIR, SEALED_DIR]) {
|
|
355
|
+
if (!existsSync(dir)) {
|
|
356
|
+
mkdirSync(dir, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function readJson(path, fallback) {
|
|
361
|
+
try {
|
|
362
|
+
if (!existsSync(path))
|
|
363
|
+
return fallback;
|
|
364
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
365
|
+
} catch {
|
|
366
|
+
return fallback;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function writeJson(path, data) {
|
|
370
|
+
ensureDir();
|
|
371
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
372
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
373
|
+
renameSync(tmp, path);
|
|
374
|
+
}
|
|
375
|
+
var init_fs = __esm({
|
|
376
|
+
"../shared/dist/utils/fs.js"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
init_paths();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ../shared/dist/utils/format.js
|
|
383
|
+
function formatDuration(seconds) {
|
|
384
|
+
if (seconds < 60)
|
|
385
|
+
return `${seconds}s`;
|
|
386
|
+
const mins = Math.round(seconds / 60);
|
|
387
|
+
if (mins < 60)
|
|
388
|
+
return `${mins}m`;
|
|
389
|
+
const hrs = Math.floor(mins / 60);
|
|
390
|
+
const remainMins = mins % 60;
|
|
391
|
+
return `${hrs}h ${remainMins}m`;
|
|
392
|
+
}
|
|
393
|
+
var init_format = __esm({
|
|
394
|
+
"../shared/dist/utils/format.js"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ../shared/dist/utils/detect-client.js
|
|
400
|
+
function detectClient() {
|
|
401
|
+
const env = process.env;
|
|
402
|
+
for (const [envVar, clientName] of Object.entries(AI_CLIENT_ENV_VARS)) {
|
|
403
|
+
if (env[envVar])
|
|
404
|
+
return clientName;
|
|
405
|
+
}
|
|
406
|
+
if (env.MCP_CLIENT_NAME)
|
|
407
|
+
return env.MCP_CLIENT_NAME;
|
|
408
|
+
return "unknown";
|
|
409
|
+
}
|
|
410
|
+
var init_detect_client = __esm({
|
|
411
|
+
"../shared/dist/utils/detect-client.js"() {
|
|
412
|
+
"use strict";
|
|
413
|
+
init_clients();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ../shared/dist/utils/id.js
|
|
418
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
419
|
+
function generateSessionId() {
|
|
420
|
+
return randomUUID2();
|
|
421
|
+
}
|
|
422
|
+
var init_id = __esm({
|
|
423
|
+
"../shared/dist/utils/id.js"() {
|
|
424
|
+
"use strict";
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ../shared/dist/utils/index.js
|
|
429
|
+
var init_utils = __esm({
|
|
430
|
+
"../shared/dist/utils/index.js"() {
|
|
431
|
+
"use strict";
|
|
432
|
+
init_fs();
|
|
433
|
+
init_format();
|
|
434
|
+
init_detect_client();
|
|
435
|
+
init_id();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ../shared/dist/daemon/resolve-npx.js
|
|
13
440
|
import { execSync } from "child_process";
|
|
14
|
-
import { existsSync as
|
|
15
|
-
import {
|
|
441
|
+
import { existsSync as existsSync2 } from "fs";
|
|
442
|
+
import { join as join2 } from "path";
|
|
16
443
|
import { homedir as homedir2 } from "os";
|
|
444
|
+
function resolveNpxPath() {
|
|
445
|
+
const whichCmd = isWindows ? "where npx" : "which npx";
|
|
446
|
+
try {
|
|
447
|
+
const result = execSync(whichCmd, { stdio: ["pipe", "pipe", "ignore"], encoding: "utf-8" }).trim();
|
|
448
|
+
if (result)
|
|
449
|
+
return result.split("\n")[0].trim();
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
if (!isWindows && process.env["SHELL"]) {
|
|
453
|
+
try {
|
|
454
|
+
const result = execSync(`${process.env["SHELL"]} -lc "which npx"`, {
|
|
455
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
456
|
+
encoding: "utf-8"
|
|
457
|
+
}).trim();
|
|
458
|
+
if (result)
|
|
459
|
+
return result;
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const p of KNOWN_PATHS) {
|
|
464
|
+
if (existsSync2(p))
|
|
465
|
+
return p;
|
|
466
|
+
}
|
|
467
|
+
throw new Error("Could not find npx. Ensure Node.js is installed and npx is in your PATH.");
|
|
468
|
+
}
|
|
469
|
+
function buildNodePath() {
|
|
470
|
+
const home2 = homedir2();
|
|
471
|
+
const dirs = [
|
|
472
|
+
"/usr/local/bin",
|
|
473
|
+
"/opt/homebrew/bin",
|
|
474
|
+
"/usr/bin",
|
|
475
|
+
"/bin",
|
|
476
|
+
join2(home2, ".nvm", "current", "bin"),
|
|
477
|
+
join2(home2, ".volta", "bin"),
|
|
478
|
+
join2(home2, ".bun", "bin")
|
|
479
|
+
];
|
|
480
|
+
try {
|
|
481
|
+
const npx = resolveNpxPath();
|
|
482
|
+
const npxDir = npx.substring(0, npx.lastIndexOf("/"));
|
|
483
|
+
if (npxDir && !dirs.includes(npxDir)) {
|
|
484
|
+
dirs.unshift(npxDir);
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return dirs.filter((d) => existsSync2(d)).join(":");
|
|
489
|
+
}
|
|
490
|
+
var isWindows, KNOWN_PATHS;
|
|
491
|
+
var init_resolve_npx = __esm({
|
|
492
|
+
"../shared/dist/daemon/resolve-npx.js"() {
|
|
493
|
+
"use strict";
|
|
494
|
+
isWindows = process.platform === "win32";
|
|
495
|
+
KNOWN_PATHS = isWindows ? [] : [
|
|
496
|
+
"/usr/local/bin/npx",
|
|
497
|
+
"/opt/homebrew/bin/npx",
|
|
498
|
+
join2(homedir2(), ".nvm", "current", "bin", "npx"),
|
|
499
|
+
join2(homedir2(), ".volta", "bin", "npx"),
|
|
500
|
+
join2(homedir2(), ".bun", "bin", "npx")
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ../shared/dist/daemon/ensure.js
|
|
506
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync } from "fs";
|
|
507
|
+
import { spawn } from "child_process";
|
|
508
|
+
function readPidFile() {
|
|
509
|
+
if (!existsSync3(DAEMON_PID_FILE))
|
|
510
|
+
return null;
|
|
511
|
+
try {
|
|
512
|
+
const raw = readFileSync2(DAEMON_PID_FILE, "utf-8").trim();
|
|
513
|
+
return JSON.parse(raw);
|
|
514
|
+
} catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function isProcessRunning(pid) {
|
|
519
|
+
try {
|
|
520
|
+
process.kill(pid, 0);
|
|
521
|
+
return true;
|
|
522
|
+
} catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function checkDaemonHealth() {
|
|
527
|
+
try {
|
|
528
|
+
const res = await fetch(DAEMON_HEALTH_URL, { signal: AbortSignal.timeout(3e3) });
|
|
529
|
+
return res.ok;
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function killDaemon() {
|
|
535
|
+
const pid = readPidFile();
|
|
536
|
+
if (!pid)
|
|
537
|
+
return;
|
|
538
|
+
if (!isProcessRunning(pid.pid)) {
|
|
539
|
+
try {
|
|
540
|
+
unlinkSync(DAEMON_PID_FILE);
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
process.kill(pid.pid, "SIGTERM");
|
|
547
|
+
} catch {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const start = Date.now();
|
|
551
|
+
while (Date.now() - start < 5e3) {
|
|
552
|
+
if (!isProcessRunning(pid.pid)) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
process.kill(pid.pid, "SIGKILL");
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
if (existsSync3(DAEMON_PID_FILE))
|
|
563
|
+
unlinkSync(DAEMON_PID_FILE);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function ensureDaemon() {
|
|
568
|
+
if (await checkDaemonHealth())
|
|
569
|
+
return true;
|
|
570
|
+
const pid = readPidFile();
|
|
571
|
+
if (pid && isProcessRunning(pid.pid)) {
|
|
572
|
+
await killDaemon();
|
|
573
|
+
} else if (pid) {
|
|
574
|
+
try {
|
|
575
|
+
unlinkSync(DAEMON_PID_FILE);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
let npxPath;
|
|
580
|
+
try {
|
|
581
|
+
npxPath = resolveNpxPath();
|
|
582
|
+
} catch {
|
|
583
|
+
npxPath = "npx";
|
|
584
|
+
}
|
|
585
|
+
const child = spawn(npxPath, ["-y", "@devness/useai@latest", "daemon", "--port", String(DAEMON_PORT)], {
|
|
586
|
+
detached: true,
|
|
587
|
+
stdio: "ignore"
|
|
588
|
+
});
|
|
589
|
+
child.unref();
|
|
590
|
+
const start = Date.now();
|
|
591
|
+
while (Date.now() - start < 8e3) {
|
|
592
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
593
|
+
if (await checkDaemonHealth())
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
var init_ensure = __esm({
|
|
599
|
+
"../shared/dist/daemon/ensure.js"() {
|
|
600
|
+
"use strict";
|
|
601
|
+
init_paths();
|
|
602
|
+
init_resolve_npx();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// ../shared/dist/daemon/autostart.js
|
|
607
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
608
|
+
import { execSync as execSync2 } from "child_process";
|
|
609
|
+
import { dirname } from "path";
|
|
610
|
+
function detectPlatform() {
|
|
611
|
+
switch (process.platform) {
|
|
612
|
+
case "darwin":
|
|
613
|
+
return "macos";
|
|
614
|
+
case "linux":
|
|
615
|
+
return "linux";
|
|
616
|
+
case "win32":
|
|
617
|
+
return "windows";
|
|
618
|
+
default:
|
|
619
|
+
return "unsupported";
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function buildPlist(npxPath, nodePath) {
|
|
623
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
624
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
625
|
+
<plist version="1.0">
|
|
626
|
+
<dict>
|
|
627
|
+
<key>Label</key>
|
|
628
|
+
<string>dev.useai.daemon</string>
|
|
629
|
+
<key>ProgramArguments</key>
|
|
630
|
+
<array>
|
|
631
|
+
<string>${npxPath}</string>
|
|
632
|
+
<string>-y</string>
|
|
633
|
+
<string>@devness/useai</string>
|
|
634
|
+
<string>daemon</string>
|
|
635
|
+
<string>--port</string>
|
|
636
|
+
<string>${DAEMON_PORT}</string>
|
|
637
|
+
</array>
|
|
638
|
+
<key>RunAtLoad</key>
|
|
639
|
+
<true/>
|
|
640
|
+
<key>KeepAlive</key>
|
|
641
|
+
<dict>
|
|
642
|
+
<key>SuccessfulExit</key>
|
|
643
|
+
<false/>
|
|
644
|
+
</dict>
|
|
645
|
+
<key>ThrottleInterval</key>
|
|
646
|
+
<integer>10</integer>
|
|
647
|
+
<key>StandardOutPath</key>
|
|
648
|
+
<string>${DAEMON_LOG_FILE}</string>
|
|
649
|
+
<key>StandardErrorPath</key>
|
|
650
|
+
<string>${DAEMON_LOG_FILE}</string>
|
|
651
|
+
<key>EnvironmentVariables</key>
|
|
652
|
+
<dict>
|
|
653
|
+
<key>PATH</key>
|
|
654
|
+
<string>${nodePath}</string>
|
|
655
|
+
</dict>
|
|
656
|
+
</dict>
|
|
657
|
+
</plist>
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
function installMacos() {
|
|
661
|
+
const npxPath = resolveNpxPath();
|
|
662
|
+
const nodePath = buildNodePath();
|
|
663
|
+
mkdirSync2(dirname(LAUNCHD_PLIST_PATH), { recursive: true });
|
|
664
|
+
writeFileSync2(LAUNCHD_PLIST_PATH, buildPlist(npxPath, nodePath));
|
|
665
|
+
try {
|
|
666
|
+
execSync2(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
execSync2(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: "ignore" });
|
|
670
|
+
}
|
|
671
|
+
function removeMacos() {
|
|
672
|
+
try {
|
|
673
|
+
execSync2(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
if (existsSync4(LAUNCHD_PLIST_PATH))
|
|
678
|
+
unlinkSync2(LAUNCHD_PLIST_PATH);
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function isMacosInstalled() {
|
|
683
|
+
return existsSync4(LAUNCHD_PLIST_PATH);
|
|
684
|
+
}
|
|
685
|
+
function buildSystemdUnit(npxPath, nodePath) {
|
|
686
|
+
return `[Unit]
|
|
687
|
+
Description=UseAI Daemon
|
|
688
|
+
After=network.target
|
|
689
|
+
|
|
690
|
+
[Service]
|
|
691
|
+
Type=simple
|
|
692
|
+
ExecStart=${npxPath} -y @devness/useai daemon --port ${DAEMON_PORT}
|
|
693
|
+
Restart=on-failure
|
|
694
|
+
RestartSec=10
|
|
695
|
+
Environment=PATH=${nodePath}
|
|
696
|
+
|
|
697
|
+
[Install]
|
|
698
|
+
WantedBy=default.target
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
function installLinux() {
|
|
702
|
+
const npxPath = resolveNpxPath();
|
|
703
|
+
const nodePath = buildNodePath();
|
|
704
|
+
mkdirSync2(dirname(SYSTEMD_SERVICE_PATH), { recursive: true });
|
|
705
|
+
writeFileSync2(SYSTEMD_SERVICE_PATH, buildSystemdUnit(npxPath, nodePath));
|
|
706
|
+
execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
707
|
+
execSync2("systemctl --user enable --now useai-daemon.service", { stdio: "ignore" });
|
|
708
|
+
}
|
|
709
|
+
function removeLinux() {
|
|
710
|
+
try {
|
|
711
|
+
execSync2("systemctl --user disable --now useai-daemon.service", { stdio: "ignore" });
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
if (existsSync4(SYSTEMD_SERVICE_PATH))
|
|
716
|
+
unlinkSync2(SYSTEMD_SERVICE_PATH);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function isLinuxInstalled() {
|
|
725
|
+
return existsSync4(SYSTEMD_SERVICE_PATH);
|
|
726
|
+
}
|
|
727
|
+
function buildVbsScript(npxPath) {
|
|
728
|
+
return `Set WshShell = CreateObject("WScript.Shell")
|
|
729
|
+
WshShell.Run """${npxPath}"" -y @devness/useai daemon --port ${DAEMON_PORT}", 0, False
|
|
730
|
+
`;
|
|
731
|
+
}
|
|
732
|
+
function installWindows() {
|
|
733
|
+
const npxPath = resolveNpxPath();
|
|
734
|
+
mkdirSync2(dirname(WINDOWS_STARTUP_SCRIPT_PATH), { recursive: true });
|
|
735
|
+
writeFileSync2(WINDOWS_STARTUP_SCRIPT_PATH, buildVbsScript(npxPath));
|
|
736
|
+
}
|
|
737
|
+
function removeWindows() {
|
|
738
|
+
try {
|
|
739
|
+
if (existsSync4(WINDOWS_STARTUP_SCRIPT_PATH))
|
|
740
|
+
unlinkSync2(WINDOWS_STARTUP_SCRIPT_PATH);
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function isWindowsInstalled() {
|
|
745
|
+
return existsSync4(WINDOWS_STARTUP_SCRIPT_PATH);
|
|
746
|
+
}
|
|
747
|
+
function installAutostart() {
|
|
748
|
+
const platform = detectPlatform();
|
|
749
|
+
switch (platform) {
|
|
750
|
+
case "macos":
|
|
751
|
+
installMacos();
|
|
752
|
+
break;
|
|
753
|
+
case "linux":
|
|
754
|
+
installLinux();
|
|
755
|
+
break;
|
|
756
|
+
case "windows":
|
|
757
|
+
installWindows();
|
|
758
|
+
break;
|
|
759
|
+
case "unsupported":
|
|
760
|
+
throw new Error(`Auto-start is not supported on ${process.platform}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function removeAutostart() {
|
|
764
|
+
const platform = detectPlatform();
|
|
765
|
+
switch (platform) {
|
|
766
|
+
case "macos":
|
|
767
|
+
removeMacos();
|
|
768
|
+
break;
|
|
769
|
+
case "linux":
|
|
770
|
+
removeLinux();
|
|
771
|
+
break;
|
|
772
|
+
case "windows":
|
|
773
|
+
removeWindows();
|
|
774
|
+
break;
|
|
775
|
+
case "unsupported":
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function isAutostartInstalled() {
|
|
780
|
+
const platform = detectPlatform();
|
|
781
|
+
switch (platform) {
|
|
782
|
+
case "macos":
|
|
783
|
+
return isMacosInstalled();
|
|
784
|
+
case "linux":
|
|
785
|
+
return isLinuxInstalled();
|
|
786
|
+
case "windows":
|
|
787
|
+
return isWindowsInstalled();
|
|
788
|
+
default:
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
var init_autostart = __esm({
|
|
793
|
+
"../shared/dist/daemon/autostart.js"() {
|
|
794
|
+
"use strict";
|
|
795
|
+
init_paths();
|
|
796
|
+
init_resolve_npx();
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// ../shared/dist/daemon/index.js
|
|
801
|
+
var init_daemon = __esm({
|
|
802
|
+
"../shared/dist/daemon/index.js"() {
|
|
803
|
+
"use strict";
|
|
804
|
+
init_resolve_npx();
|
|
805
|
+
init_ensure();
|
|
806
|
+
init_autostart();
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// ../shared/dist/index.js
|
|
811
|
+
var init_dist = __esm({
|
|
812
|
+
"../shared/dist/index.js"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
init_types();
|
|
815
|
+
init_constants();
|
|
816
|
+
init_crypto();
|
|
817
|
+
init_validation();
|
|
818
|
+
init_utils();
|
|
819
|
+
init_daemon();
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// src/session-state.ts
|
|
824
|
+
import { appendFileSync, existsSync as existsSync5 } from "fs";
|
|
825
|
+
import { join as join3 } from "path";
|
|
826
|
+
var SessionState;
|
|
827
|
+
var init_session_state = __esm({
|
|
828
|
+
"src/session-state.ts"() {
|
|
829
|
+
"use strict";
|
|
830
|
+
init_dist();
|
|
831
|
+
SessionState = class {
|
|
832
|
+
sessionId;
|
|
833
|
+
sessionStartTime;
|
|
834
|
+
heartbeatCount;
|
|
835
|
+
sessionRecordCount;
|
|
836
|
+
clientName;
|
|
837
|
+
sessionTaskType;
|
|
838
|
+
chainTipHash;
|
|
839
|
+
signingKey;
|
|
840
|
+
signingAvailable;
|
|
841
|
+
constructor() {
|
|
842
|
+
this.sessionId = generateSessionId();
|
|
843
|
+
this.sessionStartTime = Date.now();
|
|
844
|
+
this.heartbeatCount = 0;
|
|
845
|
+
this.sessionRecordCount = 0;
|
|
846
|
+
this.clientName = "unknown";
|
|
847
|
+
this.sessionTaskType = "coding";
|
|
848
|
+
this.chainTipHash = GENESIS_HASH;
|
|
849
|
+
this.signingKey = null;
|
|
850
|
+
this.signingAvailable = false;
|
|
851
|
+
}
|
|
852
|
+
reset() {
|
|
853
|
+
this.sessionStartTime = Date.now();
|
|
854
|
+
this.sessionId = generateSessionId();
|
|
855
|
+
this.heartbeatCount = 0;
|
|
856
|
+
this.sessionRecordCount = 0;
|
|
857
|
+
this.chainTipHash = GENESIS_HASH;
|
|
858
|
+
this.clientName = "unknown";
|
|
859
|
+
this.sessionTaskType = "coding";
|
|
860
|
+
}
|
|
861
|
+
setClient(name) {
|
|
862
|
+
this.clientName = name;
|
|
863
|
+
}
|
|
864
|
+
setTaskType(type) {
|
|
865
|
+
this.sessionTaskType = type;
|
|
866
|
+
}
|
|
867
|
+
incrementHeartbeat() {
|
|
868
|
+
this.heartbeatCount++;
|
|
869
|
+
}
|
|
870
|
+
getSessionDuration() {
|
|
871
|
+
return Math.round((Date.now() - this.sessionStartTime) / 1e3);
|
|
872
|
+
}
|
|
873
|
+
initializeKeystore() {
|
|
874
|
+
ensureDir();
|
|
875
|
+
if (existsSync5(KEYSTORE_FILE)) {
|
|
876
|
+
const ks = readJson(KEYSTORE_FILE, null);
|
|
877
|
+
if (ks) {
|
|
878
|
+
try {
|
|
879
|
+
this.signingKey = decryptKeystore(ks);
|
|
880
|
+
this.signingAvailable = true;
|
|
881
|
+
return;
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const result = generateKeystore();
|
|
887
|
+
writeJson(KEYSTORE_FILE, result.keystore);
|
|
888
|
+
this.signingKey = result.signingKey;
|
|
889
|
+
this.signingAvailable = true;
|
|
890
|
+
}
|
|
891
|
+
/** Path to this session's chain file in the active directory */
|
|
892
|
+
sessionChainPath() {
|
|
893
|
+
return join3(ACTIVE_DIR, `${this.sessionId}.jsonl`);
|
|
894
|
+
}
|
|
895
|
+
appendToChain(type, data) {
|
|
896
|
+
const record = buildChainRecord(type, this.sessionId, data, this.chainTipHash, this.signingKey);
|
|
897
|
+
ensureDir();
|
|
898
|
+
appendFileSync(this.sessionChainPath(), JSON.stringify(record) + "\n", "utf-8");
|
|
899
|
+
this.chainTipHash = record.hash;
|
|
900
|
+
this.sessionRecordCount++;
|
|
901
|
+
return record;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// src/register-tools.ts
|
|
908
|
+
import { z as z2 } from "zod";
|
|
909
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
910
|
+
import { existsSync as existsSync6, renameSync as renameSync2 } from "fs";
|
|
911
|
+
import { join as join4 } from "path";
|
|
912
|
+
function getConfig() {
|
|
913
|
+
return readJson(CONFIG_FILE, {
|
|
914
|
+
milestone_tracking: true,
|
|
915
|
+
auto_sync: true
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function getSessions() {
|
|
919
|
+
return readJson(SESSIONS_FILE, []);
|
|
920
|
+
}
|
|
921
|
+
function getMilestones() {
|
|
922
|
+
return readJson(MILESTONES_FILE, []);
|
|
923
|
+
}
|
|
924
|
+
function registerTools(server2, session2) {
|
|
925
|
+
server2.tool(
|
|
926
|
+
"useai_session_start",
|
|
927
|
+
"Start tracking an AI coding session. Call this at the beginning of a conversation.",
|
|
928
|
+
{
|
|
929
|
+
task_type: z2.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task is the developer working on?")
|
|
930
|
+
},
|
|
931
|
+
async ({ task_type }) => {
|
|
932
|
+
session2.reset();
|
|
933
|
+
session2.setClient(detectClient());
|
|
934
|
+
session2.setTaskType(task_type ?? "coding");
|
|
935
|
+
const record = session2.appendToChain("session_start", {
|
|
936
|
+
client: session2.clientName,
|
|
937
|
+
task_type: session2.sessionTaskType,
|
|
938
|
+
version: VERSION
|
|
939
|
+
});
|
|
940
|
+
return {
|
|
941
|
+
content: [
|
|
942
|
+
{
|
|
943
|
+
type: "text",
|
|
944
|
+
text: `useai session started \u2014 tracking ${session2.sessionTaskType} session on ${session2.clientName}.
|
|
945
|
+
Session: ${session2.sessionId.slice(0, 8)} \xB7 Chain: ${record.hash.slice(0, 12)} (${session2.signingAvailable ? "signed" : "unsigned"})`
|
|
946
|
+
}
|
|
947
|
+
]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
server2.tool(
|
|
952
|
+
"useai_heartbeat",
|
|
953
|
+
"Record a heartbeat for the current AI coding session. Call this periodically during long conversations (every 10-15 minutes).",
|
|
954
|
+
{},
|
|
955
|
+
async () => {
|
|
956
|
+
session2.incrementHeartbeat();
|
|
957
|
+
session2.appendToChain("heartbeat", {
|
|
958
|
+
heartbeat_number: session2.heartbeatCount,
|
|
959
|
+
cumulative_seconds: session2.getSessionDuration()
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
content: [
|
|
963
|
+
{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: `Heartbeat recorded. Session active for ${formatDuration(session2.getSessionDuration())}.`
|
|
966
|
+
}
|
|
967
|
+
]
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
server2.tool(
|
|
972
|
+
"useai_session_end",
|
|
973
|
+
'End the current AI coding session and record milestones. CRITICAL PRIVACY REQUIREMENT: Milestone titles must be COMPLETELY GENERIC. NEVER include project names, repository names, product names, package names, file names, file paths, class names, API endpoints, database names, or company names. Write as if describing the work to a stranger with zero knowledge of the codebase. GOOD: "Implemented user authentication", "Fixed race condition in background worker", "Added unit tests for data validation". BAD: "Fixed bug in Acme auth service", "Updated UserService.ts", "Investigated ProjectX pipeline".',
|
|
974
|
+
{
|
|
975
|
+
task_type: z2.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task was the developer working on?"),
|
|
976
|
+
languages: z2.array(z2.string()).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
|
|
977
|
+
files_touched_count: z2.number().optional().describe("Approximate number of files created or modified (count only, no names)"),
|
|
978
|
+
milestones: z2.array(z2.object({
|
|
979
|
+
title: z2.string().describe("PRIVACY-CRITICAL: Generic description of what was accomplished. NEVER include project names, repo names, product names, package names, file names, file paths, class names, API endpoints, database names, company names, or ANY identifier that could reveal which codebase this work was done in. Write as if describing the work to a stranger. GOOD: 'Implemented user authentication', 'Fixed race condition in background worker', 'Added unit tests for data validation', 'Refactored state management layer'. BAD: 'Fixed bug in Acme auth', 'Investigated ProjectX pipeline', 'Updated UserService.ts in src/services/', 'Added tests for coverit MCP tool'"),
|
|
980
|
+
category: z2.enum(["feature", "bugfix", "refactor", "test", "docs", "setup", "deployment", "other"]).describe("Type of work completed"),
|
|
981
|
+
complexity: z2.enum(["simple", "medium", "complex"]).optional().describe("How complex was this task?")
|
|
982
|
+
})).optional().describe("What was accomplished this session? List each distinct piece of work completed. Describe generically \u2014 no project names, file names, or proprietary details.")
|
|
983
|
+
},
|
|
984
|
+
async ({ task_type, languages, files_touched_count, milestones: milestonesInput }) => {
|
|
985
|
+
const duration = session2.getSessionDuration();
|
|
986
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
987
|
+
const finalTaskType = task_type ?? session2.sessionTaskType;
|
|
988
|
+
const chainStartHash = session2.chainTipHash === "GENESIS" ? "GENESIS" : session2.chainTipHash;
|
|
989
|
+
const endRecord = session2.appendToChain("session_end", {
|
|
990
|
+
duration_seconds: duration,
|
|
991
|
+
task_type: finalTaskType,
|
|
992
|
+
languages: languages ?? [],
|
|
993
|
+
files_touched: files_touched_count ?? 0,
|
|
994
|
+
heartbeat_count: session2.heartbeatCount
|
|
995
|
+
});
|
|
996
|
+
const sealData = JSON.stringify({
|
|
997
|
+
session_id: session2.sessionId,
|
|
998
|
+
client: session2.clientName,
|
|
999
|
+
task_type: finalTaskType,
|
|
1000
|
+
languages: languages ?? [],
|
|
1001
|
+
files_touched: files_touched_count ?? 0,
|
|
1002
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
1003
|
+
ended_at: now,
|
|
1004
|
+
duration_seconds: duration,
|
|
1005
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1006
|
+
record_count: session2.sessionRecordCount,
|
|
1007
|
+
chain_end_hash: endRecord.hash
|
|
1008
|
+
});
|
|
1009
|
+
const sealSignature = signHash(
|
|
1010
|
+
createHash3("sha256").update(sealData).digest("hex"),
|
|
1011
|
+
session2.signingKey
|
|
1012
|
+
);
|
|
1013
|
+
session2.appendToChain("session_seal", {
|
|
1014
|
+
seal: sealData,
|
|
1015
|
+
seal_signature: sealSignature
|
|
1016
|
+
});
|
|
1017
|
+
const activePath = join4(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
|
|
1018
|
+
const sealedPath = join4(SEALED_DIR, `${session2.sessionId}.jsonl`);
|
|
1019
|
+
try {
|
|
1020
|
+
if (existsSync6(activePath)) {
|
|
1021
|
+
renameSync2(activePath, sealedPath);
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
const seal = {
|
|
1026
|
+
session_id: session2.sessionId,
|
|
1027
|
+
client: session2.clientName,
|
|
1028
|
+
task_type: finalTaskType,
|
|
1029
|
+
languages: languages ?? [],
|
|
1030
|
+
files_touched: files_touched_count ?? 0,
|
|
1031
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
1032
|
+
ended_at: now,
|
|
1033
|
+
duration_seconds: duration,
|
|
1034
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1035
|
+
record_count: session2.sessionRecordCount,
|
|
1036
|
+
chain_start_hash: chainStartHash,
|
|
1037
|
+
chain_end_hash: endRecord.hash,
|
|
1038
|
+
seal_signature: sealSignature
|
|
1039
|
+
};
|
|
1040
|
+
const sessions2 = getSessions();
|
|
1041
|
+
sessions2.push(seal);
|
|
1042
|
+
writeJson(SESSIONS_FILE, sessions2);
|
|
1043
|
+
let milestoneCount = 0;
|
|
1044
|
+
if (milestonesInput && milestonesInput.length > 0) {
|
|
1045
|
+
const config = getConfig();
|
|
1046
|
+
if (config.milestone_tracking) {
|
|
1047
|
+
const durationMinutes = Math.round(duration / 60);
|
|
1048
|
+
const allMilestones = getMilestones();
|
|
1049
|
+
for (const m of milestonesInput) {
|
|
1050
|
+
const record = session2.appendToChain("milestone", {
|
|
1051
|
+
title: m.title,
|
|
1052
|
+
category: m.category,
|
|
1053
|
+
complexity: m.complexity ?? "medium",
|
|
1054
|
+
duration_minutes: durationMinutes,
|
|
1055
|
+
languages: languages ?? []
|
|
1056
|
+
});
|
|
1057
|
+
const milestone = {
|
|
1058
|
+
id: `m_${randomUUID3().slice(0, 8)}`,
|
|
1059
|
+
session_id: session2.sessionId,
|
|
1060
|
+
title: m.title,
|
|
1061
|
+
category: m.category,
|
|
1062
|
+
complexity: m.complexity ?? "medium",
|
|
1063
|
+
duration_minutes: durationMinutes,
|
|
1064
|
+
languages: languages ?? [],
|
|
1065
|
+
client: session2.clientName,
|
|
1066
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1067
|
+
published: false,
|
|
1068
|
+
published_at: null,
|
|
1069
|
+
chain_hash: record.hash
|
|
1070
|
+
};
|
|
1071
|
+
allMilestones.push(milestone);
|
|
1072
|
+
milestoneCount++;
|
|
1073
|
+
}
|
|
1074
|
+
writeJson(MILESTONES_FILE, allMilestones);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const durationStr = formatDuration(duration);
|
|
1078
|
+
const langStr = languages && languages.length > 0 ? ` using ${languages.join(", ")}` : "";
|
|
1079
|
+
const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
|
|
1080
|
+
return {
|
|
1081
|
+
content: [
|
|
1082
|
+
{
|
|
1083
|
+
type: "text",
|
|
1084
|
+
text: `Session ended: ${durationStr} ${finalTaskType}${langStr}${milestoneStr}`
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
var init_register_tools = __esm({
|
|
1092
|
+
"src/register-tools.ts"() {
|
|
1093
|
+
"use strict";
|
|
1094
|
+
init_dist();
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// src/tools.ts
|
|
1099
|
+
import { execSync as execSync3 } from "child_process";
|
|
1100
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
1101
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1102
|
+
import { homedir as homedir3 } from "os";
|
|
17
1103
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
18
1104
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1105
|
+
function installStandardHttp(configPath) {
|
|
1106
|
+
const config = readJsonFile(configPath);
|
|
1107
|
+
const servers = config["mcpServers"] ?? {};
|
|
1108
|
+
delete servers["useai"];
|
|
1109
|
+
servers["UseAI"] = { ...MCP_HTTP_ENTRY };
|
|
1110
|
+
config["mcpServers"] = servers;
|
|
1111
|
+
writeJsonFile(configPath, config);
|
|
1112
|
+
}
|
|
1113
|
+
function installVscodeHttp(configPath) {
|
|
1114
|
+
const config = readJsonFile(configPath);
|
|
1115
|
+
const servers = config["servers"] ?? {};
|
|
1116
|
+
delete servers["useai"];
|
|
1117
|
+
servers["UseAI"] = { type: "http", url: MCP_HTTP_URL };
|
|
1118
|
+
config["servers"] = servers;
|
|
1119
|
+
writeJsonFile(configPath, config);
|
|
1120
|
+
}
|
|
19
1121
|
function hasBinary(name) {
|
|
20
1122
|
try {
|
|
21
|
-
|
|
1123
|
+
execSync3(`which ${name}`, { stdio: "ignore" });
|
|
22
1124
|
return true;
|
|
23
1125
|
} catch {
|
|
24
1126
|
return false;
|
|
25
1127
|
}
|
|
26
1128
|
}
|
|
27
1129
|
function readJsonFile(path) {
|
|
28
|
-
if (!
|
|
1130
|
+
if (!existsSync7(path)) return {};
|
|
29
1131
|
try {
|
|
30
|
-
const raw =
|
|
1132
|
+
const raw = readFileSync3(path, "utf-8").trim();
|
|
31
1133
|
if (!raw) return {};
|
|
32
1134
|
return JSON.parse(raw);
|
|
33
1135
|
} catch {
|
|
@@ -35,8 +1137,8 @@ function readJsonFile(path) {
|
|
|
35
1137
|
}
|
|
36
1138
|
}
|
|
37
1139
|
function writeJsonFile(path, data) {
|
|
38
|
-
|
|
39
|
-
|
|
1140
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1141
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + "\n");
|
|
40
1142
|
}
|
|
41
1143
|
function isConfiguredStandard(configPath) {
|
|
42
1144
|
const config = readJsonFile(configPath);
|
|
@@ -117,9 +1219,9 @@ function removeZed(configPath) {
|
|
|
117
1219
|
}
|
|
118
1220
|
}
|
|
119
1221
|
function readTomlFile(path) {
|
|
120
|
-
if (!
|
|
1222
|
+
if (!existsSync7(path)) return {};
|
|
121
1223
|
try {
|
|
122
|
-
const raw =
|
|
1224
|
+
const raw = readFileSync3(path, "utf-8").trim();
|
|
123
1225
|
if (!raw) return {};
|
|
124
1226
|
return parseToml(raw);
|
|
125
1227
|
} catch {
|
|
@@ -127,8 +1229,8 @@ function readTomlFile(path) {
|
|
|
127
1229
|
}
|
|
128
1230
|
}
|
|
129
1231
|
function writeTomlFile(path, data) {
|
|
130
|
-
|
|
131
|
-
|
|
1232
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1233
|
+
writeFileSync3(path, stringifyToml(data) + "\n");
|
|
132
1234
|
}
|
|
133
1235
|
function isConfiguredToml(configPath) {
|
|
134
1236
|
const config = readTomlFile(configPath);
|
|
@@ -156,9 +1258,9 @@ function removeToml(configPath) {
|
|
|
156
1258
|
}
|
|
157
1259
|
}
|
|
158
1260
|
function readYamlFile(path) {
|
|
159
|
-
if (!
|
|
1261
|
+
if (!existsSync7(path)) return {};
|
|
160
1262
|
try {
|
|
161
|
-
const raw =
|
|
1263
|
+
const raw = readFileSync3(path, "utf-8").trim();
|
|
162
1264
|
if (!raw) return {};
|
|
163
1265
|
return parseYaml(raw) ?? {};
|
|
164
1266
|
} catch {
|
|
@@ -166,8 +1268,8 @@ function readYamlFile(path) {
|
|
|
166
1268
|
}
|
|
167
1269
|
}
|
|
168
1270
|
function writeYamlFile(path, data) {
|
|
169
|
-
|
|
170
|
-
|
|
1271
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1272
|
+
writeFileSync3(path, stringifyYaml(data));
|
|
171
1273
|
}
|
|
172
1274
|
function isConfiguredYaml(configPath) {
|
|
173
1275
|
const config = readYamlFile(configPath);
|
|
@@ -201,59 +1303,61 @@ function removeYaml(configPath) {
|
|
|
201
1303
|
}
|
|
202
1304
|
}
|
|
203
1305
|
function hasInstructionsBlock(filePath) {
|
|
204
|
-
if (!
|
|
1306
|
+
if (!existsSync7(filePath)) return false;
|
|
205
1307
|
try {
|
|
206
|
-
return
|
|
1308
|
+
return readFileSync3(filePath, "utf-8").includes(INSTRUCTIONS_START);
|
|
207
1309
|
} catch {
|
|
208
1310
|
return false;
|
|
209
1311
|
}
|
|
210
1312
|
}
|
|
211
1313
|
function injectInstructions(config) {
|
|
212
|
-
|
|
1314
|
+
mkdirSync3(dirname2(config.path), { recursive: true });
|
|
213
1315
|
if (config.method === "create") {
|
|
214
|
-
|
|
1316
|
+
writeFileSync3(config.path, USEAI_INSTRUCTIONS + "\n");
|
|
215
1317
|
return;
|
|
216
1318
|
}
|
|
217
1319
|
if (hasInstructionsBlock(config.path)) return;
|
|
218
1320
|
let existing = "";
|
|
219
|
-
if (
|
|
220
|
-
existing =
|
|
1321
|
+
if (existsSync7(config.path)) {
|
|
1322
|
+
existing = readFileSync3(config.path, "utf-8");
|
|
221
1323
|
}
|
|
222
1324
|
const separator = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
|
|
223
|
-
|
|
1325
|
+
writeFileSync3(config.path, existing + separator + USEAI_INSTRUCTIONS_BLOCK + "\n");
|
|
224
1326
|
}
|
|
225
1327
|
function removeInstructions(config) {
|
|
226
1328
|
if (config.method === "create") {
|
|
227
|
-
if (
|
|
1329
|
+
if (existsSync7(config.path)) {
|
|
228
1330
|
try {
|
|
229
|
-
|
|
1331
|
+
unlinkSync3(config.path);
|
|
230
1332
|
} catch {
|
|
231
1333
|
}
|
|
232
1334
|
}
|
|
233
1335
|
return;
|
|
234
1336
|
}
|
|
235
|
-
if (!
|
|
1337
|
+
if (!existsSync7(config.path)) return;
|
|
236
1338
|
try {
|
|
237
|
-
const content =
|
|
1339
|
+
const content = readFileSync3(config.path, "utf-8");
|
|
238
1340
|
const escaped = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
239
1341
|
const regex = new RegExp(
|
|
240
1342
|
`\\n?${escaped(INSTRUCTIONS_START)}[\\s\\S]*?${escaped(INSTRUCTIONS_END)}\\n?`
|
|
241
1343
|
);
|
|
242
1344
|
const cleaned = content.replace(regex, "").trim();
|
|
243
1345
|
if (cleaned) {
|
|
244
|
-
|
|
1346
|
+
writeFileSync3(config.path, cleaned + "\n");
|
|
245
1347
|
} else {
|
|
246
|
-
|
|
1348
|
+
unlinkSync3(config.path);
|
|
247
1349
|
}
|
|
248
1350
|
} catch {
|
|
249
1351
|
}
|
|
250
1352
|
}
|
|
251
1353
|
function createTool(def) {
|
|
252
1354
|
const handler = formatHandlers[def.configFormat];
|
|
1355
|
+
const urlSupported = def.supportsUrl ?? false;
|
|
253
1356
|
return {
|
|
254
1357
|
id: def.id,
|
|
255
1358
|
name: def.name,
|
|
256
1359
|
configFormat: def.configFormat,
|
|
1360
|
+
supportsUrl: urlSupported,
|
|
257
1361
|
getConfigPath: () => def.configPath,
|
|
258
1362
|
detect: def.detect,
|
|
259
1363
|
isConfigured: () => handler.isConfigured(def.configPath),
|
|
@@ -261,6 +1365,16 @@ function createTool(def) {
|
|
|
261
1365
|
handler.install(def.configPath);
|
|
262
1366
|
if (def.instructions) injectInstructions(def.instructions);
|
|
263
1367
|
},
|
|
1368
|
+
installHttp: () => {
|
|
1369
|
+
if (def.configFormat === "vscode") {
|
|
1370
|
+
installVscodeHttp(def.configPath);
|
|
1371
|
+
} else if (def.configFormat === "standard") {
|
|
1372
|
+
installStandardHttp(def.configPath);
|
|
1373
|
+
} else {
|
|
1374
|
+
handler.install(def.configPath);
|
|
1375
|
+
}
|
|
1376
|
+
if (def.instructions) injectInstructions(def.instructions);
|
|
1377
|
+
},
|
|
264
1378
|
remove: () => {
|
|
265
1379
|
handler.remove(def.configPath);
|
|
266
1380
|
if (def.instructions) removeInstructions(def.instructions);
|
|
@@ -289,28 +1403,33 @@ function resolveTools(names) {
|
|
|
289
1403
|
}
|
|
290
1404
|
return { matched, unmatched };
|
|
291
1405
|
}
|
|
292
|
-
var USEAI_INSTRUCTIONS_TEXT, MCP_ENTRY, home, INSTRUCTIONS_START, INSTRUCTIONS_END, USEAI_INSTRUCTIONS, USEAI_INSTRUCTIONS_BLOCK, formatHandlers, appSupport, AI_TOOLS;
|
|
1406
|
+
var USEAI_INSTRUCTIONS_TEXT, MCP_ENTRY, MCP_HTTP_URL, MCP_HTTP_ENTRY, home, INSTRUCTIONS_START, INSTRUCTIONS_END, USEAI_INSTRUCTIONS, USEAI_INSTRUCTIONS_BLOCK, formatHandlers, appSupport, AI_TOOLS;
|
|
293
1407
|
var init_tools = __esm({
|
|
294
1408
|
"src/tools.ts"() {
|
|
295
1409
|
"use strict";
|
|
1410
|
+
init_dist();
|
|
296
1411
|
USEAI_INSTRUCTIONS_TEXT = [
|
|
297
1412
|
"## UseAI Session Tracking",
|
|
298
1413
|
"- At the START of every conversation, call `useai_session_start` with the appropriate task_type.",
|
|
299
1414
|
"- For LONG sessions (15+ min), call `useai_heartbeat` periodically.",
|
|
300
|
-
"- At the END of every conversation, call `useai_session_end` with languages used, files_touched_count, and milestones
|
|
1415
|
+
"- At the END of every conversation, call `useai_session_end` with languages used, files_touched_count, and milestones.",
|
|
1416
|
+
'- MILESTONES PRIVACY: Milestone titles must be COMPLETELY GENERIC. NEVER mention project names, repository names, product names, file names, file paths, class names, or company names. Write as if describing the work to a stranger who knows nothing about the codebase. GOOD: "Fixed race condition in background worker". BAD: "Fixed bug in ProjectX worker service".'
|
|
301
1417
|
].join("\n");
|
|
302
1418
|
MCP_ENTRY = {
|
|
303
1419
|
command: "npx",
|
|
304
1420
|
args: ["-y", "@devness/useai"]
|
|
305
1421
|
};
|
|
306
|
-
|
|
1422
|
+
MCP_HTTP_URL = DAEMON_MCP_URL;
|
|
1423
|
+
MCP_HTTP_ENTRY = { url: MCP_HTTP_URL };
|
|
1424
|
+
home = homedir3();
|
|
307
1425
|
INSTRUCTIONS_START = "<!-- useai:start -->";
|
|
308
1426
|
INSTRUCTIONS_END = "<!-- useai:end -->";
|
|
309
1427
|
USEAI_INSTRUCTIONS = [
|
|
310
1428
|
"## UseAI Session Tracking",
|
|
311
1429
|
"- At the START of every conversation, call `useai_session_start` with the appropriate task_type.",
|
|
312
1430
|
"- For LONG sessions (15+ min), call `useai_heartbeat` periodically.",
|
|
313
|
-
"- At the END of every conversation, call `useai_session_end` with languages used, files_touched_count, and milestones
|
|
1431
|
+
"- At the END of every conversation, call `useai_session_end` with languages used, files_touched_count, and milestones.",
|
|
1432
|
+
'- MILESTONES PRIVACY: Milestone titles must be COMPLETELY GENERIC. NEVER mention project names, repository names, product names, file names, file paths, class names, or company names. Write as if describing the work to a stranger who knows nothing about the codebase. GOOD: "Fixed race condition in background worker". BAD: "Fixed bug in ProjectX worker service".'
|
|
314
1433
|
].join("\n");
|
|
315
1434
|
USEAI_INSTRUCTIONS_BLOCK = `${INSTRUCTIONS_START}
|
|
316
1435
|
${USEAI_INSTRUCTIONS}
|
|
@@ -322,69 +1441,75 @@ ${INSTRUCTIONS_END}`;
|
|
|
322
1441
|
toml: { isConfigured: isConfiguredToml, install: installToml, remove: removeToml },
|
|
323
1442
|
yaml: { isConfigured: isConfiguredYaml, install: installYaml, remove: removeYaml }
|
|
324
1443
|
};
|
|
325
|
-
appSupport =
|
|
1444
|
+
appSupport = join5(home, "Library", "Application Support");
|
|
326
1445
|
AI_TOOLS = [
|
|
327
1446
|
createTool({
|
|
328
1447
|
id: "claude-code",
|
|
329
1448
|
name: "Claude Code",
|
|
330
1449
|
configFormat: "standard",
|
|
331
|
-
configPath:
|
|
332
|
-
detect: () => hasBinary("claude") ||
|
|
333
|
-
instructions: { method: "append", path:
|
|
1450
|
+
configPath: join5(home, ".claude.json"),
|
|
1451
|
+
detect: () => hasBinary("claude") || existsSync7(join5(home, ".claude.json")),
|
|
1452
|
+
instructions: { method: "append", path: join5(home, ".claude", "CLAUDE.md") },
|
|
1453
|
+
supportsUrl: true
|
|
334
1454
|
}),
|
|
335
1455
|
createTool({
|
|
336
1456
|
id: "cursor",
|
|
337
1457
|
name: "Cursor",
|
|
338
1458
|
configFormat: "standard",
|
|
339
|
-
configPath:
|
|
340
|
-
detect: () =>
|
|
341
|
-
manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below."
|
|
1459
|
+
configPath: join5(home, ".cursor", "mcp.json"),
|
|
1460
|
+
detect: () => existsSync7(join5(home, ".cursor")),
|
|
1461
|
+
manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below.",
|
|
1462
|
+
supportsUrl: true
|
|
342
1463
|
}),
|
|
343
1464
|
createTool({
|
|
344
1465
|
id: "windsurf",
|
|
345
1466
|
name: "Windsurf",
|
|
346
1467
|
configFormat: "standard",
|
|
347
|
-
configPath:
|
|
348
|
-
detect: () =>
|
|
349
|
-
instructions: { method: "append", path:
|
|
1468
|
+
configPath: join5(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
1469
|
+
detect: () => existsSync7(join5(home, ".codeium", "windsurf")),
|
|
1470
|
+
instructions: { method: "append", path: join5(home, ".codeium", "windsurf", "memories", "global_rules.md") },
|
|
1471
|
+
supportsUrl: true
|
|
350
1472
|
}),
|
|
351
1473
|
createTool({
|
|
352
1474
|
id: "vscode",
|
|
353
1475
|
name: "VS Code",
|
|
354
1476
|
configFormat: "vscode",
|
|
355
|
-
configPath:
|
|
356
|
-
detect: () =>
|
|
357
|
-
instructions: { method: "create", path:
|
|
1477
|
+
configPath: join5(appSupport, "Code", "User", "mcp.json"),
|
|
1478
|
+
detect: () => existsSync7(join5(appSupport, "Code")),
|
|
1479
|
+
instructions: { method: "create", path: join5(appSupport, "Code", "User", "prompts", "useai.instructions.md") },
|
|
1480
|
+
supportsUrl: true
|
|
358
1481
|
}),
|
|
359
1482
|
createTool({
|
|
360
1483
|
id: "vscode-insiders",
|
|
361
1484
|
name: "VS Code Insiders",
|
|
362
1485
|
configFormat: "vscode",
|
|
363
|
-
configPath:
|
|
364
|
-
detect: () =>
|
|
365
|
-
instructions: { method: "create", path:
|
|
1486
|
+
configPath: join5(appSupport, "Code - Insiders", "User", "mcp.json"),
|
|
1487
|
+
detect: () => existsSync7(join5(appSupport, "Code - Insiders")),
|
|
1488
|
+
instructions: { method: "create", path: join5(appSupport, "Code - Insiders", "User", "prompts", "useai.instructions.md") },
|
|
1489
|
+
supportsUrl: true
|
|
366
1490
|
}),
|
|
367
1491
|
createTool({
|
|
368
1492
|
id: "gemini-cli",
|
|
369
1493
|
name: "Gemini CLI",
|
|
370
1494
|
configFormat: "standard",
|
|
371
|
-
configPath:
|
|
1495
|
+
configPath: join5(home, ".gemini", "settings.json"),
|
|
372
1496
|
detect: () => hasBinary("gemini"),
|
|
373
|
-
instructions: { method: "append", path:
|
|
1497
|
+
instructions: { method: "append", path: join5(home, ".gemini", "GEMINI.md") },
|
|
1498
|
+
supportsUrl: true
|
|
374
1499
|
}),
|
|
375
1500
|
createTool({
|
|
376
1501
|
id: "zed",
|
|
377
1502
|
name: "Zed",
|
|
378
1503
|
configFormat: "zed",
|
|
379
|
-
configPath:
|
|
380
|
-
detect: () =>
|
|
1504
|
+
configPath: join5(appSupport, "Zed", "settings.json"),
|
|
1505
|
+
detect: () => existsSync7(join5(appSupport, "Zed")),
|
|
381
1506
|
manualHint: "Open Rules Library (\u2318\u2325L) \u2192 click + \u2192 paste the instructions below."
|
|
382
1507
|
}),
|
|
383
1508
|
createTool({
|
|
384
1509
|
id: "cline",
|
|
385
1510
|
name: "Cline",
|
|
386
1511
|
configFormat: "standard",
|
|
387
|
-
configPath:
|
|
1512
|
+
configPath: join5(
|
|
388
1513
|
appSupport,
|
|
389
1514
|
"Code",
|
|
390
1515
|
"User",
|
|
@@ -393,16 +1518,17 @@ ${INSTRUCTIONS_END}`;
|
|
|
393
1518
|
"settings",
|
|
394
1519
|
"cline_mcp_settings.json"
|
|
395
1520
|
),
|
|
396
|
-
detect: () =>
|
|
397
|
-
|
|
1521
|
+
detect: () => existsSync7(
|
|
1522
|
+
join5(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
|
|
398
1523
|
),
|
|
399
|
-
instructions: { method: "create", path:
|
|
1524
|
+
instructions: { method: "create", path: join5(home, "Documents", "Cline", "Rules", "useai.md") },
|
|
1525
|
+
supportsUrl: true
|
|
400
1526
|
}),
|
|
401
1527
|
createTool({
|
|
402
1528
|
id: "roo-code",
|
|
403
1529
|
name: "Roo Code",
|
|
404
1530
|
configFormat: "standard",
|
|
405
|
-
configPath:
|
|
1531
|
+
configPath: join5(
|
|
406
1532
|
appSupport,
|
|
407
1533
|
"Code",
|
|
408
1534
|
"User",
|
|
@@ -411,57 +1537,59 @@ ${INSTRUCTIONS_END}`;
|
|
|
411
1537
|
"settings",
|
|
412
1538
|
"cline_mcp_settings.json"
|
|
413
1539
|
),
|
|
414
|
-
detect: () =>
|
|
415
|
-
|
|
1540
|
+
detect: () => existsSync7(
|
|
1541
|
+
join5(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
|
|
416
1542
|
),
|
|
417
|
-
instructions: { method: "create", path:
|
|
1543
|
+
instructions: { method: "create", path: join5(home, ".roo", "rules", "useai.md") },
|
|
1544
|
+
supportsUrl: true
|
|
418
1545
|
}),
|
|
419
1546
|
createTool({
|
|
420
1547
|
id: "amazon-q-cli",
|
|
421
1548
|
name: "Amazon Q CLI",
|
|
422
1549
|
configFormat: "standard",
|
|
423
|
-
configPath:
|
|
424
|
-
detect: () => hasBinary("q") ||
|
|
1550
|
+
configPath: join5(home, ".aws", "amazonq", "mcp.json"),
|
|
1551
|
+
detect: () => hasBinary("q") || existsSync7(join5(home, ".aws", "amazonq")),
|
|
425
1552
|
manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
|
|
426
1553
|
}),
|
|
427
1554
|
createTool({
|
|
428
1555
|
id: "amazon-q-ide",
|
|
429
1556
|
name: "Amazon Q IDE",
|
|
430
1557
|
configFormat: "standard",
|
|
431
|
-
configPath:
|
|
432
|
-
detect: () =>
|
|
1558
|
+
configPath: join5(home, ".aws", "amazonq", "default.json"),
|
|
1559
|
+
detect: () => existsSync7(join5(home, ".amazonq")) || existsSync7(join5(home, ".aws", "amazonq")),
|
|
433
1560
|
manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
|
|
434
1561
|
}),
|
|
435
1562
|
createTool({
|
|
436
1563
|
id: "codex",
|
|
437
1564
|
name: "Codex",
|
|
438
1565
|
configFormat: "toml",
|
|
439
|
-
configPath:
|
|
440
|
-
detect: () => hasBinary("codex") ||
|
|
441
|
-
instructions: { method: "append", path:
|
|
1566
|
+
configPath: join5(home, ".codex", "config.toml"),
|
|
1567
|
+
detect: () => hasBinary("codex") || existsSync7(join5(home, ".codex")) || existsSync7("/Applications/Codex.app"),
|
|
1568
|
+
instructions: { method: "append", path: join5(home, ".codex", "AGENTS.md") }
|
|
442
1569
|
}),
|
|
443
1570
|
createTool({
|
|
444
1571
|
id: "goose",
|
|
445
1572
|
name: "Goose",
|
|
446
1573
|
configFormat: "yaml",
|
|
447
|
-
configPath:
|
|
448
|
-
detect: () =>
|
|
449
|
-
instructions: { method: "append", path:
|
|
1574
|
+
configPath: join5(home, ".config", "goose", "config.yaml"),
|
|
1575
|
+
detect: () => existsSync7(join5(home, ".config", "goose")),
|
|
1576
|
+
instructions: { method: "append", path: join5(home, ".config", "goose", ".goosehints") }
|
|
450
1577
|
}),
|
|
451
1578
|
createTool({
|
|
452
1579
|
id: "opencode",
|
|
453
1580
|
name: "OpenCode",
|
|
454
1581
|
configFormat: "standard",
|
|
455
|
-
configPath:
|
|
456
|
-
detect: () => hasBinary("opencode") ||
|
|
457
|
-
instructions: { method: "append", path:
|
|
1582
|
+
configPath: join5(home, ".config", "opencode", "opencode.json"),
|
|
1583
|
+
detect: () => hasBinary("opencode") || existsSync7(join5(home, ".config", "opencode")),
|
|
1584
|
+
instructions: { method: "append", path: join5(home, ".config", "opencode", "AGENTS.md") },
|
|
1585
|
+
supportsUrl: true
|
|
458
1586
|
}),
|
|
459
1587
|
createTool({
|
|
460
1588
|
id: "junie",
|
|
461
1589
|
name: "Junie",
|
|
462
1590
|
configFormat: "standard",
|
|
463
|
-
configPath:
|
|
464
|
-
detect: () =>
|
|
1591
|
+
configPath: join5(home, ".junie", "mcp", "mcp.json"),
|
|
1592
|
+
detect: () => existsSync7(join5(home, ".junie")),
|
|
465
1593
|
manualHint: "Add the instructions below to .junie/guidelines.md in your project root."
|
|
466
1594
|
})
|
|
467
1595
|
];
|
|
@@ -531,7 +1659,59 @@ function showStatus(tools) {
|
|
|
531
1659
|
console.log(rows.join("\n"));
|
|
532
1660
|
console.log();
|
|
533
1661
|
}
|
|
534
|
-
async function
|
|
1662
|
+
async function daemonInstallFlow(tools, explicit) {
|
|
1663
|
+
console.log(dim("Ensuring UseAI daemon is running..."));
|
|
1664
|
+
const daemonOk = await ensureDaemon();
|
|
1665
|
+
let useDaemon = true;
|
|
1666
|
+
if (daemonOk) {
|
|
1667
|
+
console.log(ok(`\u2713 Daemon running on port ${DAEMON_PORT}`));
|
|
1668
|
+
} else {
|
|
1669
|
+
useDaemon = false;
|
|
1670
|
+
console.log(err("\u2717 Could not start daemon \u2014 falling back to stdio config"));
|
|
1671
|
+
console.log(dim(`(Run with --foreground to debug: npx @devness/useai daemon --port ${DAEMON_PORT})`));
|
|
1672
|
+
}
|
|
1673
|
+
if (useDaemon) {
|
|
1674
|
+
const platform = detectPlatform();
|
|
1675
|
+
if (platform !== "unsupported") {
|
|
1676
|
+
try {
|
|
1677
|
+
installAutostart();
|
|
1678
|
+
console.log(ok(`\u2713 Auto-start service installed (${platform})`));
|
|
1679
|
+
} catch {
|
|
1680
|
+
console.log(chalk.yellow(` \u26A0 Could not install auto-start service`));
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const targetTools = explicit ? tools : tools.filter((t) => t.detect());
|
|
1685
|
+
if (targetTools.length === 0) {
|
|
1686
|
+
console.log(err("\n No AI tools detected on this machine."));
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
let configuredCount = 0;
|
|
1690
|
+
console.log();
|
|
1691
|
+
for (const tool of targetTools) {
|
|
1692
|
+
try {
|
|
1693
|
+
if (useDaemon && tool.supportsUrl) {
|
|
1694
|
+
tool.installHttp();
|
|
1695
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("HTTP (daemon)")}`));
|
|
1696
|
+
} else if (useDaemon && !tool.supportsUrl) {
|
|
1697
|
+
tool.install();
|
|
1698
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("stdio (no URL support)")}`));
|
|
1699
|
+
} else {
|
|
1700
|
+
tool.install();
|
|
1701
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("stdio")}`));
|
|
1702
|
+
}
|
|
1703
|
+
configuredCount++;
|
|
1704
|
+
} catch (e) {
|
|
1705
|
+
console.log(err(`\u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
showManualHints(targetTools);
|
|
1709
|
+
const mode = useDaemon ? "daemon mode" : "stdio mode";
|
|
1710
|
+
console.log(`
|
|
1711
|
+
Done! UseAI configured in ${chalk.bold(String(configuredCount))} tool${configuredCount === 1 ? "" : "s"} (${mode}).
|
|
1712
|
+
`);
|
|
1713
|
+
}
|
|
1714
|
+
async function stdioInstallFlow(tools, autoYes, explicit) {
|
|
535
1715
|
if (explicit) {
|
|
536
1716
|
console.log();
|
|
537
1717
|
for (const tool of tools) {
|
|
@@ -618,9 +1798,9 @@ async function installFlow(tools, autoYes, explicit) {
|
|
|
618
1798
|
Done! UseAI MCP server configured in ${chalk.bold(String(toInstall.length))} tool${toInstall.length === 1 ? "" : "s"}.
|
|
619
1799
|
`);
|
|
620
1800
|
}
|
|
621
|
-
async function
|
|
1801
|
+
async function fullRemoveFlow(tools, autoYes, explicit) {
|
|
622
1802
|
if (explicit) {
|
|
623
|
-
const
|
|
1803
|
+
const toRemove = tools.filter((t) => {
|
|
624
1804
|
try {
|
|
625
1805
|
return t.isConfigured();
|
|
626
1806
|
} catch {
|
|
@@ -637,83 +1817,98 @@ async function removeFlow(tools, autoYes, explicit) {
|
|
|
637
1817
|
for (const tool of notConfigured) {
|
|
638
1818
|
console.log(dim(`${tool.name} is not configured \u2014 skipping.`));
|
|
639
1819
|
}
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1820
|
+
if (toRemove.length > 0) {
|
|
1821
|
+
console.log();
|
|
1822
|
+
for (const tool of toRemove) {
|
|
1823
|
+
try {
|
|
1824
|
+
tool.remove();
|
|
1825
|
+
console.log(ok(`\u2713 Removed from ${tool.name}`));
|
|
1826
|
+
} catch (e) {
|
|
1827
|
+
console.log(err(`\u2717 ${tool.name} \u2014 ${e.message}`));
|
|
1828
|
+
}
|
|
648
1829
|
}
|
|
649
1830
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
console.log(`
|
|
1831
|
+
} else {
|
|
1832
|
+
const configured = tools.filter((t) => {
|
|
1833
|
+
try {
|
|
1834
|
+
return t.isConfigured();
|
|
1835
|
+
} catch {
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
if (configured.length === 0) {
|
|
1840
|
+
console.log(dim("UseAI is not configured in any AI tools."));
|
|
1841
|
+
} else {
|
|
1842
|
+
console.log(`
|
|
665
1843
|
Found UseAI configured in ${chalk.bold(String(configured.length))} tool${configured.length === 1 ? "" : "s"}:
|
|
666
1844
|
`);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1845
|
+
let toRemove;
|
|
1846
|
+
if (autoYes) {
|
|
1847
|
+
toRemove = configured;
|
|
1848
|
+
} else {
|
|
1849
|
+
let selected;
|
|
1850
|
+
try {
|
|
1851
|
+
selected = await checkbox({
|
|
1852
|
+
message: "Select tools to remove UseAI from:",
|
|
1853
|
+
choices: configured.map((t) => ({
|
|
1854
|
+
name: t.name,
|
|
1855
|
+
value: t.id,
|
|
1856
|
+
checked: true
|
|
1857
|
+
}))
|
|
1858
|
+
});
|
|
1859
|
+
} catch {
|
|
1860
|
+
console.log("\n");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
toRemove = configured.filter((t) => selected.includes(t.id));
|
|
1864
|
+
}
|
|
1865
|
+
if (toRemove.length === 0) {
|
|
1866
|
+
console.log(dim("No tools selected."));
|
|
1867
|
+
} else {
|
|
1868
|
+
console.log(`
|
|
1869
|
+
Removing from ${toRemove.length} tool${toRemove.length === 1 ? "" : "s"}...
|
|
1870
|
+
`);
|
|
1871
|
+
for (const tool of toRemove) {
|
|
1872
|
+
try {
|
|
1873
|
+
tool.remove();
|
|
1874
|
+
console.log(ok(`\u2713 Removed from ${tool.name}`));
|
|
1875
|
+
} catch (e) {
|
|
1876
|
+
console.log(err(`\u2717 ${tool.name} \u2014 ${e.message}`));
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
684
1880
|
}
|
|
685
|
-
toRemove = configured.filter((t) => selected.includes(t.id));
|
|
686
1881
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1882
|
+
console.log();
|
|
1883
|
+
try {
|
|
1884
|
+
await killDaemon();
|
|
1885
|
+
console.log(ok("\u2713 Daemon stopped"));
|
|
1886
|
+
} catch {
|
|
1887
|
+
console.log(dim("Daemon was not running"));
|
|
690
1888
|
}
|
|
691
|
-
|
|
692
|
-
Removing from ${toRemove.length} tool${toRemove.length === 1 ? "" : "s"}...
|
|
693
|
-
`);
|
|
694
|
-
for (const tool of toRemove) {
|
|
1889
|
+
if (isAutostartInstalled()) {
|
|
695
1890
|
try {
|
|
696
|
-
|
|
697
|
-
console.log(ok(
|
|
698
|
-
} catch
|
|
699
|
-
console.log(err(
|
|
1891
|
+
removeAutostart();
|
|
1892
|
+
console.log(ok("\u2713 Auto-start service removed"));
|
|
1893
|
+
} catch {
|
|
1894
|
+
console.log(err("\u2717 Failed to remove auto-start service"));
|
|
700
1895
|
}
|
|
701
1896
|
}
|
|
702
|
-
console.log(
|
|
703
|
-
Done! UseAI removed from ${chalk.bold(String(toRemove.length))} tool${toRemove.length === 1 ? "" : "s"}.
|
|
704
|
-
`);
|
|
1897
|
+
console.log(dim("\nDone! UseAI fully removed.\n"));
|
|
705
1898
|
}
|
|
706
1899
|
function showHelp() {
|
|
707
1900
|
console.log(`
|
|
708
1901
|
${chalk.bold("Usage:")} npx @devness/useai mcp [tools...] [options]
|
|
709
1902
|
|
|
710
1903
|
Configure UseAI MCP server in your AI tools.
|
|
1904
|
+
Default: starts daemon, installs auto-start, configures tools with HTTP.
|
|
711
1905
|
|
|
712
1906
|
${chalk.bold("Arguments:")}
|
|
713
1907
|
tools Specific tool names (e.g. codex cursor vscode)
|
|
714
1908
|
|
|
715
1909
|
${chalk.bold("Options:")}
|
|
716
|
-
--
|
|
1910
|
+
--stdio Use stdio config (legacy mode for containers/CI)
|
|
1911
|
+
--remove Remove UseAI from configured tools, stop daemon, remove auto-start
|
|
717
1912
|
--status Show configuration status without modifying
|
|
718
1913
|
-y, --yes Skip confirmation, auto-select all detected tools
|
|
719
1914
|
-h, --help Show this help message
|
|
@@ -728,6 +1923,7 @@ async function runSetup(args) {
|
|
|
728
1923
|
}
|
|
729
1924
|
const isRemove = flags.has("--remove");
|
|
730
1925
|
const isStatus = flags.has("--status");
|
|
1926
|
+
const isStdio = flags.has("--stdio");
|
|
731
1927
|
const autoYes = flags.has("-y") || flags.has("--yes");
|
|
732
1928
|
const explicit = toolNames.length > 0;
|
|
733
1929
|
let tools = AI_TOOLS;
|
|
@@ -743,508 +1939,312 @@ async function runSetup(args) {
|
|
|
743
1939
|
if (isStatus) {
|
|
744
1940
|
showStatus(tools);
|
|
745
1941
|
} else if (isRemove) {
|
|
746
|
-
await
|
|
1942
|
+
await fullRemoveFlow(tools, autoYes, explicit);
|
|
1943
|
+
} else if (isStdio) {
|
|
1944
|
+
await stdioInstallFlow(tools, autoYes, explicit);
|
|
747
1945
|
} else {
|
|
748
|
-
await
|
|
1946
|
+
await daemonInstallFlow(tools, explicit);
|
|
749
1947
|
}
|
|
750
1948
|
}
|
|
751
1949
|
var init_setup = __esm({
|
|
752
1950
|
"src/setup.ts"() {
|
|
753
1951
|
"use strict";
|
|
1952
|
+
init_dist();
|
|
754
1953
|
init_tools();
|
|
755
1954
|
}
|
|
756
1955
|
});
|
|
757
1956
|
|
|
758
|
-
// src/
|
|
1957
|
+
// src/daemon.ts
|
|
1958
|
+
var daemon_exports = {};
|
|
1959
|
+
__export(daemon_exports, {
|
|
1960
|
+
startDaemon: () => startDaemon
|
|
1961
|
+
});
|
|
1962
|
+
import { createServer } from "http";
|
|
1963
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
1964
|
+
import { existsSync as existsSync8, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
1965
|
+
import { join as join6 } from "path";
|
|
759
1966
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
760
|
-
import {
|
|
761
|
-
import {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
// ../shared/dist/crypto/derive.js
|
|
800
|
-
import { pbkdf2Sync } from "crypto";
|
|
801
|
-
import { hostname, userInfo } from "os";
|
|
802
|
-
function deriveEncryptionKey(salt) {
|
|
803
|
-
const machineData = `${hostname()}:${userInfo().username}:useai-keystore`;
|
|
804
|
-
return pbkdf2Sync(machineData, salt, 1e5, 32, "sha256");
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// ../shared/dist/crypto/keystore.js
|
|
808
|
-
function decryptKeystore(ks) {
|
|
809
|
-
const salt = Buffer.from(ks.salt, "hex");
|
|
810
|
-
const iv = Buffer.from(ks.iv, "hex");
|
|
811
|
-
const tag = Buffer.from(ks.tag, "hex");
|
|
812
|
-
const encrypted = Buffer.from(ks.encrypted_private_key, "hex");
|
|
813
|
-
const encKey = deriveEncryptionKey(salt);
|
|
814
|
-
const decipher = createDecipheriv("aes-256-gcm", encKey, iv);
|
|
815
|
-
decipher.setAuthTag(tag);
|
|
816
|
-
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
817
|
-
return createPrivateKey(decrypted.toString("utf-8"));
|
|
818
|
-
}
|
|
819
|
-
function generateKeystore() {
|
|
820
|
-
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
821
|
-
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
822
|
-
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
823
|
-
const salt = randomBytes(32);
|
|
824
|
-
const encKey = deriveEncryptionKey(salt);
|
|
825
|
-
const iv = randomBytes(12);
|
|
826
|
-
const cipher = createCipheriv("aes-256-gcm", encKey, iv);
|
|
827
|
-
const encrypted = Buffer.concat([cipher.update(privateKeyPem, "utf-8"), cipher.final()]);
|
|
828
|
-
const tag = cipher.getAuthTag();
|
|
829
|
-
const keystore = {
|
|
830
|
-
public_key_pem: publicKeyPem,
|
|
831
|
-
encrypted_private_key: encrypted.toString("hex"),
|
|
832
|
-
iv: iv.toString("hex"),
|
|
833
|
-
tag: tag.toString("hex"),
|
|
834
|
-
salt: salt.toString("hex"),
|
|
835
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
836
|
-
};
|
|
837
|
-
return { keystore, signingKey: privateKey };
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// ../shared/dist/crypto/chain.js
|
|
841
|
-
import { createHash, sign as cryptoSign, randomUUID } from "crypto";
|
|
842
|
-
function computeHash(recordJson, prevHash) {
|
|
843
|
-
return createHash("sha256").update(recordJson + prevHash).digest("hex");
|
|
844
|
-
}
|
|
845
|
-
function signHash(hash, signingKey2) {
|
|
846
|
-
if (!signingKey2)
|
|
847
|
-
return "unsigned";
|
|
1967
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1968
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1969
|
+
function autoSealSession(active) {
|
|
1970
|
+
const { session: session2 } = active;
|
|
1971
|
+
if (session2.sessionRecordCount === 0) return;
|
|
1972
|
+
const duration = session2.getSessionDuration();
|
|
1973
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1974
|
+
const endRecord = session2.appendToChain("session_end", {
|
|
1975
|
+
duration_seconds: duration,
|
|
1976
|
+
task_type: session2.sessionTaskType,
|
|
1977
|
+
languages: [],
|
|
1978
|
+
files_touched: 0,
|
|
1979
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1980
|
+
auto_sealed: true
|
|
1981
|
+
});
|
|
1982
|
+
const sealData = JSON.stringify({
|
|
1983
|
+
session_id: session2.sessionId,
|
|
1984
|
+
client: session2.clientName,
|
|
1985
|
+
task_type: session2.sessionTaskType,
|
|
1986
|
+
languages: [],
|
|
1987
|
+
files_touched: 0,
|
|
1988
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
1989
|
+
ended_at: now,
|
|
1990
|
+
duration_seconds: duration,
|
|
1991
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1992
|
+
record_count: session2.sessionRecordCount,
|
|
1993
|
+
chain_end_hash: endRecord.hash
|
|
1994
|
+
});
|
|
1995
|
+
const sealSignature = signHash(
|
|
1996
|
+
createHash4("sha256").update(sealData).digest("hex"),
|
|
1997
|
+
session2.signingKey
|
|
1998
|
+
);
|
|
1999
|
+
session2.appendToChain("session_seal", {
|
|
2000
|
+
seal: sealData,
|
|
2001
|
+
seal_signature: sealSignature,
|
|
2002
|
+
auto_sealed: true
|
|
2003
|
+
});
|
|
2004
|
+
const activePath = join6(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
|
|
2005
|
+
const sealedPath = join6(SEALED_DIR, `${session2.sessionId}.jsonl`);
|
|
848
2006
|
try {
|
|
849
|
-
|
|
2007
|
+
if (existsSync8(activePath)) {
|
|
2008
|
+
renameSync3(activePath, sealedPath);
|
|
2009
|
+
}
|
|
850
2010
|
} catch {
|
|
851
|
-
return "unsigned";
|
|
852
2011
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
signature
|
|
2012
|
+
const chainStartHash = session2.chainTipHash === "GENESIS" ? "GENESIS" : session2.chainTipHash;
|
|
2013
|
+
const seal = {
|
|
2014
|
+
session_id: session2.sessionId,
|
|
2015
|
+
client: session2.clientName,
|
|
2016
|
+
task_type: session2.sessionTaskType,
|
|
2017
|
+
languages: [],
|
|
2018
|
+
files_touched: 0,
|
|
2019
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
2020
|
+
ended_at: now,
|
|
2021
|
+
duration_seconds: duration,
|
|
2022
|
+
heartbeat_count: session2.heartbeatCount,
|
|
2023
|
+
record_count: session2.sessionRecordCount,
|
|
2024
|
+
chain_start_hash: chainStartHash,
|
|
2025
|
+
chain_end_hash: endRecord.hash,
|
|
2026
|
+
seal_signature: sealSignature
|
|
869
2027
|
};
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
"documenting",
|
|
884
|
-
"learning",
|
|
885
|
-
"other"
|
|
886
|
-
]);
|
|
887
|
-
var milestoneCategorySchema = z.enum([
|
|
888
|
-
"feature",
|
|
889
|
-
"bugfix",
|
|
890
|
-
"refactor",
|
|
891
|
-
"test",
|
|
892
|
-
"docs",
|
|
893
|
-
"setup",
|
|
894
|
-
"deployment",
|
|
895
|
-
"other"
|
|
896
|
-
]);
|
|
897
|
-
var complexitySchema = z.enum(["simple", "medium", "complex"]);
|
|
898
|
-
var milestoneInputSchema = z.object({
|
|
899
|
-
title: z.string().min(1).max(500),
|
|
900
|
-
category: milestoneCategorySchema,
|
|
901
|
-
complexity: complexitySchema.optional()
|
|
902
|
-
});
|
|
903
|
-
var syncPayloadSchema = z.object({
|
|
904
|
-
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
905
|
-
total_seconds: z.number().int().nonnegative(),
|
|
906
|
-
clients: z.record(z.string(), z.number().int().nonnegative()),
|
|
907
|
-
task_types: z.record(z.string(), z.number().int().nonnegative()),
|
|
908
|
-
languages: z.record(z.string(), z.number().int().nonnegative()),
|
|
909
|
-
sessions: z.array(z.object({
|
|
910
|
-
session_id: z.string(),
|
|
911
|
-
client: z.string(),
|
|
912
|
-
task_type: z.string(),
|
|
913
|
-
languages: z.array(z.string()),
|
|
914
|
-
files_touched: z.number().int().nonnegative(),
|
|
915
|
-
started_at: z.string(),
|
|
916
|
-
ended_at: z.string(),
|
|
917
|
-
duration_seconds: z.number().int().nonnegative(),
|
|
918
|
-
heartbeat_count: z.number().int().nonnegative(),
|
|
919
|
-
record_count: z.number().int().nonnegative(),
|
|
920
|
-
chain_start_hash: z.string(),
|
|
921
|
-
chain_end_hash: z.string(),
|
|
922
|
-
seal_signature: z.string()
|
|
923
|
-
})),
|
|
924
|
-
sync_signature: z.string()
|
|
925
|
-
});
|
|
926
|
-
var publishPayloadSchema = z.object({
|
|
927
|
-
milestones: z.array(z.object({
|
|
928
|
-
id: z.string(),
|
|
929
|
-
title: z.string().min(1).max(500),
|
|
930
|
-
category: milestoneCategorySchema,
|
|
931
|
-
complexity: complexitySchema,
|
|
932
|
-
duration_minutes: z.number().int().nonnegative(),
|
|
933
|
-
languages: z.array(z.string()),
|
|
934
|
-
client: z.string(),
|
|
935
|
-
chain_hash: z.string()
|
|
936
|
-
}))
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// ../shared/dist/utils/fs.js
|
|
940
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
941
|
-
function ensureDir() {
|
|
942
|
-
for (const dir of [USEAI_DIR, DATA_DIR, ACTIVE_DIR, SEALED_DIR]) {
|
|
943
|
-
if (!existsSync(dir)) {
|
|
944
|
-
mkdirSync(dir, { recursive: true });
|
|
2028
|
+
const allSessions = readJson(SESSIONS_FILE, []);
|
|
2029
|
+
allSessions.push(seal);
|
|
2030
|
+
writeJson(SESSIONS_FILE, allSessions);
|
|
2031
|
+
}
|
|
2032
|
+
function resetIdleTimer(sessionId) {
|
|
2033
|
+
const active = sessions.get(sessionId);
|
|
2034
|
+
if (!active) return;
|
|
2035
|
+
clearTimeout(active.idleTimer);
|
|
2036
|
+
active.idleTimer = setTimeout(async () => {
|
|
2037
|
+
autoSealSession(active);
|
|
2038
|
+
try {
|
|
2039
|
+
await active.transport.close();
|
|
2040
|
+
} catch {
|
|
945
2041
|
}
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
|
|
2042
|
+
sessions.delete(sessionId);
|
|
2043
|
+
}, IDLE_TIMEOUT_MS);
|
|
2044
|
+
}
|
|
2045
|
+
async function cleanupSession(sessionId) {
|
|
2046
|
+
const active = sessions.get(sessionId);
|
|
2047
|
+
if (!active) return;
|
|
2048
|
+
clearTimeout(active.idleTimer);
|
|
2049
|
+
autoSealSession(active);
|
|
949
2050
|
try {
|
|
950
|
-
|
|
951
|
-
return fallback;
|
|
952
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
2051
|
+
await active.transport.close();
|
|
953
2052
|
} catch {
|
|
954
|
-
return fallback;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
function writeJson(path, data) {
|
|
958
|
-
ensureDir();
|
|
959
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
960
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
961
|
-
renameSync(tmp, path);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// ../shared/dist/utils/format.js
|
|
965
|
-
function formatDuration(seconds) {
|
|
966
|
-
if (seconds < 60)
|
|
967
|
-
return `${seconds}s`;
|
|
968
|
-
const mins = Math.round(seconds / 60);
|
|
969
|
-
if (mins < 60)
|
|
970
|
-
return `${mins}m`;
|
|
971
|
-
const hrs = Math.floor(mins / 60);
|
|
972
|
-
const remainMins = mins % 60;
|
|
973
|
-
return `${hrs}h ${remainMins}m`;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ../shared/dist/utils/detect-client.js
|
|
977
|
-
function detectClient() {
|
|
978
|
-
const env = process.env;
|
|
979
|
-
for (const [envVar, clientName2] of Object.entries(AI_CLIENT_ENV_VARS)) {
|
|
980
|
-
if (env[envVar])
|
|
981
|
-
return clientName2;
|
|
982
2053
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
var sessionRecordCount = 0;
|
|
1001
|
-
var clientName = "unknown";
|
|
1002
|
-
var sessionTaskType = "coding";
|
|
1003
|
-
var chainTipHash = GENESIS_HASH;
|
|
1004
|
-
var signingKey = null;
|
|
1005
|
-
var signingAvailable = false;
|
|
1006
|
-
function resetSession() {
|
|
1007
|
-
sessionStartTime = Date.now();
|
|
1008
|
-
sessionId = generateSessionId();
|
|
1009
|
-
heartbeatCount = 0;
|
|
1010
|
-
sessionRecordCount = 0;
|
|
1011
|
-
chainTipHash = GENESIS_HASH;
|
|
1012
|
-
clientName = "unknown";
|
|
1013
|
-
sessionTaskType = "coding";
|
|
1014
|
-
}
|
|
1015
|
-
function setClient(name) {
|
|
1016
|
-
clientName = name;
|
|
1017
|
-
}
|
|
1018
|
-
function setTaskType(type) {
|
|
1019
|
-
sessionTaskType = type;
|
|
1020
|
-
}
|
|
1021
|
-
function incrementHeartbeat() {
|
|
1022
|
-
heartbeatCount++;
|
|
1023
|
-
}
|
|
1024
|
-
function getSessionDuration() {
|
|
1025
|
-
return Math.round((Date.now() - sessionStartTime) / 1e3);
|
|
1026
|
-
}
|
|
1027
|
-
function initializeKeystore() {
|
|
1028
|
-
ensureDir();
|
|
1029
|
-
if (existsSync2(KEYSTORE_FILE)) {
|
|
1030
|
-
const ks = readJson(KEYSTORE_FILE, null);
|
|
1031
|
-
if (ks) {
|
|
2054
|
+
sessions.delete(sessionId);
|
|
2055
|
+
}
|
|
2056
|
+
function handleHealth(res) {
|
|
2057
|
+
const body = JSON.stringify({
|
|
2058
|
+
status: "ok",
|
|
2059
|
+
version: VERSION,
|
|
2060
|
+
active_sessions: sessions.size,
|
|
2061
|
+
uptime_seconds: Math.round((Date.now() - startedAt) / 1e3)
|
|
2062
|
+
});
|
|
2063
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2064
|
+
res.end(body);
|
|
2065
|
+
}
|
|
2066
|
+
function parseBody(req) {
|
|
2067
|
+
return new Promise((resolve, reject) => {
|
|
2068
|
+
const chunks = [];
|
|
2069
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2070
|
+
req.on("end", () => {
|
|
1032
2071
|
try {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
2072
|
+
const raw = Buffer.concat(chunks).toString();
|
|
2073
|
+
resolve(raw ? JSON.parse(raw) : void 0);
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
reject(e);
|
|
1037
2076
|
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
writeJson(KEYSTORE_FILE, result.keystore);
|
|
1042
|
-
signingKey = result.signingKey;
|
|
1043
|
-
signingAvailable = true;
|
|
1044
|
-
}
|
|
1045
|
-
function sessionChainPath() {
|
|
1046
|
-
return join2(ACTIVE_DIR, `${sessionId}.jsonl`);
|
|
2077
|
+
});
|
|
2078
|
+
req.on("error", reject);
|
|
2079
|
+
});
|
|
1047
2080
|
}
|
|
1048
|
-
function
|
|
1049
|
-
const
|
|
2081
|
+
async function startDaemon(port) {
|
|
2082
|
+
const listenPort = port ?? DAEMON_PORT;
|
|
1050
2083
|
ensureDir();
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
2084
|
+
const server2 = createServer(async (req, res) => {
|
|
2085
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2086
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
2087
|
+
handleHealth(res);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (url.pathname !== "/mcp") {
|
|
2091
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2092
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
try {
|
|
2096
|
+
if (req.method === "POST") {
|
|
2097
|
+
const body = await parseBody(req);
|
|
2098
|
+
const sid = req.headers["mcp-session-id"];
|
|
2099
|
+
if (sid && sessions.has(sid)) {
|
|
2100
|
+
resetIdleTimer(sid);
|
|
2101
|
+
await sessions.get(sid).transport.handleRequest(req, res, body);
|
|
2102
|
+
} else if (!sid && isInitializeRequest(body)) {
|
|
2103
|
+
const sessionState = new SessionState();
|
|
2104
|
+
try {
|
|
2105
|
+
sessionState.initializeKeystore();
|
|
2106
|
+
} catch {
|
|
2107
|
+
}
|
|
2108
|
+
const mcpServer = new McpServer({
|
|
2109
|
+
name: "UseAI",
|
|
2110
|
+
version: VERSION
|
|
2111
|
+
});
|
|
2112
|
+
registerTools(mcpServer, sessionState);
|
|
2113
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2114
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
2115
|
+
onsessioninitialized: (newSid) => {
|
|
2116
|
+
const idleTimer = setTimeout(async () => {
|
|
2117
|
+
await cleanupSession(newSid);
|
|
2118
|
+
}, IDLE_TIMEOUT_MS);
|
|
2119
|
+
sessions.set(newSid, {
|
|
2120
|
+
transport,
|
|
2121
|
+
server: mcpServer,
|
|
2122
|
+
session: sessionState,
|
|
2123
|
+
idleTimer
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
transport.onclose = () => {
|
|
2128
|
+
const closedSid = transport.sessionId;
|
|
2129
|
+
if (closedSid && sessions.has(closedSid)) {
|
|
2130
|
+
const active = sessions.get(closedSid);
|
|
2131
|
+
clearTimeout(active.idleTimer);
|
|
2132
|
+
autoSealSession(active);
|
|
2133
|
+
sessions.delete(closedSid);
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
await mcpServer.connect(transport);
|
|
2137
|
+
await transport.handleRequest(req, res, body);
|
|
2138
|
+
} else {
|
|
2139
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2140
|
+
res.end(JSON.stringify({
|
|
2141
|
+
jsonrpc: "2.0",
|
|
2142
|
+
error: { code: -32e3, message: "Bad Request: No valid session ID provided" },
|
|
2143
|
+
id: null
|
|
2144
|
+
}));
|
|
2145
|
+
}
|
|
2146
|
+
} else if (req.method === "GET") {
|
|
2147
|
+
const sid = req.headers["mcp-session-id"];
|
|
2148
|
+
if (!sid || !sessions.has(sid)) {
|
|
2149
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2150
|
+
res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
resetIdleTimer(sid);
|
|
2154
|
+
await sessions.get(sid).transport.handleRequest(req, res);
|
|
2155
|
+
} else if (req.method === "DELETE") {
|
|
2156
|
+
const sid = req.headers["mcp-session-id"];
|
|
2157
|
+
if (!sid || !sessions.has(sid)) {
|
|
2158
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2159
|
+
res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
await sessions.get(sid).transport.handleRequest(req, res);
|
|
2163
|
+
} else {
|
|
2164
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
2165
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2166
|
+
}
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
if (!res.headersSent) {
|
|
2169
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2170
|
+
res.end(JSON.stringify({
|
|
2171
|
+
jsonrpc: "2.0",
|
|
2172
|
+
error: { code: -32603, message: "Internal server error" },
|
|
2173
|
+
id: null
|
|
2174
|
+
}));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
const pidData = JSON.stringify({
|
|
2179
|
+
pid: process.pid,
|
|
2180
|
+
port: listenPort,
|
|
2181
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2182
|
+
});
|
|
2183
|
+
writeFileSync4(DAEMON_PID_FILE, pidData + "\n");
|
|
2184
|
+
const shutdown = async (signal) => {
|
|
2185
|
+
for (const [sid] of sessions) {
|
|
2186
|
+
await cleanupSession(sid);
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
if (existsSync8(DAEMON_PID_FILE)) {
|
|
2190
|
+
unlinkSync4(DAEMON_PID_FILE);
|
|
2191
|
+
}
|
|
2192
|
+
} catch {
|
|
2193
|
+
}
|
|
2194
|
+
server2.close(() => {
|
|
2195
|
+
process.exit(0);
|
|
2196
|
+
});
|
|
2197
|
+
setTimeout(() => process.exit(0), 5e3).unref();
|
|
2198
|
+
};
|
|
2199
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2200
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2201
|
+
server2.listen(listenPort, "127.0.0.1", () => {
|
|
2202
|
+
console.log(`UseAI daemon listening on http://127.0.0.1:${listenPort}`);
|
|
2203
|
+
console.log(`PID: ${process.pid}`);
|
|
2204
|
+
});
|
|
1055
2205
|
}
|
|
2206
|
+
var IDLE_TIMEOUT_MS, sessions, startedAt;
|
|
2207
|
+
var init_daemon2 = __esm({
|
|
2208
|
+
"src/daemon.ts"() {
|
|
2209
|
+
"use strict";
|
|
2210
|
+
init_dist();
|
|
2211
|
+
init_session_state();
|
|
2212
|
+
init_register_tools();
|
|
2213
|
+
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2214
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2215
|
+
startedAt = Date.now();
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
1056
2218
|
|
|
1057
2219
|
// src/index.ts
|
|
2220
|
+
init_dist();
|
|
2221
|
+
init_session_state();
|
|
2222
|
+
init_register_tools();
|
|
2223
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2224
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1058
2225
|
if (process.argv[2] === "mcp") {
|
|
1059
2226
|
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
1060
2227
|
await runSetup2(process.argv.slice(3));
|
|
1061
2228
|
process.exit(0);
|
|
1062
2229
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
2230
|
+
if (process.argv[2] === "daemon") {
|
|
2231
|
+
const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon2(), daemon_exports));
|
|
2232
|
+
const portArg = process.argv.indexOf("--port");
|
|
2233
|
+
const port = portArg !== -1 ? parseInt(process.argv[portArg + 1], 10) : void 0;
|
|
2234
|
+
await startDaemon2(port);
|
|
2235
|
+
await new Promise(() => {
|
|
1067
2236
|
});
|
|
1068
2237
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
function getMilestones() {
|
|
1073
|
-
return readJson(MILESTONES_FILE, []);
|
|
1074
|
-
}
|
|
1075
|
-
var server = new McpServer({
|
|
2238
|
+
var session = new SessionState();
|
|
2239
|
+
var server = new McpServer2({
|
|
1076
2240
|
name: "UseAI",
|
|
1077
2241
|
version: VERSION
|
|
1078
2242
|
});
|
|
1079
|
-
server
|
|
1080
|
-
"useai_session_start",
|
|
1081
|
-
"Start tracking an AI coding session. Call this at the beginning of a conversation.",
|
|
1082
|
-
{
|
|
1083
|
-
task_type: z2.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task is the developer working on?")
|
|
1084
|
-
},
|
|
1085
|
-
async ({ task_type }) => {
|
|
1086
|
-
resetSession();
|
|
1087
|
-
setClient(detectClient());
|
|
1088
|
-
setTaskType(task_type ?? "coding");
|
|
1089
|
-
const record = appendToChain("session_start", {
|
|
1090
|
-
client: clientName,
|
|
1091
|
-
task_type: sessionTaskType,
|
|
1092
|
-
version: VERSION
|
|
1093
|
-
});
|
|
1094
|
-
return {
|
|
1095
|
-
content: [
|
|
1096
|
-
{
|
|
1097
|
-
type: "text",
|
|
1098
|
-
text: `useai session started \u2014 tracking ${sessionTaskType} session on ${clientName}.
|
|
1099
|
-
Session: ${sessionId.slice(0, 8)} \xB7 Chain: ${record.hash.slice(0, 12)} (${signingAvailable ? "signed" : "unsigned"})`
|
|
1100
|
-
}
|
|
1101
|
-
]
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
);
|
|
1105
|
-
server.tool(
|
|
1106
|
-
"useai_heartbeat",
|
|
1107
|
-
"Record a heartbeat for the current AI coding session. Call this periodically during long conversations (every 10-15 minutes).",
|
|
1108
|
-
{},
|
|
1109
|
-
async () => {
|
|
1110
|
-
incrementHeartbeat();
|
|
1111
|
-
appendToChain("heartbeat", {
|
|
1112
|
-
heartbeat_number: heartbeatCount,
|
|
1113
|
-
cumulative_seconds: getSessionDuration()
|
|
1114
|
-
});
|
|
1115
|
-
return {
|
|
1116
|
-
content: [
|
|
1117
|
-
{
|
|
1118
|
-
type: "text",
|
|
1119
|
-
text: `Heartbeat recorded. Session active for ${formatDuration(getSessionDuration())}.`
|
|
1120
|
-
}
|
|
1121
|
-
]
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
);
|
|
1125
|
-
server.tool(
|
|
1126
|
-
"useai_session_end",
|
|
1127
|
-
"End the current AI coding session and record milestones. IMPORTANT: Milestone descriptions must be GENERIC \u2014 no project names, file names, file paths, or company names. Use general terms like 'authentication system', 'data validation layer', 'responsive dashboard'.",
|
|
1128
|
-
{
|
|
1129
|
-
task_type: z2.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task was the developer working on?"),
|
|
1130
|
-
languages: z2.array(z2.string()).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
|
|
1131
|
-
files_touched_count: z2.number().optional().describe("Approximate number of files created or modified (count only, no names)"),
|
|
1132
|
-
milestones: z2.array(z2.object({
|
|
1133
|
-
title: z2.string().describe("Generic description of what was accomplished. PRIVACY RULES: Do NOT include project names, repository names, file names, file paths, API endpoints, database names, company names, or any project-specific details. Use generic terms only. GOOD examples: 'Implemented user authentication', 'Fixed race condition in background worker', 'Added unit tests for data validation', 'Refactored state management layer', 'Built responsive dashboard layout'. BAD examples: 'Fixed bug in /api/stripe/webhooks', 'Added auth to acme-corp project', 'Updated UserService.ts in src/services/'"),
|
|
1134
|
-
category: z2.enum(["feature", "bugfix", "refactor", "test", "docs", "setup", "deployment", "other"]).describe("Type of work completed"),
|
|
1135
|
-
complexity: z2.enum(["simple", "medium", "complex"]).optional().describe("How complex was this task?")
|
|
1136
|
-
})).optional().describe("What was accomplished this session? List each distinct piece of work completed. Describe generically \u2014 no project names, file names, or proprietary details.")
|
|
1137
|
-
},
|
|
1138
|
-
async ({ task_type, languages, files_touched_count, milestones: milestonesInput }) => {
|
|
1139
|
-
const duration = getSessionDuration();
|
|
1140
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1141
|
-
const finalTaskType = task_type ?? sessionTaskType;
|
|
1142
|
-
const chainStartHash = chainTipHash === "GENESIS" ? "GENESIS" : chainTipHash;
|
|
1143
|
-
const endRecord = appendToChain("session_end", {
|
|
1144
|
-
duration_seconds: duration,
|
|
1145
|
-
task_type: finalTaskType,
|
|
1146
|
-
languages: languages ?? [],
|
|
1147
|
-
files_touched: files_touched_count ?? 0,
|
|
1148
|
-
heartbeat_count: heartbeatCount
|
|
1149
|
-
});
|
|
1150
|
-
const sealData = JSON.stringify({
|
|
1151
|
-
session_id: sessionId,
|
|
1152
|
-
client: clientName,
|
|
1153
|
-
task_type: finalTaskType,
|
|
1154
|
-
languages: languages ?? [],
|
|
1155
|
-
files_touched: files_touched_count ?? 0,
|
|
1156
|
-
started_at: new Date(sessionStartTime).toISOString(),
|
|
1157
|
-
ended_at: now,
|
|
1158
|
-
duration_seconds: duration,
|
|
1159
|
-
heartbeat_count: heartbeatCount,
|
|
1160
|
-
record_count: sessionRecordCount,
|
|
1161
|
-
chain_end_hash: endRecord.hash
|
|
1162
|
-
});
|
|
1163
|
-
const sealSignature = signHash(
|
|
1164
|
-
createHash3("sha256").update(sealData).digest("hex"),
|
|
1165
|
-
signingKey
|
|
1166
|
-
);
|
|
1167
|
-
appendToChain("session_seal", {
|
|
1168
|
-
seal: sealData,
|
|
1169
|
-
seal_signature: sealSignature
|
|
1170
|
-
});
|
|
1171
|
-
const activePath = join4(ACTIVE_DIR, `${sessionId}.jsonl`);
|
|
1172
|
-
const sealedPath = join4(SEALED_DIR, `${sessionId}.jsonl`);
|
|
1173
|
-
try {
|
|
1174
|
-
if (existsSync4(activePath)) {
|
|
1175
|
-
renameSync2(activePath, sealedPath);
|
|
1176
|
-
}
|
|
1177
|
-
} catch {
|
|
1178
|
-
}
|
|
1179
|
-
const seal = {
|
|
1180
|
-
session_id: sessionId,
|
|
1181
|
-
client: clientName,
|
|
1182
|
-
task_type: finalTaskType,
|
|
1183
|
-
languages: languages ?? [],
|
|
1184
|
-
files_touched: files_touched_count ?? 0,
|
|
1185
|
-
started_at: new Date(sessionStartTime).toISOString(),
|
|
1186
|
-
ended_at: now,
|
|
1187
|
-
duration_seconds: duration,
|
|
1188
|
-
heartbeat_count: heartbeatCount,
|
|
1189
|
-
record_count: sessionRecordCount,
|
|
1190
|
-
chain_start_hash: chainStartHash,
|
|
1191
|
-
chain_end_hash: endRecord.hash,
|
|
1192
|
-
seal_signature: sealSignature
|
|
1193
|
-
};
|
|
1194
|
-
const sessions = getSessions();
|
|
1195
|
-
sessions.push(seal);
|
|
1196
|
-
writeJson(SESSIONS_FILE, sessions);
|
|
1197
|
-
let milestoneCount = 0;
|
|
1198
|
-
if (milestonesInput && milestonesInput.length > 0) {
|
|
1199
|
-
const config = getConfig();
|
|
1200
|
-
if (config.milestone_tracking) {
|
|
1201
|
-
const durationMinutes = Math.round(duration / 60);
|
|
1202
|
-
const allMilestones = getMilestones();
|
|
1203
|
-
for (const m of milestonesInput) {
|
|
1204
|
-
const record = appendToChain("milestone", {
|
|
1205
|
-
title: m.title,
|
|
1206
|
-
category: m.category,
|
|
1207
|
-
complexity: m.complexity ?? "medium",
|
|
1208
|
-
duration_minutes: durationMinutes,
|
|
1209
|
-
languages: languages ?? []
|
|
1210
|
-
});
|
|
1211
|
-
const milestone = {
|
|
1212
|
-
id: `m_${randomUUID3().slice(0, 8)}`,
|
|
1213
|
-
session_id: sessionId,
|
|
1214
|
-
title: m.title,
|
|
1215
|
-
category: m.category,
|
|
1216
|
-
complexity: m.complexity ?? "medium",
|
|
1217
|
-
duration_minutes: durationMinutes,
|
|
1218
|
-
languages: languages ?? [],
|
|
1219
|
-
client: clientName,
|
|
1220
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1221
|
-
published: false,
|
|
1222
|
-
published_at: null,
|
|
1223
|
-
chain_hash: record.hash
|
|
1224
|
-
};
|
|
1225
|
-
allMilestones.push(milestone);
|
|
1226
|
-
milestoneCount++;
|
|
1227
|
-
}
|
|
1228
|
-
writeJson(MILESTONES_FILE, allMilestones);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
const durationStr = formatDuration(duration);
|
|
1232
|
-
const langStr = languages && languages.length > 0 ? ` using ${languages.join(", ")}` : "";
|
|
1233
|
-
const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
|
|
1234
|
-
return {
|
|
1235
|
-
content: [
|
|
1236
|
-
{
|
|
1237
|
-
type: "text",
|
|
1238
|
-
text: `Session ended: ${durationStr} ${finalTaskType}${langStr}${milestoneStr}`
|
|
1239
|
-
}
|
|
1240
|
-
]
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
);
|
|
2243
|
+
registerTools(server, session);
|
|
1244
2244
|
async function main() {
|
|
1245
2245
|
ensureDir();
|
|
1246
2246
|
try {
|
|
1247
|
-
initializeKeystore();
|
|
2247
|
+
session.initializeKeystore();
|
|
1248
2248
|
} catch {
|
|
1249
2249
|
}
|
|
1250
2250
|
const transport = new StdioServerTransport();
|