flyspace-cli 0.1.13 → 0.1.15
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/{chunk-VKTJBYA3.js → chunk-KFT2YP5W.js} +386 -13
- package/dist/cli.js +925 -961
- package/dist/index.js +1 -1
- package/package.json +3 -1
- package/templates/general/01-welcome.md +20 -0
- package/templates/general/02-working-with-the-agent.md +21 -0
- package/templates/general/03-channels-and-folders.md +19 -0
- package/templates/general/04-docs.md +14 -0
- package/templates/general/05-inviting-your-team.md +17 -0
- package/templates/general/06-automations.md +21 -0
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
HandleTakenError,
|
|
14
14
|
InvalidTransferError,
|
|
15
15
|
LastAdminError,
|
|
16
|
+
MEMORY_TYPES,
|
|
16
17
|
MessageQueue,
|
|
17
18
|
OutputStyleConflictError,
|
|
18
19
|
SKIP_SUBTYPES,
|
|
@@ -39,12 +40,15 @@ import {
|
|
|
39
40
|
createOutputStyle,
|
|
40
41
|
createSkillFile,
|
|
41
42
|
createWorkspace,
|
|
43
|
+
deleteChannelMemory,
|
|
42
44
|
deleteMembership,
|
|
43
45
|
deleteOutputStyle,
|
|
44
46
|
deleteSkillFile,
|
|
45
47
|
deleteWorkspace,
|
|
48
|
+
disableFeatureFlag,
|
|
46
49
|
displayNameFor,
|
|
47
50
|
docsAccessPromptNote,
|
|
51
|
+
enableFeatureFlag,
|
|
48
52
|
ensureAgentDmChannel,
|
|
49
53
|
ensureGeneralChannel,
|
|
50
54
|
ensureLogicalCwdDir,
|
|
@@ -52,12 +56,15 @@ import {
|
|
|
52
56
|
ensureUsersCached,
|
|
53
57
|
expandPath,
|
|
54
58
|
extractCategories,
|
|
59
|
+
featureFlagIds,
|
|
60
|
+
featureFlags,
|
|
55
61
|
findChannel,
|
|
56
62
|
findChannelByAgentName,
|
|
57
63
|
findChannelById,
|
|
58
64
|
findSkillDetail,
|
|
59
65
|
getAgentPaused,
|
|
60
66
|
getAllowGuestAccess,
|
|
67
|
+
getChannelMemory,
|
|
61
68
|
getChannelRegistry,
|
|
62
69
|
getMembershipByEmail,
|
|
63
70
|
getMembershipByUserId,
|
|
@@ -68,17 +75,24 @@ import {
|
|
|
68
75
|
getWorkspaceById,
|
|
69
76
|
getWorkspaceBySlug,
|
|
70
77
|
getWorkspaceByStripeCustomerId,
|
|
78
|
+
getWorkspaceGreetedAt,
|
|
71
79
|
getWorkspacePlanStatus,
|
|
72
80
|
installUsageRecording,
|
|
73
81
|
isAllowedModel,
|
|
74
82
|
isCodexModel,
|
|
75
83
|
isCwdUsable,
|
|
76
84
|
isEffort,
|
|
85
|
+
isFeatureFlagId,
|
|
86
|
+
isFeatureVisible,
|
|
77
87
|
isLockedPlan,
|
|
78
88
|
isLogicalCwd,
|
|
89
|
+
isMemoryType,
|
|
90
|
+
isUuid,
|
|
79
91
|
isValidSkillName,
|
|
80
92
|
lastRunsForWorkspace,
|
|
81
93
|
listActiveUsers,
|
|
94
|
+
listChannelMemories,
|
|
95
|
+
listEnabledFeatureFlags,
|
|
82
96
|
listMemberChannels,
|
|
83
97
|
listMemberships,
|
|
84
98
|
listMembershipsByEmail,
|
|
@@ -86,9 +100,11 @@ import {
|
|
|
86
100
|
listPluginSkillDetails,
|
|
87
101
|
listSkillFiles,
|
|
88
102
|
listSkills,
|
|
103
|
+
listThreadMemoryEvents,
|
|
89
104
|
listUsersWithAvatars,
|
|
90
105
|
listWorkspaceSummaries,
|
|
91
106
|
listWorkspaces,
|
|
107
|
+
loadAgentMemory,
|
|
92
108
|
loadAutomations,
|
|
93
109
|
loadAutomationsFile,
|
|
94
110
|
loadBotIdentity,
|
|
@@ -103,6 +119,7 @@ import {
|
|
|
103
119
|
lookupById,
|
|
104
120
|
markInviteResent,
|
|
105
121
|
markWorkspaceAgentConnected,
|
|
122
|
+
markWorkspaceGreeted,
|
|
106
123
|
markWorkspaceProjectsSetup,
|
|
107
124
|
markdownToBlocks,
|
|
108
125
|
mcpStrictConfigEnabled,
|
|
@@ -120,6 +137,7 @@ import {
|
|
|
120
137
|
resolveChannelId,
|
|
121
138
|
resolveChannelPrompt,
|
|
122
139
|
resolveCwd,
|
|
140
|
+
resolveFeatureFlags,
|
|
123
141
|
resolveLogicalCwd,
|
|
124
142
|
resolveSelectedOutputStyle,
|
|
125
143
|
resolveUserId,
|
|
@@ -128,9 +146,11 @@ import {
|
|
|
128
146
|
runnerForModel,
|
|
129
147
|
saveAgentProfile,
|
|
130
148
|
saveBotIdentity,
|
|
149
|
+
saveChannelMemory,
|
|
131
150
|
saveDashboardSettings,
|
|
132
151
|
saveWorkspaceBranding,
|
|
133
152
|
saveWorkspacePrompt,
|
|
153
|
+
searchChannelMemories,
|
|
134
154
|
setAgentPaused,
|
|
135
155
|
setAllowGuestAccess,
|
|
136
156
|
setMembershipHandle,
|
|
@@ -143,17 +163,20 @@ import {
|
|
|
143
163
|
syncSlackAvatars,
|
|
144
164
|
transferWorkspaceAdmin,
|
|
145
165
|
tsToIso,
|
|
166
|
+
undoMemoryEvent,
|
|
146
167
|
updateChannelById,
|
|
168
|
+
updateChannelMemory,
|
|
147
169
|
updateChannelsMcpConnections,
|
|
148
170
|
updateMembershipRole,
|
|
149
171
|
updateOutputStyle,
|
|
150
172
|
updateWorkspacePlanStatus,
|
|
151
173
|
usageLimitedModels,
|
|
152
174
|
usageLimitedUntil,
|
|
175
|
+
validateMemoryInput,
|
|
153
176
|
warnOnBrokenChannelCwds,
|
|
154
177
|
workspaceSkillsDir,
|
|
155
178
|
writeSkillFile
|
|
156
|
-
} from "./chunk-
|
|
179
|
+
} from "./chunk-KFT2YP5W.js";
|
|
157
180
|
import {
|
|
158
181
|
firstReplyBody,
|
|
159
182
|
getThread,
|
|
@@ -220,8 +243,8 @@ import { dirname, join } from "path";
|
|
|
220
243
|
var REQUIRED_ENV = ["SUPABASE_URL", "SUPABASE_SECRET_KEY"];
|
|
221
244
|
var RENOTIFY_AFTER_MS = 30 * 60 * 1e3;
|
|
222
245
|
var ts = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
223
|
-
function findMissingEnv(env = process.env) {
|
|
224
|
-
const required = slackConfigured(env) ? [
|
|
246
|
+
function findMissingEnv(env = process.env, supabaseViaHost = false) {
|
|
247
|
+
const required = [...slackConfigured(env) ? SLACK_ENV : [], ...supabaseViaHost ? [] : REQUIRED_ENV];
|
|
225
248
|
return required.filter((name) => !env[name]?.trim());
|
|
226
249
|
}
|
|
227
250
|
function shouldNotify(missing, statePath, now) {
|
|
@@ -290,7 +313,7 @@ async function preflight(opts = {}) {
|
|
|
290
313
|
const statePath = opts.statePath ?? join(paths.DATA_DIR, "preflight-alert.json");
|
|
291
314
|
const notify = opts.notify ?? notifyOwnerOverSlack;
|
|
292
315
|
const exit = opts.exit ?? ((code) => process.exit(code));
|
|
293
|
-
const missing = findMissingEnv(env);
|
|
316
|
+
const missing = findMissingEnv(env, opts.supabaseViaHost);
|
|
294
317
|
if (missing.length === 0) {
|
|
295
318
|
rmSync(statePath, { force: true });
|
|
296
319
|
return;
|
|
@@ -307,15 +330,392 @@ async function preflight(opts = {}) {
|
|
|
307
330
|
exit(1);
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
// src/auth/device-auth.ts
|
|
334
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
335
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
336
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
337
|
+
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
338
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
339
|
+
var AuthApiError = class extends Error {
|
|
340
|
+
constructor(message, status) {
|
|
341
|
+
super(message);
|
|
342
|
+
this.status = status;
|
|
343
|
+
this.name = "AuthApiError";
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
function apiUrl() {
|
|
347
|
+
return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
348
|
+
}
|
|
349
|
+
function deviceKeyPath() {
|
|
350
|
+
return join2(paths.BASE_CONFIG_DIR, "device.key");
|
|
351
|
+
}
|
|
352
|
+
function credentialsPath() {
|
|
353
|
+
return join2(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
354
|
+
}
|
|
355
|
+
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
356
|
+
let created = false;
|
|
357
|
+
if (!existsSync(path)) {
|
|
358
|
+
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
359
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
360
|
+
writeFileSync2(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
361
|
+
created = true;
|
|
362
|
+
}
|
|
363
|
+
const privateKey = createPrivateKey(readFileSync2(path, "utf8"));
|
|
364
|
+
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
365
|
+
"base64"
|
|
366
|
+
);
|
|
367
|
+
return { privateKey, publicKeyBase64, created };
|
|
368
|
+
}
|
|
369
|
+
function loadCredentials(path = credentialsPath()) {
|
|
370
|
+
if (!existsSync(path)) return null;
|
|
371
|
+
try {
|
|
372
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function saveCredentials(creds, path = credentialsPath()) {
|
|
378
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
379
|
+
writeFileSync2(path, `${JSON.stringify(creds, null, 2)}
|
|
380
|
+
`, { mode: 384 });
|
|
381
|
+
}
|
|
382
|
+
function clearLocalAuth() {
|
|
383
|
+
rmSync2(credentialsPath(), { force: true });
|
|
384
|
+
rmSync2(deviceKeyPath(), { force: true });
|
|
385
|
+
}
|
|
386
|
+
async function post(base, path, body, token) {
|
|
387
|
+
return fetch(`${base}${path}`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: {
|
|
390
|
+
"content-type": "application/json",
|
|
391
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify(body)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async function readJson(res) {
|
|
397
|
+
try {
|
|
398
|
+
return await res.json();
|
|
399
|
+
} catch {
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function expectOk(res, what) {
|
|
404
|
+
const json = await readJson(res);
|
|
405
|
+
if (!res.ok) {
|
|
406
|
+
const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
|
|
407
|
+
throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
|
|
408
|
+
}
|
|
409
|
+
return json;
|
|
410
|
+
}
|
|
411
|
+
function toSlugs(value) {
|
|
412
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
413
|
+
}
|
|
414
|
+
function toGrant(json) {
|
|
415
|
+
const user = json["user"] ?? {};
|
|
416
|
+
return {
|
|
417
|
+
accessToken: String(json["access_token"]),
|
|
418
|
+
expiresIn: Number(json["expires_in"]),
|
|
419
|
+
deviceId: String(json["device_id"]),
|
|
420
|
+
userEmail: String(user["email"] ?? ""),
|
|
421
|
+
workspaces: toSlugs(json["workspaces"])
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async function requestDeviceCode(base, input2) {
|
|
425
|
+
const res = await post(base, "/api/auth/device/code", {
|
|
426
|
+
public_key: input2.publicKey,
|
|
427
|
+
name: input2.name,
|
|
428
|
+
platform: input2.platform
|
|
429
|
+
});
|
|
430
|
+
const json = await expectOk(res, "Requesting a device code");
|
|
431
|
+
return {
|
|
432
|
+
deviceCode: String(json["device_code"]),
|
|
433
|
+
userCode: String(json["user_code"]),
|
|
434
|
+
verificationUri: String(json["verification_uri"]),
|
|
435
|
+
expiresIn: Number(json["expires_in"]),
|
|
436
|
+
interval: Number(json["interval"] ?? 5)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async function pollDeviceToken(base, deviceCode) {
|
|
440
|
+
const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
|
|
441
|
+
const json = await readJson(res);
|
|
442
|
+
if (res.ok) return { status: "approved", grant: toGrant(json) };
|
|
443
|
+
switch (json["error"]) {
|
|
444
|
+
case "authorization_pending":
|
|
445
|
+
return { status: "pending" };
|
|
446
|
+
case "slow_down":
|
|
447
|
+
return { status: "slow_down" };
|
|
448
|
+
case "access_denied":
|
|
449
|
+
return { status: "denied" };
|
|
450
|
+
case "expired_token":
|
|
451
|
+
return { status: "expired" };
|
|
452
|
+
default:
|
|
453
|
+
throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function refreshAccessToken(creds, privateKey) {
|
|
457
|
+
const challenge = await expectOk(
|
|
458
|
+
await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
|
|
459
|
+
"Requesting a refresh challenge"
|
|
460
|
+
);
|
|
461
|
+
const nonce = String(challenge["nonce"]);
|
|
462
|
+
const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
|
|
463
|
+
const json = await expectOk(
|
|
464
|
+
await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
|
|
465
|
+
"Refreshing the access token"
|
|
466
|
+
);
|
|
467
|
+
return {
|
|
468
|
+
...creds,
|
|
469
|
+
accessToken: String(json["access_token"]),
|
|
470
|
+
accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function fetchWhoAmI(base, token) {
|
|
474
|
+
const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
475
|
+
const json = await expectOk(res, "Checking the session");
|
|
476
|
+
const user = json["user"] ?? {};
|
|
477
|
+
const device = json["device"] ?? {};
|
|
478
|
+
return {
|
|
479
|
+
userEmail: String(user["email"] ?? ""),
|
|
480
|
+
deviceId: String(device["id"] ?? ""),
|
|
481
|
+
deviceName: String(device["name"] ?? ""),
|
|
482
|
+
workspaces: toSlugs(json["workspaces"])
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
async function boundWorkspaces() {
|
|
486
|
+
const creds = loadCredentials();
|
|
487
|
+
if (!creds) return null;
|
|
488
|
+
try {
|
|
489
|
+
const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
|
|
490
|
+
saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
|
|
491
|
+
return { workspaces: me.workspaces, fresh: true };
|
|
492
|
+
} catch {
|
|
493
|
+
return { workspaces: creds.workspaces ?? [], fresh: false };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function revokeDevice(base, token) {
|
|
497
|
+
await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
|
|
498
|
+
}
|
|
499
|
+
async function getAccessToken() {
|
|
500
|
+
const fromEnv = process.env["FLYSPACE_TOKEN"];
|
|
501
|
+
if (fromEnv) return fromEnv;
|
|
502
|
+
const creds = loadCredentials();
|
|
503
|
+
if (!creds) return null;
|
|
504
|
+
if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
|
|
505
|
+
const { privateKey } = ensureDeviceKey();
|
|
506
|
+
const refreshed = await refreshAccessToken(creds, privateKey);
|
|
507
|
+
saveCredentials(refreshed);
|
|
508
|
+
return refreshed.accessToken;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/dashboard/host-db-client.ts
|
|
512
|
+
import { createClient } from "@supabase/supabase-js";
|
|
513
|
+
import { randomUUID } from "crypto";
|
|
514
|
+
|
|
515
|
+
// src/dashboard/host-lock.ts
|
|
516
|
+
import { execFileSync } from "child_process";
|
|
517
|
+
import { randomBytes } from "crypto";
|
|
518
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
519
|
+
import { join as join3 } from "path";
|
|
520
|
+
function hostLockPath() {
|
|
521
|
+
return join3(paths.BASE_DATA_DIR, "host.lock");
|
|
522
|
+
}
|
|
523
|
+
function isLiveFlyspace(pid) {
|
|
524
|
+
try {
|
|
525
|
+
process.kill(pid, 0);
|
|
526
|
+
const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
527
|
+
encoding: "utf8"
|
|
528
|
+
});
|
|
529
|
+
return command.includes("flyspace") || command.includes("host-server");
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function readLockPid(path) {
|
|
535
|
+
try {
|
|
536
|
+
const pid = Number(JSON.parse(readFileSync3(path, "utf8")).pid);
|
|
537
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function acquireHostLock() {
|
|
543
|
+
const path = hostLockPath();
|
|
544
|
+
mkdirSync3(paths.BASE_DATA_DIR, { recursive: true });
|
|
545
|
+
const body = JSON.stringify({
|
|
546
|
+
pid: process.pid,
|
|
547
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
548
|
+
execPath: process.argv[1] ?? ""
|
|
549
|
+
});
|
|
550
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
551
|
+
try {
|
|
552
|
+
writeFileSync3(path, body, { flag: "wx", mode: 384 });
|
|
553
|
+
const release2 = () => {
|
|
554
|
+
if (readLockPid(path) === process.pid) rmSync3(path, { force: true });
|
|
555
|
+
};
|
|
556
|
+
process.once("exit", release2);
|
|
557
|
+
return release2;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (err.code !== "EEXIST") throw err;
|
|
560
|
+
const holder = readLockPid(path);
|
|
561
|
+
if (holder === process.pid) return () => {
|
|
562
|
+
};
|
|
563
|
+
if (holder && isLiveFlyspace(holder)) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
rmSync3(path, { force: true });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
throw new Error(`Could not take the Host lock at ${path}`);
|
|
572
|
+
}
|
|
573
|
+
function publishHostEndpoint(port) {
|
|
574
|
+
const path = hostLockPath();
|
|
575
|
+
if (readLockPid(path) !== process.pid) return null;
|
|
576
|
+
const token = randomBytes(32).toString("hex");
|
|
577
|
+
const lock = JSON.parse(readFileSync3(path, "utf8"));
|
|
578
|
+
writeFileSync3(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
579
|
+
return token;
|
|
580
|
+
}
|
|
581
|
+
function readHostEndpoint() {
|
|
582
|
+
try {
|
|
583
|
+
const { pid, port, token } = JSON.parse(readFileSync3(hostLockPath(), "utf8"));
|
|
584
|
+
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
585
|
+
return { port, token };
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/dashboard/host-db-client.ts
|
|
592
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
593
|
+
var pending = /* @__PURE__ */ new Map();
|
|
594
|
+
var sender = null;
|
|
595
|
+
var onSender = [];
|
|
596
|
+
function setHostDbSender(fn) {
|
|
597
|
+
sender = fn;
|
|
598
|
+
if (!fn) return;
|
|
599
|
+
const waiting = onSender;
|
|
600
|
+
onSender = [];
|
|
601
|
+
for (const wake of waiting) wake();
|
|
602
|
+
}
|
|
603
|
+
function handleHostDbResult(id, res) {
|
|
604
|
+
const p = pending.get(id);
|
|
605
|
+
if (!p) return;
|
|
606
|
+
pending.delete(id);
|
|
607
|
+
clearTimeout(p.timer);
|
|
608
|
+
p.resolve(res);
|
|
609
|
+
}
|
|
610
|
+
function whenConnected() {
|
|
611
|
+
if (sender) return Promise.resolve(sender);
|
|
612
|
+
return new Promise((resolve7, reject) => {
|
|
613
|
+
const timer2 = setTimeout(() => {
|
|
614
|
+
onSender = onSender.filter((w) => w !== wake);
|
|
615
|
+
reject(new Error("Host is not connected to the Data API"));
|
|
616
|
+
}, REQUEST_TIMEOUT_MS);
|
|
617
|
+
const wake = () => {
|
|
618
|
+
clearTimeout(timer2);
|
|
619
|
+
resolve7(sender);
|
|
620
|
+
};
|
|
621
|
+
onSender.push(wake);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
async function requestHostDb(req) {
|
|
625
|
+
const send2 = await whenConnected();
|
|
626
|
+
const id = randomUUID();
|
|
627
|
+
return new Promise((resolve7, reject) => {
|
|
628
|
+
const timer2 = setTimeout(() => {
|
|
629
|
+
pending.delete(id);
|
|
630
|
+
reject(new Error("Data API did not answer in time"));
|
|
631
|
+
}, REQUEST_TIMEOUT_MS);
|
|
632
|
+
pending.set(id, { resolve: resolve7, reject, timer: timer2 });
|
|
633
|
+
send2({ type: "db", id, req });
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
637
|
+
function dbFetch(send2) {
|
|
638
|
+
return async (input2, init2) => {
|
|
639
|
+
const req = new Request(input2, init2);
|
|
640
|
+
const url = new URL(req.url);
|
|
641
|
+
const method = req.method.toUpperCase();
|
|
642
|
+
const res = await send2({
|
|
643
|
+
method,
|
|
644
|
+
path: `${url.pathname}${url.search}`,
|
|
645
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
646
|
+
body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
|
|
647
|
+
});
|
|
648
|
+
const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
|
|
649
|
+
return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
var hostDbFetch = dbFetch(requestHostDb);
|
|
653
|
+
var LOCAL_DB_PATH = "/internal/db";
|
|
654
|
+
async function requestViaLocalHost(req) {
|
|
655
|
+
const deadline = Date.now() + REQUEST_TIMEOUT_MS;
|
|
656
|
+
let endpoint = readHostEndpoint();
|
|
657
|
+
while (!endpoint && Date.now() < deadline) {
|
|
658
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
659
|
+
endpoint = readHostEndpoint();
|
|
660
|
+
}
|
|
661
|
+
if (!endpoint) throw new Error("No local Host to reach the Data API through");
|
|
662
|
+
const res = await fetch(`http://127.0.0.1:${endpoint.port}${LOCAL_DB_PATH}`, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
headers: { authorization: `Bearer ${endpoint.token}`, "content-type": "application/json" },
|
|
665
|
+
body: JSON.stringify(req),
|
|
666
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS + 5e3)
|
|
667
|
+
});
|
|
668
|
+
if (!res.ok) throw new Error(`Local Host DB relay answered ${res.status}`);
|
|
669
|
+
return await res.json();
|
|
670
|
+
}
|
|
671
|
+
function relayChannel(topic) {
|
|
672
|
+
const channel = {
|
|
673
|
+
on: () => channel,
|
|
674
|
+
subscribe: (callback) => {
|
|
675
|
+
queueMicrotask(() => callback?.("SUBSCRIBED"));
|
|
676
|
+
return channel;
|
|
677
|
+
},
|
|
678
|
+
send: async (msg) => {
|
|
679
|
+
if (msg.type !== "broadcast" || !sender) return "error";
|
|
680
|
+
sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
|
|
681
|
+
return "ok";
|
|
682
|
+
},
|
|
683
|
+
unsubscribe: async () => "ok"
|
|
684
|
+
};
|
|
685
|
+
return channel;
|
|
686
|
+
}
|
|
687
|
+
function createHostDbClient(via = "socket") {
|
|
688
|
+
const client2 = createClient("https://host-db.invalid", "host-device", {
|
|
689
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
690
|
+
global: { fetch: via === "socket" ? hostDbFetch : dbFetch(requestViaLocalHost) }
|
|
691
|
+
});
|
|
692
|
+
return new Proxy(client2, {
|
|
693
|
+
get(target, prop) {
|
|
694
|
+
if (prop === "channel") return relayChannel;
|
|
695
|
+
if (prop === "removeChannel") return async () => "ok";
|
|
696
|
+
const value = Reflect.get(target, prop, target);
|
|
697
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/commands/supabase-via-host.ts
|
|
703
|
+
function useHostDbWithoutCredentials() {
|
|
704
|
+
const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
|
|
705
|
+
if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
|
|
706
|
+
return viaHost;
|
|
707
|
+
}
|
|
708
|
+
|
|
310
709
|
// src/commands/start.ts
|
|
311
710
|
async function runStart() {
|
|
312
711
|
loadEnvFiles();
|
|
313
|
-
|
|
712
|
+
const viaHost = useHostDbWithoutCredentials();
|
|
713
|
+
await preflight({ supabaseViaHost: viaHost });
|
|
314
714
|
await startServer();
|
|
315
715
|
}
|
|
316
716
|
|
|
317
717
|
// src/commands/config.ts
|
|
318
|
-
import { existsSync, readFileSync as
|
|
718
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
319
719
|
import { spawn } from "child_process";
|
|
320
720
|
import { resolve } from "path";
|
|
321
721
|
function maskToken(value) {
|
|
@@ -333,12 +733,12 @@ function printConfig() {
|
|
|
333
733
|
console.log("\n Paths:");
|
|
334
734
|
console.log(` Config dir: ${paths.CONFIG_DIR}`);
|
|
335
735
|
console.log(` Data dir: ${paths.DATA_DIR}`);
|
|
336
|
-
console.log(` Config file: ${paths.CONFIG_FILE} ${
|
|
337
|
-
console.log(` Prompt file: ${paths.PROMPT_FILE} ${
|
|
338
|
-
console.log(` Database: ${paths.DB_FILE} ${
|
|
736
|
+
console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
|
|
737
|
+
console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
|
|
738
|
+
console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
|
|
339
739
|
console.log(` Log dir: ${paths.LOG_DIR}`);
|
|
340
740
|
const repoEnv = resolve(process.cwd(), ".env");
|
|
341
|
-
console.log(` Repo .env: ${repoEnv} ${
|
|
741
|
+
console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
|
|
342
742
|
const envKeys = [
|
|
343
743
|
"SLACK_BOT_TOKEN",
|
|
344
744
|
"SLACK_APP_TOKEN",
|
|
@@ -362,13 +762,13 @@ function printConfig() {
|
|
|
362
762
|
}
|
|
363
763
|
function editConfig() {
|
|
364
764
|
ensureDirs();
|
|
365
|
-
if (!
|
|
765
|
+
if (!existsSync2(paths.CONFIG_FILE)) {
|
|
366
766
|
const defaultEnv = resolve(paths.PACKAGE_ROOT, ".env.example");
|
|
367
|
-
if (
|
|
368
|
-
const content =
|
|
369
|
-
|
|
767
|
+
if (existsSync2(defaultEnv)) {
|
|
768
|
+
const content = readFileSync4(defaultEnv, "utf-8");
|
|
769
|
+
writeFileSync4(paths.CONFIG_FILE, content);
|
|
370
770
|
} else {
|
|
371
|
-
|
|
771
|
+
writeFileSync4(paths.CONFIG_FILE, "");
|
|
372
772
|
}
|
|
373
773
|
console.log(`[flyspace] Created ${paths.CONFIG_FILE}`);
|
|
374
774
|
}
|
|
@@ -402,14 +802,14 @@ async function runConfig(args) {
|
|
|
402
802
|
|
|
403
803
|
// src/commands/setup.ts
|
|
404
804
|
import { execSync as execSync4 } from "child_process";
|
|
405
|
-
import { mkdirSync as
|
|
406
|
-
import { join as
|
|
805
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
806
|
+
import { join as join8 } from "path";
|
|
407
807
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
408
808
|
|
|
409
809
|
// src/service.ts
|
|
410
810
|
import { execSync } from "child_process";
|
|
411
|
-
import { existsSync as
|
|
412
|
-
import { basename, dirname as
|
|
811
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, realpathSync } from "fs";
|
|
812
|
+
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
413
813
|
import { homedir, platform } from "os";
|
|
414
814
|
function flyspaceHome(entry = process.argv[1]) {
|
|
415
815
|
let cliPath;
|
|
@@ -422,8 +822,8 @@ function flyspaceHome(entry = process.argv[1]) {
|
|
|
422
822
|
if (at <= 0) return void 0;
|
|
423
823
|
const npmPrefix = cliPath.slice(0, at);
|
|
424
824
|
if (basename(npmPrefix) !== "cli") return void 0;
|
|
425
|
-
const home =
|
|
426
|
-
return
|
|
825
|
+
const home = dirname3(npmPrefix);
|
|
826
|
+
return existsSync3(join4(home, "bin", "flyspace")) && existsSync3(join4(home, "node", "bin", "node")) ? home : void 0;
|
|
427
827
|
}
|
|
428
828
|
function serviceLabel(profile) {
|
|
429
829
|
return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
|
|
@@ -432,40 +832,40 @@ function plistName(profile) {
|
|
|
432
832
|
return `${serviceLabel(profile)}.plist`;
|
|
433
833
|
}
|
|
434
834
|
function plistPath(profile) {
|
|
435
|
-
return
|
|
835
|
+
return join4(homedir(), "Library", "LaunchAgents", plistName(profile));
|
|
436
836
|
}
|
|
437
837
|
function legacyServiceLabel(profile) {
|
|
438
838
|
return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
|
|
439
839
|
}
|
|
440
840
|
function legacyPlistPath(profile) {
|
|
441
|
-
return
|
|
841
|
+
return join4(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
|
|
442
842
|
}
|
|
443
843
|
function systemdUnitName(profile) {
|
|
444
844
|
return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
|
|
445
845
|
}
|
|
446
846
|
function systemdUnitPath(profile) {
|
|
447
|
-
return
|
|
847
|
+
return join4(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
|
|
448
848
|
}
|
|
449
849
|
function isServiceInstalled(profile) {
|
|
450
|
-
if (platform() === "darwin") return
|
|
451
|
-
if (platform() === "linux") return
|
|
850
|
+
if (platform() === "darwin") return existsSync3(plistPath(profile));
|
|
851
|
+
if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
|
|
452
852
|
return false;
|
|
453
853
|
}
|
|
454
854
|
function serviceBinaryMissing(file) {
|
|
455
855
|
let text;
|
|
456
856
|
try {
|
|
457
|
-
text =
|
|
857
|
+
text = readFileSync5(file, "utf-8");
|
|
458
858
|
} catch {
|
|
459
859
|
return false;
|
|
460
860
|
}
|
|
461
861
|
const bin = text.match(/<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/)?.[1] ?? text.match(/^ExecStart=(\S+)/m)?.[1];
|
|
462
|
-
return !!bin && !
|
|
862
|
+
return !!bin && !existsSync3(bin);
|
|
463
863
|
}
|
|
464
864
|
function profileServiceFile(profile) {
|
|
465
865
|
return platform() === "darwin" ? plistPath(profile) : systemdUnitPath(profile);
|
|
466
866
|
}
|
|
467
867
|
function hostServiceFile() {
|
|
468
|
-
return platform() === "darwin" ? hostPlistPath() :
|
|
868
|
+
return platform() === "darwin" ? hostPlistPath() : join4(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`);
|
|
469
869
|
}
|
|
470
870
|
function restartService(profile) {
|
|
471
871
|
const os = platform();
|
|
@@ -495,10 +895,10 @@ function restartService(profile) {
|
|
|
495
895
|
var HOST_SERVICE_LABEL = "ai.flyspace.dashboard";
|
|
496
896
|
var HOST_SYSTEMD_UNIT = "flyspace-host";
|
|
497
897
|
function hostPlistPath() {
|
|
498
|
-
return
|
|
898
|
+
return join4(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
|
|
499
899
|
}
|
|
500
900
|
function isHostServiceInstalled() {
|
|
501
|
-
if (platform() === "darwin" || platform() === "linux") return
|
|
901
|
+
if (platform() === "darwin" || platform() === "linux") return existsSync3(hostServiceFile());
|
|
502
902
|
return false;
|
|
503
903
|
}
|
|
504
904
|
function restartHostService() {
|
|
@@ -538,9 +938,9 @@ function isHostServiceRunning() {
|
|
|
538
938
|
}
|
|
539
939
|
|
|
540
940
|
// src/commands/install.ts
|
|
541
|
-
import { existsSync as
|
|
941
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
|
|
542
942
|
import { execSync as execSync2 } from "child_process";
|
|
543
|
-
import { dirname as
|
|
943
|
+
import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
|
|
544
944
|
import { homedir as homedir2, platform as platform2 } from "os";
|
|
545
945
|
function log(msg) {
|
|
546
946
|
console.log(`
|
|
@@ -561,23 +961,23 @@ function run(cmd, opts = {}) {
|
|
|
561
961
|
function detectClaudeBinDir() {
|
|
562
962
|
try {
|
|
563
963
|
const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
564
|
-
return claudePath ?
|
|
964
|
+
return claudePath ? dirname4(claudePath) : void 0;
|
|
565
965
|
} catch {
|
|
566
966
|
return void 0;
|
|
567
967
|
}
|
|
568
968
|
}
|
|
569
969
|
function detectNodeBinDir() {
|
|
570
970
|
const home = flyspaceHome();
|
|
571
|
-
return home ?
|
|
971
|
+
return home ? join5(home, "node", "bin") : dirname4(process.execPath);
|
|
572
972
|
}
|
|
573
973
|
function nodeShimDirs() {
|
|
574
|
-
return [
|
|
575
|
-
(d) =>
|
|
974
|
+
return [join5(homedir2(), ".local", "share", "mise", "shims"), join5(homedir2(), ".asdf", "shims")].filter(
|
|
975
|
+
(d) => existsSync4(d)
|
|
576
976
|
);
|
|
577
977
|
}
|
|
578
978
|
function detectCustieBin() {
|
|
579
979
|
const home = flyspaceHome();
|
|
580
|
-
if (home) return
|
|
980
|
+
if (home) return join5(home, "bin", "flyspace");
|
|
581
981
|
const argv1 = process.argv[1];
|
|
582
982
|
if (argv1) {
|
|
583
983
|
return resolve2(argv1);
|
|
@@ -589,9 +989,9 @@ function detectCustieBin() {
|
|
|
589
989
|
}
|
|
590
990
|
}
|
|
591
991
|
function readEnvFile() {
|
|
592
|
-
if (!
|
|
992
|
+
if (!existsSync4(paths.CONFIG_FILE)) return {};
|
|
593
993
|
const vars = {};
|
|
594
|
-
for (const line of
|
|
994
|
+
for (const line of readFileSync6(paths.CONFIG_FILE, "utf-8").split("\n")) {
|
|
595
995
|
const trimmed = line.trim();
|
|
596
996
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
597
997
|
const eq = trimmed.indexOf("=");
|
|
@@ -635,9 +1035,9 @@ async function installMacOS(spec) {
|
|
|
635
1035
|
const custieBin = detectCustieBin();
|
|
636
1036
|
const claudeBinDir = detectClaudeBinDir();
|
|
637
1037
|
const logDir = paths.LOG_DIR;
|
|
638
|
-
|
|
1038
|
+
mkdirSync4(logDir, { recursive: true });
|
|
639
1039
|
const envVars = spec.env;
|
|
640
|
-
const homebrewBin =
|
|
1040
|
+
const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
|
|
641
1041
|
const pathParts = [
|
|
642
1042
|
detectNodeBinDir(),
|
|
643
1043
|
claudeBinDir,
|
|
@@ -676,14 +1076,14 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
676
1076
|
<true/>
|
|
677
1077
|
|
|
678
1078
|
<key>StandardOutPath</key>
|
|
679
|
-
<string>${
|
|
1079
|
+
<string>${join5(logDir, `${spec.logName}.log`)}</string>
|
|
680
1080
|
|
|
681
1081
|
<key>StandardErrorPath</key>
|
|
682
|
-
<string>${
|
|
1082
|
+
<string>${join5(logDir, `${spec.logName}-error.log`)}</string>
|
|
683
1083
|
</dict>
|
|
684
1084
|
</plist>`;
|
|
685
1085
|
const agentPlistPath = spec.plistPath;
|
|
686
|
-
|
|
1086
|
+
mkdirSync4(dirname4(agentPlistPath), { recursive: true });
|
|
687
1087
|
try {
|
|
688
1088
|
run(`launchctl unload "${agentPlistPath}"`, { silent: true });
|
|
689
1089
|
} catch {
|
|
@@ -692,7 +1092,7 @@ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
|
692
1092
|
unlinkSync(agentPlistPath);
|
|
693
1093
|
} catch {
|
|
694
1094
|
}
|
|
695
|
-
|
|
1095
|
+
writeFileSync5(agentPlistPath, plist);
|
|
696
1096
|
run(`launchctl load "${agentPlistPath}"`, { silent: true });
|
|
697
1097
|
success(`LaunchAgent installed and loaded (${spec.label}).`);
|
|
698
1098
|
console.log(` Logs: ${logDir}`);
|
|
@@ -703,9 +1103,9 @@ async function installLinux(spec) {
|
|
|
703
1103
|
const custieBin = detectCustieBin();
|
|
704
1104
|
const claudeBinDir = detectClaudeBinDir();
|
|
705
1105
|
const logDir = paths.LOG_DIR;
|
|
706
|
-
|
|
1106
|
+
mkdirSync4(logDir, { recursive: true });
|
|
707
1107
|
const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
|
|
708
|
-
const linuxbrewBin =
|
|
1108
|
+
const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
|
|
709
1109
|
const pathParts = [
|
|
710
1110
|
detectNodeBinDir(),
|
|
711
1111
|
claudeBinDir,
|
|
@@ -729,16 +1129,16 @@ RestartSec=5
|
|
|
729
1129
|
${envLines}
|
|
730
1130
|
Environment="PATH=${pathParts.join(":")}"
|
|
731
1131
|
|
|
732
|
-
StandardOutput=append:${
|
|
733
|
-
StandardError=append:${
|
|
1132
|
+
StandardOutput=append:${join5(logDir, `${spec.logName}.log`)}
|
|
1133
|
+
StandardError=append:${join5(logDir, `${spec.logName}-error.log`)}
|
|
734
1134
|
|
|
735
1135
|
[Install]
|
|
736
1136
|
WantedBy=default.target
|
|
737
1137
|
`;
|
|
738
1138
|
const unitName = spec.unitName;
|
|
739
|
-
const unitDir =
|
|
740
|
-
|
|
741
|
-
|
|
1139
|
+
const unitDir = join5(homedir2(), ".config", "systemd", "user");
|
|
1140
|
+
mkdirSync4(unitDir, { recursive: true });
|
|
1141
|
+
writeFileSync5(join5(unitDir, `${unitName}.service`), unit);
|
|
742
1142
|
run("systemctl --user daemon-reload", { silent: true });
|
|
743
1143
|
run(`systemctl --user enable --now ${unitName}`, { silent: true });
|
|
744
1144
|
success(`systemd user service installed and started (${unitName}).`);
|
|
@@ -765,16 +1165,16 @@ async function installHostService() {
|
|
|
765
1165
|
}
|
|
766
1166
|
|
|
767
1167
|
// src/commands/profiles.ts
|
|
768
|
-
import { existsSync as
|
|
1168
|
+
import { existsSync as existsSync5, readdirSync, statSync } from "fs";
|
|
769
1169
|
import { execSync as execSync3 } from "child_process";
|
|
770
|
-
import { join as
|
|
1170
|
+
import { join as join6 } from "path";
|
|
771
1171
|
import { platform as platform3 } from "os";
|
|
772
1172
|
function listProfiles() {
|
|
773
1173
|
const profiles = [];
|
|
774
|
-
if (
|
|
1174
|
+
if (existsSync5(paths.PROFILES_DIR)) {
|
|
775
1175
|
for (const entry of readdirSync(paths.PROFILES_DIR)) {
|
|
776
1176
|
try {
|
|
777
|
-
if (statSync(
|
|
1177
|
+
if (statSync(join6(paths.PROFILES_DIR, entry)).isDirectory()) {
|
|
778
1178
|
profiles.push(entry);
|
|
779
1179
|
}
|
|
780
1180
|
} catch {
|
|
@@ -798,227 +1198,50 @@ function isServiceRunning2(profile) {
|
|
|
798
1198
|
return false;
|
|
799
1199
|
}
|
|
800
1200
|
|
|
801
|
-
// src/auth/device-auth.ts
|
|
802
|
-
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
|
|
803
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
804
|
-
import { dirname as dirname4, join as join5 } from "path";
|
|
805
|
-
var DEFAULT_API_URL = "https://app.flyspace.ai";
|
|
806
|
-
var EXPIRY_SKEW_MS = 6e4;
|
|
807
|
-
var AuthApiError = class extends Error {
|
|
808
|
-
constructor(message, status) {
|
|
809
|
-
super(message);
|
|
810
|
-
this.status = status;
|
|
811
|
-
this.name = "AuthApiError";
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
function apiUrl() {
|
|
815
|
-
return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
816
|
-
}
|
|
817
|
-
function deviceKeyPath() {
|
|
818
|
-
return join5(paths.BASE_CONFIG_DIR, "device.key");
|
|
819
|
-
}
|
|
820
|
-
function credentialsPath() {
|
|
821
|
-
return join5(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
822
|
-
}
|
|
823
|
-
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
824
|
-
let created = false;
|
|
825
|
-
if (!existsSync5(path)) {
|
|
826
|
-
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
827
|
-
mkdirSync3(dirname4(path), { recursive: true });
|
|
828
|
-
writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
|
|
829
|
-
created = true;
|
|
830
|
-
}
|
|
831
|
-
const privateKey = createPrivateKey(readFileSync5(path, "utf8"));
|
|
832
|
-
const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
|
|
833
|
-
"base64"
|
|
834
|
-
);
|
|
835
|
-
return { privateKey, publicKeyBase64, created };
|
|
836
|
-
}
|
|
837
|
-
function loadCredentials(path = credentialsPath()) {
|
|
838
|
-
if (!existsSync5(path)) return null;
|
|
839
|
-
try {
|
|
840
|
-
return JSON.parse(readFileSync5(path, "utf8"));
|
|
841
|
-
} catch {
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
function saveCredentials(creds, path = credentialsPath()) {
|
|
846
|
-
mkdirSync3(dirname4(path), { recursive: true });
|
|
847
|
-
writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
|
|
848
|
-
`, { mode: 384 });
|
|
849
|
-
}
|
|
850
|
-
function clearLocalAuth() {
|
|
851
|
-
rmSync2(credentialsPath(), { force: true });
|
|
852
|
-
rmSync2(deviceKeyPath(), { force: true });
|
|
853
|
-
}
|
|
854
|
-
async function post(base, path, body, token) {
|
|
855
|
-
return fetch(`${base}${path}`, {
|
|
856
|
-
method: "POST",
|
|
857
|
-
headers: {
|
|
858
|
-
"content-type": "application/json",
|
|
859
|
-
...token ? { authorization: `Bearer ${token}` } : {}
|
|
860
|
-
},
|
|
861
|
-
body: JSON.stringify(body)
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
async function readJson(res) {
|
|
865
|
-
try {
|
|
866
|
-
return await res.json();
|
|
867
|
-
} catch {
|
|
868
|
-
return {};
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
async function expectOk(res, what) {
|
|
872
|
-
const json = await readJson(res);
|
|
873
|
-
if (!res.ok) {
|
|
874
|
-
const reason = typeof json["error"] === "string" ? json["error"] : res.statusText;
|
|
875
|
-
throw new AuthApiError(`${what} failed (${res.status}): ${reason}`, res.status);
|
|
876
|
-
}
|
|
877
|
-
return json;
|
|
878
|
-
}
|
|
879
|
-
function toSlugs(value) {
|
|
880
|
-
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
881
|
-
}
|
|
882
|
-
function toGrant(json) {
|
|
883
|
-
const user = json["user"] ?? {};
|
|
884
|
-
return {
|
|
885
|
-
accessToken: String(json["access_token"]),
|
|
886
|
-
expiresIn: Number(json["expires_in"]),
|
|
887
|
-
deviceId: String(json["device_id"]),
|
|
888
|
-
userEmail: String(user["email"] ?? ""),
|
|
889
|
-
workspaces: toSlugs(json["workspaces"])
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
async function requestDeviceCode(base, input2) {
|
|
893
|
-
const res = await post(base, "/api/auth/device/code", {
|
|
894
|
-
public_key: input2.publicKey,
|
|
895
|
-
name: input2.name,
|
|
896
|
-
platform: input2.platform
|
|
897
|
-
});
|
|
898
|
-
const json = await expectOk(res, "Requesting a device code");
|
|
899
|
-
return {
|
|
900
|
-
deviceCode: String(json["device_code"]),
|
|
901
|
-
userCode: String(json["user_code"]),
|
|
902
|
-
verificationUri: String(json["verification_uri"]),
|
|
903
|
-
expiresIn: Number(json["expires_in"]),
|
|
904
|
-
interval: Number(json["interval"] ?? 5)
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
async function pollDeviceToken(base, deviceCode) {
|
|
908
|
-
const res = await post(base, "/api/auth/device/token", { device_code: deviceCode });
|
|
909
|
-
const json = await readJson(res);
|
|
910
|
-
if (res.ok) return { status: "approved", grant: toGrant(json) };
|
|
911
|
-
switch (json["error"]) {
|
|
912
|
-
case "authorization_pending":
|
|
913
|
-
return { status: "pending" };
|
|
914
|
-
case "slow_down":
|
|
915
|
-
return { status: "slow_down" };
|
|
916
|
-
case "access_denied":
|
|
917
|
-
return { status: "denied" };
|
|
918
|
-
case "expired_token":
|
|
919
|
-
return { status: "expired" };
|
|
920
|
-
default:
|
|
921
|
-
throw new AuthApiError(`Polling for approval failed (${res.status}): ${String(json["error"] ?? res.statusText)}`, res.status);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
async function refreshAccessToken(creds, privateKey) {
|
|
925
|
-
const challenge = await expectOk(
|
|
926
|
-
await post(creds.apiUrl, "/api/auth/device/challenge", { device_id: creds.deviceId }),
|
|
927
|
-
"Requesting a refresh challenge"
|
|
928
|
-
);
|
|
929
|
-
const nonce = String(challenge["nonce"]);
|
|
930
|
-
const signature = sign(null, Buffer.from(nonce), privateKey).toString("base64");
|
|
931
|
-
const json = await expectOk(
|
|
932
|
-
await post(creds.apiUrl, "/api/auth/device/refresh", { device_id: creds.deviceId, nonce, signature }),
|
|
933
|
-
"Refreshing the access token"
|
|
934
|
-
);
|
|
935
|
-
return {
|
|
936
|
-
...creds,
|
|
937
|
-
accessToken: String(json["access_token"]),
|
|
938
|
-
accessTokenExpiresAt: Date.now() + Number(json["expires_in"]) * 1e3
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
async function fetchWhoAmI(base, token) {
|
|
942
|
-
const res = await fetch(`${base}/api/auth/device/me`, { headers: { authorization: `Bearer ${token}` } });
|
|
943
|
-
const json = await expectOk(res, "Checking the session");
|
|
944
|
-
const user = json["user"] ?? {};
|
|
945
|
-
const device = json["device"] ?? {};
|
|
946
|
-
return {
|
|
947
|
-
userEmail: String(user["email"] ?? ""),
|
|
948
|
-
deviceId: String(device["id"] ?? ""),
|
|
949
|
-
deviceName: String(device["name"] ?? ""),
|
|
950
|
-
workspaces: toSlugs(json["workspaces"])
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
async function boundWorkspaces() {
|
|
954
|
-
const creds = loadCredentials();
|
|
955
|
-
if (!creds) return null;
|
|
956
|
-
try {
|
|
957
|
-
const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
|
|
958
|
-
saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
|
|
959
|
-
return { workspaces: me.workspaces, fresh: true };
|
|
960
|
-
} catch {
|
|
961
|
-
return { workspaces: creds.workspaces ?? [], fresh: false };
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
async function revokeDevice(base, token) {
|
|
965
|
-
await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
|
|
966
|
-
}
|
|
967
|
-
async function getAccessToken() {
|
|
968
|
-
const fromEnv = process.env["FLYSPACE_TOKEN"];
|
|
969
|
-
if (fromEnv) return fromEnv;
|
|
970
|
-
const creds = loadCredentials();
|
|
971
|
-
if (!creds) return null;
|
|
972
|
-
if (creds.accessTokenExpiresAt - EXPIRY_SKEW_MS > Date.now()) return creds.accessToken;
|
|
973
|
-
const { privateKey } = ensureDeviceKey();
|
|
974
|
-
const refreshed = await refreshAccessToken(creds, privateKey);
|
|
975
|
-
saveCredentials(refreshed);
|
|
976
|
-
return refreshed.accessToken;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
1201
|
// src/dashboard/onboarding-scan.ts
|
|
980
|
-
import { execFileSync } from "child_process";
|
|
1202
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
981
1203
|
import {
|
|
1204
|
+
cpSync,
|
|
982
1205
|
existsSync as existsSync6,
|
|
983
1206
|
lstatSync,
|
|
984
|
-
mkdirSync as
|
|
1207
|
+
mkdirSync as mkdirSync5,
|
|
985
1208
|
readdirSync as readdirSync2,
|
|
986
|
-
readFileSync as
|
|
1209
|
+
readFileSync as readFileSync7,
|
|
987
1210
|
statSync as statSync2,
|
|
988
1211
|
symlinkSync
|
|
989
1212
|
} from "fs";
|
|
990
1213
|
import { homedir as homedir3 } from "os";
|
|
991
|
-
import { basename as basename2, join as
|
|
1214
|
+
import { basename as basename2, join as join7, resolve as resolve3 } from "path";
|
|
992
1215
|
var MAX_JSONL_HEAD_BYTES = 64 * 1024;
|
|
993
1216
|
var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
|
|
994
1217
|
function flyspaceRoot() {
|
|
995
|
-
return process.env["FLYSPACE_ROOT"] ||
|
|
1218
|
+
return process.env["FLYSPACE_ROOT"] || join7(homedir3(), "Flyspace");
|
|
996
1219
|
}
|
|
997
1220
|
function onPath(cmd) {
|
|
998
1221
|
try {
|
|
999
|
-
|
|
1222
|
+
execFileSync2(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
1000
1223
|
return true;
|
|
1001
1224
|
} catch {
|
|
1002
1225
|
return false;
|
|
1003
1226
|
}
|
|
1004
1227
|
}
|
|
1005
1228
|
function claudeDir() {
|
|
1006
|
-
return process.env["CLAUDE_CONFIG_DIR"] ||
|
|
1229
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join7(homedir3(), ".claude");
|
|
1007
1230
|
}
|
|
1008
1231
|
function codexDir() {
|
|
1009
|
-
return process.env["CODEX_HOME"] ||
|
|
1232
|
+
return process.env["CODEX_HOME"] || join7(homedir3(), ".codex");
|
|
1010
1233
|
}
|
|
1011
1234
|
function claudeLoggedIn() {
|
|
1012
1235
|
if (process.env["ANTHROPIC_API_KEY"]) return true;
|
|
1013
|
-
if (existsSync6(
|
|
1236
|
+
if (existsSync6(join7(claudeDir(), ".credentials.json"))) return true;
|
|
1014
1237
|
try {
|
|
1015
|
-
const raw = JSON.parse(
|
|
1238
|
+
const raw = JSON.parse(readFileSync7(join7(homedir3(), ".claude.json"), "utf-8"));
|
|
1016
1239
|
if (raw.oauthAccount) return true;
|
|
1017
1240
|
} catch {
|
|
1018
1241
|
}
|
|
1019
1242
|
if (process.platform !== "darwin") return false;
|
|
1020
1243
|
try {
|
|
1021
|
-
|
|
1244
|
+
execFileSync2("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
1022
1245
|
stdio: "ignore"
|
|
1023
1246
|
});
|
|
1024
1247
|
return true;
|
|
@@ -1027,7 +1250,7 @@ function claudeLoggedIn() {
|
|
|
1027
1250
|
}
|
|
1028
1251
|
}
|
|
1029
1252
|
function codexLoggedIn() {
|
|
1030
|
-
return !!process.env["OPENAI_API_KEY"] || existsSync6(
|
|
1253
|
+
return !!process.env["OPENAI_API_KEY"] || existsSync6(join7(codexDir(), "auth.json"));
|
|
1031
1254
|
}
|
|
1032
1255
|
function jsonlFiles(dir, depth) {
|
|
1033
1256
|
const out = [];
|
|
@@ -1038,7 +1261,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1038
1261
|
return out;
|
|
1039
1262
|
}
|
|
1040
1263
|
for (const e of entries2) {
|
|
1041
|
-
const full =
|
|
1264
|
+
const full = join7(dir, e.name);
|
|
1042
1265
|
if (e.isDirectory()) {
|
|
1043
1266
|
if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
|
|
1044
1267
|
} else if (e.name.endsWith(".jsonl")) {
|
|
@@ -1053,7 +1276,7 @@ function jsonlFiles(dir, depth) {
|
|
|
1053
1276
|
function cwdFromTranscript(file) {
|
|
1054
1277
|
let head;
|
|
1055
1278
|
try {
|
|
1056
|
-
head =
|
|
1279
|
+
head = readFileSync7(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
|
|
1057
1280
|
} catch {
|
|
1058
1281
|
return null;
|
|
1059
1282
|
}
|
|
@@ -1070,7 +1293,7 @@ function cwdFromTranscript(file) {
|
|
|
1070
1293
|
}
|
|
1071
1294
|
function codexTrustedPaths() {
|
|
1072
1295
|
try {
|
|
1073
|
-
const toml =
|
|
1296
|
+
const toml = readFileSync7(join7(codexDir(), "config.toml"), "utf-8");
|
|
1074
1297
|
return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
|
|
1075
1298
|
} catch {
|
|
1076
1299
|
return [];
|
|
@@ -1093,7 +1316,7 @@ function gitRepos() {
|
|
|
1093
1316
|
const home = homedir3();
|
|
1094
1317
|
const found = [];
|
|
1095
1318
|
for (const rootName of GIT_SCAN_ROOTS) {
|
|
1096
|
-
const root =
|
|
1319
|
+
const root = join7(home, rootName);
|
|
1097
1320
|
let entries2;
|
|
1098
1321
|
try {
|
|
1099
1322
|
entries2 = readdirSync2(root, { withFileTypes: true });
|
|
@@ -1101,7 +1324,7 @@ function gitRepos() {
|
|
|
1101
1324
|
continue;
|
|
1102
1325
|
}
|
|
1103
1326
|
for (const e of entries2) {
|
|
1104
|
-
if (e.isDirectory() && existsSync6(
|
|
1327
|
+
if (e.isDirectory() && existsSync6(join7(root, e.name, ".git"))) found.push(join7(root, e.name));
|
|
1105
1328
|
}
|
|
1106
1329
|
}
|
|
1107
1330
|
return found;
|
|
@@ -1145,11 +1368,11 @@ function scanOnboarding() {
|
|
|
1145
1368
|
});
|
|
1146
1369
|
}
|
|
1147
1370
|
};
|
|
1148
|
-
const claudeProjects =
|
|
1371
|
+
const claudeProjects = join7(claudeDir(), "projects");
|
|
1149
1372
|
try {
|
|
1150
1373
|
for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
|
|
1151
1374
|
if (!e.isDirectory()) continue;
|
|
1152
|
-
const files = jsonlFiles(
|
|
1375
|
+
const files = jsonlFiles(join7(claudeProjects, e.name), 0);
|
|
1153
1376
|
if (!files.length) continue;
|
|
1154
1377
|
history.claude = true;
|
|
1155
1378
|
files.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -1162,7 +1385,7 @@ function scanOnboarding() {
|
|
|
1162
1385
|
}
|
|
1163
1386
|
} catch {
|
|
1164
1387
|
}
|
|
1165
|
-
const codexSessions = jsonlFiles(
|
|
1388
|
+
const codexSessions = jsonlFiles(join7(codexDir(), "sessions"), 4);
|
|
1166
1389
|
if (codexSessions.length) history.codex = true;
|
|
1167
1390
|
const perCwd = /* @__PURE__ */ new Map();
|
|
1168
1391
|
for (const f of codexSessions) {
|
|
@@ -1199,17 +1422,18 @@ function scanOnboarding() {
|
|
|
1199
1422
|
fromGitScan
|
|
1200
1423
|
};
|
|
1201
1424
|
}
|
|
1425
|
+
var GENERAL_TEMPLATE_DIR = join7(paths.PACKAGE_ROOT, "templates", "general");
|
|
1202
1426
|
function makeLink(target, linkPath) {
|
|
1203
1427
|
symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
1204
1428
|
}
|
|
1205
1429
|
function linkProjectFolders(slug, items) {
|
|
1206
1430
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
|
|
1207
|
-
const dir =
|
|
1208
|
-
|
|
1431
|
+
const dir = join7(flyspaceRoot(), slug);
|
|
1432
|
+
mkdirSync5(dir, { recursive: true });
|
|
1209
1433
|
const results = [];
|
|
1210
1434
|
for (const item of items) {
|
|
1211
1435
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
|
|
1212
|
-
const dest =
|
|
1436
|
+
const dest = join7(dir, item.name);
|
|
1213
1437
|
let exists = true;
|
|
1214
1438
|
try {
|
|
1215
1439
|
lstatSync(dest);
|
|
@@ -1222,7 +1446,8 @@ function linkProjectFolders(slug, items) {
|
|
|
1222
1446
|
if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
|
|
1223
1447
|
makeLink(target, dest);
|
|
1224
1448
|
} else {
|
|
1225
|
-
|
|
1449
|
+
mkdirSync5(dest, { recursive: true });
|
|
1450
|
+
if (item.name === "general") cpSync(GENERAL_TEMPLATE_DIR, dest, { recursive: true });
|
|
1226
1451
|
}
|
|
1227
1452
|
}
|
|
1228
1453
|
results.push({ name: item.name, cwd: `${slug}/${item.name}` });
|
|
@@ -1540,7 +1765,7 @@ async function runAuthCmd(args) {
|
|
|
1540
1765
|
}
|
|
1541
1766
|
|
|
1542
1767
|
// src/version.ts
|
|
1543
|
-
var CLI_VERSION = true ? "0.1.
|
|
1768
|
+
var CLI_VERSION = true ? "0.1.15" : null;
|
|
1544
1769
|
function isOlderVersion(a, b) {
|
|
1545
1770
|
const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1546
1771
|
const [pa, pb] = [parse2(a), parse2(b)];
|
|
@@ -1654,8 +1879,8 @@ ${BOLD2}Flyspace Setup${RESET2}
|
|
|
1654
1879
|
const anyReady = runners.some((r) => r.installed && r.loggedIn);
|
|
1655
1880
|
for (const slug of workspaces) {
|
|
1656
1881
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
|
|
1657
|
-
const dir =
|
|
1658
|
-
|
|
1882
|
+
const dir = join8(flyspaceRoot(), slug);
|
|
1883
|
+
mkdirSync6(dir, { recursive: true });
|
|
1659
1884
|
ok(`Folder ${tilde2(dir)}`);
|
|
1660
1885
|
}
|
|
1661
1886
|
let hostRunning = false;
|
|
@@ -1695,9 +1920,9 @@ import { tmpdir } from "os";
|
|
|
1695
1920
|
import { promisify } from "util";
|
|
1696
1921
|
var execAsync = promisify(execFile);
|
|
1697
1922
|
var exec = (cmd, args, opts) => {
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
return
|
|
1923
|
+
const run4 = execAsync(cmd, args, opts);
|
|
1924
|
+
run4.child.stdin?.end();
|
|
1925
|
+
return run4;
|
|
1701
1926
|
};
|
|
1702
1927
|
var BOLD3 = "\x1B[1m";
|
|
1703
1928
|
var DIM3 = "\x1B[2m";
|
|
@@ -1818,10 +2043,10 @@ ${BOLD3}Flyspace Doctor${RESET3}
|
|
|
1818
2043
|
}
|
|
1819
2044
|
|
|
1820
2045
|
// src/commands/uninstall.ts
|
|
1821
|
-
import { existsSync as existsSync7, readFileSync as
|
|
2046
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1822
2047
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1823
2048
|
import { execSync as execSync5 } from "child_process";
|
|
1824
|
-
import { basename as basename3, join as
|
|
2049
|
+
import { basename as basename3, join as join9 } from "path";
|
|
1825
2050
|
import { homedir as homedir4, platform as platform5 } from "os";
|
|
1826
2051
|
function log2(msg) {
|
|
1827
2052
|
console.log(`
|
|
@@ -1864,7 +2089,7 @@ async function runServiceUninstall(profile = getProfile()) {
|
|
|
1864
2089
|
run2(`systemctl --user disable --now ${unitName}`);
|
|
1865
2090
|
} catch {
|
|
1866
2091
|
}
|
|
1867
|
-
const unitPath =
|
|
2092
|
+
const unitPath = join9(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
|
|
1868
2093
|
try {
|
|
1869
2094
|
unlinkSync2(unitPath);
|
|
1870
2095
|
} catch {
|
|
@@ -1879,7 +2104,7 @@ async function runServiceUninstall(profile = getProfile()) {
|
|
|
1879
2104
|
}
|
|
1880
2105
|
}
|
|
1881
2106
|
async function runUninstall(args = []) {
|
|
1882
|
-
const prefix = process.env["FLYSPACE_PREFIX"] ||
|
|
2107
|
+
const prefix = process.env["FLYSPACE_PREFIX"] || join9(homedir4(), ".flyspace");
|
|
1883
2108
|
const targets = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR, prefix].filter(existsSync7);
|
|
1884
2109
|
console.log("\n This removes Flyspace from this machine:");
|
|
1885
2110
|
console.log(" - revokes this device and removes every background service");
|
|
@@ -1905,15 +2130,15 @@ async function runUninstall(args = []) {
|
|
|
1905
2130
|
await runServiceUninstall(profile);
|
|
1906
2131
|
}
|
|
1907
2132
|
for (const t of targets) {
|
|
1908
|
-
|
|
2133
|
+
rmSync4(t, { recursive: true, force: true });
|
|
1909
2134
|
success2(`Deleted ${t}`);
|
|
1910
2135
|
}
|
|
1911
|
-
for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) =>
|
|
2136
|
+
for (const rc of [".zshrc", ".bashrc", ".profile"].map((f) => join9(homedir4(), f))) {
|
|
1912
2137
|
if (!existsSync7(rc)) continue;
|
|
1913
|
-
const before =
|
|
2138
|
+
const before = readFileSync8(rc, "utf-8");
|
|
1914
2139
|
const after = before.replace(/\n# Flyspace CLI\nexport PATH="[^"\n]*\.flyspace\/bin:\$PATH"\n?/g, "\n");
|
|
1915
2140
|
if (after !== before) {
|
|
1916
|
-
|
|
2141
|
+
writeFileSync6(rc, after);
|
|
1917
2142
|
success2(`Removed PATH entry from ${rc}`);
|
|
1918
2143
|
}
|
|
1919
2144
|
}
|
|
@@ -1921,7 +2146,7 @@ async function runUninstall(args = []) {
|
|
|
1921
2146
|
}
|
|
1922
2147
|
|
|
1923
2148
|
// src/commands/profiles-cmd.ts
|
|
1924
|
-
import { rmSync as
|
|
2149
|
+
import { rmSync as rmSync5 } from "fs";
|
|
1925
2150
|
var USAGE = `
|
|
1926
2151
|
Usage: flyspace profiles [list]
|
|
1927
2152
|
flyspace profiles remove <name>
|
|
@@ -1978,7 +2203,7 @@ async function removeCmd(name) {
|
|
|
1978
2203
|
}
|
|
1979
2204
|
const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
|
|
1980
2205
|
if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
|
|
1981
|
-
|
|
2206
|
+
rmSync5(configDir2, { recursive: true, force: true });
|
|
1982
2207
|
console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
|
|
1983
2208
|
console.log(`
|
|
1984
2209
|
Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
|
|
@@ -1996,7 +2221,7 @@ async function runProfiles(args = []) {
|
|
|
1996
2221
|
// src/commands/upgrade.ts
|
|
1997
2222
|
import { execSync as execSync6 } from "child_process";
|
|
1998
2223
|
import { realpathSync as realpathSync2 } from "fs";
|
|
1999
|
-
import { join as
|
|
2224
|
+
import { join as join10 } from "path";
|
|
2000
2225
|
function restartInstalledProfiles() {
|
|
2001
2226
|
if (isHostServiceInstalled()) {
|
|
2002
2227
|
console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
|
|
@@ -2029,10 +2254,10 @@ function npmInstallCommand() {
|
|
|
2029
2254
|
}
|
|
2030
2255
|
function npmEnv() {
|
|
2031
2256
|
const home = flyspaceHome();
|
|
2032
|
-
return home ? { ...process.env, PATH: `${
|
|
2257
|
+
return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
|
|
2033
2258
|
}
|
|
2034
2259
|
async function runUpgrade() {
|
|
2035
|
-
const current = "0.1.
|
|
2260
|
+
const current = "0.1.15";
|
|
2036
2261
|
console.log(`Current version: ${current}`);
|
|
2037
2262
|
console.log("Checking for updates...\n");
|
|
2038
2263
|
try {
|
|
@@ -2352,6 +2577,7 @@ async function handleImport(manager) {
|
|
|
2352
2577
|
async function runAutomationCmd(args) {
|
|
2353
2578
|
const subcommand = args[0];
|
|
2354
2579
|
loadEnvFiles();
|
|
2580
|
+
useHostDbWithoutCredentials();
|
|
2355
2581
|
await refreshChannelRegistry();
|
|
2356
2582
|
await refreshAutomationRegistry();
|
|
2357
2583
|
const manager = new AutomationManager();
|
|
@@ -2646,11 +2872,11 @@ async function runRestart() {
|
|
|
2646
2872
|
// src/commands/logs.ts
|
|
2647
2873
|
import { spawn as spawn3 } from "child_process";
|
|
2648
2874
|
import { existsSync as existsSync9 } from "fs";
|
|
2649
|
-
import { join as
|
|
2875
|
+
import { join as join11 } from "path";
|
|
2650
2876
|
async function runLogs(args) {
|
|
2651
2877
|
const showErrors = args.includes("--error") || args.includes("-e");
|
|
2652
2878
|
const filename = showErrors ? "custie-error.log" : "custie.log";
|
|
2653
|
-
const logFile =
|
|
2879
|
+
const logFile = join11(paths.LOG_DIR, filename);
|
|
2654
2880
|
if (!existsSync9(logFile)) {
|
|
2655
2881
|
console.error(`Log file not found: ${logFile}`);
|
|
2656
2882
|
console.error("Is the service installed? Run `flyspace install` first.");
|
|
@@ -2672,14 +2898,14 @@ async function runLogs(args) {
|
|
|
2672
2898
|
|
|
2673
2899
|
// src/commands/slack.ts
|
|
2674
2900
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2675
|
-
import { existsSync as existsSync26, readFileSync as
|
|
2901
|
+
import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
|
|
2676
2902
|
import { resolve as resolve6 } from "path";
|
|
2677
|
-
import { tmpdir as
|
|
2903
|
+
import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
|
|
2678
2904
|
import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
|
|
2679
2905
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
2680
2906
|
|
|
2681
2907
|
// src/store/auth-links.ts
|
|
2682
|
-
import { createClient } from "@supabase/supabase-js";
|
|
2908
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
2683
2909
|
function toAuthLink(data) {
|
|
2684
2910
|
const userId = data.user?.id;
|
|
2685
2911
|
const actionLink = data.properties?.action_link;
|
|
@@ -2703,7 +2929,7 @@ async function mintSession(email) {
|
|
|
2703
2929
|
if (error2) throw new Error(error2.message);
|
|
2704
2930
|
const tokenHash = data.properties?.hashed_token;
|
|
2705
2931
|
if (!tokenHash) throw new Error("Supabase returned no token hash");
|
|
2706
|
-
const scratch =
|
|
2932
|
+
const scratch = createClient2(process.env["SUPABASE_URL"], process.env["SUPABASE_SECRET_KEY"], {
|
|
2707
2933
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
2708
2934
|
});
|
|
2709
2935
|
const verified = await scratch.auth.verifyOtp({ token_hash: tokenHash, type: "magiclink" });
|
|
@@ -2729,7 +2955,7 @@ async function emailForToken(token, type) {
|
|
|
2729
2955
|
|
|
2730
2956
|
// src/dashboard/profile-config.ts
|
|
2731
2957
|
import { homedir as homedir5 } from "os";
|
|
2732
|
-
import { join as
|
|
2958
|
+
import { join as join12 } from "path";
|
|
2733
2959
|
|
|
2734
2960
|
// src/dashboard/profile-config-cache.ts
|
|
2735
2961
|
var cache = /* @__PURE__ */ new Map();
|
|
@@ -2748,7 +2974,7 @@ function profileEnv(key) {
|
|
|
2748
2974
|
function profileClaudeConfigDir() {
|
|
2749
2975
|
const config = cachedProfileConfig();
|
|
2750
2976
|
if (!config) return process.env["CLAUDE_CONFIG_DIR"] || void 0;
|
|
2751
|
-
return config.env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
2977
|
+
return config.env["CLAUDE_CONFIG_DIR"]?.trim() || join12(homedir5(), ".claude");
|
|
2752
2978
|
}
|
|
2753
2979
|
function profileOauthToken() {
|
|
2754
2980
|
const env = cachedProfileConfig()?.env ?? {};
|
|
@@ -2850,7 +3076,7 @@ async function sendEmail(email) {
|
|
|
2850
3076
|
}
|
|
2851
3077
|
|
|
2852
3078
|
// src/store/invite-store.ts
|
|
2853
|
-
import { createHash, randomBytes } from "crypto";
|
|
3079
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2854
3080
|
var INVITE_TTL_DAYS = 7;
|
|
2855
3081
|
var INVITE_MESSAGE_MAX = 500;
|
|
2856
3082
|
function hashInviteToken(token) {
|
|
@@ -2860,7 +3086,7 @@ async function issueInvite(membershipId, message) {
|
|
|
2860
3086
|
const db = getSupabaseClient();
|
|
2861
3087
|
const { error: clearError } = await db.from("workspace_invites").delete().eq("membership_id", membershipId).is("accepted_at", null);
|
|
2862
3088
|
if (clearError) throw new Error(`issueInvite(${membershipId}): ${clearError.message}`);
|
|
2863
|
-
const token =
|
|
3089
|
+
const token = randomBytes2(32).toString("hex");
|
|
2864
3090
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
2865
3091
|
const { error: error2 } = await db.from("workspace_invites").insert({
|
|
2866
3092
|
membership_id: membershipId,
|
|
@@ -3229,6 +3455,13 @@ function profileFromHost(host, known) {
|
|
|
3229
3455
|
const bare = label2.startsWith("api-") ? label2.slice(4) : label2.startsWith("dev-") ? label2.slice(4) : label2;
|
|
3230
3456
|
return known.includes(bare) ? bare : null;
|
|
3231
3457
|
}
|
|
3458
|
+
function isUnknownWorkspaceHost(host, known) {
|
|
3459
|
+
const name = bareHost(host);
|
|
3460
|
+
if (!name?.endsWith(".flyspace.ai")) return false;
|
|
3461
|
+
const label2 = subdomainLabel(host);
|
|
3462
|
+
if (!label2 || label2 === "dev-admin") return false;
|
|
3463
|
+
return profileFromHost(host, known) === null;
|
|
3464
|
+
}
|
|
3232
3465
|
function workspaceHostname(host, profile) {
|
|
3233
3466
|
const name = bareHost(host);
|
|
3234
3467
|
if (!name || !subdomainLabel(host)) return null;
|
|
@@ -3300,9 +3533,9 @@ function workspaceOrigin(c, slug) {
|
|
|
3300
3533
|
}
|
|
3301
3534
|
|
|
3302
3535
|
// src/store/setup-invite-store.ts
|
|
3303
|
-
import { randomBytes as
|
|
3536
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3304
3537
|
async function issueSetupInvite(params) {
|
|
3305
|
-
const token =
|
|
3538
|
+
const token = randomBytes3(32).toString("hex");
|
|
3306
3539
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
3307
3540
|
const { error: error2 } = await getSupabaseClient().from("workspace_setup_invites").insert({
|
|
3308
3541
|
email: params.email,
|
|
@@ -3329,7 +3562,7 @@ async function reissueSetupInvite(id) {
|
|
|
3329
3562
|
const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message").eq("id", id).is("accepted_at", null).maybeSingle();
|
|
3330
3563
|
if (error2) throw new Error(`reissueSetupInvite(${id}): ${error2.message}`);
|
|
3331
3564
|
if (!data) throw new Error("no pending invite with that id");
|
|
3332
|
-
const token =
|
|
3565
|
+
const token = randomBytes3(32).toString("hex");
|
|
3333
3566
|
const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
3334
3567
|
const { error: updateError } = await db.from("workspace_setup_invites").update({ token_hash: hashInviteToken(token), expires_at: expiresAt.toISOString() }).eq("id", id);
|
|
3335
3568
|
if (updateError) throw new Error(`reissueSetupInvite(${id}): ${updateError.message}`);
|
|
@@ -3639,7 +3872,9 @@ function getStripeClient() {
|
|
|
3639
3872
|
|
|
3640
3873
|
// src/store/stripe-trial.ts
|
|
3641
3874
|
var PLAN_INFO = {
|
|
3642
|
-
|
|
3875
|
+
// Single-member: a personal Claude subscription can't be shared (see
|
|
3876
|
+
// members-routes.ts, which blocks invites on this plan).
|
|
3877
|
+
local: { name: "Solo", priceUsd: 29 },
|
|
3643
3878
|
cloud: { name: "Cloud Agent", priceUsd: 49 }
|
|
3644
3879
|
};
|
|
3645
3880
|
function priceIdForPlan(plan) {
|
|
@@ -3664,6 +3899,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
|
|
|
3664
3899
|
metadata: { workspace_id: workspace.id, plan }
|
|
3665
3900
|
});
|
|
3666
3901
|
await setWorkspaceStripeSubscription(workspace.id, {
|
|
3902
|
+
plan,
|
|
3667
3903
|
stripeCustomerId: customer.id,
|
|
3668
3904
|
stripeSubscriptionId: subscription.id,
|
|
3669
3905
|
planStatus: "trialing"
|
|
@@ -4202,13 +4438,25 @@ async function deliverPasswordResetLink(email, redirectTo) {
|
|
|
4202
4438
|
// src/dashboard/server.ts
|
|
4203
4439
|
import { existsSync as existsSync25 } from "fs";
|
|
4204
4440
|
import { createServer } from "net";
|
|
4205
|
-
import { join as
|
|
4441
|
+
import { join as join24 } from "path";
|
|
4206
4442
|
import { Hono as Hono34 } from "hono";
|
|
4443
|
+
|
|
4444
|
+
// src/dashboard/sentry.ts
|
|
4445
|
+
import * as Sentry from "@sentry/node";
|
|
4446
|
+
import { HTTPException } from "hono/http-exception";
|
|
4447
|
+
var reportingErrorHandler = (err, c) => {
|
|
4448
|
+
if (err instanceof HTTPException) return err.getResponse();
|
|
4449
|
+
Sentry.captureException(err, { extra: { method: c.req.method, path: c.req.path } });
|
|
4450
|
+
console.error(err);
|
|
4451
|
+
return c.text("Internal Server Error", 500);
|
|
4452
|
+
};
|
|
4453
|
+
|
|
4454
|
+
// src/dashboard/server.ts
|
|
4207
4455
|
import { serve } from "@hono/node-server";
|
|
4208
4456
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4209
4457
|
|
|
4210
4458
|
// src/reconcile.ts
|
|
4211
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4459
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4212
4460
|
async function boundWorkspaces2() {
|
|
4213
4461
|
return (await boundWorkspaces())?.workspaces ?? null;
|
|
4214
4462
|
}
|
|
@@ -4225,7 +4473,7 @@ async function reconcileProfile(profile) {
|
|
|
4225
4473
|
if (!hadConfigDir) log5.push(`config dir created (${paths.CONFIG_DIR})`);
|
|
4226
4474
|
if (!hadDataDir) log5.push(`data dir created (${paths.DATA_DIR})`);
|
|
4227
4475
|
if (!existsSync10(paths.CONFIG_FILE)) {
|
|
4228
|
-
|
|
4476
|
+
writeFileSync7(paths.CONFIG_FILE, `BOT_NAME=${profile}
|
|
4229
4477
|
`);
|
|
4230
4478
|
log5.push(`config.env written (${paths.CONFIG_FILE})`);
|
|
4231
4479
|
}
|
|
@@ -4234,7 +4482,7 @@ async function reconcileProfile(profile) {
|
|
|
4234
4482
|
if (!channel.cwd) continue;
|
|
4235
4483
|
const cwd = expandPath(channel.cwd);
|
|
4236
4484
|
if (isCwdUsable(cwd)) continue;
|
|
4237
|
-
|
|
4485
|
+
mkdirSync7(cwd, { recursive: true });
|
|
4238
4486
|
log5.push(`channel "${channel.name}" cwd created (${cwd})`);
|
|
4239
4487
|
}
|
|
4240
4488
|
return log5;
|
|
@@ -4396,8 +4644,10 @@ async function filterReadableDocsChannels(ctx, channelIds) {
|
|
|
4396
4644
|
// src/dashboard/docs-access-gate.ts
|
|
4397
4645
|
var requireDocsAccess = async (c, next) => {
|
|
4398
4646
|
const auth = c.get("auth");
|
|
4647
|
+
const channelId = c.req.param("id") ?? "";
|
|
4648
|
+
if (findChannelById(channelId)?.profile !== getProfile()) return c.json({ error: "channel not found" }, 404);
|
|
4399
4649
|
const need = c.req.method === "GET" ? "read" : "write";
|
|
4400
|
-
const have = auth ? await resolveDocsAccess(auth,
|
|
4650
|
+
const have = auth ? await resolveDocsAccess(auth, channelId) : "none";
|
|
4401
4651
|
if (!docsAccessAtLeast(have, need)) return c.json({ error: "docs access required" }, 403);
|
|
4402
4652
|
await next();
|
|
4403
4653
|
};
|
|
@@ -4406,8 +4656,8 @@ var requireDocsAccess = async (c, next) => {
|
|
|
4406
4656
|
import { Hono as Hono31 } from "hono";
|
|
4407
4657
|
|
|
4408
4658
|
// src/dashboard/public-share-routes.ts
|
|
4409
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4410
|
-
import { join as
|
|
4659
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
4660
|
+
import { join as join14 } from "path";
|
|
4411
4661
|
import { Hono as Hono3 } from "hono";
|
|
4412
4662
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
|
4413
4663
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
@@ -4547,15 +4797,15 @@ async function createDocShare(input2) {
|
|
|
4547
4797
|
if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
|
|
4548
4798
|
return rowToShare(data);
|
|
4549
4799
|
}
|
|
4550
|
-
async function updateDocShareSnapshot(id,
|
|
4800
|
+
async function updateDocShareSnapshot(id, snapshot2, features) {
|
|
4551
4801
|
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
|
|
4552
|
-
title:
|
|
4553
|
-
content:
|
|
4554
|
-
asset_keys:
|
|
4802
|
+
title: snapshot2.title,
|
|
4803
|
+
content: snapshot2.content,
|
|
4804
|
+
asset_keys: snapshot2.assetKeys,
|
|
4555
4805
|
// Re-detected with the snapshot: a re-publish can carry a rewritten
|
|
4556
4806
|
// article, and one written in another language than last time is exactly
|
|
4557
4807
|
// the case a stale value would get wrong.
|
|
4558
|
-
source_lang:
|
|
4808
|
+
source_lang: snapshot2.sourceLang,
|
|
4559
4809
|
audio_enabled: features.audioEnabled,
|
|
4560
4810
|
ask_enabled: features.askEnabled,
|
|
4561
4811
|
social_enabled: features.socialEnabled,
|
|
@@ -4872,12 +5122,12 @@ function injectShareHead(html, head) {
|
|
|
4872
5122
|
|
|
4873
5123
|
// src/dashboard/app-icon.ts
|
|
4874
5124
|
import { createHash as createHash2 } from "crypto";
|
|
4875
|
-
import { readFileSync as
|
|
4876
|
-
import { join as
|
|
5125
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
5126
|
+
import { join as join13 } from "path";
|
|
4877
5127
|
import sharp from "sharp";
|
|
4878
5128
|
var PLATE = "#0A0A0F";
|
|
4879
|
-
var MARK_FILE =
|
|
4880
|
-
var MARK_SVG_FILE =
|
|
5129
|
+
var MARK_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "icons", "icon-512.png");
|
|
5130
|
+
var MARK_SVG_FILE = join13(paths.PACKAGE_ROOT, "web", "public", "favicon.svg");
|
|
4881
5131
|
var LAYOUT = {
|
|
4882
5132
|
standard: { logo: 0.7, logoCx: 0.5, logoCy: 0.48, badge: 0.27, badgeCx: 0.22, badgeCy: 0.76 },
|
|
4883
5133
|
maskable: { logo: 0.56, logoCx: 0.5, logoCy: 0.48, badge: 0.23, badgeCx: 0.33, badgeCy: 0.66 }
|
|
@@ -4900,7 +5150,7 @@ function roundedPlate(size, fill = PLATE) {
|
|
|
4900
5150
|
}
|
|
4901
5151
|
async function renderBadge(box) {
|
|
4902
5152
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
4903
|
-
const raster = await sharp(
|
|
5153
|
+
const raster = await sharp(readFileSync9(MARK_SVG_FILE)).resize(512, 512, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4904
5154
|
return sharp(raster).trim().resize(box, box, { fit: "contain", background: transparent }).png().toBuffer();
|
|
4905
5155
|
}
|
|
4906
5156
|
async function sampleBackground(logo) {
|
|
@@ -4955,7 +5205,7 @@ async function renderAppIcon({
|
|
|
4955
5205
|
if (hit) return hit;
|
|
4956
5206
|
const out = logo && await composeIcon(logo, size, maskable, plain).catch(() => null) || // No workspace logo to lead with — the stock mark is still the best icon,
|
|
4957
5207
|
// either on its usual dark plate or, for a favicon, trimmed and bare.
|
|
4958
|
-
(transparent ? await renderBadge(size) : await sharp(
|
|
5208
|
+
(transparent ? await renderBadge(size) : await sharp(readFileSync9(MARK_FILE)).resize(size, size, { fit: "cover" }).png().toBuffer());
|
|
4959
5209
|
cache3.set(key, out);
|
|
4960
5210
|
return out;
|
|
4961
5211
|
}
|
|
@@ -5483,9 +5733,9 @@ async function synthesizeWithRetry(script, voice) {
|
|
|
5483
5733
|
}
|
|
5484
5734
|
var renderQueue = Promise.resolve();
|
|
5485
5735
|
function queued(job) {
|
|
5486
|
-
const
|
|
5487
|
-
renderQueue =
|
|
5488
|
-
return
|
|
5736
|
+
const run4 = renderQueue.then(job, job);
|
|
5737
|
+
renderQueue = run4.catch(() => void 0);
|
|
5738
|
+
return run4;
|
|
5489
5739
|
}
|
|
5490
5740
|
function renderArticleAudio(title, content, lang) {
|
|
5491
5741
|
return queued(() => renderArticleAudioNow(title, content, lang));
|
|
@@ -5559,12 +5809,12 @@ function queueShareAudio(share) {
|
|
|
5559
5809
|
}
|
|
5560
5810
|
const current = inFlight2.get(share.id);
|
|
5561
5811
|
if (current?.hash === hash) return current.done;
|
|
5562
|
-
const
|
|
5812
|
+
const run4 = () => runShareAudioJob(share, hash).catch((err) => {
|
|
5563
5813
|
console.error(`share audio job crashed (${share.id}):`, err);
|
|
5564
5814
|
}).finally(() => {
|
|
5565
5815
|
if (inFlight2.get(share.id)?.hash === hash) inFlight2.delete(share.id);
|
|
5566
5816
|
});
|
|
5567
|
-
const done = current ? current.done.then(
|
|
5817
|
+
const done = current ? current.done.then(run4) : run4();
|
|
5568
5818
|
inFlight2.set(share.id, { hash, done });
|
|
5569
5819
|
return done;
|
|
5570
5820
|
}
|
|
@@ -6173,7 +6423,7 @@ var shellCache = null;
|
|
|
6173
6423
|
function readShell(path) {
|
|
6174
6424
|
const { mtimeMs } = statSync3(path);
|
|
6175
6425
|
if (shellCache?.mtimeMs === mtimeMs) return shellCache.html;
|
|
6176
|
-
const html =
|
|
6426
|
+
const html = readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`);
|
|
6177
6427
|
shellCache = { mtimeMs, html };
|
|
6178
6428
|
return html;
|
|
6179
6429
|
}
|
|
@@ -6193,17 +6443,17 @@ function serveBuildFile(c, webDist, dir, name) {
|
|
|
6193
6443
|
if (!type || !/^[A-Za-z0-9._-]+$/.test(name) || name.includes("..")) {
|
|
6194
6444
|
return c.text("not found", 404);
|
|
6195
6445
|
}
|
|
6196
|
-
const path =
|
|
6446
|
+
const path = join14(webDist, dir, name);
|
|
6197
6447
|
if (!existsSync11(path)) return c.text("not found", 404);
|
|
6198
6448
|
c.header("Content-Type", type);
|
|
6199
6449
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
6200
6450
|
if (ext === ".css") {
|
|
6201
|
-
return c.body(
|
|
6451
|
+
return c.body(readFileSync10(path, "utf8").replaceAll("url(/fonts/", `url(${BUILD_PREFIX}/fonts/`));
|
|
6202
6452
|
}
|
|
6203
6453
|
if (ext === ".js") {
|
|
6204
|
-
return c.body(
|
|
6454
|
+
return c.body(readFileSync10(path, "utf8").replaceAll('"/assets/', `"${BUILD_PREFIX}/`));
|
|
6205
6455
|
}
|
|
6206
|
-
return c.body(new Uint8Array(
|
|
6456
|
+
return c.body(new Uint8Array(readFileSync10(path)));
|
|
6207
6457
|
}
|
|
6208
6458
|
var HTML_SHARE_CSP = "sandbox allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox";
|
|
6209
6459
|
function servePublishedHtml(c, share) {
|
|
@@ -6339,7 +6589,7 @@ ${analytics}` : head);
|
|
|
6339
6589
|
const requested = c.req.param("lang");
|
|
6340
6590
|
const lang = canonicalLanguage(requested);
|
|
6341
6591
|
if (!isTranslatableLanguage(lang)) return next();
|
|
6342
|
-
const shell =
|
|
6592
|
+
const shell = join14(webDist, "index.html");
|
|
6343
6593
|
if (!existsSync11(shell)) return next();
|
|
6344
6594
|
const share = await getDocShare(c.req.param("id"));
|
|
6345
6595
|
if (!share) return next();
|
|
@@ -6375,7 +6625,7 @@ ${analytics}` : head);
|
|
|
6375
6625
|
const share = await getDocShare(c.req.param("id"));
|
|
6376
6626
|
if (!share) return next();
|
|
6377
6627
|
if (share.kind === "html") return servePublishedHtml(c, share);
|
|
6378
|
-
const shell =
|
|
6628
|
+
const shell = join14(webDist, "index.html");
|
|
6379
6629
|
if (!existsSync11(shell)) return next();
|
|
6380
6630
|
return readerPage(
|
|
6381
6631
|
c,
|
|
@@ -6488,9 +6738,9 @@ function routeCookieWrites(all, label2, writes) {
|
|
|
6488
6738
|
}
|
|
6489
6739
|
|
|
6490
6740
|
// src/dashboard/supabase-server-client.ts
|
|
6491
|
-
var
|
|
6492
|
-
function fetchWithTimeout(input2,
|
|
6493
|
-
return fetch(input2, { ...
|
|
6741
|
+
var REQUEST_TIMEOUT_MS2 = 5e3;
|
|
6742
|
+
function fetchWithTimeout(input2, init2) {
|
|
6743
|
+
return fetch(input2, { ...init2, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
|
|
6494
6744
|
}
|
|
6495
6745
|
function createRequestSupabaseClient(c) {
|
|
6496
6746
|
const url = process.env["SUPABASE_URL"];
|
|
@@ -6532,7 +6782,7 @@ function createRequestSupabaseClient(c) {
|
|
|
6532
6782
|
}
|
|
6533
6783
|
|
|
6534
6784
|
// src/dashboard/auth-context.ts
|
|
6535
|
-
import { HTTPException } from "hono/http-exception";
|
|
6785
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
6536
6786
|
import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
|
|
6537
6787
|
function onboardingPending(ctx) {
|
|
6538
6788
|
const pending3 = [];
|
|
@@ -6589,10 +6839,10 @@ async function resolveAuthContextUncached(c) {
|
|
|
6589
6839
|
} = await getUserWithRetry(supabase));
|
|
6590
6840
|
} catch (err) {
|
|
6591
6841
|
if (isAuthRetryableFetchError(err)) {
|
|
6592
|
-
throw new
|
|
6842
|
+
throw new HTTPException2(503, { message: "auth service unreachable" });
|
|
6593
6843
|
}
|
|
6594
6844
|
if (isAuthApiError(err) && err.status === 429) {
|
|
6595
|
-
throw new
|
|
6845
|
+
throw new HTTPException2(503, { message: "auth service rate limited" });
|
|
6596
6846
|
}
|
|
6597
6847
|
return null;
|
|
6598
6848
|
}
|
|
@@ -6642,13 +6892,13 @@ async function signAvatarUrl(key) {
|
|
|
6642
6892
|
|
|
6643
6893
|
// src/dashboard/turnstile.ts
|
|
6644
6894
|
var VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
6645
|
-
var
|
|
6895
|
+
var REQUEST_TIMEOUT_MS3 = 5e3;
|
|
6646
6896
|
function siteverify(secret, token) {
|
|
6647
6897
|
return fetch(VERIFY_URL, {
|
|
6648
6898
|
method: "POST",
|
|
6649
6899
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
6650
6900
|
body: new URLSearchParams({ secret, response: token }),
|
|
6651
|
-
signal: AbortSignal.timeout(
|
|
6901
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
|
|
6652
6902
|
});
|
|
6653
6903
|
}
|
|
6654
6904
|
async function verifyTurnstileToken(token) {
|
|
@@ -6991,101 +7241,26 @@ async function resolveAgentIdentities() {
|
|
|
6991
7241
|
})
|
|
6992
7242
|
);
|
|
6993
7243
|
}
|
|
6994
|
-
async function offlineAgentIdentities() {
|
|
6995
|
-
const identity = await loadBotIdentity();
|
|
6996
|
-
const dm = findChannelByAgentName(BOT_DM_KEY) ?? await ensureAgentDmChannel(BOT_DM_KEY, {
|
|
6997
|
-
name: identity.displayName ?? BOT_LABEL,
|
|
6998
|
-
cwd: resolveChannelConfig("").cwd
|
|
6999
|
-
});
|
|
7000
|
-
return [{ name: BOT_DM_KEY, label: BOT_LABEL, kind: "main", channelId: dm.id, ...identity }];
|
|
7001
|
-
}
|
|
7002
|
-
|
|
7003
|
-
// src/dashboard/host-api.ts
|
|
7004
|
-
import { existsSync as existsSync20, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
|
|
7005
|
-
import { join as join20 } from "path";
|
|
7006
|
-
import sharp3 from "sharp";
|
|
7007
|
-
|
|
7008
|
-
// src/dashboard/channels-routes.ts
|
|
7009
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
7010
|
-
import { Hono as Hono9 } from "hono";
|
|
7011
|
-
|
|
7012
|
-
// src/dashboard/feature-flags-routes.ts
|
|
7013
|
-
import { Hono as Hono8 } from "hono";
|
|
7014
|
-
|
|
7015
|
-
// src/feature-flags.ts
|
|
7016
|
-
var featureFlags = {
|
|
7017
|
-
"ai-cost": {
|
|
7018
|
-
title: "AI cost",
|
|
7019
|
-
description: "Shows admins what the agent has spent: a Cost section in a channel's AI overview and a per-thread cost in the thread menu. Estimated at API list rates, so on a Claude subscription it will not match your bill.",
|
|
7020
|
-
stage: "beta"
|
|
7021
|
-
},
|
|
7022
|
-
"read-aloud": {
|
|
7023
|
-
title: "Read aloud",
|
|
7024
|
-
description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
|
|
7025
|
-
stage: "beta"
|
|
7026
|
-
},
|
|
7027
|
-
excalidraw: {
|
|
7028
|
-
title: "Excalidraw",
|
|
7029
|
-
description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
|
|
7030
|
-
stage: "beta"
|
|
7031
|
-
},
|
|
7032
|
-
sandbox: {
|
|
7033
|
-
title: "Sandbox runtime",
|
|
7034
|
-
description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
|
|
7035
|
-
stage: "beta"
|
|
7036
|
-
},
|
|
7037
|
-
dictation: {
|
|
7038
|
-
title: "Dictation",
|
|
7039
|
-
description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
|
|
7040
|
-
stage: "dev"
|
|
7041
|
-
},
|
|
7042
|
-
"agent-memory": {
|
|
7043
|
-
title: "Agent memory",
|
|
7044
|
-
description: "Channel memory the agent reads every turn, saved by the agent and picked up from idle threads, with a Memory tab and an undo chip. While on, Claude's built-in auto memory is switched off so the two can be compared; turning this off keeps every memory.",
|
|
7045
|
-
stage: "beta"
|
|
7046
|
-
}
|
|
7047
|
-
};
|
|
7048
|
-
var featureFlagIds = Object.keys(featureFlags);
|
|
7049
|
-
function isFeatureFlagId(id) {
|
|
7050
|
-
return Object.hasOwn(featureFlags, id);
|
|
7051
|
-
}
|
|
7052
|
-
function isFeatureVisible(stage, devHost) {
|
|
7053
|
-
return stage === "beta" || stage === "dev" && devHost;
|
|
7054
|
-
}
|
|
7055
|
-
function resolveFeatureFlags(enabledIds, devHost) {
|
|
7056
|
-
const enabled = new Set(enabledIds);
|
|
7057
|
-
const out = {};
|
|
7058
|
-
for (const id of featureFlagIds) {
|
|
7059
|
-
const stage = featureFlags[id].stage;
|
|
7060
|
-
out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
|
|
7061
|
-
}
|
|
7062
|
-
return out;
|
|
7063
|
-
}
|
|
7064
|
-
|
|
7065
|
-
// src/store/feature-flag-store.ts
|
|
7066
|
-
async function listEnabledFeatureFlags(workspaceId) {
|
|
7067
|
-
const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
|
|
7068
|
-
if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
|
|
7069
|
-
return (data ?? []).map((r) => r.flag_id);
|
|
7070
|
-
}
|
|
7071
|
-
async function listEnabledFeatureFlagsBySlug(slug) {
|
|
7072
|
-
const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
|
|
7073
|
-
if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
|
|
7074
|
-
return data ? listEnabledFeatureFlags(data.id) : [];
|
|
7075
|
-
}
|
|
7076
|
-
async function enableFeatureFlag(workspaceId, flagId, userId) {
|
|
7077
|
-
const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
|
|
7078
|
-
{ workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
|
|
7079
|
-
{ onConflict: "workspace_id,flag_id" }
|
|
7080
|
-
);
|
|
7081
|
-
if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
7082
|
-
}
|
|
7083
|
-
async function disableFeatureFlag(workspaceId, flagId) {
|
|
7084
|
-
const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
|
|
7085
|
-
if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
7244
|
+
async function offlineAgentIdentities() {
|
|
7245
|
+
const identity = await loadBotIdentity();
|
|
7246
|
+
const dm = findChannelByAgentName(BOT_DM_KEY) ?? await ensureAgentDmChannel(BOT_DM_KEY, {
|
|
7247
|
+
name: identity.displayName ?? BOT_LABEL,
|
|
7248
|
+
cwd: resolveChannelConfig("").cwd
|
|
7249
|
+
});
|
|
7250
|
+
return [{ name: BOT_DM_KEY, label: BOT_LABEL, kind: "main", channelId: dm.id, ...identity }];
|
|
7086
7251
|
}
|
|
7087
7252
|
|
|
7253
|
+
// src/dashboard/host-api.ts
|
|
7254
|
+
import { existsSync as existsSync20, readFileSync as readFileSync15, statSync as statSync8 } from "fs";
|
|
7255
|
+
import { join as join22 } from "path";
|
|
7256
|
+
import sharp3 from "sharp";
|
|
7257
|
+
|
|
7258
|
+
// src/dashboard/channels-routes.ts
|
|
7259
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
|
|
7260
|
+
import { Hono as Hono9 } from "hono";
|
|
7261
|
+
|
|
7088
7262
|
// src/dashboard/feature-flags-routes.ts
|
|
7263
|
+
import { Hono as Hono8 } from "hono";
|
|
7089
7264
|
async function isFeatureEnabled(c, flagId) {
|
|
7090
7265
|
const auth = c.get("auth");
|
|
7091
7266
|
const enabled = await listEnabledFeatureFlags(auth.workspaceId);
|
|
@@ -7154,7 +7329,7 @@ function coerceMemberEmails(raw) {
|
|
|
7154
7329
|
}
|
|
7155
7330
|
function ensureCwdDir(cwd) {
|
|
7156
7331
|
const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
|
|
7157
|
-
if (!existsSync12(expanded))
|
|
7332
|
+
if (!existsSync12(expanded)) mkdirSync8(expanded, { recursive: true });
|
|
7158
7333
|
}
|
|
7159
7334
|
function toResponse(channel) {
|
|
7160
7335
|
return {
|
|
@@ -7305,24 +7480,24 @@ import {
|
|
|
7305
7480
|
closeSync,
|
|
7306
7481
|
existsSync as existsSync16,
|
|
7307
7482
|
lstatSync as lstatSync2,
|
|
7308
|
-
mkdirSync as
|
|
7483
|
+
mkdirSync as mkdirSync9,
|
|
7309
7484
|
openSync,
|
|
7310
7485
|
readSync,
|
|
7311
7486
|
readdirSync as readdirSync4,
|
|
7312
|
-
readFileSync as
|
|
7487
|
+
readFileSync as readFileSync14,
|
|
7313
7488
|
realpathSync as realpathSync3,
|
|
7314
7489
|
renameSync,
|
|
7315
|
-
rmSync as
|
|
7490
|
+
rmSync as rmSync8,
|
|
7316
7491
|
statSync as statSync5,
|
|
7317
|
-
writeFileSync as
|
|
7492
|
+
writeFileSync as writeFileSync9
|
|
7318
7493
|
} from "fs";
|
|
7319
|
-
import { basename as basename6, dirname as dirname5, extname, join as
|
|
7494
|
+
import { basename as basename6, dirname as dirname5, extname, join as join20, relative, resolve as resolve4, sep } from "path";
|
|
7320
7495
|
import { Hono as Hono10 } from "hono";
|
|
7321
7496
|
import { streamSSE } from "hono/streaming";
|
|
7322
7497
|
|
|
7323
7498
|
// src/dashboard/docs-watcher.ts
|
|
7324
7499
|
import { existsSync as existsSync13, watch } from "fs";
|
|
7325
|
-
import { basename as basename4, join as
|
|
7500
|
+
import { basename as basename4, join as join15 } from "path";
|
|
7326
7501
|
var DEBOUNCE_MS = 200;
|
|
7327
7502
|
var MAX_PENDING = 200;
|
|
7328
7503
|
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
@@ -7363,7 +7538,7 @@ function watchDocs(cwd, listener) {
|
|
|
7363
7538
|
const current = entries.get(cwd);
|
|
7364
7539
|
if (!current || filename === null) return;
|
|
7365
7540
|
const rel = filename.split("\\").join("/");
|
|
7366
|
-
if (rel === root && !existsSync13(
|
|
7541
|
+
if (rel === root && !existsSync13(join15(cwd, rel))) return;
|
|
7367
7542
|
if (isIgnoredDocPath(rel)) return;
|
|
7368
7543
|
if (current.pending.size < MAX_PENDING) current.pending.add(rel);
|
|
7369
7544
|
if (current.timer === null) current.timer = setTimeout(() => flush(cwd), DEBOUNCE_MS);
|
|
@@ -7600,18 +7775,18 @@ async function readOrQueueDocAudio(input2) {
|
|
|
7600
7775
|
}
|
|
7601
7776
|
|
|
7602
7777
|
// src/dashboard/artifact-export.ts
|
|
7603
|
-
import { readFileSync as
|
|
7778
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
7604
7779
|
|
|
7605
7780
|
// src/dashboard/chat-agent.ts
|
|
7606
7781
|
import { homedir as homedir7 } from "os";
|
|
7607
7782
|
|
|
7608
7783
|
// src/mcp/config.ts
|
|
7609
|
-
import { mkdtempSync, rmSync as
|
|
7784
|
+
import { mkdtempSync, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
7610
7785
|
import { tmpdir as tmpdir2 } from "os";
|
|
7611
|
-
import { join as
|
|
7786
|
+
import { join as join16 } from "path";
|
|
7612
7787
|
|
|
7613
7788
|
// src/mcp/crypto.ts
|
|
7614
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
7789
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
|
|
7615
7790
|
var ALGORITHM = "aes-256-gcm";
|
|
7616
7791
|
var KEY_BYTES = 32;
|
|
7617
7792
|
var IV_BYTES = 12;
|
|
@@ -7644,7 +7819,7 @@ function currentKeyVersion() {
|
|
|
7644
7819
|
);
|
|
7645
7820
|
}
|
|
7646
7821
|
function encrypt(key, purpose, plaintext) {
|
|
7647
|
-
const iv =
|
|
7822
|
+
const iv = randomBytes4(IV_BYTES);
|
|
7648
7823
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
7649
7824
|
cipher.setAAD(Buffer.from(purpose, "utf8"));
|
|
7650
7825
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
@@ -7664,7 +7839,7 @@ function decrypt(key, purpose, blob) {
|
|
|
7664
7839
|
}
|
|
7665
7840
|
function createEnvelope() {
|
|
7666
7841
|
const keyVersion = currentKeyVersion();
|
|
7667
|
-
const key =
|
|
7842
|
+
const key = randomBytes4(KEY_BYTES);
|
|
7668
7843
|
const wrappedDek = encrypt(
|
|
7669
7844
|
readMasterKey(keyVersion),
|
|
7670
7845
|
purposeForDek(keyVersion),
|
|
@@ -7730,7 +7905,7 @@ function secretsMatch(a, b) {
|
|
|
7730
7905
|
}
|
|
7731
7906
|
|
|
7732
7907
|
// src/mcp/oauth.ts
|
|
7733
|
-
import { createHash as createHash7, randomBytes as
|
|
7908
|
+
import { createHash as createHash7, randomBytes as randomBytes5 } from "crypto";
|
|
7734
7909
|
var RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
|
|
7735
7910
|
"client_id",
|
|
7736
7911
|
"redirect_uri",
|
|
@@ -7744,10 +7919,10 @@ function base64url(buffer) {
|
|
|
7744
7919
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7745
7920
|
}
|
|
7746
7921
|
function createStateToken() {
|
|
7747
|
-
return base64url(
|
|
7922
|
+
return base64url(randomBytes5(32));
|
|
7748
7923
|
}
|
|
7749
7924
|
function createPkcePair() {
|
|
7750
|
-
const verifier = base64url(
|
|
7925
|
+
const verifier = base64url(randomBytes5(32));
|
|
7751
7926
|
const challenge = base64url(createHash7("sha256").update(verifier).digest());
|
|
7752
7927
|
return { verifier, challenge };
|
|
7753
7928
|
}
|
|
@@ -8294,12 +8469,12 @@ async function buildMcpConfig(profile, connectionIds) {
|
|
|
8294
8469
|
return { config, skipped };
|
|
8295
8470
|
}
|
|
8296
8471
|
function writeMcpConfigFile(config) {
|
|
8297
|
-
const dir = mkdtempSync(
|
|
8298
|
-
const path =
|
|
8299
|
-
|
|
8472
|
+
const dir = mkdtempSync(join16(tmpdir2(), "flyspace-mcp-"));
|
|
8473
|
+
const path = join16(dir, "mcp-config.json");
|
|
8474
|
+
writeFileSync8(path, JSON.stringify(config), { mode: 384 });
|
|
8300
8475
|
return {
|
|
8301
8476
|
path,
|
|
8302
|
-
cleanup: () =>
|
|
8477
|
+
cleanup: () => rmSync6(dir, { recursive: true, force: true })
|
|
8303
8478
|
};
|
|
8304
8479
|
}
|
|
8305
8480
|
async function withMcpConfig(profile, connectionIds, fn) {
|
|
@@ -8316,22 +8491,22 @@ async function withMcpConfig(profile, connectionIds, fn) {
|
|
|
8316
8491
|
}
|
|
8317
8492
|
|
|
8318
8493
|
// src/claude/agents.ts
|
|
8319
|
-
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as
|
|
8494
|
+
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "fs";
|
|
8320
8495
|
import { homedir as homedir6 } from "os";
|
|
8321
|
-
import { basename as basename5, join as
|
|
8496
|
+
import { basename as basename5, join as join17 } from "path";
|
|
8322
8497
|
import yaml from "js-yaml";
|
|
8323
8498
|
function defaultConfigDir() {
|
|
8324
|
-
return
|
|
8499
|
+
return join17(homedir6(), ".claude");
|
|
8325
8500
|
}
|
|
8326
8501
|
function expandHome(p) {
|
|
8327
8502
|
if (p === "~") return homedir6();
|
|
8328
|
-
if (p.startsWith("~/")) return
|
|
8503
|
+
if (p.startsWith("~/")) return join17(homedir6(), p.slice(2));
|
|
8329
8504
|
return p;
|
|
8330
8505
|
}
|
|
8331
8506
|
function parseFrontmatter(file) {
|
|
8332
8507
|
let content;
|
|
8333
8508
|
try {
|
|
8334
|
-
content =
|
|
8509
|
+
content = readFileSync11(file, "utf-8");
|
|
8335
8510
|
} catch {
|
|
8336
8511
|
return {};
|
|
8337
8512
|
}
|
|
@@ -8355,7 +8530,7 @@ function collectFrom(root, source) {
|
|
|
8355
8530
|
return [];
|
|
8356
8531
|
}
|
|
8357
8532
|
return entries2.map((file) => {
|
|
8358
|
-
const path =
|
|
8533
|
+
const path = join17(root, file);
|
|
8359
8534
|
const fm = parseFrontmatter(path);
|
|
8360
8535
|
const name = fm.name?.trim() || basename5(file, ".md");
|
|
8361
8536
|
return { name, description: (fm.description ?? "").trim(), source, path };
|
|
@@ -8364,8 +8539,8 @@ function collectFrom(root, source) {
|
|
|
8364
8539
|
function listAgents(configDir2, cwd) {
|
|
8365
8540
|
const base = configDir2 ? expandHome(configDir2) : defaultConfigDir();
|
|
8366
8541
|
const collected = [
|
|
8367
|
-
...collectFrom(
|
|
8368
|
-
...collectFrom(
|
|
8542
|
+
...collectFrom(join17(cwd, ".claude", "agents"), "project"),
|
|
8543
|
+
...collectFrom(join17(base, "agents"), "user")
|
|
8369
8544
|
];
|
|
8370
8545
|
const byName = /* @__PURE__ */ new Map();
|
|
8371
8546
|
for (const agent of collected) {
|
|
@@ -8377,241 +8552,135 @@ function readAgentPersona(configDir2, cwd, name) {
|
|
|
8377
8552
|
const agent = listAgents(configDir2, cwd).find((a) => a.name === name);
|
|
8378
8553
|
if (!agent) return void 0;
|
|
8379
8554
|
try {
|
|
8380
|
-
const content =
|
|
8555
|
+
const content = readFileSync11(agent.path, "utf-8");
|
|
8381
8556
|
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
|
|
8382
8557
|
} catch {
|
|
8383
8558
|
return void 0;
|
|
8384
8559
|
}
|
|
8385
8560
|
}
|
|
8386
8561
|
|
|
8387
|
-
// src/
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
var
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
function
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8562
|
+
// src/dashboard/memory-extraction-runner.ts
|
|
8563
|
+
import { spawn as spawn4 } from "child_process";
|
|
8564
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7 } from "fs";
|
|
8565
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
8566
|
+
import { join as join18 } from "path";
|
|
8567
|
+
var CLAUDE_MODEL = "sonnet";
|
|
8568
|
+
var CODEX_MODEL = "gpt-6-astra";
|
|
8569
|
+
var CODEX_EFFORT = "low";
|
|
8570
|
+
var TIMEOUT_MS = 15e4;
|
|
8571
|
+
function buildInvocation(runner, req, dir, auth, baseEnv = process.env) {
|
|
8572
|
+
const env = { ...baseEnv, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
|
|
8573
|
+
if (runner === "codex") {
|
|
8574
|
+
const outputFile = join18(dir, "last-message.txt");
|
|
8575
|
+
return {
|
|
8576
|
+
command: "codex",
|
|
8577
|
+
args: [
|
|
8578
|
+
"exec",
|
|
8579
|
+
"--ephemeral",
|
|
8580
|
+
// No MCP servers, profiles or hooks from the user's config.toml; auth
|
|
8581
|
+
// still comes from CODEX_HOME.
|
|
8582
|
+
"--ignore-user-config",
|
|
8583
|
+
"--skip-git-repo-check",
|
|
8584
|
+
"-s",
|
|
8585
|
+
"read-only",
|
|
8586
|
+
"-m",
|
|
8587
|
+
CODEX_MODEL,
|
|
8588
|
+
"-c",
|
|
8589
|
+
`model_reasoning_effort=${CODEX_EFFORT}`,
|
|
8590
|
+
"--output-last-message",
|
|
8591
|
+
outputFile,
|
|
8592
|
+
// Codex has no separate system prompt flag; `-` reads the prompt from stdin.
|
|
8593
|
+
"-"
|
|
8594
|
+
],
|
|
8595
|
+
stdin: `${req.system}
|
|
8596
|
+
|
|
8597
|
+
---
|
|
8598
|
+
|
|
8599
|
+
${req.user}`,
|
|
8600
|
+
env,
|
|
8601
|
+
outputFile
|
|
8602
|
+
};
|
|
8407
8603
|
}
|
|
8408
|
-
|
|
8409
|
-
|
|
8604
|
+
const pinned = pinnedClaudeConfigDir(auth.claudeConfigDir);
|
|
8605
|
+
if (pinned) env["CLAUDE_CONFIG_DIR"] = pinned;
|
|
8606
|
+
else delete env["CLAUDE_CONFIG_DIR"];
|
|
8607
|
+
if (auth.claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = auth.claudeCodeOauthToken;
|
|
8608
|
+
else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
8410
8609
|
return {
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8610
|
+
command: "claude",
|
|
8611
|
+
args: [
|
|
8612
|
+
"-p",
|
|
8613
|
+
"--model",
|
|
8614
|
+
CLAUDE_MODEL,
|
|
8615
|
+
"--no-session-persistence",
|
|
8616
|
+
"--tools",
|
|
8617
|
+
"",
|
|
8618
|
+
"--strict-mcp-config",
|
|
8619
|
+
"--setting-sources",
|
|
8620
|
+
"",
|
|
8621
|
+
"--system-prompt",
|
|
8622
|
+
req.system
|
|
8623
|
+
],
|
|
8624
|
+
stdin: req.user,
|
|
8625
|
+
env
|
|
8421
8626
|
};
|
|
8422
8627
|
}
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
|
|
8430
|
-
if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
|
|
8431
|
-
return data ? rowToMemory(data) : null;
|
|
8432
|
-
}
|
|
8433
|
-
async function saveChannelMemory(input2, actor) {
|
|
8434
|
-
validateMemoryInput(input2);
|
|
8435
|
-
const supabase = getSupabaseClient();
|
|
8436
|
-
const existing = await getChannelMemory(input2.channelId, input2.name);
|
|
8437
|
-
const fields = {
|
|
8438
|
-
description: input2.description.trim(),
|
|
8439
|
-
type: input2.type,
|
|
8440
|
-
body: input2.body.trim(),
|
|
8441
|
-
source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
|
|
8442
|
-
created_by: input2.createdBy
|
|
8443
|
-
};
|
|
8444
|
-
if (existing && sameContent(existing, fields)) {
|
|
8445
|
-
return { memory: existing, created: false, changed: false };
|
|
8446
|
-
}
|
|
8447
|
-
const { data, error: error2 } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, scope: "channel", channel_id: input2.channelId, name: input2.name }).select("*").single();
|
|
8448
|
-
if (error2)
|
|
8449
|
-
throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
|
|
8450
|
-
const memory = rowToMemory(data);
|
|
8451
|
-
await logMemoryEvent({
|
|
8452
|
-
memory,
|
|
8453
|
-
action: existing ? "update" : "create",
|
|
8454
|
-
before: existing ? snapshot(existing) : null,
|
|
8455
|
-
after: snapshot(memory),
|
|
8456
|
-
actor,
|
|
8457
|
-
threadId: input2.sourceThreadId ?? null
|
|
8458
|
-
});
|
|
8459
|
-
return { memory, created: !existing, changed: true };
|
|
8460
|
-
}
|
|
8461
|
-
async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
8462
|
-
const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
|
|
8463
|
-
if (!existing) return null;
|
|
8464
|
-
const { memory } = await saveChannelMemory(
|
|
8465
|
-
{
|
|
8466
|
-
channelId,
|
|
8467
|
-
name: existing.name,
|
|
8468
|
-
description: patch.description ?? existing.description,
|
|
8469
|
-
type: patch.type ?? existing.type,
|
|
8470
|
-
body: patch.body ?? existing.body,
|
|
8471
|
-
createdBy: existing.createdBy
|
|
8472
|
-
},
|
|
8473
|
-
actor
|
|
8474
|
-
);
|
|
8475
|
-
return memory;
|
|
8476
|
-
}
|
|
8477
|
-
async function searchChannelMemories(channelId, query) {
|
|
8478
|
-
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
8479
|
-
if (!words.length) return [];
|
|
8480
|
-
let request2 = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
|
|
8481
|
-
for (const word of words) {
|
|
8482
|
-
request2 = request2.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
8483
|
-
}
|
|
8484
|
-
const { data, error: error2 } = await request2.order("updated_at", { ascending: false }).limit(20);
|
|
8485
|
-
if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
|
|
8486
|
-
return (data ?? []).map(rowToMemory);
|
|
8487
|
-
}
|
|
8488
|
-
async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
|
|
8489
|
-
const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
|
|
8490
|
-
if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
|
|
8491
|
-
const deleted = (data ?? []).map(rowToMemory);
|
|
8492
|
-
for (const memory of deleted) {
|
|
8493
|
-
await logMemoryEvent({
|
|
8494
|
-
memory,
|
|
8495
|
-
action: "delete",
|
|
8496
|
-
before: snapshot(memory),
|
|
8497
|
-
after: null,
|
|
8498
|
-
actor,
|
|
8499
|
-
threadId: threadId ?? null
|
|
8628
|
+
function run3(inv, cwd) {
|
|
8629
|
+
return new Promise((resolve7) => {
|
|
8630
|
+
const child = spawn4(inv.command, inv.args, {
|
|
8631
|
+
cwd,
|
|
8632
|
+
env: inv.env,
|
|
8633
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8500
8634
|
});
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
}
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8635
|
+
let stdout = "";
|
|
8636
|
+
let stderr = "";
|
|
8637
|
+
const timer2 = setTimeout(() => child.kill("SIGTERM"), TIMEOUT_MS);
|
|
8638
|
+
child.stdout.on("data", (chunk3) => stdout += chunk3.toString());
|
|
8639
|
+
child.stderr.on("data", (chunk3) => stderr += chunk3.toString());
|
|
8640
|
+
child.on("error", (err) => {
|
|
8641
|
+
clearTimeout(timer2);
|
|
8642
|
+
console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
|
|
8643
|
+
resolve7({ ok: false });
|
|
8644
|
+
});
|
|
8645
|
+
child.on("close", (code) => {
|
|
8646
|
+
clearTimeout(timer2);
|
|
8647
|
+
if (code !== 0) {
|
|
8648
|
+
const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
|
|
8649
|
+
console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
|
|
8650
|
+
resolve7({ ok: false });
|
|
8651
|
+
return;
|
|
8652
|
+
}
|
|
8653
|
+
let raw = stdout;
|
|
8654
|
+
if (inv.outputFile) {
|
|
8655
|
+
try {
|
|
8656
|
+
raw = readFileSync12(inv.outputFile, "utf-8");
|
|
8657
|
+
} catch {
|
|
8658
|
+
raw = "";
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
resolve7({ ok: true, raw });
|
|
8662
|
+
});
|
|
8663
|
+
child.stdin.end(inv.stdin);
|
|
8526
8664
|
});
|
|
8527
|
-
if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
|
|
8528
|
-
}
|
|
8529
|
-
function rowToEvent(row) {
|
|
8530
|
-
return {
|
|
8531
|
-
id: row["id"],
|
|
8532
|
-
channelId: row["channel_id"],
|
|
8533
|
-
memoryId: row["memory_id"] ?? null,
|
|
8534
|
-
threadId: row["thread_id"] ?? null,
|
|
8535
|
-
name: row["name"],
|
|
8536
|
-
action: row["action"],
|
|
8537
|
-
before: row["before"] ?? null,
|
|
8538
|
-
after: row["after"] ?? null,
|
|
8539
|
-
actor: row["actor"],
|
|
8540
|
-
createdAt: row["created_at"],
|
|
8541
|
-
undoneAt: row["undone_at"] ?? null
|
|
8542
|
-
};
|
|
8543
8665
|
}
|
|
8544
|
-
async function
|
|
8545
|
-
const
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
const current = await getChannelMemory(channelId, event.name);
|
|
8557
|
-
const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
|
|
8558
|
-
if (!unchangedSince) return "conflict";
|
|
8559
|
-
const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
|
|
8560
|
-
if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
|
|
8561
|
-
if (!(claimed.data ?? []).length) return "already-undone";
|
|
8562
|
-
const before = event.before;
|
|
8563
|
-
const restore = before ? {
|
|
8564
|
-
description: before.description,
|
|
8565
|
-
type: before.type,
|
|
8566
|
-
body: before.body,
|
|
8567
|
-
source_thread_id: before.sourceThreadId,
|
|
8568
|
-
created_by: before.createdBy
|
|
8569
|
-
} : null;
|
|
8570
|
-
const { error: writeError } = !restore ? await supabase.from("memories").delete().eq("id", current.id) : current ? await supabase.from("memories").update({ ...restore, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", current.id) : await supabase.from("memories").insert({ ...restore, scope: "channel", channel_id: channelId, name: event.name });
|
|
8571
|
-
if (writeError) {
|
|
8572
|
-
await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
|
|
8573
|
-
throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
|
|
8574
|
-
}
|
|
8575
|
-
return "undone";
|
|
8576
|
-
}
|
|
8577
|
-
function isUuid(value) {
|
|
8578
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
8579
|
-
}
|
|
8580
|
-
function formatMemoryPrompt(channelId, memories) {
|
|
8581
|
-
const lines = [];
|
|
8582
|
-
let used = 0;
|
|
8583
|
-
let omitted = 0;
|
|
8584
|
-
for (const m of memories) {
|
|
8585
|
-
const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
|
|
8586
|
-
if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
|
|
8587
|
-
omitted++;
|
|
8588
|
-
continue;
|
|
8589
|
-
}
|
|
8590
|
-
lines.push(line);
|
|
8591
|
-
used += line.length + 1;
|
|
8666
|
+
async function completeMemoryExtraction(req) {
|
|
8667
|
+
const runner = runnerForModel(loadDashboardSettings().agent);
|
|
8668
|
+
const config = resolveChannelConfig(req.channelId);
|
|
8669
|
+
const dir = mkdtempSync2(join18(tmpdir3(), "flyspace-memory-"));
|
|
8670
|
+
try {
|
|
8671
|
+
const inv = buildInvocation(runner, req, dir, {
|
|
8672
|
+
...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
|
|
8673
|
+
...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
|
|
8674
|
+
});
|
|
8675
|
+
return await run3(inv, dir);
|
|
8676
|
+
} finally {
|
|
8677
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
8592
8678
|
}
|
|
8593
|
-
const index = lines.length ? lines.join("\n") + (omitted ? `
|
|
8594
|
-
- (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
|
|
8595
|
-
return [
|
|
8596
|
-
`## Channel memory`,
|
|
8597
|
-
``,
|
|
8598
|
-
`This channel has a persistent memory shared by every thread in it and by every agent runner. Use it instead of any built-in memory directory of your own CLI.`,
|
|
8599
|
-
``,
|
|
8600
|
-
`- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
|
|
8601
|
-
`- Search: \`flyspace memory search --channel ${channelId} <words>\``,
|
|
8602
|
-
`- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
|
|
8603
|
-
`- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
|
|
8604
|
-
``,
|
|
8605
|
-
`Save when someone asks you to remember something, corrects you, or a decision is made that later threads will need. Save what is not derivable from the code, git history or docs: decisions and why, preferences, corrections, where external things live. Do not save one-off details of this conversation. Check the index first and update an existing memory rather than adding a near-duplicate. Write memories in English. Pass \`--thread\` with the thread id from the \`[context: ...]\` header, so the thread shows what changed and it can be undone there.`,
|
|
8606
|
-
``,
|
|
8607
|
-
`Index:`,
|
|
8608
|
-
index
|
|
8609
|
-
].join("\n");
|
|
8610
8679
|
}
|
|
8611
8680
|
|
|
8612
8681
|
// src/dashboard/rollover-summary.ts
|
|
8613
|
-
import { spawn as
|
|
8614
|
-
import { tmpdir as
|
|
8682
|
+
import { spawn as spawn5 } from "child_process";
|
|
8683
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
8615
8684
|
var HAIKU_MODEL = "haiku";
|
|
8616
8685
|
var CJK_PATTERN = /[ -〿-ヿ㐀-䶿一-鿿豈--]/;
|
|
8617
8686
|
function estimateTokens(text) {
|
|
@@ -8670,11 +8739,11 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
|
8670
8739
|
if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
|
|
8671
8740
|
if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
|
|
8672
8741
|
else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
8673
|
-
const child =
|
|
8742
|
+
const child = spawn5("claude", args, {
|
|
8674
8743
|
// A stateless rewrite touches no files — tmpdir keeps it that way and
|
|
8675
8744
|
// avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
|
|
8676
8745
|
// unlike the channel's own cwd the main turn runs in.
|
|
8677
|
-
cwd:
|
|
8746
|
+
cwd: tmpdir4(),
|
|
8678
8747
|
env,
|
|
8679
8748
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8680
8749
|
});
|
|
@@ -8877,7 +8946,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
|
|
|
8877
8946
|
|
|
8878
8947
|
// src/dashboard/attachment-downloader.ts
|
|
8879
8948
|
import { writeFile } from "fs/promises";
|
|
8880
|
-
import { join as
|
|
8949
|
+
import { join as join19 } from "path";
|
|
8881
8950
|
import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
|
|
8882
8951
|
var debug = process.env["DEBUG"] === "true";
|
|
8883
8952
|
var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
|
|
@@ -8924,7 +8993,7 @@ async function downloadOne(key) {
|
|
|
8924
8993
|
if (bytes.length > sizeLimit) {
|
|
8925
8994
|
return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
|
|
8926
8995
|
}
|
|
8927
|
-
const localPath =
|
|
8996
|
+
const localPath = join19(paths.UPLOADS_DIR, lastSegment);
|
|
8928
8997
|
await writeFile(localPath, bytes);
|
|
8929
8998
|
if (debug)
|
|
8930
8999
|
console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
|
|
@@ -9096,21 +9165,21 @@ var ChatAgent = class {
|
|
|
9096
9165
|
* kill.
|
|
9097
9166
|
*/
|
|
9098
9167
|
async maybeSupersede(msg) {
|
|
9099
|
-
const
|
|
9100
|
-
if (!
|
|
9168
|
+
const run4 = this.activeRuns.get(msg.threadId);
|
|
9169
|
+
if (!run4 || run4.msg.author !== msg.author || run4.msg.replyId === msg.replyId) return;
|
|
9101
9170
|
try {
|
|
9102
|
-
if (!await this.shouldInterrupt(
|
|
9171
|
+
if (!await this.shouldInterrupt(run4.msg.body, msg.body)) return;
|
|
9103
9172
|
} catch (err) {
|
|
9104
9173
|
console.error("[chat-agent] interrupt classifier failed:", err.message);
|
|
9105
9174
|
return;
|
|
9106
9175
|
}
|
|
9107
|
-
if (this.activeRuns.get(msg.threadId) !==
|
|
9176
|
+
if (this.activeRuns.get(msg.threadId) !== run4) return;
|
|
9108
9177
|
console.log(
|
|
9109
9178
|
`[chat-agent] follow-up supersedes in-flight run \u2014 aborting. thread=${msg.threadId}`
|
|
9110
9179
|
);
|
|
9111
|
-
|
|
9180
|
+
run4.superseded = true;
|
|
9112
9181
|
this.supersedingReplies.add(msg.replyId);
|
|
9113
|
-
|
|
9182
|
+
run4.abort.abort();
|
|
9114
9183
|
}
|
|
9115
9184
|
/**
|
|
9116
9185
|
* Picks up messages nobody answered. `handleMessage` is fire-and-forget and
|
|
@@ -9273,6 +9342,11 @@ var ChatAgent = class {
|
|
|
9273
9342
|
return data.filter((r) => r.id !== excludeReplyId).reverse().slice(-limit).map((r) => ({ author: r.author, body: r.body }));
|
|
9274
9343
|
}
|
|
9275
9344
|
/** Whether a reply run is currently in flight for this thread. */
|
|
9345
|
+
/** One idle memory extraction call (see memory-extraction-runner.ts). Not a
|
|
9346
|
+
* turn: no thread, session, queue slot or usage row. */
|
|
9347
|
+
completeMemoryExtraction(req) {
|
|
9348
|
+
return completeMemoryExtraction(req);
|
|
9349
|
+
}
|
|
9276
9350
|
isThinking(threadId) {
|
|
9277
9351
|
return this.activeRuns.has(threadId);
|
|
9278
9352
|
}
|
|
@@ -9308,9 +9382,9 @@ var ChatAgent = class {
|
|
|
9308
9382
|
* continue with `--resume`. Returns whether a run was actually in flight —
|
|
9309
9383
|
* a queued-but-not-started message is not affected. */
|
|
9310
9384
|
cancel(threadId) {
|
|
9311
|
-
const
|
|
9312
|
-
if (!
|
|
9313
|
-
|
|
9385
|
+
const run4 = this.activeRuns.get(threadId);
|
|
9386
|
+
if (!run4) return false;
|
|
9387
|
+
run4.abort.abort();
|
|
9314
9388
|
return true;
|
|
9315
9389
|
}
|
|
9316
9390
|
/**
|
|
@@ -9472,18 +9546,11 @@ var ChatAgent = class {
|
|
|
9472
9546
|
...docsAccessUnverified ? { docsAccessUnverified } : {}
|
|
9473
9547
|
};
|
|
9474
9548
|
}
|
|
9475
|
-
const
|
|
9476
|
-
|
|
9477
|
-
return false;
|
|
9549
|
+
const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
|
|
9550
|
+
readable: docsAccess !== "none"
|
|
9478
9551
|
});
|
|
9479
|
-
if (
|
|
9480
|
-
if (
|
|
9481
|
-
const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
|
|
9482
|
-
console.error("[chat-agent] memory lookup failed:", err.message);
|
|
9483
|
-
return void 0;
|
|
9484
|
-
});
|
|
9485
|
-
if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
|
|
9486
|
-
}
|
|
9552
|
+
if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
|
|
9553
|
+
if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
|
|
9487
9554
|
const store = this.openSessionStore();
|
|
9488
9555
|
try {
|
|
9489
9556
|
const channelKey = `chat:${thread.channel_id}`;
|
|
@@ -10211,7 +10278,7 @@ async function exportDocAsArtifact(channelId, docPath) {
|
|
|
10211
10278
|
);
|
|
10212
10279
|
const abs = resolveDocPath(channelConfig.cwd, docPath);
|
|
10213
10280
|
if (!abs) throw new Error(`doc not found: ${docPath}`);
|
|
10214
|
-
const content =
|
|
10281
|
+
const content = readFileSync13(abs, "utf8");
|
|
10215
10282
|
const title = extractTitle(abs, "md");
|
|
10216
10283
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10217
10284
|
const prompt = `Publish the document at \`${docPath}\` (already read below) as a Claude Artifact, then record the result as a stub file under this channel's \`docs/artifacts/\` folder (create the folder if it doesn't exist).
|
|
@@ -10468,7 +10535,7 @@ function probeDir(absDir, showHidden) {
|
|
|
10468
10535
|
return false;
|
|
10469
10536
|
}
|
|
10470
10537
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10471
|
-
const abs =
|
|
10538
|
+
const abs = join20(dir, entry.name);
|
|
10472
10539
|
const kind = entryKind(entry, abs);
|
|
10473
10540
|
if (kind === "dir") {
|
|
10474
10541
|
if (SKIP_DIRS.has(entry.name)) hasOther = true;
|
|
@@ -10488,14 +10555,14 @@ function probeDir(absDir, showHidden) {
|
|
|
10488
10555
|
}
|
|
10489
10556
|
function readGitBranch(dir) {
|
|
10490
10557
|
try {
|
|
10491
|
-
let gitDir =
|
|
10558
|
+
let gitDir = join20(dir, ".git");
|
|
10492
10559
|
if (statSync5(gitDir).isFile()) {
|
|
10493
|
-
const pointer =
|
|
10560
|
+
const pointer = readFileSync14(gitDir, "utf8").trim();
|
|
10494
10561
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
10495
10562
|
if (!match) return void 0;
|
|
10496
10563
|
gitDir = resolve4(dir, match[1]);
|
|
10497
10564
|
}
|
|
10498
|
-
const head =
|
|
10565
|
+
const head = readFileSync14(join20(gitDir, "HEAD"), "utf8").trim();
|
|
10499
10566
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
10500
10567
|
if (branchMatch) return branchMatch[1];
|
|
10501
10568
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -10517,14 +10584,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
10517
10584
|
for (const entry of entries2) {
|
|
10518
10585
|
if (dirs.length + docs.length >= MAX_ENTRIES) break;
|
|
10519
10586
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10520
|
-
const abs =
|
|
10587
|
+
const abs = join20(absDir, entry.name);
|
|
10521
10588
|
const path = relative(root, abs).split(sep).join("/");
|
|
10522
10589
|
const kind = entryKind(entry, abs);
|
|
10523
10590
|
if (kind === "dir") {
|
|
10524
10591
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10525
10592
|
const probe3 = probeDir(abs, showHidden);
|
|
10526
10593
|
if (!probe3.hasDocs && probe3.hasOther) continue;
|
|
10527
|
-
const isRepo = existsSync16(
|
|
10594
|
+
const isRepo = existsSync16(join20(abs, ".git"));
|
|
10528
10595
|
dirs.push({
|
|
10529
10596
|
path,
|
|
10530
10597
|
name: entry.name,
|
|
@@ -10560,7 +10627,7 @@ function flatDocs(root, {
|
|
|
10560
10627
|
for (const entry of entries2) {
|
|
10561
10628
|
if (hits.length >= MAX_FLAT_DOCS) return;
|
|
10562
10629
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10563
|
-
const abs =
|
|
10630
|
+
const abs = join20(dir, entry.name);
|
|
10564
10631
|
const kind = entryKind(entry, abs);
|
|
10565
10632
|
if (kind === "dir") {
|
|
10566
10633
|
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
@@ -10602,7 +10669,7 @@ function writeDocContent(root, rel, content, baseMtime) {
|
|
|
10602
10669
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) {
|
|
10603
10670
|
return { status: "too-large" };
|
|
10604
10671
|
}
|
|
10605
|
-
|
|
10672
|
+
writeFileSync9(abs, content, "utf8");
|
|
10606
10673
|
return { status: "ok", mtime: statSync5(abs).mtime.toISOString() };
|
|
10607
10674
|
}
|
|
10608
10675
|
function resolveRawPath(root, rel) {
|
|
@@ -10640,7 +10707,7 @@ function uniqueName(absDir, name) {
|
|
|
10640
10707
|
let candidate = name;
|
|
10641
10708
|
for (let n = 1; n < 1e3; n++) {
|
|
10642
10709
|
try {
|
|
10643
|
-
statSync5(
|
|
10710
|
+
statSync5(join20(absDir, candidate));
|
|
10644
10711
|
} catch {
|
|
10645
10712
|
return candidate;
|
|
10646
10713
|
}
|
|
@@ -10662,8 +10729,8 @@ function saveUploadedFile(root, relDir, name, data) {
|
|
|
10662
10729
|
if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
|
|
10663
10730
|
if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
|
|
10664
10731
|
const final = uniqueName(absDir, safe);
|
|
10665
|
-
const abs =
|
|
10666
|
-
|
|
10732
|
+
const abs = join20(absDir, final);
|
|
10733
|
+
writeFileSync9(abs, data);
|
|
10667
10734
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
|
|
10668
10735
|
}
|
|
10669
10736
|
function createDocFolder(root, relDir, name) {
|
|
@@ -10676,13 +10743,13 @@ function createDocFolder(root, relDir, name) {
|
|
|
10676
10743
|
}
|
|
10677
10744
|
const safe = sanitizeUploadName(name);
|
|
10678
10745
|
if (!safe) return { status: "bad-name" };
|
|
10679
|
-
const abs =
|
|
10746
|
+
const abs = join20(absDir, safe);
|
|
10680
10747
|
try {
|
|
10681
10748
|
statSync5(abs);
|
|
10682
10749
|
return { status: "exists" };
|
|
10683
10750
|
} catch {
|
|
10684
10751
|
}
|
|
10685
|
-
|
|
10752
|
+
mkdirSync9(abs);
|
|
10686
10753
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
|
|
10687
10754
|
}
|
|
10688
10755
|
function createDocFile(root, relDir, name, content) {
|
|
@@ -10699,9 +10766,9 @@ function createDocFile(root, relDir, name, content) {
|
|
|
10699
10766
|
if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
|
|
10700
10767
|
if (SKIP_FILES.has(safe)) return { status: "bad-name" };
|
|
10701
10768
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
|
|
10702
|
-
const abs =
|
|
10769
|
+
const abs = join20(absDir, safe);
|
|
10703
10770
|
try {
|
|
10704
|
-
|
|
10771
|
+
writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
|
|
10705
10772
|
} catch (err) {
|
|
10706
10773
|
if (err.code === "EEXIST") return { status: "exists" };
|
|
10707
10774
|
throw err;
|
|
@@ -10737,7 +10804,7 @@ function moveDocEntry(root, from, to) {
|
|
|
10737
10804
|
} catch {
|
|
10738
10805
|
return { status: "bad-target" };
|
|
10739
10806
|
}
|
|
10740
|
-
const finalAbs =
|
|
10807
|
+
const finalAbs = join20(absToDir, name);
|
|
10741
10808
|
if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
|
|
10742
10809
|
return { status: "into-self" };
|
|
10743
10810
|
}
|
|
@@ -10760,7 +10827,7 @@ function deleteDocEntry(root, path) {
|
|
|
10760
10827
|
}
|
|
10761
10828
|
const isDir2 = stat.isDirectory();
|
|
10762
10829
|
if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
|
|
10763
|
-
|
|
10830
|
+
rmSync8(abs, { recursive: isDir2 });
|
|
10764
10831
|
return {
|
|
10765
10832
|
status: "ok",
|
|
10766
10833
|
path: relative(root, abs).split(sep).join("/"),
|
|
@@ -10799,7 +10866,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
10799
10866
|
}
|
|
10800
10867
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10801
10868
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10802
|
-
const data =
|
|
10869
|
+
const data = readFileSync14(resolved2.abs);
|
|
10803
10870
|
const headers = { "Content-Type": resolved2.contentType };
|
|
10804
10871
|
if (download) {
|
|
10805
10872
|
const filename = basename6(resolved2.abs).replace(/"/g, "");
|
|
@@ -10819,7 +10886,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
10819
10886
|
}
|
|
10820
10887
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
10821
10888
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
10822
|
-
return c.body(new Uint8Array(
|
|
10889
|
+
return c.body(new Uint8Array(readFileSync14(abs)), 200, {
|
|
10823
10890
|
"Content-Type": "text/html; charset=utf-8",
|
|
10824
10891
|
"Content-Security-Policy": HTML_SHARE_CSP,
|
|
10825
10892
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -11004,7 +11071,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11004
11071
|
title: kind ? extractTitle(abs, kind) : basename6(abs),
|
|
11005
11072
|
mtime: stat.mtime.toISOString(),
|
|
11006
11073
|
tooLarge: tooLarge || void 0,
|
|
11007
|
-
content: tooLarge ? "" :
|
|
11074
|
+
content: tooLarge ? "" : readFileSync14(abs, "utf8")
|
|
11008
11075
|
});
|
|
11009
11076
|
});
|
|
11010
11077
|
app.put("/:id/docs/content", async (c) => {
|
|
@@ -11047,7 +11114,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11047
11114
|
channel,
|
|
11048
11115
|
rel,
|
|
11049
11116
|
title: extractTitle(abs, "md"),
|
|
11050
|
-
content:
|
|
11117
|
+
content: readFileSync14(abs, "utf8")
|
|
11051
11118
|
};
|
|
11052
11119
|
};
|
|
11053
11120
|
app.get("/:id/docs/translation/:lang", async (c) => {
|
|
@@ -11270,7 +11337,7 @@ function ownSkills(scope, dir) {
|
|
|
11270
11337
|
return [{ scope, name, description, body, path, plugin: null, mtime }];
|
|
11271
11338
|
});
|
|
11272
11339
|
}
|
|
11273
|
-
function
|
|
11340
|
+
function snapshot(scopes) {
|
|
11274
11341
|
const wants = (scope) => !scopes || scopes.has(scope);
|
|
11275
11342
|
const configDir2 = profileClaudeConfigDir();
|
|
11276
11343
|
const rows = [];
|
|
@@ -11305,7 +11372,7 @@ async function pushSkillScopes(scopes) {
|
|
|
11305
11372
|
if (!mirrorEnabled()) return;
|
|
11306
11373
|
const workspace = getProfile();
|
|
11307
11374
|
const supabase = getSupabaseClient();
|
|
11308
|
-
const rows =
|
|
11375
|
+
const rows = snapshot(scopes ? new Set(scopes) : void 0);
|
|
11309
11376
|
let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
|
|
11310
11377
|
if (scopes) query = query.in("scope", scopes);
|
|
11311
11378
|
const { data, error: error2 } = await query;
|
|
@@ -11586,10 +11653,10 @@ function registerMirroredSkillRoutes(api, agent) {
|
|
|
11586
11653
|
}
|
|
11587
11654
|
|
|
11588
11655
|
// src/dashboard/providers.ts
|
|
11589
|
-
import { execFileSync as
|
|
11656
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
11590
11657
|
import { existsSync as existsSync19 } from "fs";
|
|
11591
11658
|
import { homedir as homedir8 } from "os";
|
|
11592
|
-
import { join as
|
|
11659
|
+
import { join as join21, resolve as resolve5 } from "path";
|
|
11593
11660
|
var PROVIDER_LABELS = {
|
|
11594
11661
|
claude: "Claude Code",
|
|
11595
11662
|
codex: "Codex"
|
|
@@ -11598,7 +11665,7 @@ var TTL_MS2 = 6e4;
|
|
|
11598
11665
|
var cache6 = /* @__PURE__ */ new Map();
|
|
11599
11666
|
function binaryOnPath(bin) {
|
|
11600
11667
|
try {
|
|
11601
|
-
|
|
11668
|
+
execFileSync3(bin, ["--version"], { stdio: "ignore" });
|
|
11602
11669
|
return true;
|
|
11603
11670
|
} catch {
|
|
11604
11671
|
return false;
|
|
@@ -11607,10 +11674,10 @@ function binaryOnPath(bin) {
|
|
|
11607
11674
|
function claudeAuthAvailable() {
|
|
11608
11675
|
if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
|
|
11609
11676
|
const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
|
|
11610
|
-
if (existsSync19(
|
|
11677
|
+
if (existsSync19(join21(configDir2, ".credentials.json"))) return true;
|
|
11611
11678
|
if (process.platform !== "darwin") return false;
|
|
11612
11679
|
try {
|
|
11613
|
-
|
|
11680
|
+
execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
11614
11681
|
stdio: "ignore"
|
|
11615
11682
|
});
|
|
11616
11683
|
return true;
|
|
@@ -11760,7 +11827,7 @@ function registerHostRoutes(api) {
|
|
|
11760
11827
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
11761
11828
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
11762
11829
|
const filename = errors ? "custie-error.log" : "custie.log";
|
|
11763
|
-
const file =
|
|
11830
|
+
const file = join22(paths.LOG_DIR, filename);
|
|
11764
11831
|
const exists = existsSync20(file);
|
|
11765
11832
|
return c.json({
|
|
11766
11833
|
file,
|
|
@@ -11772,7 +11839,7 @@ function registerHostRoutes(api) {
|
|
|
11772
11839
|
}
|
|
11773
11840
|
function tailFile(file, limit) {
|
|
11774
11841
|
if (!existsSync20(file)) return [];
|
|
11775
|
-
const lines =
|
|
11842
|
+
const lines = readFileSync15(file, "utf-8").split("\n");
|
|
11776
11843
|
if (lines.at(-1) === "") lines.pop();
|
|
11777
11844
|
return lines.slice(-limit);
|
|
11778
11845
|
}
|
|
@@ -13445,7 +13512,7 @@ function createSearchRouter() {
|
|
|
13445
13512
|
}
|
|
13446
13513
|
|
|
13447
13514
|
// src/dashboard/uploads-routes.ts
|
|
13448
|
-
import { randomUUID } from "crypto";
|
|
13515
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
13449
13516
|
import { Hono as Hono18 } from "hono";
|
|
13450
13517
|
import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
|
|
13451
13518
|
import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
|
|
@@ -13469,7 +13536,7 @@ function createUploadsRouter() {
|
|
|
13469
13536
|
if (file.size > MAX_FILE_BYTES) {
|
|
13470
13537
|
return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
|
|
13471
13538
|
}
|
|
13472
|
-
const key = `${KEY_PREFIX2}/${getProfile()}/${
|
|
13539
|
+
const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID2()}-${sanitizeFilename(file.name)}`;
|
|
13473
13540
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
13474
13541
|
await getR2Client().send(
|
|
13475
13542
|
new PutObjectCommand3({
|
|
@@ -13552,7 +13619,7 @@ function createTranscribeRouter() {
|
|
|
13552
13619
|
}
|
|
13553
13620
|
|
|
13554
13621
|
// src/dashboard/avatars-routes.ts
|
|
13555
|
-
import { createHash as createHash10, randomUUID as
|
|
13622
|
+
import { createHash as createHash10, randomUUID as randomUUID3 } from "crypto";
|
|
13556
13623
|
import { Hono as Hono20 } from "hono";
|
|
13557
13624
|
import sharp4 from "sharp";
|
|
13558
13625
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
|
|
@@ -13591,7 +13658,7 @@ async function storeAvatar(file, owner) {
|
|
|
13591
13658
|
if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
|
|
13592
13659
|
const original = Buffer.from(await file.arrayBuffer());
|
|
13593
13660
|
const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
|
|
13594
|
-
const key = `${avatarKeyPrefix()}${owner}-${
|
|
13661
|
+
const key = `${avatarKeyPrefix()}${owner}-${randomUUID3()}.png`;
|
|
13595
13662
|
await getR2Client().send(
|
|
13596
13663
|
new PutObjectCommand4({
|
|
13597
13664
|
Bucket: getR2Bucket(),
|
|
@@ -13756,6 +13823,10 @@ function createMembersRouter() {
|
|
|
13756
13823
|
if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
|
|
13757
13824
|
return c.json({ error: "unknown avatar" }, 400);
|
|
13758
13825
|
}
|
|
13826
|
+
const { plan } = await getWorkspaceBilling(ctx.workspaceId);
|
|
13827
|
+
if (plan === "local") {
|
|
13828
|
+
return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
|
|
13829
|
+
}
|
|
13759
13830
|
const existing = await getMembershipByEmail(ctx.workspaceId, email);
|
|
13760
13831
|
if (existing) return c.json({ error: "already a member of this workspace" }, 409);
|
|
13761
13832
|
let membership;
|
|
@@ -14953,8 +15024,16 @@ function uniqueChannelName(raw, taken) {
|
|
|
14953
15024
|
// src/dashboard/workspace-greeting.ts
|
|
14954
15025
|
var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
|
|
14955
15026
|
var GREETING_MODEL = "openai/gpt-oss-20b";
|
|
14956
|
-
var
|
|
15027
|
+
var TIMEOUT_MS2 = 6e3;
|
|
14957
15028
|
var MAX_LISTED_CHANNELS = 8;
|
|
15029
|
+
var GUIDES = [
|
|
15030
|
+
"Welcome to Flyspace",
|
|
15031
|
+
"Working with the agent",
|
|
15032
|
+
"Channels and folders",
|
|
15033
|
+
"Docs",
|
|
15034
|
+
"Inviting your team",
|
|
15035
|
+
"Automations"
|
|
15036
|
+
];
|
|
14958
15037
|
var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner sees, posted in their #general channel. Flyspace is a team chat where an AI agent works inside every channel.
|
|
14959
15038
|
|
|
14960
15039
|
- Plain Markdown, 60 to 110 words, in English. No headings, no emoji, no em or en dashes, no sign-off.
|
|
@@ -14962,11 +15041,13 @@ var SYSTEM_PROMPT4 = `You write the first message a new Flyspace workspace owner
|
|
|
14962
15041
|
- Say that #general is public: everyone invited to the workspace can see it.
|
|
14963
15042
|
- If project channels are listed, mention them as #name and say each one is linked to that project folder. Never invent a channel.
|
|
14964
15043
|
- Mention the trial or the missing Host only if the facts say so.
|
|
15044
|
+
- If guides are listed, say they are in the Docs pane of #general and suggest starting with the first one.
|
|
14965
15045
|
- Use only the facts given. Do not add buttons or a list of next steps: those are appended for you.`;
|
|
14966
15046
|
function greetingButtons(input2) {
|
|
14967
15047
|
const buttons = [];
|
|
14968
15048
|
if (!input2.hostConnected) buttons.push("Connect a machine");
|
|
14969
15049
|
if (input2.channels[0]) buttons.push(`Summarise #${input2.channels[0]} for me`);
|
|
15050
|
+
if (input2.hostConnected) buttons.push("Give me a quick tour of the guides");
|
|
14970
15051
|
if (input2.memberCount <= 1) buttons.push("Invite my team");
|
|
14971
15052
|
buttons.push("Set up a daily digest");
|
|
14972
15053
|
return buttons.slice(0, 3).map((b) => b.replace(/[|\]]/g, ""));
|
|
@@ -14977,6 +15058,7 @@ function fallbackGreeting(input2) {
|
|
|
14977
15058
|
`Welcome to ${input2.workspaceName}, ${input2.ownerName}.`,
|
|
14978
15059
|
"This is #general. It is public, so everyone you invite to the workspace can see it.",
|
|
14979
15060
|
channels.length ? `Your project channels are ready: ${channels.join(", ")}. Each one is linked to its project folder, so the agent there works on the real files.` : "",
|
|
15061
|
+
input2.hostConnected ? "A few short guides are waiting in the Docs pane of #general. Start with Welcome to Flyspace." : "",
|
|
14980
15062
|
"Mention the agent in any channel to get started."
|
|
14981
15063
|
].filter(Boolean).join("\n\n");
|
|
14982
15064
|
}
|
|
@@ -14987,11 +15069,12 @@ async function groqGreeting(input2, apiKey) {
|
|
|
14987
15069
|
`Plan: ${input2.trialing ? "14-day free trial, just started" : "not on a trial"}`,
|
|
14988
15070
|
`Host: ${input2.hostConnected ? "a machine is connected, agents can run" : "no machine connected yet, agents cannot run until one is"}`,
|
|
14989
15071
|
`Project channels: ${input2.channels.slice(0, MAX_LISTED_CHANNELS).join(", ") || "(none yet)"}`,
|
|
14990
|
-
`Members: ${input2.memberCount}
|
|
15072
|
+
`Members: ${input2.memberCount}`,
|
|
15073
|
+
`Guides in #general: ${input2.hostConnected ? GUIDES.join(", ") : "(none, no machine connected)"}`
|
|
14991
15074
|
].join("\n");
|
|
14992
15075
|
const res = await fetch(GROQ_API_URL8, {
|
|
14993
15076
|
method: "POST",
|
|
14994
|
-
signal: AbortSignal.timeout(
|
|
15077
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2),
|
|
14995
15078
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
14996
15079
|
body: JSON.stringify({
|
|
14997
15080
|
model: GREETING_MODEL,
|
|
@@ -15069,6 +15152,14 @@ function createOnboardingRouter(agent) {
|
|
|
15069
15152
|
await markWorkspaceProjectsSetup(ctx.workspaceId);
|
|
15070
15153
|
return c.json({ channels: created.map((ch) => ({ id: ch.id, name: ch.name })) }, 201);
|
|
15071
15154
|
});
|
|
15155
|
+
app.get("/greeting", async (c) => {
|
|
15156
|
+
const ctx = await resolveAuthContext(c);
|
|
15157
|
+
if (ctx?.role !== "admin") return c.json({ pending: false });
|
|
15158
|
+
const greetedAt = await getWorkspaceGreetedAt(ctx.workspaceId);
|
|
15159
|
+
const pending3 = !!ctx.projectsSetupAt && !greetedAt;
|
|
15160
|
+
const general = pending3 ? await ensureGeneralChannel() : null;
|
|
15161
|
+
return c.json({ pending: pending3, channelId: general?.id ?? null });
|
|
15162
|
+
});
|
|
15072
15163
|
app.post("/greeting", async (c) => {
|
|
15073
15164
|
const ctx = await resolveAuthContext(c);
|
|
15074
15165
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -15077,6 +15168,8 @@ function createOnboardingRouter(agent) {
|
|
|
15077
15168
|
const existing = () => supabase.from("threads").select("id").eq("client_key", ctx.workspaceId).maybeSingle();
|
|
15078
15169
|
const found = await existing();
|
|
15079
15170
|
if (found.data) return c.json({ channelId: general.id, threadId: found.data.id });
|
|
15171
|
+
if (await getWorkspaceGreetedAt(ctx.workspaceId))
|
|
15172
|
+
return c.json({ error: "already greeted" }, 409);
|
|
15080
15173
|
const [branding, billing, members] = await Promise.all([
|
|
15081
15174
|
getWorkspaceBranding(getProfile()).catch(() => null),
|
|
15082
15175
|
getWorkspaceBilling(ctx.workspaceId).catch(() => null),
|
|
@@ -15107,6 +15200,7 @@ function createOnboardingRouter(agent) {
|
|
|
15107
15200
|
await supabase.from("threads").delete().eq("id", thread.id);
|
|
15108
15201
|
return c.json({ error: reply.error.message }, 500);
|
|
15109
15202
|
}
|
|
15203
|
+
await markWorkspaceGreeted(ctx.workspaceId);
|
|
15110
15204
|
return c.json({ channelId: general.id, threadId: thread.id }, 201);
|
|
15111
15205
|
});
|
|
15112
15206
|
return app;
|
|
@@ -15281,17 +15375,17 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15281
15375
|
const auto = loadEffectiveAutomations();
|
|
15282
15376
|
const runs = /* @__PURE__ */ new Map();
|
|
15283
15377
|
try {
|
|
15284
|
-
for (const [name,
|
|
15285
|
-
runs.set(name, { lastRun:
|
|
15378
|
+
for (const [name, run4] of await lastRunsForWorkspace(getProfile())) {
|
|
15379
|
+
runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
|
|
15286
15380
|
}
|
|
15287
15381
|
} catch {
|
|
15288
15382
|
if (existsSync22(paths.DB_FILE)) {
|
|
15289
15383
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
15290
15384
|
try {
|
|
15291
|
-
for (const
|
|
15292
|
-
runs.set(
|
|
15293
|
-
lastRun:
|
|
15294
|
-
lastStatus:
|
|
15385
|
+
for (const run4 of runStore.listLastRuns()) {
|
|
15386
|
+
runs.set(run4.name, {
|
|
15387
|
+
lastRun: run4.lastRunAt.toISOString(),
|
|
15388
|
+
lastStatus: run4.lastStatus
|
|
15295
15389
|
});
|
|
15296
15390
|
}
|
|
15297
15391
|
} finally {
|
|
@@ -15481,17 +15575,17 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15481
15575
|
}
|
|
15482
15576
|
const threadLinks = await resolveThreadLinks(raw);
|
|
15483
15577
|
const sessions = raw.map((s) => {
|
|
15484
|
-
const
|
|
15485
|
-
const turns =
|
|
15486
|
-
const total =
|
|
15578
|
+
const run4 = latest.get(s.threadTs);
|
|
15579
|
+
const turns = run4?.numTurns ?? 1;
|
|
15580
|
+
const total = run4 ? run4.inputTokens + run4.cacheReadTokens + run4.cacheCreationTokens : void 0;
|
|
15487
15581
|
const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
|
|
15488
15582
|
return {
|
|
15489
15583
|
...s,
|
|
15490
15584
|
channelLabel: channelLabel(s.channelId),
|
|
15491
|
-
model:
|
|
15492
|
-
numTurns:
|
|
15585
|
+
model: run4?.model,
|
|
15586
|
+
numTurns: run4?.numTurns,
|
|
15493
15587
|
contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
|
|
15494
|
-
rolledOver:
|
|
15588
|
+
rolledOver: run4?.label === "rollover",
|
|
15495
15589
|
threadTitle: link?.title ?? void 0,
|
|
15496
15590
|
chatChannelId: link?.chatChannelId,
|
|
15497
15591
|
chatThreadId: link?.chatThreadId,
|
|
@@ -15594,16 +15688,16 @@ function relay2(agent) {
|
|
|
15594
15688
|
}
|
|
15595
15689
|
|
|
15596
15690
|
// src/dashboard/agent-bridge.ts
|
|
15597
|
-
import { existsSync as existsSync24, readFileSync as
|
|
15691
|
+
import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
|
|
15598
15692
|
import dotenv from "dotenv";
|
|
15599
15693
|
import { WebSocket, WebSocketServer } from "ws";
|
|
15600
15694
|
|
|
15601
15695
|
// src/dashboard/host-identity.ts
|
|
15602
15696
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
15603
|
-
import { existsSync as existsSync23, mkdirSync as
|
|
15604
|
-
import { dirname as dirname7, join as
|
|
15697
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
|
|
15698
|
+
import { dirname as dirname7, join as join23 } from "path";
|
|
15605
15699
|
function hostPrivateKeyPath() {
|
|
15606
|
-
return process.env["FLYSPACE_HOST_KEY_PATH"] ||
|
|
15700
|
+
return process.env["FLYSPACE_HOST_KEY_PATH"] || join23(paths.BASE_CONFIG_DIR, "host.key");
|
|
15607
15701
|
}
|
|
15608
15702
|
function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
15609
15703
|
if (!existsSync23(path)) {
|
|
@@ -15611,7 +15705,7 @@ function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
|
15611
15705
|
`No Host identity at ${path}. Run \`pnpm run generate-host-key\` on this machine, then set FLYSPACE_HOST_PUBLIC_KEY on the Data process to the printed public key.`
|
|
15612
15706
|
);
|
|
15613
15707
|
}
|
|
15614
|
-
return createPrivateKey2(
|
|
15708
|
+
return createPrivateKey2(readFileSync16(path, "utf8"));
|
|
15615
15709
|
}
|
|
15616
15710
|
function loadHostPublicKey() {
|
|
15617
15711
|
const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
|
|
@@ -15624,82 +15718,6 @@ function loadHostPublicKey() {
|
|
|
15624
15718
|
}
|
|
15625
15719
|
}
|
|
15626
15720
|
|
|
15627
|
-
// src/dashboard/host-lock.ts
|
|
15628
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
15629
|
-
import { randomBytes as randomBytes5 } from "crypto";
|
|
15630
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
15631
|
-
import { join as join22 } from "path";
|
|
15632
|
-
function hostLockPath() {
|
|
15633
|
-
return join22(paths.BASE_DATA_DIR, "host.lock");
|
|
15634
|
-
}
|
|
15635
|
-
function isLiveFlyspace(pid) {
|
|
15636
|
-
try {
|
|
15637
|
-
process.kill(pid, 0);
|
|
15638
|
-
const command = execFileSync3("ps", ["-p", String(pid), "-o", "command="], {
|
|
15639
|
-
encoding: "utf8"
|
|
15640
|
-
});
|
|
15641
|
-
return command.includes("flyspace") || command.includes("host-server");
|
|
15642
|
-
} catch {
|
|
15643
|
-
return false;
|
|
15644
|
-
}
|
|
15645
|
-
}
|
|
15646
|
-
function readLockPid(path) {
|
|
15647
|
-
try {
|
|
15648
|
-
const pid = Number(JSON.parse(readFileSync15(path, "utf8")).pid);
|
|
15649
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
15650
|
-
} catch {
|
|
15651
|
-
return null;
|
|
15652
|
-
}
|
|
15653
|
-
}
|
|
15654
|
-
function acquireHostLock() {
|
|
15655
|
-
const path = hostLockPath();
|
|
15656
|
-
mkdirSync10(paths.BASE_DATA_DIR, { recursive: true });
|
|
15657
|
-
const body = JSON.stringify({
|
|
15658
|
-
pid: process.pid,
|
|
15659
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15660
|
-
execPath: process.argv[1] ?? ""
|
|
15661
|
-
});
|
|
15662
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
15663
|
-
try {
|
|
15664
|
-
writeFileSync10(path, body, { flag: "wx", mode: 384 });
|
|
15665
|
-
const release2 = () => {
|
|
15666
|
-
if (readLockPid(path) === process.pid) rmSync7(path, { force: true });
|
|
15667
|
-
};
|
|
15668
|
-
process.once("exit", release2);
|
|
15669
|
-
return release2;
|
|
15670
|
-
} catch (err) {
|
|
15671
|
-
if (err.code !== "EEXIST") throw err;
|
|
15672
|
-
const holder = readLockPid(path);
|
|
15673
|
-
if (holder === process.pid) return () => {
|
|
15674
|
-
};
|
|
15675
|
-
if (holder && isLiveFlyspace(holder)) {
|
|
15676
|
-
throw new Error(
|
|
15677
|
-
`Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
|
|
15678
|
-
);
|
|
15679
|
-
}
|
|
15680
|
-
rmSync7(path, { force: true });
|
|
15681
|
-
}
|
|
15682
|
-
}
|
|
15683
|
-
throw new Error(`Could not take the Host lock at ${path}`);
|
|
15684
|
-
}
|
|
15685
|
-
function publishHostEndpoint(port) {
|
|
15686
|
-
const path = hostLockPath();
|
|
15687
|
-
if (readLockPid(path) !== process.pid) return null;
|
|
15688
|
-
const token = randomBytes5(32).toString("hex");
|
|
15689
|
-
const lock = JSON.parse(readFileSync15(path, "utf8"));
|
|
15690
|
-
writeFileSync10(path, JSON.stringify({ ...lock, port, token }), { mode: 384 });
|
|
15691
|
-
return token;
|
|
15692
|
-
}
|
|
15693
|
-
function readHostEndpoint() {
|
|
15694
|
-
try {
|
|
15695
|
-
const { pid, port, token } = JSON.parse(readFileSync15(hostLockPath(), "utf8"));
|
|
15696
|
-
if (pid === process.pid || !Number.isInteger(port) || typeof token !== "string") return null;
|
|
15697
|
-
return { port, token };
|
|
15698
|
-
} catch {
|
|
15699
|
-
return null;
|
|
15700
|
-
}
|
|
15701
|
-
}
|
|
15702
|
-
|
|
15703
15721
|
// src/store/device-store.ts
|
|
15704
15722
|
import { createHash as createHash11, createPublicKey as createPublicKey3, randomBytes as randomBytes6, randomInt } from "crypto";
|
|
15705
15723
|
var DEVICE_CODE_TTL_MS = 10 * 6e4;
|
|
@@ -15932,7 +15950,7 @@ async function userEmail(userId) {
|
|
|
15932
15950
|
|
|
15933
15951
|
// src/dashboard/doc-share-publisher.ts
|
|
15934
15952
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
15935
|
-
import { readFileSync as
|
|
15953
|
+
import { readFileSync as readFileSync17, statSync as statSync9 } from "fs";
|
|
15936
15954
|
import { extname as extname2, posix } from "path";
|
|
15937
15955
|
import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
|
|
15938
15956
|
|
|
@@ -16119,7 +16137,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16119
16137
|
}
|
|
16120
16138
|
if (!stat.isFile()) return { status: "not-found" };
|
|
16121
16139
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
16122
|
-
const source =
|
|
16140
|
+
const source = readFileSync17(abs, "utf8");
|
|
16123
16141
|
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
16124
16142
|
if (kind === "excalidraw") {
|
|
16125
16143
|
try {
|
|
@@ -16199,7 +16217,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16199
16217
|
let text = null;
|
|
16200
16218
|
try {
|
|
16201
16219
|
const size = statSync9(docAbs).size;
|
|
16202
|
-
if (size <= budget) text =
|
|
16220
|
+
if (size <= budget) text = readFileSync17(docAbs, "utf8");
|
|
16203
16221
|
} catch {
|
|
16204
16222
|
continue;
|
|
16205
16223
|
}
|
|
@@ -16216,7 +16234,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16216
16234
|
let body;
|
|
16217
16235
|
try {
|
|
16218
16236
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
16219
|
-
body =
|
|
16237
|
+
body = readFileSync17(resolved2.abs);
|
|
16220
16238
|
} catch {
|
|
16221
16239
|
continue;
|
|
16222
16240
|
}
|
|
@@ -16258,7 +16276,7 @@ async function publishDocShare(input2) {
|
|
|
16258
16276
|
...HTML_FEATURES,
|
|
16259
16277
|
brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
|
|
16260
16278
|
} : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
|
|
16261
|
-
const
|
|
16279
|
+
const snapshot2 = {
|
|
16262
16280
|
...built,
|
|
16263
16281
|
sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
|
|
16264
16282
|
};
|
|
@@ -16268,8 +16286,8 @@ async function publishDocShare(input2) {
|
|
|
16268
16286
|
const renamed = await setDocShareSlug(existing.id, slug);
|
|
16269
16287
|
if (!renamed) return { status: "not-found" };
|
|
16270
16288
|
}
|
|
16271
|
-
const share2 = await updateDocShareSnapshot(existing.id,
|
|
16272
|
-
const stale = existing.assetKeys.filter((key) => !
|
|
16289
|
+
const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
|
|
16290
|
+
const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
|
|
16273
16291
|
await deleteShareAssets(stale);
|
|
16274
16292
|
if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
|
|
16275
16293
|
void queueShareAudio(share2);
|
|
@@ -16283,7 +16301,7 @@ async function publishDocShare(input2) {
|
|
|
16283
16301
|
channelId: input2.channelId,
|
|
16284
16302
|
sourcePath: input2.sourcePath,
|
|
16285
16303
|
createdBy: input2.createdBy,
|
|
16286
|
-
...
|
|
16304
|
+
...snapshot2,
|
|
16287
16305
|
...features
|
|
16288
16306
|
});
|
|
16289
16307
|
void queueShareAudio(share);
|
|
@@ -16310,9 +16328,12 @@ var METHODS = {
|
|
|
16310
16328
|
thread_replies: ["GET", "HEAD", "POST", "PATCH"],
|
|
16311
16329
|
memberships: ["GET", "HEAD"],
|
|
16312
16330
|
channel_members: ["GET", "HEAD"],
|
|
16313
|
-
channel_docs_access: ["GET", "HEAD"]
|
|
16331
|
+
channel_docs_access: ["GET", "HEAD"],
|
|
16332
|
+
memories: ALL,
|
|
16333
|
+
memory_events: ["GET", "HEAD", "POST", "PATCH"]
|
|
16314
16334
|
};
|
|
16315
16335
|
var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members", "channel_docs_access"]);
|
|
16336
|
+
var CHANNEL_OWNED = /* @__PURE__ */ new Set(["memories", "memory_events"]);
|
|
16316
16337
|
var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
|
|
16317
16338
|
var deny = (message, status = 403) => ({
|
|
16318
16339
|
status,
|
|
@@ -16428,6 +16449,16 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
16428
16449
|
if (!channelIds) return deny("Could not resolve the device workspaces.", 502);
|
|
16429
16450
|
if (CHANNEL_SCOPED.has(table)) {
|
|
16430
16451
|
params.append("channel_id", inList(channelIds));
|
|
16452
|
+
} else if (CHANNEL_OWNED.has(table)) {
|
|
16453
|
+
if (method === "POST") {
|
|
16454
|
+
const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
|
|
16455
|
+
if (!ok3) return deny(`Every ${table} row must belong to a channel of the device workspaces.`);
|
|
16456
|
+
}
|
|
16457
|
+
if (method === "PATCH") {
|
|
16458
|
+
const patch = bodyRows(req.body);
|
|
16459
|
+
if (!patch || patch.some((r) => "channel_id" in r)) return deny(`A ${table} row cannot move channels.`);
|
|
16460
|
+
}
|
|
16461
|
+
params.append("channel_id", inList(channelIds));
|
|
16431
16462
|
} else if (table === "threads") {
|
|
16432
16463
|
if (method === "POST") {
|
|
16433
16464
|
const ok3 = rows.length > 0 && rows.every((r) => typeof r["channel_id"] === "string" && channelIds.includes(r["channel_id"]));
|
|
@@ -16490,98 +16521,6 @@ var forwardToSupabase = async (req) => {
|
|
|
16490
16521
|
};
|
|
16491
16522
|
};
|
|
16492
16523
|
|
|
16493
|
-
// src/dashboard/host-db-client.ts
|
|
16494
|
-
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
16495
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
16496
|
-
var REQUEST_TIMEOUT_MS3 = 3e4;
|
|
16497
|
-
var pending = /* @__PURE__ */ new Map();
|
|
16498
|
-
var sender = null;
|
|
16499
|
-
var onSender = [];
|
|
16500
|
-
function setHostDbSender(fn) {
|
|
16501
|
-
sender = fn;
|
|
16502
|
-
if (!fn) return;
|
|
16503
|
-
const waiting = onSender;
|
|
16504
|
-
onSender = [];
|
|
16505
|
-
for (const wake of waiting) wake();
|
|
16506
|
-
}
|
|
16507
|
-
function handleHostDbResult(id, res) {
|
|
16508
|
-
const p = pending.get(id);
|
|
16509
|
-
if (!p) return;
|
|
16510
|
-
pending.delete(id);
|
|
16511
|
-
clearTimeout(p.timer);
|
|
16512
|
-
p.resolve(res);
|
|
16513
|
-
}
|
|
16514
|
-
function whenConnected() {
|
|
16515
|
-
if (sender) return Promise.resolve(sender);
|
|
16516
|
-
return new Promise((resolve7, reject) => {
|
|
16517
|
-
const timer2 = setTimeout(() => {
|
|
16518
|
-
onSender = onSender.filter((w) => w !== wake);
|
|
16519
|
-
reject(new Error("Host is not connected to the Data API"));
|
|
16520
|
-
}, REQUEST_TIMEOUT_MS3);
|
|
16521
|
-
const wake = () => {
|
|
16522
|
-
clearTimeout(timer2);
|
|
16523
|
-
resolve7(sender);
|
|
16524
|
-
};
|
|
16525
|
-
onSender.push(wake);
|
|
16526
|
-
});
|
|
16527
|
-
}
|
|
16528
|
-
async function request(req) {
|
|
16529
|
-
const send2 = await whenConnected();
|
|
16530
|
-
const id = randomUUID3();
|
|
16531
|
-
return new Promise((resolve7, reject) => {
|
|
16532
|
-
const timer2 = setTimeout(() => {
|
|
16533
|
-
pending.delete(id);
|
|
16534
|
-
reject(new Error("Data API did not answer in time"));
|
|
16535
|
-
}, REQUEST_TIMEOUT_MS3);
|
|
16536
|
-
pending.set(id, { resolve: resolve7, reject, timer: timer2 });
|
|
16537
|
-
send2({ type: "db", id, req });
|
|
16538
|
-
});
|
|
16539
|
-
}
|
|
16540
|
-
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
16541
|
-
var hostDbFetch = async (input2, init) => {
|
|
16542
|
-
const req = new Request(input2, init);
|
|
16543
|
-
const url = new URL(req.url);
|
|
16544
|
-
const method = req.method.toUpperCase();
|
|
16545
|
-
const res = await request({
|
|
16546
|
-
method,
|
|
16547
|
-
path: `${url.pathname}${url.search}`,
|
|
16548
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
16549
|
-
body: method === "GET" || method === "HEAD" ? void 0 : await req.text()
|
|
16550
|
-
});
|
|
16551
|
-
const noBody = method === "HEAD" || BODYLESS_STATUS.has(res.status);
|
|
16552
|
-
return new Response(noBody ? null : res.body, { status: res.status, headers: res.headers });
|
|
16553
|
-
};
|
|
16554
|
-
function relayChannel(topic) {
|
|
16555
|
-
const channel = {
|
|
16556
|
-
on: () => channel,
|
|
16557
|
-
subscribe: (callback) => {
|
|
16558
|
-
queueMicrotask(() => callback?.("SUBSCRIBED"));
|
|
16559
|
-
return channel;
|
|
16560
|
-
},
|
|
16561
|
-
send: async (msg) => {
|
|
16562
|
-
if (msg.type !== "broadcast" || !sender) return "error";
|
|
16563
|
-
sender({ type: "db-broadcast", topic, event: msg.event, payload: msg.payload ?? {} });
|
|
16564
|
-
return "ok";
|
|
16565
|
-
},
|
|
16566
|
-
unsubscribe: async () => "ok"
|
|
16567
|
-
};
|
|
16568
|
-
return channel;
|
|
16569
|
-
}
|
|
16570
|
-
function createHostDbClient() {
|
|
16571
|
-
const client2 = createClient2("https://host-db.invalid", "host-device", {
|
|
16572
|
-
auth: { persistSession: false, autoRefreshToken: false },
|
|
16573
|
-
global: { fetch: hostDbFetch }
|
|
16574
|
-
});
|
|
16575
|
-
return new Proxy(client2, {
|
|
16576
|
-
get(target, prop) {
|
|
16577
|
-
if (prop === "channel") return relayChannel;
|
|
16578
|
-
if (prop === "removeChannel") return async () => "ok";
|
|
16579
|
-
const value = Reflect.get(target, prop, target);
|
|
16580
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
16581
|
-
}
|
|
16582
|
-
});
|
|
16583
|
-
}
|
|
16584
|
-
|
|
16585
16524
|
// src/dashboard/agent-bridge.ts
|
|
16586
16525
|
var HOST_SOCKET_PATH = "/internal/host-socket";
|
|
16587
16526
|
var DEFAULT_DATA_API_PORT = 4757;
|
|
@@ -16609,6 +16548,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
16609
16548
|
cancelAgentRun: (threadId) => chatAgent.cancel(threadId),
|
|
16610
16549
|
regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
|
|
16611
16550
|
listAgentRuns: () => chatAgent.runningThreadIds(),
|
|
16551
|
+
completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
|
|
16612
16552
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
16613
16553
|
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
16614
16554
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
@@ -16628,14 +16568,14 @@ function localAgentCallbacks(chatAgent) {
|
|
|
16628
16568
|
}
|
|
16629
16569
|
var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
|
|
16630
16570
|
function buildHostConfigSnapshot() {
|
|
16631
|
-
const
|
|
16571
|
+
const snapshot2 = {};
|
|
16632
16572
|
for (const profile of listProfiles()) {
|
|
16633
|
-
|
|
16634
|
-
env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(
|
|
16573
|
+
snapshot2[profile] = runWithProfile(profile, () => ({
|
|
16574
|
+
env: existsSync24(paths.CONFIG_FILE) ? dotenv.parse(readFileSync18(paths.CONFIG_FILE, "utf-8")) : {},
|
|
16635
16575
|
agentModel: loadDashboardSettings().agent
|
|
16636
16576
|
}));
|
|
16637
16577
|
}
|
|
16638
|
-
return
|
|
16578
|
+
return snapshot2;
|
|
16639
16579
|
}
|
|
16640
16580
|
function startLocalProfileConfigHydration() {
|
|
16641
16581
|
setProfileConfigCache(buildHostConfigSnapshot());
|
|
@@ -16644,6 +16584,7 @@ function startLocalProfileConfigHydration() {
|
|
|
16644
16584
|
}
|
|
16645
16585
|
var CALL_TIMEOUT_MS = 15e3;
|
|
16646
16586
|
var DOCS_CALL_TIMEOUT_MS = 12e4;
|
|
16587
|
+
var MEMORY_EXTRACTION_TIMEOUT_MS = 165e3;
|
|
16647
16588
|
var AUTH_TIMEOUT_MS = 5e3;
|
|
16648
16589
|
var HEARTBEAT_INTERVAL_MS = 15e3;
|
|
16649
16590
|
var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
|
|
@@ -16842,6 +16783,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
|
|
|
16842
16783
|
);
|
|
16843
16784
|
return Promise.resolve(null);
|
|
16844
16785
|
}
|
|
16786
|
+
if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
|
|
16845
16787
|
const id = randomUUID4();
|
|
16846
16788
|
return new Promise((resolve7) => {
|
|
16847
16789
|
const timer2 = setTimeout(() => {
|
|
@@ -16870,6 +16812,9 @@ function remoteAgentCallbacks() {
|
|
|
16870
16812
|
cancelAgentRun: async (threadId) => await callHost("cancelAgentRun", [threadId]) ?? false,
|
|
16871
16813
|
regenerateAgentRun: async (threadId) => await callHost("regenerateAgentRun", [threadId]) ?? false,
|
|
16872
16814
|
listAgentRuns: async () => await callHost("listAgentRuns", []) ?? [],
|
|
16815
|
+
// `null` (no Host, or no answer in time) passes through: the extractor
|
|
16816
|
+
// reads it as a failed call and retries the thread next sweep.
|
|
16817
|
+
completeMemoryExtraction: (req) => callHost("completeMemoryExtraction", [req], MEMORY_EXTRACTION_TIMEOUT_MS),
|
|
16873
16818
|
// Deliberately passes `null` through rather than defaulting to `[]`: with
|
|
16874
16819
|
// no Host connected, claiming every folder is fine would be a guess.
|
|
16875
16820
|
listMissingCwds: () => callHost("listMissingCwds", []),
|
|
@@ -17066,12 +17011,13 @@ function relayedAgentCallbacks(local) {
|
|
|
17066
17011
|
...Object.fromEntries(RELAYED_METHODS.map((method) => [method, relay3(method)]))
|
|
17067
17012
|
};
|
|
17068
17013
|
}
|
|
17069
|
-
|
|
17014
|
+
function relayAuthorized(req, token) {
|
|
17070
17015
|
const given = Buffer.from(req.headers.get("authorization") ?? "");
|
|
17071
17016
|
const expected = Buffer.from(`Bearer ${token}`);
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17017
|
+
return !!token && given.length === expected.length && timingSafeEqual2(given, expected);
|
|
17018
|
+
}
|
|
17019
|
+
async function handleLocalRelay(req, callbacks, token) {
|
|
17020
|
+
if (!relayAuthorized(req, token)) return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
17075
17021
|
const body = await req.json().catch(() => null);
|
|
17076
17022
|
const method = RELAYED_METHODS.find((m) => m === body?.method);
|
|
17077
17023
|
if (!body || !method || typeof body.profile !== "string" || !Array.isArray(body.args)) {
|
|
@@ -17354,6 +17300,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
17354
17300
|
const resolved2 = requested && known.includes(requested) ? requested : profileFromHost(trustedHost(c), known);
|
|
17355
17301
|
if (!resolved2) {
|
|
17356
17302
|
return c.json({
|
|
17303
|
+
unknownWorkspace: isUnknownWorkspaceHost(trustedHost(c), known),
|
|
17357
17304
|
theme: BRANDING_DEFAULTS.theme,
|
|
17358
17305
|
font: BRANDING_DEFAULTS.font,
|
|
17359
17306
|
displayName: "Flyspace",
|
|
@@ -17674,7 +17621,7 @@ function startHostStatusBroadcast() {
|
|
|
17674
17621
|
|
|
17675
17622
|
// src/dashboard/server.ts
|
|
17676
17623
|
var DEFAULT_DASHBOARD_PORT = 4747;
|
|
17677
|
-
var WEB_DIST =
|
|
17624
|
+
var WEB_DIST = join24(paths.PACKAGE_ROOT, "web", "dist");
|
|
17678
17625
|
var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
17679
17626
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
17680
17627
|
for (let port = start; port < start + attempts; port++) {
|
|
@@ -17724,6 +17671,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17724
17671
|
const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
|
|
17725
17672
|
};
|
|
17726
17673
|
const app = new Hono34();
|
|
17674
|
+
app.onError(reportingErrorHandler);
|
|
17727
17675
|
app.use("*", async (c, next) => {
|
|
17728
17676
|
await next();
|
|
17729
17677
|
if (c.req.path.startsWith("/assets/")) {
|
|
@@ -17738,6 +17686,20 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17738
17686
|
let relayToken = null;
|
|
17739
17687
|
if (mode === "host") {
|
|
17740
17688
|
app.post(LOCAL_RELAY_PATH, (c) => handleLocalRelay(c.req.raw, agent, relayToken));
|
|
17689
|
+
if (earlyHostSocket) {
|
|
17690
|
+
app.post(LOCAL_DB_PATH, async (c) => {
|
|
17691
|
+
if (!relayAuthorized(c.req.raw, relayToken)) return c.json({ error: "unauthorized" }, 401);
|
|
17692
|
+
const req = await c.req.json().catch(() => null);
|
|
17693
|
+
if (!req || typeof req.method !== "string" || typeof req.path !== "string") {
|
|
17694
|
+
return c.json({ error: "bad request" }, 400);
|
|
17695
|
+
}
|
|
17696
|
+
try {
|
|
17697
|
+
return c.json(await requestHostDb(req));
|
|
17698
|
+
} catch (err) {
|
|
17699
|
+
return c.json({ error: err.message }, 503);
|
|
17700
|
+
}
|
|
17701
|
+
});
|
|
17702
|
+
}
|
|
17741
17703
|
}
|
|
17742
17704
|
app.route("/stripe", createStripeWebhookRouter());
|
|
17743
17705
|
if (!servesHost) {
|
|
@@ -17748,14 +17710,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17748
17710
|
} else {
|
|
17749
17711
|
app.route("/share", createPublicShareAssetRouter(WEB_DIST));
|
|
17750
17712
|
app.route("/", createAppIconRouter());
|
|
17751
|
-
if (existsSync25(WEB_DIST) || existsSync25(
|
|
17713
|
+
if (existsSync25(WEB_DIST) || existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"))) {
|
|
17752
17714
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
17753
17715
|
}
|
|
17754
17716
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
17755
|
-
const spaFallback = serveStatic({ path:
|
|
17717
|
+
const spaFallback = serveStatic({ path: join24(WEB_DIST, "index.html") });
|
|
17756
17718
|
app.get("*", async (c, next) => {
|
|
17757
|
-
if (!existsSync25(
|
|
17758
|
-
const isCheckout = existsSync25(
|
|
17719
|
+
if (!existsSync25(join24(WEB_DIST, "index.html"))) {
|
|
17720
|
+
const isCheckout = existsSync25(join24(paths.PACKAGE_ROOT, "web", "package.json"));
|
|
17759
17721
|
return c.text(
|
|
17760
17722
|
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
17761
17723
|
Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
|
|
@@ -18193,7 +18155,7 @@ async function threadReplies(client2, nameOrId, ts2) {
|
|
|
18193
18155
|
async function postMessage(client2, args) {
|
|
18194
18156
|
const channelArg = getArg2(args, "--channel");
|
|
18195
18157
|
const textFilePath = getArg2(args, "--text-file");
|
|
18196
|
-
const text = textFilePath ?
|
|
18158
|
+
const text = textFilePath ? readFileSync19(textFilePath, "utf8") : getArg2(args, "--text");
|
|
18197
18159
|
const threadTs = getArg2(args, "--thread");
|
|
18198
18160
|
const markdown = args.includes("--markdown");
|
|
18199
18161
|
const broadcast = args.includes("--broadcast") && Boolean(threadTs);
|
|
@@ -18348,7 +18310,7 @@ async function ensureClaudeCodeReady() {
|
|
|
18348
18310
|
}
|
|
18349
18311
|
function getManifestYaml(botName) {
|
|
18350
18312
|
const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
18351
|
-
const raw =
|
|
18313
|
+
const raw = readFileSync19(manifestPath, "utf-8");
|
|
18352
18314
|
return raw.replace(/name: Custie/g, `name: ${botName}`);
|
|
18353
18315
|
}
|
|
18354
18316
|
async function stepPersonalise() {
|
|
@@ -18561,7 +18523,7 @@ async function stepBrowserTokens(botName) {
|
|
|
18561
18523
|
})
|
|
18562
18524
|
));
|
|
18563
18525
|
const DEBUG = process.argv.includes("--debug");
|
|
18564
|
-
const USER_DATA_DIR = resolve6(
|
|
18526
|
+
const USER_DATA_DIR = resolve6(tmpdir5(), "custie-playwright-profile");
|
|
18565
18527
|
let context;
|
|
18566
18528
|
try {
|
|
18567
18529
|
log4("Opening browser \u2014 a Slack login page will appear.\n");
|
|
@@ -18780,7 +18742,7 @@ function copyDefaultPrompt(botName) {
|
|
|
18780
18742
|
if (!existsSync26(paths.PROMPT_FILE)) {
|
|
18781
18743
|
const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
|
|
18782
18744
|
if (existsSync26(defaultPrompt)) {
|
|
18783
|
-
const content =
|
|
18745
|
+
const content = readFileSync19(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
|
|
18784
18746
|
writeFileSync11(paths.PROMPT_FILE, content);
|
|
18785
18747
|
success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
18786
18748
|
}
|
|
@@ -19075,6 +19037,7 @@ async function runPost(args) {
|
|
|
19075
19037
|
}
|
|
19076
19038
|
async function runChatCmd(args) {
|
|
19077
19039
|
loadEnvFiles();
|
|
19040
|
+
useHostDbWithoutCredentials();
|
|
19078
19041
|
const subcommand = args[0];
|
|
19079
19042
|
const rest = args.slice(1);
|
|
19080
19043
|
switch (subcommand) {
|
|
@@ -19103,21 +19066,21 @@ async function runChatCmd(args) {
|
|
|
19103
19066
|
}
|
|
19104
19067
|
|
|
19105
19068
|
// src/commands/memory.ts
|
|
19106
|
-
import { readFileSync as
|
|
19069
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
19107
19070
|
|
|
19108
19071
|
// src/store/claude-memory-import.ts
|
|
19109
|
-
import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as
|
|
19072
|
+
import { existsSync as existsSync27, readdirSync as readdirSync5, readFileSync as readFileSync20 } from "fs";
|
|
19110
19073
|
import { homedir as homedir10 } from "os";
|
|
19111
|
-
import { basename as basename8, join as
|
|
19074
|
+
import { basename as basename8, join as join25 } from "path";
|
|
19112
19075
|
import dotenv2 from "dotenv";
|
|
19113
19076
|
function claudeMemoryDir(cwd, configDir2) {
|
|
19114
|
-
return
|
|
19077
|
+
return join25(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
|
|
19115
19078
|
}
|
|
19116
19079
|
function profileClaudeConfigDirFor(profile) {
|
|
19117
|
-
const dir = profile === "default" ? paths.BASE_CONFIG_DIR :
|
|
19118
|
-
const file =
|
|
19119
|
-
const env = existsSync27(file) ? dotenv2.parse(
|
|
19120
|
-
return env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
19080
|
+
const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join25(paths.PROFILES_DIR, profile);
|
|
19081
|
+
const file = join25(dir, "config.env");
|
|
19082
|
+
const env = existsSync27(file) ? dotenv2.parse(readFileSync20(file)) : {};
|
|
19083
|
+
return env["CLAUDE_CONFIG_DIR"]?.trim() || join25(homedir10(), ".claude");
|
|
19121
19084
|
}
|
|
19122
19085
|
var TYPE_MAP = {
|
|
19123
19086
|
user: "preference",
|
|
@@ -19125,7 +19088,7 @@ var TYPE_MAP = {
|
|
|
19125
19088
|
project: "fact",
|
|
19126
19089
|
reference: "reference"
|
|
19127
19090
|
};
|
|
19128
|
-
var
|
|
19091
|
+
var DESCRIPTION_MAX = 200;
|
|
19129
19092
|
function unquote2(value) {
|
|
19130
19093
|
const v = value.trim();
|
|
19131
19094
|
return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
|
|
@@ -19143,8 +19106,8 @@ function parseClaudeMemory(fileName, text, channelId) {
|
|
|
19143
19106
|
}
|
|
19144
19107
|
const name = slugify(fields["name"] || basename8(fileName, ".md"));
|
|
19145
19108
|
let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
|
|
19146
|
-
if (description.length >
|
|
19147
|
-
description = `${description.slice(0,
|
|
19109
|
+
if (description.length > DESCRIPTION_MAX) {
|
|
19110
|
+
description = `${description.slice(0, DESCRIPTION_MAX - 3).trimEnd()}...`;
|
|
19148
19111
|
}
|
|
19149
19112
|
const input2 = {
|
|
19150
19113
|
channelId,
|
|
@@ -19166,7 +19129,7 @@ async function importClaudeMemories(dir, channelId, opts = {}) {
|
|
|
19166
19129
|
const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
|
|
19167
19130
|
const results = [];
|
|
19168
19131
|
for (const file of files) {
|
|
19169
|
-
const parsed = parseClaudeMemory(file,
|
|
19132
|
+
const parsed = parseClaudeMemory(file, readFileSync20(join25(dir, file), "utf-8"), channelId);
|
|
19170
19133
|
if ("skip" in parsed) {
|
|
19171
19134
|
results.push({ file, outcome: "skipped", reason: parsed.skip });
|
|
19172
19135
|
continue;
|
|
@@ -19220,6 +19183,7 @@ function indexLine(m) {
|
|
|
19220
19183
|
}
|
|
19221
19184
|
async function runMemoryCmd(args) {
|
|
19222
19185
|
loadEnvFiles();
|
|
19186
|
+
useHostDbWithoutCredentials();
|
|
19223
19187
|
const subcommand = args[0];
|
|
19224
19188
|
const rest = args.slice(1);
|
|
19225
19189
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
@@ -19265,7 +19229,7 @@ ${memory.body}`);
|
|
|
19265
19229
|
);
|
|
19266
19230
|
}
|
|
19267
19231
|
if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
19268
|
-
const body = bodyArg === "-" ?
|
|
19232
|
+
const body = bodyArg === "-" ? readFileSync21(0, "utf-8") : bodyArg;
|
|
19269
19233
|
const thread = getArg4(rest, "--thread");
|
|
19270
19234
|
const { created, changed } = await saveChannelMemory(
|
|
19271
19235
|
{
|
|
@@ -19349,10 +19313,10 @@ async function runMcpCmd(args) {
|
|
|
19349
19313
|
}
|
|
19350
19314
|
|
|
19351
19315
|
// src/commands/dashboard.ts
|
|
19352
|
-
import { spawn as
|
|
19316
|
+
import { spawn as spawn6, spawnSync } from "child_process";
|
|
19353
19317
|
import { existsSync as existsSync28 } from "fs";
|
|
19354
19318
|
import { platform as platform7 } from "os";
|
|
19355
|
-
import { join as
|
|
19319
|
+
import { join as join26 } from "path";
|
|
19356
19320
|
function flagValue(args, name) {
|
|
19357
19321
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
19358
19322
|
if (eq) return eq.slice(`--${name}=`.length);
|
|
@@ -19362,13 +19326,13 @@ function flagValue(args, name) {
|
|
|
19362
19326
|
function openBrowser2(url) {
|
|
19363
19327
|
const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
|
|
19364
19328
|
try {
|
|
19365
|
-
|
|
19329
|
+
spawn6(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
19366
19330
|
} catch {
|
|
19367
19331
|
}
|
|
19368
19332
|
}
|
|
19369
19333
|
function ensureWebBuild() {
|
|
19370
|
-
if (existsSync28(
|
|
19371
|
-
if (!existsSync28(
|
|
19334
|
+
if (existsSync28(join26(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
|
|
19335
|
+
if (!existsSync28(join26(paths.PACKAGE_ROOT, "web", "package.json"))) return;
|
|
19372
19336
|
console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
|
|
19373
19337
|
const result = spawnSync("pnpm", ["--dir", "web", "build"], {
|
|
19374
19338
|
cwd: paths.PACKAGE_ROOT,
|
|
@@ -19731,7 +19695,7 @@ async function main() {
|
|
|
19731
19695
|
break;
|
|
19732
19696
|
case "-v":
|
|
19733
19697
|
case "--version":
|
|
19734
|
-
printRaw("0.1.
|
|
19698
|
+
printRaw("0.1.15");
|
|
19735
19699
|
break;
|
|
19736
19700
|
case "-h":
|
|
19737
19701
|
case "--help":
|