@devness/useai 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1594 -601
- 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.3.0";
|
|
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", "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. 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'.",
|
|
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("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/'"),
|
|
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,10 +1403,11 @@ 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.",
|
|
@@ -303,7 +1418,9 @@ var init_tools = __esm({
|
|
|
303
1418
|
command: "npx",
|
|
304
1419
|
args: ["-y", "@devness/useai"]
|
|
305
1420
|
};
|
|
306
|
-
|
|
1421
|
+
MCP_HTTP_URL = DAEMON_MCP_URL;
|
|
1422
|
+
MCP_HTTP_ENTRY = { url: MCP_HTTP_URL };
|
|
1423
|
+
home = homedir3();
|
|
307
1424
|
INSTRUCTIONS_START = "<!-- useai:start -->";
|
|
308
1425
|
INSTRUCTIONS_END = "<!-- useai:end -->";
|
|
309
1426
|
USEAI_INSTRUCTIONS = [
|
|
@@ -322,69 +1439,75 @@ ${INSTRUCTIONS_END}`;
|
|
|
322
1439
|
toml: { isConfigured: isConfiguredToml, install: installToml, remove: removeToml },
|
|
323
1440
|
yaml: { isConfigured: isConfiguredYaml, install: installYaml, remove: removeYaml }
|
|
324
1441
|
};
|
|
325
|
-
appSupport =
|
|
1442
|
+
appSupport = join5(home, "Library", "Application Support");
|
|
326
1443
|
AI_TOOLS = [
|
|
327
1444
|
createTool({
|
|
328
1445
|
id: "claude-code",
|
|
329
1446
|
name: "Claude Code",
|
|
330
1447
|
configFormat: "standard",
|
|
331
|
-
configPath:
|
|
332
|
-
detect: () => hasBinary("claude") ||
|
|
333
|
-
instructions: { method: "append", path:
|
|
1448
|
+
configPath: join5(home, ".claude.json"),
|
|
1449
|
+
detect: () => hasBinary("claude") || existsSync7(join5(home, ".claude.json")),
|
|
1450
|
+
instructions: { method: "append", path: join5(home, ".claude", "CLAUDE.md") },
|
|
1451
|
+
supportsUrl: true
|
|
334
1452
|
}),
|
|
335
1453
|
createTool({
|
|
336
1454
|
id: "cursor",
|
|
337
1455
|
name: "Cursor",
|
|
338
1456
|
configFormat: "standard",
|
|
339
|
-
configPath:
|
|
340
|
-
detect: () =>
|
|
341
|
-
manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below."
|
|
1457
|
+
configPath: join5(home, ".cursor", "mcp.json"),
|
|
1458
|
+
detect: () => existsSync7(join5(home, ".cursor")),
|
|
1459
|
+
manualHint: "Open Cursor Settings \u2192 Rules \u2192 User Rules and paste the instructions below.",
|
|
1460
|
+
supportsUrl: true
|
|
342
1461
|
}),
|
|
343
1462
|
createTool({
|
|
344
1463
|
id: "windsurf",
|
|
345
1464
|
name: "Windsurf",
|
|
346
1465
|
configFormat: "standard",
|
|
347
|
-
configPath:
|
|
348
|
-
detect: () =>
|
|
349
|
-
instructions: { method: "append", path:
|
|
1466
|
+
configPath: join5(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
1467
|
+
detect: () => existsSync7(join5(home, ".codeium", "windsurf")),
|
|
1468
|
+
instructions: { method: "append", path: join5(home, ".codeium", "windsurf", "memories", "global_rules.md") },
|
|
1469
|
+
supportsUrl: true
|
|
350
1470
|
}),
|
|
351
1471
|
createTool({
|
|
352
1472
|
id: "vscode",
|
|
353
1473
|
name: "VS Code",
|
|
354
1474
|
configFormat: "vscode",
|
|
355
|
-
configPath:
|
|
356
|
-
detect: () =>
|
|
357
|
-
instructions: { method: "create", path:
|
|
1475
|
+
configPath: join5(appSupport, "Code", "User", "mcp.json"),
|
|
1476
|
+
detect: () => existsSync7(join5(appSupport, "Code")),
|
|
1477
|
+
instructions: { method: "create", path: join5(appSupport, "Code", "User", "prompts", "useai.instructions.md") },
|
|
1478
|
+
supportsUrl: true
|
|
358
1479
|
}),
|
|
359
1480
|
createTool({
|
|
360
1481
|
id: "vscode-insiders",
|
|
361
1482
|
name: "VS Code Insiders",
|
|
362
1483
|
configFormat: "vscode",
|
|
363
|
-
configPath:
|
|
364
|
-
detect: () =>
|
|
365
|
-
instructions: { method: "create", path:
|
|
1484
|
+
configPath: join5(appSupport, "Code - Insiders", "User", "mcp.json"),
|
|
1485
|
+
detect: () => existsSync7(join5(appSupport, "Code - Insiders")),
|
|
1486
|
+
instructions: { method: "create", path: join5(appSupport, "Code - Insiders", "User", "prompts", "useai.instructions.md") },
|
|
1487
|
+
supportsUrl: true
|
|
366
1488
|
}),
|
|
367
1489
|
createTool({
|
|
368
1490
|
id: "gemini-cli",
|
|
369
1491
|
name: "Gemini CLI",
|
|
370
1492
|
configFormat: "standard",
|
|
371
|
-
configPath:
|
|
1493
|
+
configPath: join5(home, ".gemini", "settings.json"),
|
|
372
1494
|
detect: () => hasBinary("gemini"),
|
|
373
|
-
instructions: { method: "append", path:
|
|
1495
|
+
instructions: { method: "append", path: join5(home, ".gemini", "GEMINI.md") },
|
|
1496
|
+
supportsUrl: true
|
|
374
1497
|
}),
|
|
375
1498
|
createTool({
|
|
376
1499
|
id: "zed",
|
|
377
1500
|
name: "Zed",
|
|
378
1501
|
configFormat: "zed",
|
|
379
|
-
configPath:
|
|
380
|
-
detect: () =>
|
|
1502
|
+
configPath: join5(appSupport, "Zed", "settings.json"),
|
|
1503
|
+
detect: () => existsSync7(join5(appSupport, "Zed")),
|
|
381
1504
|
manualHint: "Open Rules Library (\u2318\u2325L) \u2192 click + \u2192 paste the instructions below."
|
|
382
1505
|
}),
|
|
383
1506
|
createTool({
|
|
384
1507
|
id: "cline",
|
|
385
1508
|
name: "Cline",
|
|
386
1509
|
configFormat: "standard",
|
|
387
|
-
configPath:
|
|
1510
|
+
configPath: join5(
|
|
388
1511
|
appSupport,
|
|
389
1512
|
"Code",
|
|
390
1513
|
"User",
|
|
@@ -393,16 +1516,17 @@ ${INSTRUCTIONS_END}`;
|
|
|
393
1516
|
"settings",
|
|
394
1517
|
"cline_mcp_settings.json"
|
|
395
1518
|
),
|
|
396
|
-
detect: () =>
|
|
397
|
-
|
|
1519
|
+
detect: () => existsSync7(
|
|
1520
|
+
join5(appSupport, "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
|
|
398
1521
|
),
|
|
399
|
-
instructions: { method: "create", path:
|
|
1522
|
+
instructions: { method: "create", path: join5(home, "Documents", "Cline", "Rules", "useai.md") },
|
|
1523
|
+
supportsUrl: true
|
|
400
1524
|
}),
|
|
401
1525
|
createTool({
|
|
402
1526
|
id: "roo-code",
|
|
403
1527
|
name: "Roo Code",
|
|
404
1528
|
configFormat: "standard",
|
|
405
|
-
configPath:
|
|
1529
|
+
configPath: join5(
|
|
406
1530
|
appSupport,
|
|
407
1531
|
"Code",
|
|
408
1532
|
"User",
|
|
@@ -411,57 +1535,59 @@ ${INSTRUCTIONS_END}`;
|
|
|
411
1535
|
"settings",
|
|
412
1536
|
"cline_mcp_settings.json"
|
|
413
1537
|
),
|
|
414
|
-
detect: () =>
|
|
415
|
-
|
|
1538
|
+
detect: () => existsSync7(
|
|
1539
|
+
join5(appSupport, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline")
|
|
416
1540
|
),
|
|
417
|
-
instructions: { method: "create", path:
|
|
1541
|
+
instructions: { method: "create", path: join5(home, ".roo", "rules", "useai.md") },
|
|
1542
|
+
supportsUrl: true
|
|
418
1543
|
}),
|
|
419
1544
|
createTool({
|
|
420
1545
|
id: "amazon-q-cli",
|
|
421
1546
|
name: "Amazon Q CLI",
|
|
422
1547
|
configFormat: "standard",
|
|
423
|
-
configPath:
|
|
424
|
-
detect: () => hasBinary("q") ||
|
|
1548
|
+
configPath: join5(home, ".aws", "amazonq", "mcp.json"),
|
|
1549
|
+
detect: () => hasBinary("q") || existsSync7(join5(home, ".aws", "amazonq")),
|
|
425
1550
|
manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
|
|
426
1551
|
}),
|
|
427
1552
|
createTool({
|
|
428
1553
|
id: "amazon-q-ide",
|
|
429
1554
|
name: "Amazon Q IDE",
|
|
430
1555
|
configFormat: "standard",
|
|
431
|
-
configPath:
|
|
432
|
-
detect: () =>
|
|
1556
|
+
configPath: join5(home, ".aws", "amazonq", "default.json"),
|
|
1557
|
+
detect: () => existsSync7(join5(home, ".amazonq")) || existsSync7(join5(home, ".aws", "amazonq")),
|
|
433
1558
|
manualHint: "Create .amazonq/rules/useai.md in your project root with the instructions below."
|
|
434
1559
|
}),
|
|
435
1560
|
createTool({
|
|
436
1561
|
id: "codex",
|
|
437
1562
|
name: "Codex",
|
|
438
1563
|
configFormat: "toml",
|
|
439
|
-
configPath:
|
|
440
|
-
detect: () => hasBinary("codex") ||
|
|
441
|
-
instructions: { method: "append", path:
|
|
1564
|
+
configPath: join5(home, ".codex", "config.toml"),
|
|
1565
|
+
detect: () => hasBinary("codex") || existsSync7(join5(home, ".codex")) || existsSync7("/Applications/Codex.app"),
|
|
1566
|
+
instructions: { method: "append", path: join5(home, ".codex", "AGENTS.md") }
|
|
442
1567
|
}),
|
|
443
1568
|
createTool({
|
|
444
1569
|
id: "goose",
|
|
445
1570
|
name: "Goose",
|
|
446
1571
|
configFormat: "yaml",
|
|
447
|
-
configPath:
|
|
448
|
-
detect: () =>
|
|
449
|
-
instructions: { method: "append", path:
|
|
1572
|
+
configPath: join5(home, ".config", "goose", "config.yaml"),
|
|
1573
|
+
detect: () => existsSync7(join5(home, ".config", "goose")),
|
|
1574
|
+
instructions: { method: "append", path: join5(home, ".config", "goose", ".goosehints") }
|
|
450
1575
|
}),
|
|
451
1576
|
createTool({
|
|
452
1577
|
id: "opencode",
|
|
453
1578
|
name: "OpenCode",
|
|
454
1579
|
configFormat: "standard",
|
|
455
|
-
configPath:
|
|
456
|
-
detect: () => hasBinary("opencode") ||
|
|
457
|
-
instructions: { method: "append", path:
|
|
1580
|
+
configPath: join5(home, ".config", "opencode", "opencode.json"),
|
|
1581
|
+
detect: () => hasBinary("opencode") || existsSync7(join5(home, ".config", "opencode")),
|
|
1582
|
+
instructions: { method: "append", path: join5(home, ".config", "opencode", "AGENTS.md") },
|
|
1583
|
+
supportsUrl: true
|
|
458
1584
|
}),
|
|
459
1585
|
createTool({
|
|
460
1586
|
id: "junie",
|
|
461
1587
|
name: "Junie",
|
|
462
1588
|
configFormat: "standard",
|
|
463
|
-
configPath:
|
|
464
|
-
detect: () =>
|
|
1589
|
+
configPath: join5(home, ".junie", "mcp", "mcp.json"),
|
|
1590
|
+
detect: () => existsSync7(join5(home, ".junie")),
|
|
465
1591
|
manualHint: "Add the instructions below to .junie/guidelines.md in your project root."
|
|
466
1592
|
})
|
|
467
1593
|
];
|
|
@@ -531,7 +1657,59 @@ function showStatus(tools) {
|
|
|
531
1657
|
console.log(rows.join("\n"));
|
|
532
1658
|
console.log();
|
|
533
1659
|
}
|
|
534
|
-
async function
|
|
1660
|
+
async function daemonInstallFlow(tools, explicit) {
|
|
1661
|
+
console.log(dim("Ensuring UseAI daemon is running..."));
|
|
1662
|
+
const daemonOk = await ensureDaemon();
|
|
1663
|
+
let useDaemon = true;
|
|
1664
|
+
if (daemonOk) {
|
|
1665
|
+
console.log(ok(`\u2713 Daemon running on port ${DAEMON_PORT}`));
|
|
1666
|
+
} else {
|
|
1667
|
+
useDaemon = false;
|
|
1668
|
+
console.log(err("\u2717 Could not start daemon \u2014 falling back to stdio config"));
|
|
1669
|
+
console.log(dim(`(Run with --foreground to debug: npx @devness/useai daemon --port ${DAEMON_PORT})`));
|
|
1670
|
+
}
|
|
1671
|
+
if (useDaemon) {
|
|
1672
|
+
const platform = detectPlatform();
|
|
1673
|
+
if (platform !== "unsupported") {
|
|
1674
|
+
try {
|
|
1675
|
+
installAutostart();
|
|
1676
|
+
console.log(ok(`\u2713 Auto-start service installed (${platform})`));
|
|
1677
|
+
} catch {
|
|
1678
|
+
console.log(chalk.yellow(` \u26A0 Could not install auto-start service`));
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const targetTools = explicit ? tools : tools.filter((t) => t.detect());
|
|
1683
|
+
if (targetTools.length === 0) {
|
|
1684
|
+
console.log(err("\n No AI tools detected on this machine."));
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
let configuredCount = 0;
|
|
1688
|
+
console.log();
|
|
1689
|
+
for (const tool of targetTools) {
|
|
1690
|
+
try {
|
|
1691
|
+
if (useDaemon && tool.supportsUrl) {
|
|
1692
|
+
tool.installHttp();
|
|
1693
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("HTTP (daemon)")}`));
|
|
1694
|
+
} else if (useDaemon && !tool.supportsUrl) {
|
|
1695
|
+
tool.install();
|
|
1696
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("stdio (no URL support)")}`));
|
|
1697
|
+
} else {
|
|
1698
|
+
tool.install();
|
|
1699
|
+
console.log(ok(`\u2713 ${tool.name.padEnd(18)} \u2192 ${chalk.dim("stdio")}`));
|
|
1700
|
+
}
|
|
1701
|
+
configuredCount++;
|
|
1702
|
+
} catch (e) {
|
|
1703
|
+
console.log(err(`\u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
showManualHints(targetTools);
|
|
1707
|
+
const mode = useDaemon ? "daemon mode" : "stdio mode";
|
|
1708
|
+
console.log(`
|
|
1709
|
+
Done! UseAI configured in ${chalk.bold(String(configuredCount))} tool${configuredCount === 1 ? "" : "s"} (${mode}).
|
|
1710
|
+
`);
|
|
1711
|
+
}
|
|
1712
|
+
async function stdioInstallFlow(tools, autoYes, explicit) {
|
|
535
1713
|
if (explicit) {
|
|
536
1714
|
console.log();
|
|
537
1715
|
for (const tool of tools) {
|
|
@@ -618,9 +1796,9 @@ async function installFlow(tools, autoYes, explicit) {
|
|
|
618
1796
|
Done! UseAI MCP server configured in ${chalk.bold(String(toInstall.length))} tool${toInstall.length === 1 ? "" : "s"}.
|
|
619
1797
|
`);
|
|
620
1798
|
}
|
|
621
|
-
async function
|
|
1799
|
+
async function fullRemoveFlow(tools, autoYes, explicit) {
|
|
622
1800
|
if (explicit) {
|
|
623
|
-
const
|
|
1801
|
+
const toRemove = tools.filter((t) => {
|
|
624
1802
|
try {
|
|
625
1803
|
return t.isConfigured();
|
|
626
1804
|
} catch {
|
|
@@ -637,83 +1815,98 @@ async function removeFlow(tools, autoYes, explicit) {
|
|
|
637
1815
|
for (const tool of notConfigured) {
|
|
638
1816
|
console.log(dim(`${tool.name} is not configured \u2014 skipping.`));
|
|
639
1817
|
}
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1818
|
+
if (toRemove.length > 0) {
|
|
1819
|
+
console.log();
|
|
1820
|
+
for (const tool of toRemove) {
|
|
1821
|
+
try {
|
|
1822
|
+
tool.remove();
|
|
1823
|
+
console.log(ok(`\u2713 Removed from ${tool.name}`));
|
|
1824
|
+
} catch (e) {
|
|
1825
|
+
console.log(err(`\u2717 ${tool.name} \u2014 ${e.message}`));
|
|
1826
|
+
}
|
|
648
1827
|
}
|
|
649
1828
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
console.log(`
|
|
1829
|
+
} else {
|
|
1830
|
+
const configured = tools.filter((t) => {
|
|
1831
|
+
try {
|
|
1832
|
+
return t.isConfigured();
|
|
1833
|
+
} catch {
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
if (configured.length === 0) {
|
|
1838
|
+
console.log(dim("UseAI is not configured in any AI tools."));
|
|
1839
|
+
} else {
|
|
1840
|
+
console.log(`
|
|
665
1841
|
Found UseAI configured in ${chalk.bold(String(configured.length))} tool${configured.length === 1 ? "" : "s"}:
|
|
666
1842
|
`);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1843
|
+
let toRemove;
|
|
1844
|
+
if (autoYes) {
|
|
1845
|
+
toRemove = configured;
|
|
1846
|
+
} else {
|
|
1847
|
+
let selected;
|
|
1848
|
+
try {
|
|
1849
|
+
selected = await checkbox({
|
|
1850
|
+
message: "Select tools to remove UseAI from:",
|
|
1851
|
+
choices: configured.map((t) => ({
|
|
1852
|
+
name: t.name,
|
|
1853
|
+
value: t.id,
|
|
1854
|
+
checked: true
|
|
1855
|
+
}))
|
|
1856
|
+
});
|
|
1857
|
+
} catch {
|
|
1858
|
+
console.log("\n");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
toRemove = configured.filter((t) => selected.includes(t.id));
|
|
1862
|
+
}
|
|
1863
|
+
if (toRemove.length === 0) {
|
|
1864
|
+
console.log(dim("No tools selected."));
|
|
1865
|
+
} else {
|
|
1866
|
+
console.log(`
|
|
1867
|
+
Removing from ${toRemove.length} tool${toRemove.length === 1 ? "" : "s"}...
|
|
1868
|
+
`);
|
|
1869
|
+
for (const tool of toRemove) {
|
|
1870
|
+
try {
|
|
1871
|
+
tool.remove();
|
|
1872
|
+
console.log(ok(`\u2713 Removed from ${tool.name}`));
|
|
1873
|
+
} catch (e) {
|
|
1874
|
+
console.log(err(`\u2717 ${tool.name} \u2014 ${e.message}`));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
684
1878
|
}
|
|
685
|
-
toRemove = configured.filter((t) => selected.includes(t.id));
|
|
686
1879
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1880
|
+
console.log();
|
|
1881
|
+
try {
|
|
1882
|
+
await killDaemon();
|
|
1883
|
+
console.log(ok("\u2713 Daemon stopped"));
|
|
1884
|
+
} catch {
|
|
1885
|
+
console.log(dim("Daemon was not running"));
|
|
690
1886
|
}
|
|
691
|
-
|
|
692
|
-
Removing from ${toRemove.length} tool${toRemove.length === 1 ? "" : "s"}...
|
|
693
|
-
`);
|
|
694
|
-
for (const tool of toRemove) {
|
|
1887
|
+
if (isAutostartInstalled()) {
|
|
695
1888
|
try {
|
|
696
|
-
|
|
697
|
-
console.log(ok(
|
|
698
|
-
} catch
|
|
699
|
-
console.log(err(
|
|
1889
|
+
removeAutostart();
|
|
1890
|
+
console.log(ok("\u2713 Auto-start service removed"));
|
|
1891
|
+
} catch {
|
|
1892
|
+
console.log(err("\u2717 Failed to remove auto-start service"));
|
|
700
1893
|
}
|
|
701
1894
|
}
|
|
702
|
-
console.log(
|
|
703
|
-
Done! UseAI removed from ${chalk.bold(String(toRemove.length))} tool${toRemove.length === 1 ? "" : "s"}.
|
|
704
|
-
`);
|
|
1895
|
+
console.log(dim("\nDone! UseAI fully removed.\n"));
|
|
705
1896
|
}
|
|
706
1897
|
function showHelp() {
|
|
707
1898
|
console.log(`
|
|
708
1899
|
${chalk.bold("Usage:")} npx @devness/useai mcp [tools...] [options]
|
|
709
1900
|
|
|
710
1901
|
Configure UseAI MCP server in your AI tools.
|
|
1902
|
+
Default: starts daemon, installs auto-start, configures tools with HTTP.
|
|
711
1903
|
|
|
712
1904
|
${chalk.bold("Arguments:")}
|
|
713
1905
|
tools Specific tool names (e.g. codex cursor vscode)
|
|
714
1906
|
|
|
715
1907
|
${chalk.bold("Options:")}
|
|
716
|
-
--
|
|
1908
|
+
--stdio Use stdio config (legacy mode for containers/CI)
|
|
1909
|
+
--remove Remove UseAI from configured tools, stop daemon, remove auto-start
|
|
717
1910
|
--status Show configuration status without modifying
|
|
718
1911
|
-y, --yes Skip confirmation, auto-select all detected tools
|
|
719
1912
|
-h, --help Show this help message
|
|
@@ -728,6 +1921,7 @@ async function runSetup(args) {
|
|
|
728
1921
|
}
|
|
729
1922
|
const isRemove = flags.has("--remove");
|
|
730
1923
|
const isStatus = flags.has("--status");
|
|
1924
|
+
const isStdio = flags.has("--stdio");
|
|
731
1925
|
const autoYes = flags.has("-y") || flags.has("--yes");
|
|
732
1926
|
const explicit = toolNames.length > 0;
|
|
733
1927
|
let tools = AI_TOOLS;
|
|
@@ -743,513 +1937,312 @@ async function runSetup(args) {
|
|
|
743
1937
|
if (isStatus) {
|
|
744
1938
|
showStatus(tools);
|
|
745
1939
|
} else if (isRemove) {
|
|
746
|
-
await
|
|
1940
|
+
await fullRemoveFlow(tools, autoYes, explicit);
|
|
1941
|
+
} else if (isStdio) {
|
|
1942
|
+
await stdioInstallFlow(tools, autoYes, explicit);
|
|
747
1943
|
} else {
|
|
748
|
-
await
|
|
1944
|
+
await daemonInstallFlow(tools, explicit);
|
|
749
1945
|
}
|
|
750
1946
|
}
|
|
751
1947
|
var init_setup = __esm({
|
|
752
1948
|
"src/setup.ts"() {
|
|
753
1949
|
"use strict";
|
|
1950
|
+
init_dist();
|
|
754
1951
|
init_tools();
|
|
755
1952
|
}
|
|
756
1953
|
});
|
|
757
1954
|
|
|
758
|
-
// src/
|
|
1955
|
+
// src/daemon.ts
|
|
1956
|
+
var daemon_exports = {};
|
|
1957
|
+
__export(daemon_exports, {
|
|
1958
|
+
startDaemon: () => startDaemon
|
|
1959
|
+
});
|
|
1960
|
+
import { createServer } from "http";
|
|
1961
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
1962
|
+
import { existsSync as existsSync8, renameSync as renameSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
1963
|
+
import { join as join6 } from "path";
|
|
759
1964
|
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";
|
|
1965
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1966
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1967
|
+
function autoSealSession(active) {
|
|
1968
|
+
const { session: session2 } = active;
|
|
1969
|
+
if (session2.sessionRecordCount === 0) return;
|
|
1970
|
+
const duration = session2.getSessionDuration();
|
|
1971
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1972
|
+
const endRecord = session2.appendToChain("session_end", {
|
|
1973
|
+
duration_seconds: duration,
|
|
1974
|
+
task_type: session2.sessionTaskType,
|
|
1975
|
+
languages: [],
|
|
1976
|
+
files_touched: 0,
|
|
1977
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1978
|
+
auto_sealed: true
|
|
1979
|
+
});
|
|
1980
|
+
const sealData = JSON.stringify({
|
|
1981
|
+
session_id: session2.sessionId,
|
|
1982
|
+
client: session2.clientName,
|
|
1983
|
+
task_type: session2.sessionTaskType,
|
|
1984
|
+
languages: [],
|
|
1985
|
+
files_touched: 0,
|
|
1986
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
1987
|
+
ended_at: now,
|
|
1988
|
+
duration_seconds: duration,
|
|
1989
|
+
heartbeat_count: session2.heartbeatCount,
|
|
1990
|
+
record_count: session2.sessionRecordCount,
|
|
1991
|
+
chain_end_hash: endRecord.hash
|
|
1992
|
+
});
|
|
1993
|
+
const sealSignature = signHash(
|
|
1994
|
+
createHash4("sha256").update(sealData).digest("hex"),
|
|
1995
|
+
session2.signingKey
|
|
1996
|
+
);
|
|
1997
|
+
session2.appendToChain("session_seal", {
|
|
1998
|
+
seal: sealData,
|
|
1999
|
+
seal_signature: sealSignature,
|
|
2000
|
+
auto_sealed: true
|
|
2001
|
+
});
|
|
2002
|
+
const activePath = join6(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
|
|
2003
|
+
const sealedPath = join6(SEALED_DIR, `${session2.sessionId}.jsonl`);
|
|
848
2004
|
try {
|
|
849
|
-
|
|
2005
|
+
if (existsSync8(activePath)) {
|
|
2006
|
+
renameSync3(activePath, sealedPath);
|
|
2007
|
+
}
|
|
850
2008
|
} catch {
|
|
851
|
-
return "unsigned";
|
|
852
2009
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
signature
|
|
2010
|
+
const chainStartHash = session2.chainTipHash === "GENESIS" ? "GENESIS" : session2.chainTipHash;
|
|
2011
|
+
const seal = {
|
|
2012
|
+
session_id: session2.sessionId,
|
|
2013
|
+
client: session2.clientName,
|
|
2014
|
+
task_type: session2.sessionTaskType,
|
|
2015
|
+
languages: [],
|
|
2016
|
+
files_touched: 0,
|
|
2017
|
+
started_at: new Date(session2.sessionStartTime).toISOString(),
|
|
2018
|
+
ended_at: now,
|
|
2019
|
+
duration_seconds: duration,
|
|
2020
|
+
heartbeat_count: session2.heartbeatCount,
|
|
2021
|
+
record_count: session2.sessionRecordCount,
|
|
2022
|
+
chain_start_hash: chainStartHash,
|
|
2023
|
+
chain_end_hash: endRecord.hash,
|
|
2024
|
+
seal_signature: sealSignature
|
|
869
2025
|
};
|
|
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 });
|
|
2026
|
+
const allSessions = readJson(SESSIONS_FILE, []);
|
|
2027
|
+
allSessions.push(seal);
|
|
2028
|
+
writeJson(SESSIONS_FILE, allSessions);
|
|
2029
|
+
}
|
|
2030
|
+
function resetIdleTimer(sessionId) {
|
|
2031
|
+
const active = sessions.get(sessionId);
|
|
2032
|
+
if (!active) return;
|
|
2033
|
+
clearTimeout(active.idleTimer);
|
|
2034
|
+
active.idleTimer = setTimeout(async () => {
|
|
2035
|
+
autoSealSession(active);
|
|
2036
|
+
try {
|
|
2037
|
+
await active.transport.close();
|
|
2038
|
+
} catch {
|
|
945
2039
|
}
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
|
|
2040
|
+
sessions.delete(sessionId);
|
|
2041
|
+
}, IDLE_TIMEOUT_MS);
|
|
2042
|
+
}
|
|
2043
|
+
async function cleanupSession(sessionId) {
|
|
2044
|
+
const active = sessions.get(sessionId);
|
|
2045
|
+
if (!active) return;
|
|
2046
|
+
clearTimeout(active.idleTimer);
|
|
2047
|
+
autoSealSession(active);
|
|
949
2048
|
try {
|
|
950
|
-
|
|
951
|
-
return fallback;
|
|
952
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
2049
|
+
await active.transport.close();
|
|
953
2050
|
} 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
2051
|
}
|
|
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) {
|
|
2052
|
+
sessions.delete(sessionId);
|
|
2053
|
+
}
|
|
2054
|
+
function handleHealth(res) {
|
|
2055
|
+
const body = JSON.stringify({
|
|
2056
|
+
status: "ok",
|
|
2057
|
+
version: VERSION,
|
|
2058
|
+
active_sessions: sessions.size,
|
|
2059
|
+
uptime_seconds: Math.round((Date.now() - startedAt) / 1e3)
|
|
2060
|
+
});
|
|
2061
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2062
|
+
res.end(body);
|
|
2063
|
+
}
|
|
2064
|
+
function parseBody(req) {
|
|
2065
|
+
return new Promise((resolve, reject) => {
|
|
2066
|
+
const chunks = [];
|
|
2067
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2068
|
+
req.on("end", () => {
|
|
1032
2069
|
try {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
2070
|
+
const raw = Buffer.concat(chunks).toString();
|
|
2071
|
+
resolve(raw ? JSON.parse(raw) : void 0);
|
|
2072
|
+
} catch (e) {
|
|
2073
|
+
reject(e);
|
|
1037
2074
|
}
|
|
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`);
|
|
2075
|
+
});
|
|
2076
|
+
req.on("error", reject);
|
|
2077
|
+
});
|
|
1047
2078
|
}
|
|
1048
|
-
function
|
|
1049
|
-
const
|
|
2079
|
+
async function startDaemon(port) {
|
|
2080
|
+
const listenPort = port ?? DAEMON_PORT;
|
|
1050
2081
|
ensureDir();
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
2082
|
+
const server2 = createServer(async (req, res) => {
|
|
2083
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2084
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
2085
|
+
handleHealth(res);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (url.pathname !== "/mcp") {
|
|
2089
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2090
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
try {
|
|
2094
|
+
if (req.method === "POST") {
|
|
2095
|
+
const body = await parseBody(req);
|
|
2096
|
+
const sid = req.headers["mcp-session-id"];
|
|
2097
|
+
if (sid && sessions.has(sid)) {
|
|
2098
|
+
resetIdleTimer(sid);
|
|
2099
|
+
await sessions.get(sid).transport.handleRequest(req, res, body);
|
|
2100
|
+
} else if (!sid && isInitializeRequest(body)) {
|
|
2101
|
+
const sessionState = new SessionState();
|
|
2102
|
+
try {
|
|
2103
|
+
sessionState.initializeKeystore();
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
const mcpServer = new McpServer({
|
|
2107
|
+
name: "UseAI",
|
|
2108
|
+
version: VERSION
|
|
2109
|
+
});
|
|
2110
|
+
registerTools(mcpServer, sessionState);
|
|
2111
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2112
|
+
sessionIdGenerator: () => randomUUID4(),
|
|
2113
|
+
onsessioninitialized: (newSid) => {
|
|
2114
|
+
const idleTimer = setTimeout(async () => {
|
|
2115
|
+
await cleanupSession(newSid);
|
|
2116
|
+
}, IDLE_TIMEOUT_MS);
|
|
2117
|
+
sessions.set(newSid, {
|
|
2118
|
+
transport,
|
|
2119
|
+
server: mcpServer,
|
|
2120
|
+
session: sessionState,
|
|
2121
|
+
idleTimer
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
transport.onclose = () => {
|
|
2126
|
+
const closedSid = transport.sessionId;
|
|
2127
|
+
if (closedSid && sessions.has(closedSid)) {
|
|
2128
|
+
const active = sessions.get(closedSid);
|
|
2129
|
+
clearTimeout(active.idleTimer);
|
|
2130
|
+
autoSealSession(active);
|
|
2131
|
+
sessions.delete(closedSid);
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
await mcpServer.connect(transport);
|
|
2135
|
+
await transport.handleRequest(req, res, body);
|
|
2136
|
+
} else {
|
|
2137
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2138
|
+
res.end(JSON.stringify({
|
|
2139
|
+
jsonrpc: "2.0",
|
|
2140
|
+
error: { code: -32e3, message: "Bad Request: No valid session ID provided" },
|
|
2141
|
+
id: null
|
|
2142
|
+
}));
|
|
2143
|
+
}
|
|
2144
|
+
} else if (req.method === "GET") {
|
|
2145
|
+
const sid = req.headers["mcp-session-id"];
|
|
2146
|
+
if (!sid || !sessions.has(sid)) {
|
|
2147
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2148
|
+
res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
resetIdleTimer(sid);
|
|
2152
|
+
await sessions.get(sid).transport.handleRequest(req, res);
|
|
2153
|
+
} else if (req.method === "DELETE") {
|
|
2154
|
+
const sid = req.headers["mcp-session-id"];
|
|
2155
|
+
if (!sid || !sessions.has(sid)) {
|
|
2156
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2157
|
+
res.end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
await sessions.get(sid).transport.handleRequest(req, res);
|
|
2161
|
+
} else {
|
|
2162
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
2163
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2164
|
+
}
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
if (!res.headersSent) {
|
|
2167
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2168
|
+
res.end(JSON.stringify({
|
|
2169
|
+
jsonrpc: "2.0",
|
|
2170
|
+
error: { code: -32603, message: "Internal server error" },
|
|
2171
|
+
id: null
|
|
2172
|
+
}));
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
const pidData = JSON.stringify({
|
|
2177
|
+
pid: process.pid,
|
|
2178
|
+
port: listenPort,
|
|
2179
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2180
|
+
});
|
|
2181
|
+
writeFileSync4(DAEMON_PID_FILE, pidData + "\n");
|
|
2182
|
+
const shutdown = async (signal) => {
|
|
2183
|
+
for (const [sid] of sessions) {
|
|
2184
|
+
await cleanupSession(sid);
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
if (existsSync8(DAEMON_PID_FILE)) {
|
|
2188
|
+
unlinkSync4(DAEMON_PID_FILE);
|
|
2189
|
+
}
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
server2.close(() => {
|
|
2193
|
+
process.exit(0);
|
|
2194
|
+
});
|
|
2195
|
+
setTimeout(() => process.exit(0), 5e3).unref();
|
|
2196
|
+
};
|
|
2197
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2198
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2199
|
+
server2.listen(listenPort, "127.0.0.1", () => {
|
|
2200
|
+
console.log(`UseAI daemon listening on http://127.0.0.1:${listenPort}`);
|
|
2201
|
+
console.log(`PID: ${process.pid}`);
|
|
2202
|
+
});
|
|
1055
2203
|
}
|
|
2204
|
+
var IDLE_TIMEOUT_MS, sessions, startedAt;
|
|
2205
|
+
var init_daemon2 = __esm({
|
|
2206
|
+
"src/daemon.ts"() {
|
|
2207
|
+
"use strict";
|
|
2208
|
+
init_dist();
|
|
2209
|
+
init_session_state();
|
|
2210
|
+
init_register_tools();
|
|
2211
|
+
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2212
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2213
|
+
startedAt = Date.now();
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
1056
2216
|
|
|
1057
2217
|
// src/index.ts
|
|
2218
|
+
init_dist();
|
|
2219
|
+
init_session_state();
|
|
2220
|
+
init_register_tools();
|
|
2221
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2222
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1058
2223
|
if (process.argv[2] === "mcp") {
|
|
1059
2224
|
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
1060
2225
|
await runSetup2(process.argv.slice(3));
|
|
1061
2226
|
process.exit(0);
|
|
1062
2227
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
2228
|
+
if (process.argv[2] === "daemon") {
|
|
2229
|
+
const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon2(), daemon_exports));
|
|
2230
|
+
const portArg = process.argv.indexOf("--port");
|
|
2231
|
+
const port = portArg !== -1 ? parseInt(process.argv[portArg + 1], 10) : void 0;
|
|
2232
|
+
await startDaemon2(port);
|
|
2233
|
+
await new Promise(() => {
|
|
1067
2234
|
});
|
|
1068
2235
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
function getMilestones() {
|
|
1073
|
-
return readJson(MILESTONES_FILE, []);
|
|
1074
|
-
}
|
|
1075
|
-
var server = new McpServer({
|
|
2236
|
+
var session = new SessionState();
|
|
2237
|
+
var server = new McpServer2({
|
|
1076
2238
|
name: "UseAI",
|
|
1077
2239
|
version: VERSION
|
|
1078
2240
|
});
|
|
1079
|
-
server
|
|
1080
|
-
"useai_session_start",
|
|
1081
|
-
"Start tracking an AI coding session. Call this at the beginning of a conversation. Records which AI tool and task type \u2014 stored LOCALLY at ~/.useai/. No data is sent anywhere. No code, prompts, or private data is captured.",
|
|
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
|
-
All data stored locally at ${USEAI_DIR}. No network calls made.`
|
|
1101
|
-
}
|
|
1102
|
-
]
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
);
|
|
1106
|
-
server.tool(
|
|
1107
|
-
"useai_heartbeat",
|
|
1108
|
-
"Record a heartbeat for the current AI coding session. Call this periodically during long conversations (every 10-15 minutes). Only records a timestamp \u2014 stored locally, never sent anywhere.",
|
|
1109
|
-
{},
|
|
1110
|
-
async () => {
|
|
1111
|
-
incrementHeartbeat();
|
|
1112
|
-
appendToChain("heartbeat", {
|
|
1113
|
-
heartbeat_number: heartbeatCount,
|
|
1114
|
-
cumulative_seconds: getSessionDuration()
|
|
1115
|
-
});
|
|
1116
|
-
return {
|
|
1117
|
-
content: [
|
|
1118
|
-
{
|
|
1119
|
-
type: "text",
|
|
1120
|
-
text: `Heartbeat recorded locally. Session active for ${formatDuration(getSessionDuration())}.`
|
|
1121
|
-
}
|
|
1122
|
-
]
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
);
|
|
1126
|
-
server.tool(
|
|
1127
|
-
"useai_session_end",
|
|
1128
|
-
"End the current AI coding session. Creates a cryptographically sealed session record and optionally records milestones \u2014 what was accomplished. Stats auto-sync to useai.dev daily. To publish milestones: run `useai publish` from the CLI. IMPORTANT: Milestone descriptions must be GENERIC. Do not include project names, file names, file paths, API endpoints, database names, company names, or any project-specific information. Use general terms like 'authentication system', 'data validation layer', 'responsive dashboard'. Think: would this description reveal what company or project this is for? If yes, make it more generic.",
|
|
1129
|
-
{
|
|
1130
|
-
task_type: z2.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task was the developer working on?"),
|
|
1131
|
-
languages: z2.array(z2.string()).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
|
|
1132
|
-
files_touched_count: z2.number().optional().describe("Approximate number of files created or modified (count only, no names)"),
|
|
1133
|
-
milestones: z2.array(z2.object({
|
|
1134
|
-
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/'"),
|
|
1135
|
-
category: z2.enum(["feature", "bugfix", "refactor", "test", "docs", "setup", "deployment", "other"]).describe("Type of work completed"),
|
|
1136
|
-
complexity: z2.enum(["simple", "medium", "complex"]).optional().describe("How complex was this task?")
|
|
1137
|
-
})).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.")
|
|
1138
|
-
},
|
|
1139
|
-
async ({ task_type, languages, files_touched_count, milestones: milestonesInput }) => {
|
|
1140
|
-
const duration = getSessionDuration();
|
|
1141
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1142
|
-
const finalTaskType = task_type ?? sessionTaskType;
|
|
1143
|
-
const chainStartHash = chainTipHash === "GENESIS" ? "GENESIS" : chainTipHash;
|
|
1144
|
-
const endRecord = appendToChain("session_end", {
|
|
1145
|
-
duration_seconds: duration,
|
|
1146
|
-
task_type: finalTaskType,
|
|
1147
|
-
languages: languages ?? [],
|
|
1148
|
-
files_touched: files_touched_count ?? 0,
|
|
1149
|
-
heartbeat_count: heartbeatCount
|
|
1150
|
-
});
|
|
1151
|
-
const sealData = JSON.stringify({
|
|
1152
|
-
session_id: sessionId,
|
|
1153
|
-
client: clientName,
|
|
1154
|
-
task_type: finalTaskType,
|
|
1155
|
-
languages: languages ?? [],
|
|
1156
|
-
files_touched: files_touched_count ?? 0,
|
|
1157
|
-
started_at: new Date(sessionStartTime).toISOString(),
|
|
1158
|
-
ended_at: now,
|
|
1159
|
-
duration_seconds: duration,
|
|
1160
|
-
heartbeat_count: heartbeatCount,
|
|
1161
|
-
record_count: sessionRecordCount,
|
|
1162
|
-
chain_end_hash: endRecord.hash
|
|
1163
|
-
});
|
|
1164
|
-
const sealSignature = signHash(
|
|
1165
|
-
createHash3("sha256").update(sealData).digest("hex"),
|
|
1166
|
-
signingKey
|
|
1167
|
-
);
|
|
1168
|
-
appendToChain("session_seal", {
|
|
1169
|
-
seal: sealData,
|
|
1170
|
-
seal_signature: sealSignature
|
|
1171
|
-
});
|
|
1172
|
-
const activePath = join4(ACTIVE_DIR, `${sessionId}.jsonl`);
|
|
1173
|
-
const sealedPath = join4(SEALED_DIR, `${sessionId}.jsonl`);
|
|
1174
|
-
try {
|
|
1175
|
-
if (existsSync4(activePath)) {
|
|
1176
|
-
renameSync2(activePath, sealedPath);
|
|
1177
|
-
}
|
|
1178
|
-
} catch {
|
|
1179
|
-
}
|
|
1180
|
-
const seal = {
|
|
1181
|
-
session_id: sessionId,
|
|
1182
|
-
client: clientName,
|
|
1183
|
-
task_type: finalTaskType,
|
|
1184
|
-
languages: languages ?? [],
|
|
1185
|
-
files_touched: files_touched_count ?? 0,
|
|
1186
|
-
started_at: new Date(sessionStartTime).toISOString(),
|
|
1187
|
-
ended_at: now,
|
|
1188
|
-
duration_seconds: duration,
|
|
1189
|
-
heartbeat_count: heartbeatCount,
|
|
1190
|
-
record_count: sessionRecordCount,
|
|
1191
|
-
chain_start_hash: chainStartHash,
|
|
1192
|
-
chain_end_hash: endRecord.hash,
|
|
1193
|
-
seal_signature: sealSignature
|
|
1194
|
-
};
|
|
1195
|
-
const sessions = getSessions();
|
|
1196
|
-
sessions.push(seal);
|
|
1197
|
-
writeJson(SESSIONS_FILE, sessions);
|
|
1198
|
-
let milestoneCount = 0;
|
|
1199
|
-
if (milestonesInput && milestonesInput.length > 0) {
|
|
1200
|
-
const config = getConfig();
|
|
1201
|
-
if (config.milestone_tracking) {
|
|
1202
|
-
const durationMinutes = Math.round(duration / 60);
|
|
1203
|
-
const allMilestones = getMilestones();
|
|
1204
|
-
for (const m of milestonesInput) {
|
|
1205
|
-
const record = appendToChain("milestone", {
|
|
1206
|
-
title: m.title,
|
|
1207
|
-
category: m.category,
|
|
1208
|
-
complexity: m.complexity ?? "medium",
|
|
1209
|
-
duration_minutes: durationMinutes,
|
|
1210
|
-
languages: languages ?? []
|
|
1211
|
-
});
|
|
1212
|
-
const milestone = {
|
|
1213
|
-
id: `m_${randomUUID3().slice(0, 8)}`,
|
|
1214
|
-
session_id: sessionId,
|
|
1215
|
-
title: m.title,
|
|
1216
|
-
category: m.category,
|
|
1217
|
-
complexity: m.complexity ?? "medium",
|
|
1218
|
-
duration_minutes: durationMinutes,
|
|
1219
|
-
languages: languages ?? [],
|
|
1220
|
-
client: clientName,
|
|
1221
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1222
|
-
published: false,
|
|
1223
|
-
published_at: null,
|
|
1224
|
-
chain_hash: record.hash
|
|
1225
|
-
};
|
|
1226
|
-
allMilestones.push(milestone);
|
|
1227
|
-
milestoneCount++;
|
|
1228
|
-
}
|
|
1229
|
-
writeJson(MILESTONES_FILE, allMilestones);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
const durationStr = formatDuration(duration);
|
|
1233
|
-
const langStr = languages && languages.length > 0 ? ` using ${languages.join(", ")}` : "";
|
|
1234
|
-
const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
|
|
1235
|
-
return {
|
|
1236
|
-
content: [
|
|
1237
|
-
{
|
|
1238
|
-
type: "text",
|
|
1239
|
-
text: `Session sealed: ${durationStr} ${finalTaskType} on ${clientName}${langStr}
|
|
1240
|
-
${sessionRecordCount} chain records \xB7 ${signingAvailable ? "Ed25519 signed" : "unsigned"}${milestoneStr}
|
|
1241
|
-
Stored at: ${sealedPath}
|
|
1242
|
-
|
|
1243
|
-
To publish: run \`useai sync\` from your terminal.`
|
|
1244
|
-
}
|
|
1245
|
-
]
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
);
|
|
2241
|
+
registerTools(server, session);
|
|
1249
2242
|
async function main() {
|
|
1250
2243
|
ensureDir();
|
|
1251
2244
|
try {
|
|
1252
|
-
initializeKeystore();
|
|
2245
|
+
session.initializeKeystore();
|
|
1253
2246
|
} catch {
|
|
1254
2247
|
}
|
|
1255
2248
|
const transport = new StdioServerTransport();
|