flyspace-cli 0.1.14 → 0.1.16
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-DA7SYYKU.js → chunk-2OGLCT7I.js} +555 -19
- package/dist/cli.js +907 -648
- package/dist/index.js +1 -1
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
BOT_PROFILE_KEY,
|
|
6
6
|
BRANDING_DEFAULTS,
|
|
7
7
|
BUILT_IN_OUTPUT_STYLES,
|
|
8
|
+
ChannelCwdError,
|
|
8
9
|
DEFAULT_CODEX_MODEL,
|
|
9
10
|
DEFAULT_TIMEZONE,
|
|
10
11
|
DEFAULT_TITLE_RULES,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
HandleTakenError,
|
|
14
15
|
InvalidTransferError,
|
|
15
16
|
LastAdminError,
|
|
17
|
+
MEMORY_TYPES,
|
|
16
18
|
MessageQueue,
|
|
17
19
|
OutputStyleConflictError,
|
|
18
20
|
SKIP_SUBTYPES,
|
|
@@ -33,18 +35,23 @@ import {
|
|
|
33
35
|
canSeeChannel,
|
|
34
36
|
channelSkillsDir,
|
|
35
37
|
claudeAuthFailed,
|
|
38
|
+
clearClaudeAuthFailed,
|
|
36
39
|
codexAuthAvailable,
|
|
37
40
|
createChannel,
|
|
38
41
|
createMembership,
|
|
39
42
|
createOutputStyle,
|
|
40
43
|
createSkillFile,
|
|
41
44
|
createWorkspace,
|
|
45
|
+
deleteChannelMemory,
|
|
42
46
|
deleteMembership,
|
|
47
|
+
deleteMemory,
|
|
43
48
|
deleteOutputStyle,
|
|
44
49
|
deleteSkillFile,
|
|
45
50
|
deleteWorkspace,
|
|
51
|
+
disableFeatureFlag,
|
|
46
52
|
displayNameFor,
|
|
47
53
|
docsAccessPromptNote,
|
|
54
|
+
enableFeatureFlag,
|
|
48
55
|
ensureAgentDmChannel,
|
|
49
56
|
ensureGeneralChannel,
|
|
50
57
|
ensureLogicalCwdDir,
|
|
@@ -52,15 +59,19 @@ import {
|
|
|
52
59
|
ensureUsersCached,
|
|
53
60
|
expandPath,
|
|
54
61
|
extractCategories,
|
|
62
|
+
featureFlagIds,
|
|
63
|
+
featureFlags,
|
|
55
64
|
findChannel,
|
|
56
65
|
findChannelByAgentName,
|
|
57
66
|
findChannelById,
|
|
58
67
|
findSkillDetail,
|
|
59
68
|
getAgentPaused,
|
|
60
69
|
getAllowGuestAccess,
|
|
70
|
+
getChannelMemory,
|
|
61
71
|
getChannelRegistry,
|
|
62
72
|
getMembershipByEmail,
|
|
63
73
|
getMembershipByUserId,
|
|
74
|
+
getMemory,
|
|
64
75
|
getOutputStyleDefault,
|
|
65
76
|
getProfileChannelRegistry,
|
|
66
77
|
getWorkspaceBilling,
|
|
@@ -75,21 +86,31 @@ import {
|
|
|
75
86
|
isCodexModel,
|
|
76
87
|
isCwdUsable,
|
|
77
88
|
isEffort,
|
|
89
|
+
isFeatureFlagId,
|
|
90
|
+
isFeatureVisible,
|
|
78
91
|
isLockedPlan,
|
|
79
92
|
isLogicalCwd,
|
|
93
|
+
isMemoryType,
|
|
94
|
+
isUuid,
|
|
80
95
|
isValidSkillName,
|
|
81
96
|
lastRunsForWorkspace,
|
|
82
97
|
listActiveUsers,
|
|
98
|
+
listChannelMemories,
|
|
99
|
+
listEnabledFeatureFlags,
|
|
83
100
|
listMemberChannels,
|
|
84
101
|
listMemberships,
|
|
85
102
|
listMembershipsByEmail,
|
|
103
|
+
listMemories,
|
|
104
|
+
listMemoryEvents,
|
|
86
105
|
listOutputStyles,
|
|
87
106
|
listPluginSkillDetails,
|
|
88
107
|
listSkillFiles,
|
|
89
108
|
listSkills,
|
|
109
|
+
listThreadMemoryEvents,
|
|
90
110
|
listUsersWithAvatars,
|
|
91
111
|
listWorkspaceSummaries,
|
|
92
112
|
listWorkspaces,
|
|
113
|
+
loadAgentMemory,
|
|
93
114
|
loadAutomations,
|
|
94
115
|
loadAutomationsFile,
|
|
95
116
|
loadBotIdentity,
|
|
@@ -103,6 +124,7 @@ import {
|
|
|
103
124
|
localDefaults,
|
|
104
125
|
lookupById,
|
|
105
126
|
markInviteResent,
|
|
127
|
+
markMemoryUsed,
|
|
106
128
|
markWorkspaceAgentConnected,
|
|
107
129
|
markWorkspaceGreeted,
|
|
108
130
|
markWorkspaceProjectsSetup,
|
|
@@ -111,6 +133,7 @@ import {
|
|
|
111
133
|
mimetypeSizeLimit,
|
|
112
134
|
modelLabel,
|
|
113
135
|
pinnedClaudeConfigDir,
|
|
136
|
+
promoteChannelMemory,
|
|
114
137
|
readSkillFile,
|
|
115
138
|
recentRunsForWorkspace,
|
|
116
139
|
refreshAutomationRegistry,
|
|
@@ -122,6 +145,7 @@ import {
|
|
|
122
145
|
resolveChannelId,
|
|
123
146
|
resolveChannelPrompt,
|
|
124
147
|
resolveCwd,
|
|
148
|
+
resolveFeatureFlags,
|
|
125
149
|
resolveLogicalCwd,
|
|
126
150
|
resolveSelectedOutputStyle,
|
|
127
151
|
resolveUserId,
|
|
@@ -130,12 +154,16 @@ import {
|
|
|
130
154
|
runnerForModel,
|
|
131
155
|
saveAgentProfile,
|
|
132
156
|
saveBotIdentity,
|
|
157
|
+
saveChannelMemory,
|
|
133
158
|
saveDashboardSettings,
|
|
159
|
+
saveMemory,
|
|
134
160
|
saveWorkspaceBranding,
|
|
135
161
|
saveWorkspacePrompt,
|
|
162
|
+
searchMemories,
|
|
136
163
|
setAgentPaused,
|
|
137
164
|
setAllowGuestAccess,
|
|
138
165
|
setMembershipHandle,
|
|
166
|
+
setMemoryAutoPaused,
|
|
139
167
|
setOutputStyleDefault,
|
|
140
168
|
setWorkspaceStripeSubscription,
|
|
141
169
|
slackConfigured,
|
|
@@ -145,17 +173,21 @@ import {
|
|
|
145
173
|
syncSlackAvatars,
|
|
146
174
|
transferWorkspaceAdmin,
|
|
147
175
|
tsToIso,
|
|
176
|
+
undoMemoryEvent,
|
|
148
177
|
updateChannelById,
|
|
178
|
+
updateChannelMemory,
|
|
149
179
|
updateChannelsMcpConnections,
|
|
150
180
|
updateMembershipRole,
|
|
181
|
+
updateMemory,
|
|
151
182
|
updateOutputStyle,
|
|
152
183
|
updateWorkspacePlanStatus,
|
|
153
184
|
usageLimitedModels,
|
|
154
185
|
usageLimitedUntil,
|
|
186
|
+
validateMemoryInput,
|
|
155
187
|
warnOnBrokenChannelCwds,
|
|
156
188
|
workspaceSkillsDir,
|
|
157
189
|
writeSkillFile
|
|
158
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-2OGLCT7I.js";
|
|
159
191
|
import {
|
|
160
192
|
firstReplyBody,
|
|
161
193
|
getThread,
|
|
@@ -614,8 +646,8 @@ async function requestHostDb(req) {
|
|
|
614
646
|
}
|
|
615
647
|
var BODYLESS_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
616
648
|
function dbFetch(send2) {
|
|
617
|
-
return async (input2,
|
|
618
|
-
const req = new Request(input2,
|
|
649
|
+
return async (input2, init2) => {
|
|
650
|
+
const req = new Request(input2, init2);
|
|
619
651
|
const url = new URL(req.url);
|
|
620
652
|
const method = req.method.toUpperCase();
|
|
621
653
|
const res = await send2({
|
|
@@ -678,11 +710,17 @@ function createHostDbClient(via = "socket") {
|
|
|
678
710
|
});
|
|
679
711
|
}
|
|
680
712
|
|
|
713
|
+
// src/commands/supabase-via-host.ts
|
|
714
|
+
function useHostDbWithoutCredentials() {
|
|
715
|
+
const viaHost = !hasSupabaseCredentials() && !!loadCredentials();
|
|
716
|
+
if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
|
|
717
|
+
return viaHost;
|
|
718
|
+
}
|
|
719
|
+
|
|
681
720
|
// src/commands/start.ts
|
|
682
721
|
async function runStart() {
|
|
683
722
|
loadEnvFiles();
|
|
684
|
-
const viaHost =
|
|
685
|
-
if (viaHost) setSupabaseClient(createHostDbClient("local-host"));
|
|
723
|
+
const viaHost = useHostDbWithoutCredentials();
|
|
686
724
|
await preflight({ supabaseViaHost: viaHost });
|
|
687
725
|
await startServer();
|
|
688
726
|
}
|
|
@@ -1321,14 +1359,14 @@ function detectRunners(history = { claude: false, codex: false }) {
|
|
|
1321
1359
|
function scanOnboarding() {
|
|
1322
1360
|
const byPath = /* @__PURE__ */ new Map();
|
|
1323
1361
|
const history = { claude: false, codex: false };
|
|
1324
|
-
const add = (rawPath, source, mtime,
|
|
1362
|
+
const add = (rawPath, source, mtime, sessions2, trusted = false) => {
|
|
1325
1363
|
const path = resolve3(rawPath);
|
|
1326
1364
|
if (isExcluded(path)) return;
|
|
1327
1365
|
const existing = byPath.get(path);
|
|
1328
1366
|
if (existing) {
|
|
1329
1367
|
if (!existing.sources.includes(source)) existing.sources.push(source);
|
|
1330
1368
|
existing.lastUsed = Math.max(existing.lastUsed, mtime);
|
|
1331
|
-
existing.sessions +=
|
|
1369
|
+
existing.sessions += sessions2;
|
|
1332
1370
|
existing.trusted ||= trusted;
|
|
1333
1371
|
} else {
|
|
1334
1372
|
byPath.set(path, {
|
|
@@ -1336,7 +1374,7 @@ function scanOnboarding() {
|
|
|
1336
1374
|
name: basename2(path),
|
|
1337
1375
|
sources: [source],
|
|
1338
1376
|
lastUsed: mtime,
|
|
1339
|
-
sessions,
|
|
1377
|
+
sessions: sessions2,
|
|
1340
1378
|
trusted
|
|
1341
1379
|
});
|
|
1342
1380
|
}
|
|
@@ -1738,7 +1776,7 @@ async function runAuthCmd(args) {
|
|
|
1738
1776
|
}
|
|
1739
1777
|
|
|
1740
1778
|
// src/version.ts
|
|
1741
|
-
var CLI_VERSION = true ? "0.1.
|
|
1779
|
+
var CLI_VERSION = true ? "0.1.16" : null;
|
|
1742
1780
|
function isOlderVersion(a, b) {
|
|
1743
1781
|
const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
1744
1782
|
const [pa, pb] = [parse2(a), parse2(b)];
|
|
@@ -1893,9 +1931,9 @@ import { tmpdir } from "os";
|
|
|
1893
1931
|
import { promisify } from "util";
|
|
1894
1932
|
var execAsync = promisify(execFile);
|
|
1895
1933
|
var exec = (cmd, args, opts) => {
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
return
|
|
1934
|
+
const run4 = execAsync(cmd, args, opts);
|
|
1935
|
+
run4.child.stdin?.end();
|
|
1936
|
+
return run4;
|
|
1899
1937
|
};
|
|
1900
1938
|
var BOLD3 = "\x1B[1m";
|
|
1901
1939
|
var DIM3 = "\x1B[2m";
|
|
@@ -2016,7 +2054,7 @@ ${BOLD3}Flyspace Doctor${RESET3}
|
|
|
2016
2054
|
}
|
|
2017
2055
|
|
|
2018
2056
|
// src/commands/uninstall.ts
|
|
2019
|
-
import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2057
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2020
2058
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
2021
2059
|
import { execSync as execSync5 } from "child_process";
|
|
2022
2060
|
import { basename as basename3, join as join9 } from "path";
|
|
@@ -2083,12 +2121,31 @@ async function runUninstall(args = []) {
|
|
|
2083
2121
|
console.log(" - revokes this device and removes every background service");
|
|
2084
2122
|
for (const t of targets) console.log(` - deletes ${t}`);
|
|
2085
2123
|
console.log(" Your channel folders (~/Flyspace) are kept.\n");
|
|
2086
|
-
|
|
2124
|
+
const backupSources = [paths.BASE_CONFIG_DIR, paths.BASE_DATA_DIR].filter(existsSync7);
|
|
2125
|
+
const interactive = !args.includes("--yes") && !args.includes("-y");
|
|
2126
|
+
let backup = !args.includes("--no-backup") && backupSources.length > 0;
|
|
2127
|
+
if (interactive) {
|
|
2128
|
+
if (backup) backup = await confirm3({ message: "Back up local data first?", default: true });
|
|
2087
2129
|
if (!await confirm3({ message: "Continue?", default: false })) {
|
|
2088
2130
|
console.log(" Cancelled.");
|
|
2089
2131
|
return;
|
|
2090
2132
|
}
|
|
2091
2133
|
}
|
|
2134
|
+
let backupFile;
|
|
2135
|
+
if (backup) {
|
|
2136
|
+
const dir = join9(homedir4(), "Flyspace", ".uninstall-backups");
|
|
2137
|
+
mkdirSync7(dir, { recursive: true });
|
|
2138
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2139
|
+
backupFile = join9(dir, `flyspace-${stamp}.tar.gz`);
|
|
2140
|
+
const rel = backupSources.map((s) => `"${s.replace(/^\//, "")}"`).join(" ");
|
|
2141
|
+
try {
|
|
2142
|
+
run2(`tar -czf "${backupFile}" -C / ${rel}`);
|
|
2143
|
+
success2(`Backup saved to ${backupFile}`);
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
warn2(`Backup failed, nothing was removed: ${err.message}`);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2092
2149
|
const creds = loadCredentials();
|
|
2093
2150
|
if (creds) {
|
|
2094
2151
|
try {
|
|
@@ -2115,7 +2172,11 @@ async function runUninstall(args = []) {
|
|
|
2115
2172
|
success2(`Removed PATH entry from ${rc}`);
|
|
2116
2173
|
}
|
|
2117
2174
|
}
|
|
2118
|
-
console.log("\n Flyspace is uninstalled. Open a new terminal to finish
|
|
2175
|
+
console.log("\n Flyspace is uninstalled. Open a new terminal to finish.");
|
|
2176
|
+
if (backupFile) {
|
|
2177
|
+
console.log(` To restore your local data after reinstalling: tar -xzf "${backupFile}" -C /`);
|
|
2178
|
+
}
|
|
2179
|
+
console.log("");
|
|
2119
2180
|
}
|
|
2120
2181
|
|
|
2121
2182
|
// src/commands/profiles-cmd.ts
|
|
@@ -2230,7 +2291,7 @@ function npmEnv() {
|
|
|
2230
2291
|
return home ? { ...process.env, PATH: `${join10(home, "node", "bin")}:${process.env.PATH ?? ""}` } : process.env;
|
|
2231
2292
|
}
|
|
2232
2293
|
async function runUpgrade() {
|
|
2233
|
-
const current = "0.1.
|
|
2294
|
+
const current = "0.1.16";
|
|
2234
2295
|
console.log(`Current version: ${current}`);
|
|
2235
2296
|
console.log("Checking for updates...\n");
|
|
2236
2297
|
try {
|
|
@@ -2550,6 +2611,7 @@ async function handleImport(manager) {
|
|
|
2550
2611
|
async function runAutomationCmd(args) {
|
|
2551
2612
|
const subcommand = args[0];
|
|
2552
2613
|
loadEnvFiles();
|
|
2614
|
+
useHostDbWithoutCredentials();
|
|
2553
2615
|
await refreshChannelRegistry();
|
|
2554
2616
|
await refreshAutomationRegistry();
|
|
2555
2617
|
const manager = new AutomationManager();
|
|
@@ -2870,9 +2932,9 @@ async function runLogs(args) {
|
|
|
2870
2932
|
|
|
2871
2933
|
// src/commands/slack.ts
|
|
2872
2934
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2873
|
-
import { existsSync as
|
|
2935
|
+
import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
|
|
2874
2936
|
import { resolve as resolve6 } from "path";
|
|
2875
|
-
import { tmpdir as
|
|
2937
|
+
import { tmpdir as tmpdir5, homedir as homedir9 } from "os";
|
|
2876
2938
|
import { input, confirm as confirm4, select, checkbox } from "@inquirer/prompts";
|
|
2877
2939
|
import { WebClient as WebClient3 } from "@slack/web-api";
|
|
2878
2940
|
|
|
@@ -3427,6 +3489,13 @@ function profileFromHost(host, known) {
|
|
|
3427
3489
|
const bare = label2.startsWith("api-") ? label2.slice(4) : label2.startsWith("dev-") ? label2.slice(4) : label2;
|
|
3428
3490
|
return known.includes(bare) ? bare : null;
|
|
3429
3491
|
}
|
|
3492
|
+
function isUnknownWorkspaceHost(host, known) {
|
|
3493
|
+
const name = bareHost(host);
|
|
3494
|
+
if (!name?.endsWith(".flyspace.ai")) return false;
|
|
3495
|
+
const label2 = subdomainLabel(host);
|
|
3496
|
+
if (!label2 || label2 === "dev-admin") return false;
|
|
3497
|
+
return profileFromHost(host, known) === null;
|
|
3498
|
+
}
|
|
3430
3499
|
function workspaceHostname(host, profile) {
|
|
3431
3500
|
const name = bareHost(host);
|
|
3432
3501
|
if (!name || !subdomainLabel(host)) return null;
|
|
@@ -3837,7 +3906,9 @@ function getStripeClient() {
|
|
|
3837
3906
|
|
|
3838
3907
|
// src/store/stripe-trial.ts
|
|
3839
3908
|
var PLAN_INFO = {
|
|
3840
|
-
|
|
3909
|
+
// Single-member: a personal Claude subscription can't be shared (see
|
|
3910
|
+
// members-routes.ts, which blocks invites on this plan).
|
|
3911
|
+
local: { name: "Solo", priceUsd: 29 },
|
|
3841
3912
|
cloud: { name: "Cloud Agent", priceUsd: 49 }
|
|
3842
3913
|
};
|
|
3843
3914
|
function priceIdForPlan(plan) {
|
|
@@ -3862,6 +3933,7 @@ async function startWorkspaceTrial(workspace, plan, ownerEmail) {
|
|
|
3862
3933
|
metadata: { workspace_id: workspace.id, plan }
|
|
3863
3934
|
});
|
|
3864
3935
|
await setWorkspaceStripeSubscription(workspace.id, {
|
|
3936
|
+
plan,
|
|
3865
3937
|
stripeCustomerId: customer.id,
|
|
3866
3938
|
stripeSubscriptionId: subscription.id,
|
|
3867
3939
|
planStatus: "trialing"
|
|
@@ -4398,15 +4470,27 @@ async function deliverPasswordResetLink(email, redirectTo) {
|
|
|
4398
4470
|
}
|
|
4399
4471
|
|
|
4400
4472
|
// src/dashboard/server.ts
|
|
4401
|
-
import { existsSync as
|
|
4473
|
+
import { existsSync as existsSync26 } from "fs";
|
|
4402
4474
|
import { createServer } from "net";
|
|
4403
|
-
import { join as
|
|
4404
|
-
import { Hono as
|
|
4475
|
+
import { join as join25 } from "path";
|
|
4476
|
+
import { Hono as Hono35 } from "hono";
|
|
4477
|
+
|
|
4478
|
+
// src/dashboard/sentry.ts
|
|
4479
|
+
import * as Sentry from "@sentry/node";
|
|
4480
|
+
import { HTTPException } from "hono/http-exception";
|
|
4481
|
+
var reportingErrorHandler = (err, c) => {
|
|
4482
|
+
if (err instanceof HTTPException) return err.getResponse();
|
|
4483
|
+
Sentry.captureException(err, { extra: { method: c.req.method, path: c.req.path } });
|
|
4484
|
+
console.error(err);
|
|
4485
|
+
return c.text("Internal Server Error", 500);
|
|
4486
|
+
};
|
|
4487
|
+
|
|
4488
|
+
// src/dashboard/server.ts
|
|
4405
4489
|
import { serve } from "@hono/node-server";
|
|
4406
4490
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4407
4491
|
|
|
4408
4492
|
// src/reconcile.ts
|
|
4409
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4493
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4410
4494
|
async function boundWorkspaces2() {
|
|
4411
4495
|
return (await boundWorkspaces())?.workspaces ?? null;
|
|
4412
4496
|
}
|
|
@@ -4432,7 +4516,7 @@ async function reconcileProfile(profile) {
|
|
|
4432
4516
|
if (!channel.cwd) continue;
|
|
4433
4517
|
const cwd = expandPath(channel.cwd);
|
|
4434
4518
|
if (isCwdUsable(cwd)) continue;
|
|
4435
|
-
|
|
4519
|
+
mkdirSync8(cwd, { recursive: true });
|
|
4436
4520
|
log5.push(`channel "${channel.name}" cwd created (${cwd})`);
|
|
4437
4521
|
}
|
|
4438
4522
|
return log5;
|
|
@@ -4506,45 +4590,18 @@ var RANK = { none: 0, read: 1, write: 2 };
|
|
|
4506
4590
|
function docsAccessAtLeast(have, need) {
|
|
4507
4591
|
return RANK[have] >= RANK[need];
|
|
4508
4592
|
}
|
|
4509
|
-
function
|
|
4510
|
-
return value === "read" || value === "write";
|
|
4511
|
-
}
|
|
4512
|
-
function rowToGrant(row) {
|
|
4513
|
-
return {
|
|
4514
|
-
id: row["id"],
|
|
4515
|
-
channelId: row["channel_id"],
|
|
4516
|
-
membershipId: row["membership_id"],
|
|
4517
|
-
access: row["access"],
|
|
4518
|
-
grantedAt: row["granted_at"]
|
|
4519
|
-
};
|
|
4520
|
-
}
|
|
4521
|
-
async function listChannelDocsAccess(channelId) {
|
|
4522
|
-
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").select("*").eq("channel_id", channelId).order("granted_at", { ascending: true });
|
|
4523
|
-
if (error2) throw new Error(`listChannelDocsAccess(${channelId}): ${error2.message}`);
|
|
4524
|
-
return (data ?? []).map(rowToGrant);
|
|
4593
|
+
function isDocsAccess(value) {
|
|
4594
|
+
return value === "none" || value === "read" || value === "write";
|
|
4525
4595
|
}
|
|
4526
|
-
async function
|
|
4527
|
-
const { data, error: error2 } = await getSupabaseClient().from("
|
|
4528
|
-
if (error2) throw new Error(`
|
|
4529
|
-
|
|
4596
|
+
async function getChannelDocsMemberAccess(channelId) {
|
|
4597
|
+
const { data, error: error2 } = await getSupabaseClient().from("channels").select("docs_member_access").eq("id", channelId).maybeSingle();
|
|
4598
|
+
if (error2) throw new Error(`getChannelDocsMemberAccess(${channelId}): ${error2.message}`);
|
|
4599
|
+
const level = data?.docs_member_access;
|
|
4600
|
+
return isDocsAccess(level) ? level : "none";
|
|
4530
4601
|
}
|
|
4531
|
-
async function
|
|
4532
|
-
const { data, error: error2 } = await getSupabaseClient().from("
|
|
4533
|
-
|
|
4534
|
-
{ onConflict: "channel_id,membership_id" }
|
|
4535
|
-
).select("*").single();
|
|
4536
|
-
if (error2) {
|
|
4537
|
-
throw new Error(
|
|
4538
|
-
`upsertChannelDocsAccess(${params.channelId}, ${params.membershipId}): ${error2.message}`
|
|
4539
|
-
);
|
|
4540
|
-
}
|
|
4541
|
-
return rowToGrant(data);
|
|
4542
|
-
}
|
|
4543
|
-
async function removeChannelDocsAccess(channelId, membershipId) {
|
|
4544
|
-
const { data, error: error2 } = await getSupabaseClient().from("channel_docs_access").delete().eq("channel_id", channelId).eq("membership_id", membershipId).select("id");
|
|
4545
|
-
if (error2) {
|
|
4546
|
-
throw new Error(`removeChannelDocsAccess(${channelId}, ${membershipId}): ${error2.message}`);
|
|
4547
|
-
}
|
|
4602
|
+
async function setChannelDocsMemberAccess(channelId, access) {
|
|
4603
|
+
const { data, error: error2 } = await getSupabaseClient().from("channels").update({ docs_member_access: access }).eq("id", channelId).select("id");
|
|
4604
|
+
if (error2) throw new Error(`setChannelDocsMemberAccess(${channelId}): ${error2.message}`);
|
|
4548
4605
|
return data?.length ? true : false;
|
|
4549
4606
|
}
|
|
4550
4607
|
async function resolveForMembership(membership, channelId) {
|
|
@@ -4553,8 +4610,7 @@ async function resolveForMembership(membership, channelId) {
|
|
|
4553
4610
|
if (members.some((m) => m.membershipId === membership.id && m.role === "channel_admin")) {
|
|
4554
4611
|
return "write";
|
|
4555
4612
|
}
|
|
4556
|
-
|
|
4557
|
-
return grant?.access ?? "none";
|
|
4613
|
+
return getChannelDocsMemberAccess(channelId);
|
|
4558
4614
|
}
|
|
4559
4615
|
async function resolveDocsAccess(ctx, channelId) {
|
|
4560
4616
|
if (ctx.role === "admin") return "write";
|
|
@@ -4576,15 +4632,16 @@ async function filterReadableDocsChannels(ctx, channelIds) {
|
|
|
4576
4632
|
if (ctx.role === "admin") return new Set(channelIds);
|
|
4577
4633
|
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
4578
4634
|
if (!membership) return /* @__PURE__ */ new Set();
|
|
4635
|
+
if (channelIds.length === 0) return /* @__PURE__ */ new Set();
|
|
4579
4636
|
const db = getSupabaseClient();
|
|
4580
|
-
const [
|
|
4581
|
-
db.from("
|
|
4637
|
+
const [open, roles] = await Promise.all([
|
|
4638
|
+
db.from("channels").select("id").in("id", channelIds).neq("docs_member_access", "none"),
|
|
4582
4639
|
db.from("channel_members").select("channel_id, role").eq("membership_id", membership.id)
|
|
4583
4640
|
]);
|
|
4584
|
-
if (
|
|
4641
|
+
if (open.error) throw new Error(`filterReadableDocsChannels: ${open.error.message}`);
|
|
4585
4642
|
if (roles.error) throw new Error(`filterReadableDocsChannels: ${roles.error.message}`);
|
|
4586
4643
|
const allowed = /* @__PURE__ */ new Set();
|
|
4587
|
-
for (const r of
|
|
4644
|
+
for (const r of open.data ?? []) allowed.add(r.id);
|
|
4588
4645
|
for (const r of roles.data ?? []) {
|
|
4589
4646
|
if (r.role === "channel_admin") allowed.add(r.channel_id);
|
|
4590
4647
|
}
|
|
@@ -4603,7 +4660,7 @@ var requireDocsAccess = async (c, next) => {
|
|
|
4603
4660
|
};
|
|
4604
4661
|
|
|
4605
4662
|
// src/dashboard/api.ts
|
|
4606
|
-
import { Hono as
|
|
4663
|
+
import { Hono as Hono32 } from "hono";
|
|
4607
4664
|
|
|
4608
4665
|
// src/dashboard/public-share-routes.ts
|
|
4609
4666
|
import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
@@ -4747,15 +4804,15 @@ async function createDocShare(input2) {
|
|
|
4747
4804
|
if (error2) throw new Error(`createDocShare(${input2.sourcePath}): ${error2.message}`);
|
|
4748
4805
|
return rowToShare(data);
|
|
4749
4806
|
}
|
|
4750
|
-
async function updateDocShareSnapshot(id,
|
|
4807
|
+
async function updateDocShareSnapshot(id, snapshot2, features) {
|
|
4751
4808
|
const { data, error: error2 } = await getSupabaseClient().from("doc_shares").update({
|
|
4752
|
-
title:
|
|
4753
|
-
content:
|
|
4754
|
-
asset_keys:
|
|
4809
|
+
title: snapshot2.title,
|
|
4810
|
+
content: snapshot2.content,
|
|
4811
|
+
asset_keys: snapshot2.assetKeys,
|
|
4755
4812
|
// Re-detected with the snapshot: a re-publish can carry a rewritten
|
|
4756
4813
|
// article, and one written in another language than last time is exactly
|
|
4757
4814
|
// the case a stale value would get wrong.
|
|
4758
|
-
source_lang:
|
|
4815
|
+
source_lang: snapshot2.sourceLang,
|
|
4759
4816
|
audio_enabled: features.audioEnabled,
|
|
4760
4817
|
ask_enabled: features.askEnabled,
|
|
4761
4818
|
social_enabled: features.socialEnabled,
|
|
@@ -5683,9 +5740,9 @@ async function synthesizeWithRetry(script, voice) {
|
|
|
5683
5740
|
}
|
|
5684
5741
|
var renderQueue = Promise.resolve();
|
|
5685
5742
|
function queued(job) {
|
|
5686
|
-
const
|
|
5687
|
-
renderQueue =
|
|
5688
|
-
return
|
|
5743
|
+
const run4 = renderQueue.then(job, job);
|
|
5744
|
+
renderQueue = run4.catch(() => void 0);
|
|
5745
|
+
return run4;
|
|
5689
5746
|
}
|
|
5690
5747
|
function renderArticleAudio(title, content, lang) {
|
|
5691
5748
|
return queued(() => renderArticleAudioNow(title, content, lang));
|
|
@@ -5759,12 +5816,12 @@ function queueShareAudio(share) {
|
|
|
5759
5816
|
}
|
|
5760
5817
|
const current = inFlight2.get(share.id);
|
|
5761
5818
|
if (current?.hash === hash) return current.done;
|
|
5762
|
-
const
|
|
5819
|
+
const run4 = () => runShareAudioJob(share, hash).catch((err) => {
|
|
5763
5820
|
console.error(`share audio job crashed (${share.id}):`, err);
|
|
5764
5821
|
}).finally(() => {
|
|
5765
5822
|
if (inFlight2.get(share.id)?.hash === hash) inFlight2.delete(share.id);
|
|
5766
5823
|
});
|
|
5767
|
-
const done = current ? current.done.then(
|
|
5824
|
+
const done = current ? current.done.then(run4) : run4();
|
|
5768
5825
|
inFlight2.set(share.id, { hash, done });
|
|
5769
5826
|
return done;
|
|
5770
5827
|
}
|
|
@@ -6689,8 +6746,8 @@ function routeCookieWrites(all, label2, writes) {
|
|
|
6689
6746
|
|
|
6690
6747
|
// src/dashboard/supabase-server-client.ts
|
|
6691
6748
|
var REQUEST_TIMEOUT_MS2 = 5e3;
|
|
6692
|
-
function fetchWithTimeout(input2,
|
|
6693
|
-
return fetch(input2, { ...
|
|
6749
|
+
function fetchWithTimeout(input2, init2) {
|
|
6750
|
+
return fetch(input2, { ...init2, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2) });
|
|
6694
6751
|
}
|
|
6695
6752
|
function createRequestSupabaseClient(c) {
|
|
6696
6753
|
const url = process.env["SUPABASE_URL"];
|
|
@@ -6732,7 +6789,7 @@ function createRequestSupabaseClient(c) {
|
|
|
6732
6789
|
}
|
|
6733
6790
|
|
|
6734
6791
|
// src/dashboard/auth-context.ts
|
|
6735
|
-
import { HTTPException } from "hono/http-exception";
|
|
6792
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
6736
6793
|
import { isAuthApiError, isAuthRetryableFetchError } from "@supabase/supabase-js";
|
|
6737
6794
|
function onboardingPending(ctx) {
|
|
6738
6795
|
const pending3 = [];
|
|
@@ -6789,10 +6846,10 @@ async function resolveAuthContextUncached(c) {
|
|
|
6789
6846
|
} = await getUserWithRetry(supabase));
|
|
6790
6847
|
} catch (err) {
|
|
6791
6848
|
if (isAuthRetryableFetchError(err)) {
|
|
6792
|
-
throw new
|
|
6849
|
+
throw new HTTPException2(503, { message: "auth service unreachable" });
|
|
6793
6850
|
}
|
|
6794
6851
|
if (isAuthApiError(err) && err.status === 429) {
|
|
6795
|
-
throw new
|
|
6852
|
+
throw new HTTPException2(503, { message: "auth service rate limited" });
|
|
6796
6853
|
}
|
|
6797
6854
|
return null;
|
|
6798
6855
|
}
|
|
@@ -7201,91 +7258,16 @@ async function offlineAgentIdentities() {
|
|
|
7201
7258
|
}
|
|
7202
7259
|
|
|
7203
7260
|
// src/dashboard/host-api.ts
|
|
7204
|
-
import { existsSync as
|
|
7205
|
-
import { join as
|
|
7261
|
+
import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync8 } from "fs";
|
|
7262
|
+
import { join as join23 } from "path";
|
|
7206
7263
|
import sharp3 from "sharp";
|
|
7207
7264
|
|
|
7208
7265
|
// src/dashboard/channels-routes.ts
|
|
7209
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
7266
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
|
|
7210
7267
|
import { Hono as Hono9 } from "hono";
|
|
7211
7268
|
|
|
7212
7269
|
// src/dashboard/feature-flags-routes.ts
|
|
7213
7270
|
import { Hono as Hono8 } from "hono";
|
|
7214
|
-
|
|
7215
|
-
// src/feature-flags.ts
|
|
7216
|
-
var featureFlags = {
|
|
7217
|
-
"ai-cost": {
|
|
7218
|
-
title: "AI cost",
|
|
7219
|
-
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.",
|
|
7220
|
-
stage: "beta"
|
|
7221
|
-
},
|
|
7222
|
-
"read-aloud": {
|
|
7223
|
-
title: "Read aloud",
|
|
7224
|
-
description: "Listen to messages, threads and docs as audio. Still rough in places, and each playback uses text-to-speech.",
|
|
7225
|
-
stage: "beta"
|
|
7226
|
-
},
|
|
7227
|
-
excalidraw: {
|
|
7228
|
-
title: "Excalidraw",
|
|
7229
|
-
description: "Create and edit whiteboard drawings (.excalidraw files) in the Docs pane. Existing drawings stay viewable with this off.",
|
|
7230
|
-
stage: "beta"
|
|
7231
|
-
},
|
|
7232
|
-
sandbox: {
|
|
7233
|
-
title: "Sandbox runtime",
|
|
7234
|
-
description: "Run a channel agent in an isolated sandbox without Docker. Channels already on Sandbox keep working with this off.",
|
|
7235
|
-
stage: "beta"
|
|
7236
|
-
},
|
|
7237
|
-
dictation: {
|
|
7238
|
-
title: "Dictation",
|
|
7239
|
-
description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
|
|
7240
|
-
stage: "dev"
|
|
7241
|
-
},
|
|
7242
|
-
"agent-memory": {
|
|
7243
|
-
title: "Agent memory",
|
|
7244
|
-
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.",
|
|
7245
|
-
stage: "beta"
|
|
7246
|
-
}
|
|
7247
|
-
};
|
|
7248
|
-
var featureFlagIds = Object.keys(featureFlags);
|
|
7249
|
-
function isFeatureFlagId(id) {
|
|
7250
|
-
return Object.hasOwn(featureFlags, id);
|
|
7251
|
-
}
|
|
7252
|
-
function isFeatureVisible(stage, devHost) {
|
|
7253
|
-
return stage === "beta" || stage === "dev" && devHost;
|
|
7254
|
-
}
|
|
7255
|
-
function resolveFeatureFlags(enabledIds, devHost) {
|
|
7256
|
-
const enabled = new Set(enabledIds);
|
|
7257
|
-
const out = {};
|
|
7258
|
-
for (const id of featureFlagIds) {
|
|
7259
|
-
const stage = featureFlags[id].stage;
|
|
7260
|
-
out[id] = stage === "ga" || isFeatureVisible(stage, devHost) && enabled.has(id);
|
|
7261
|
-
}
|
|
7262
|
-
return out;
|
|
7263
|
-
}
|
|
7264
|
-
|
|
7265
|
-
// src/store/feature-flag-store.ts
|
|
7266
|
-
async function listEnabledFeatureFlags(workspaceId) {
|
|
7267
|
-
const { data, error: error2 } = await getSupabaseClient().from("feature_flags").select("flag_id").eq("workspace_id", workspaceId);
|
|
7268
|
-
if (error2) throw new Error(`listEnabledFeatureFlags(${workspaceId}): ${error2.message}`);
|
|
7269
|
-
return (data ?? []).map((r) => r.flag_id);
|
|
7270
|
-
}
|
|
7271
|
-
async function listEnabledFeatureFlagsBySlug(slug) {
|
|
7272
|
-
const { data, error: error2 } = await getSupabaseClient().from("workspaces").select("id").eq("slug", slug).maybeSingle();
|
|
7273
|
-
if (error2) throw new Error(`listEnabledFeatureFlagsBySlug(${slug}): ${error2.message}`);
|
|
7274
|
-
return data ? listEnabledFeatureFlags(data.id) : [];
|
|
7275
|
-
}
|
|
7276
|
-
async function enableFeatureFlag(workspaceId, flagId, userId) {
|
|
7277
|
-
const { error: error2 } = await getSupabaseClient().from("feature_flags").upsert(
|
|
7278
|
-
{ workspace_id: workspaceId, flag_id: flagId, enabled_by: userId },
|
|
7279
|
-
{ onConflict: "workspace_id,flag_id" }
|
|
7280
|
-
);
|
|
7281
|
-
if (error2) throw new Error(`enableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
7282
|
-
}
|
|
7283
|
-
async function disableFeatureFlag(workspaceId, flagId) {
|
|
7284
|
-
const { error: error2 } = await getSupabaseClient().from("feature_flags").delete().eq("workspace_id", workspaceId).eq("flag_id", flagId);
|
|
7285
|
-
if (error2) throw new Error(`disableFeatureFlag(${workspaceId}, ${flagId}): ${error2.message}`);
|
|
7286
|
-
}
|
|
7287
|
-
|
|
7288
|
-
// src/dashboard/feature-flags-routes.ts
|
|
7289
7271
|
async function isFeatureEnabled(c, flagId) {
|
|
7290
7272
|
const auth = c.get("auth");
|
|
7291
7273
|
const enabled = await listEnabledFeatureFlags(auth.workspaceId);
|
|
@@ -7354,7 +7336,7 @@ function coerceMemberEmails(raw) {
|
|
|
7354
7336
|
}
|
|
7355
7337
|
function ensureCwdDir(cwd) {
|
|
7356
7338
|
const expanded = isLogicalCwd(cwd) ? resolveLogicalCwd(cwd) : expandPath(cwd);
|
|
7357
|
-
if (!existsSync12(expanded))
|
|
7339
|
+
if (!existsSync12(expanded)) mkdirSync9(expanded, { recursive: true });
|
|
7358
7340
|
}
|
|
7359
7341
|
function toResponse(channel) {
|
|
7360
7342
|
return {
|
|
@@ -7392,7 +7374,6 @@ function createChannelWriteRouter() {
|
|
|
7392
7374
|
const externalChannelId = typeof body["externalChannelId"] === "string" && body["externalChannelId"].trim() ? body["externalChannelId"].trim() : null;
|
|
7393
7375
|
const categoryId = typeof body["categoryId"] === "string" && body["categoryId"].trim() ? body["categoryId"].trim() : null;
|
|
7394
7376
|
try {
|
|
7395
|
-
ensureCwdDir(cwd);
|
|
7396
7377
|
const channel = await createChannel({
|
|
7397
7378
|
name,
|
|
7398
7379
|
cwd,
|
|
@@ -7405,9 +7386,10 @@ function createChannelWriteRouter() {
|
|
|
7405
7386
|
externalChannelId,
|
|
7406
7387
|
categoryId
|
|
7407
7388
|
});
|
|
7389
|
+
ensureCwdDir(cwd);
|
|
7408
7390
|
return c.json(toResponse(channel), 201);
|
|
7409
7391
|
} catch (err) {
|
|
7410
|
-
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
7392
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
|
|
7411
7393
|
}
|
|
7412
7394
|
});
|
|
7413
7395
|
app.put("/:id", async (c) => {
|
|
@@ -7421,7 +7403,6 @@ function createChannelWriteRouter() {
|
|
|
7421
7403
|
if (coerceRuntime(body["runtime"]) === "sandbox" && existing.runtime !== "sandbox" && !await isFeatureEnabled(c, "sandbox"))
|
|
7422
7404
|
return c.json({ error: "feature not enabled" }, 403);
|
|
7423
7405
|
try {
|
|
7424
|
-
ensureCwdDir(cwd);
|
|
7425
7406
|
const channel = await updateChannelById(id, {
|
|
7426
7407
|
name: typeof body["name"] === "string" ? body["name"] : void 0,
|
|
7427
7408
|
cwd,
|
|
@@ -7434,10 +7415,11 @@ function createChannelWriteRouter() {
|
|
|
7434
7415
|
runtime: coerceRuntime(body["runtime"]),
|
|
7435
7416
|
mounts: coerceMounts(body["mounts"])
|
|
7436
7417
|
});
|
|
7418
|
+
ensureCwdDir(cwd);
|
|
7437
7419
|
dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
|
|
7438
7420
|
return c.json(toResponse(channel), 200);
|
|
7439
7421
|
} catch (err) {
|
|
7440
|
-
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
7422
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
|
|
7441
7423
|
}
|
|
7442
7424
|
});
|
|
7443
7425
|
app.patch("/reorder", async (c) => {
|
|
@@ -7489,12 +7471,12 @@ function createChannelWriteRouter() {
|
|
|
7489
7471
|
patch.archivedAt = body["archived"] ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
7490
7472
|
}
|
|
7491
7473
|
try {
|
|
7492
|
-
if (patch.cwd) ensureCwdDir(patch.cwd);
|
|
7493
7474
|
const channel = await updateChannelById(id, patch);
|
|
7475
|
+
if (patch.cwd) ensureCwdDir(patch.cwd);
|
|
7494
7476
|
dropSessionsIfRuntimeChanged(id, existing.runtime, channel.runtime);
|
|
7495
7477
|
return c.json(toResponse(channel), 200);
|
|
7496
7478
|
} catch (err) {
|
|
7497
|
-
return c.json({ error: err.message }, err instanceof DefaultChannelError ? 400 : 500);
|
|
7479
|
+
return c.json({ error: err.message }, err instanceof DefaultChannelError || err instanceof ChannelCwdError ? 400 : 500);
|
|
7498
7480
|
}
|
|
7499
7481
|
});
|
|
7500
7482
|
return app;
|
|
@@ -7503,20 +7485,22 @@ function createChannelWriteRouter() {
|
|
|
7503
7485
|
// src/dashboard/docs-routes.ts
|
|
7504
7486
|
import {
|
|
7505
7487
|
closeSync,
|
|
7488
|
+
createReadStream,
|
|
7506
7489
|
existsSync as existsSync16,
|
|
7507
7490
|
lstatSync as lstatSync2,
|
|
7508
|
-
mkdirSync as
|
|
7491
|
+
mkdirSync as mkdirSync10,
|
|
7509
7492
|
openSync,
|
|
7510
7493
|
readSync,
|
|
7511
7494
|
readdirSync as readdirSync4,
|
|
7512
|
-
readFileSync as
|
|
7495
|
+
readFileSync as readFileSync14,
|
|
7513
7496
|
realpathSync as realpathSync3,
|
|
7514
7497
|
renameSync,
|
|
7515
|
-
rmSync as
|
|
7498
|
+
rmSync as rmSync8,
|
|
7516
7499
|
statSync as statSync5,
|
|
7517
7500
|
writeFileSync as writeFileSync9
|
|
7518
7501
|
} from "fs";
|
|
7519
|
-
import {
|
|
7502
|
+
import { Readable } from "stream";
|
|
7503
|
+
import { basename as basename6, dirname as dirname5, extname, join as join20, relative, resolve as resolve4, sep } from "path";
|
|
7520
7504
|
import { Hono as Hono10 } from "hono";
|
|
7521
7505
|
import { streamSSE } from "hono/streaming";
|
|
7522
7506
|
|
|
@@ -7800,7 +7784,7 @@ async function readOrQueueDocAudio(input2) {
|
|
|
7800
7784
|
}
|
|
7801
7785
|
|
|
7802
7786
|
// src/dashboard/artifact-export.ts
|
|
7803
|
-
import { readFileSync as
|
|
7787
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
7804
7788
|
|
|
7805
7789
|
// src/dashboard/chat-agent.ts
|
|
7806
7790
|
import { homedir as homedir7 } from "os";
|
|
@@ -8584,234 +8568,128 @@ function readAgentPersona(configDir2, cwd, name) {
|
|
|
8584
8568
|
}
|
|
8585
8569
|
}
|
|
8586
8570
|
|
|
8587
|
-
// src/
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
var
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
function
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8571
|
+
// src/dashboard/memory-extraction-runner.ts
|
|
8572
|
+
import { spawn as spawn4 } from "child_process";
|
|
8573
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7 } from "fs";
|
|
8574
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
8575
|
+
import { join as join18 } from "path";
|
|
8576
|
+
var CLAUDE_MODEL = "sonnet";
|
|
8577
|
+
var CODEX_MODEL = "gpt-6-astra";
|
|
8578
|
+
var CODEX_EFFORT = "low";
|
|
8579
|
+
var TIMEOUT_MS = 15e4;
|
|
8580
|
+
function buildInvocation(runner, req, dir, auth, baseEnv = process.env) {
|
|
8581
|
+
const env = { ...baseEnv, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
|
|
8582
|
+
if (runner === "codex") {
|
|
8583
|
+
const outputFile = join18(dir, "last-message.txt");
|
|
8584
|
+
return {
|
|
8585
|
+
command: "codex",
|
|
8586
|
+
args: [
|
|
8587
|
+
"exec",
|
|
8588
|
+
"--ephemeral",
|
|
8589
|
+
// No MCP servers, profiles or hooks from the user's config.toml; auth
|
|
8590
|
+
// still comes from CODEX_HOME.
|
|
8591
|
+
"--ignore-user-config",
|
|
8592
|
+
"--skip-git-repo-check",
|
|
8593
|
+
"-s",
|
|
8594
|
+
"read-only",
|
|
8595
|
+
"-m",
|
|
8596
|
+
CODEX_MODEL,
|
|
8597
|
+
"-c",
|
|
8598
|
+
`model_reasoning_effort=${CODEX_EFFORT}`,
|
|
8599
|
+
"--output-last-message",
|
|
8600
|
+
outputFile,
|
|
8601
|
+
// Codex has no separate system prompt flag; `-` reads the prompt from stdin.
|
|
8602
|
+
"-"
|
|
8603
|
+
],
|
|
8604
|
+
stdin: `${req.system}
|
|
8605
|
+
|
|
8606
|
+
---
|
|
8607
|
+
|
|
8608
|
+
${req.user}`,
|
|
8609
|
+
env,
|
|
8610
|
+
outputFile
|
|
8611
|
+
};
|
|
8607
8612
|
}
|
|
8608
|
-
|
|
8609
|
-
|
|
8613
|
+
const pinned = pinnedClaudeConfigDir(auth.claudeConfigDir);
|
|
8614
|
+
if (pinned) env["CLAUDE_CONFIG_DIR"] = pinned;
|
|
8615
|
+
else delete env["CLAUDE_CONFIG_DIR"];
|
|
8616
|
+
if (auth.claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = auth.claudeCodeOauthToken;
|
|
8617
|
+
else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
8610
8618
|
return {
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8619
|
+
command: "claude",
|
|
8620
|
+
args: [
|
|
8621
|
+
"-p",
|
|
8622
|
+
"--model",
|
|
8623
|
+
CLAUDE_MODEL,
|
|
8624
|
+
"--no-session-persistence",
|
|
8625
|
+
"--tools",
|
|
8626
|
+
"",
|
|
8627
|
+
"--strict-mcp-config",
|
|
8628
|
+
"--setting-sources",
|
|
8629
|
+
"",
|
|
8630
|
+
"--system-prompt",
|
|
8631
|
+
req.system
|
|
8632
|
+
],
|
|
8633
|
+
stdin: req.user,
|
|
8634
|
+
env
|
|
8621
8635
|
};
|
|
8622
8636
|
}
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
const { data, error: error2 } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
|
|
8630
|
-
if (error2) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error2.message}`);
|
|
8631
|
-
return data ? rowToMemory(data) : null;
|
|
8632
|
-
}
|
|
8633
|
-
async function saveChannelMemory(input2, actor) {
|
|
8634
|
-
validateMemoryInput(input2);
|
|
8635
|
-
const supabase = getSupabaseClient();
|
|
8636
|
-
const existing = await getChannelMemory(input2.channelId, input2.name);
|
|
8637
|
-
const fields = {
|
|
8638
|
-
description: input2.description.trim(),
|
|
8639
|
-
type: input2.type,
|
|
8640
|
-
body: input2.body.trim(),
|
|
8641
|
-
source_thread_id: input2.sourceThreadId ?? existing?.sourceThreadId ?? null,
|
|
8642
|
-
created_by: input2.createdBy
|
|
8643
|
-
};
|
|
8644
|
-
if (existing && sameContent(existing, fields)) {
|
|
8645
|
-
return { memory: existing, created: false, changed: false };
|
|
8646
|
-
}
|
|
8647
|
-
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();
|
|
8648
|
-
if (error2)
|
|
8649
|
-
throw new Error(`saveChannelMemory(${input2.channelId}, ${input2.name}): ${error2.message}`);
|
|
8650
|
-
const memory = rowToMemory(data);
|
|
8651
|
-
await logMemoryEvent({
|
|
8652
|
-
memory,
|
|
8653
|
-
action: existing ? "update" : "create",
|
|
8654
|
-
before: existing ? snapshot(existing) : null,
|
|
8655
|
-
after: snapshot(memory),
|
|
8656
|
-
actor,
|
|
8657
|
-
threadId: input2.sourceThreadId ?? null
|
|
8658
|
-
});
|
|
8659
|
-
return { memory, created: !existing, changed: true };
|
|
8660
|
-
}
|
|
8661
|
-
async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
8662
|
-
const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
|
|
8663
|
-
if (!existing) return null;
|
|
8664
|
-
const { memory } = await saveChannelMemory(
|
|
8665
|
-
{
|
|
8666
|
-
channelId,
|
|
8667
|
-
name: existing.name,
|
|
8668
|
-
description: patch.description ?? existing.description,
|
|
8669
|
-
type: patch.type ?? existing.type,
|
|
8670
|
-
body: patch.body ?? existing.body,
|
|
8671
|
-
createdBy: existing.createdBy
|
|
8672
|
-
},
|
|
8673
|
-
actor
|
|
8674
|
-
);
|
|
8675
|
-
return memory;
|
|
8676
|
-
}
|
|
8677
|
-
async function searchChannelMemories(channelId, query) {
|
|
8678
|
-
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
8679
|
-
if (!words.length) return [];
|
|
8680
|
-
let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
|
|
8681
|
-
for (const word of words) {
|
|
8682
|
-
request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
8683
|
-
}
|
|
8684
|
-
const { data, error: error2 } = await request.order("updated_at", { ascending: false }).limit(20);
|
|
8685
|
-
if (error2) throw new Error(`searchChannelMemories(${channelId}): ${error2.message}`);
|
|
8686
|
-
return (data ?? []).map(rowToMemory);
|
|
8687
|
-
}
|
|
8688
|
-
async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
|
|
8689
|
-
const { data, error: error2 } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
|
|
8690
|
-
if (error2) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error2.message}`);
|
|
8691
|
-
const deleted = (data ?? []).map(rowToMemory);
|
|
8692
|
-
for (const memory of deleted) {
|
|
8693
|
-
await logMemoryEvent({
|
|
8694
|
-
memory,
|
|
8695
|
-
action: "delete",
|
|
8696
|
-
before: snapshot(memory),
|
|
8697
|
-
after: null,
|
|
8698
|
-
actor,
|
|
8699
|
-
threadId: threadId ?? null
|
|
8637
|
+
function run3(inv, cwd) {
|
|
8638
|
+
return new Promise((resolve7) => {
|
|
8639
|
+
const child = spawn4(inv.command, inv.args, {
|
|
8640
|
+
cwd,
|
|
8641
|
+
env: inv.env,
|
|
8642
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
8700
8643
|
});
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
}
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8644
|
+
let stdout = "";
|
|
8645
|
+
let stderr = "";
|
|
8646
|
+
const timer2 = setTimeout(() => child.kill("SIGTERM"), TIMEOUT_MS);
|
|
8647
|
+
child.stdout.on("data", (chunk3) => stdout += chunk3.toString());
|
|
8648
|
+
child.stderr.on("data", (chunk3) => stderr += chunk3.toString());
|
|
8649
|
+
child.on("error", (err) => {
|
|
8650
|
+
clearTimeout(timer2);
|
|
8651
|
+
console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
|
|
8652
|
+
resolve7({ ok: false });
|
|
8653
|
+
});
|
|
8654
|
+
child.on("close", (code) => {
|
|
8655
|
+
clearTimeout(timer2);
|
|
8656
|
+
if (code !== 0) {
|
|
8657
|
+
const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
|
|
8658
|
+
console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
|
|
8659
|
+
resolve7({ ok: false });
|
|
8660
|
+
return;
|
|
8661
|
+
}
|
|
8662
|
+
let raw = stdout;
|
|
8663
|
+
if (inv.outputFile) {
|
|
8664
|
+
try {
|
|
8665
|
+
raw = readFileSync12(inv.outputFile, "utf-8");
|
|
8666
|
+
} catch {
|
|
8667
|
+
raw = "";
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
resolve7({ ok: true, raw });
|
|
8671
|
+
});
|
|
8672
|
+
child.stdin.end(inv.stdin);
|
|
8726
8673
|
});
|
|
8727
|
-
if (error2) console.error(`[memory] event log failed for ${e.memory.name}:`, error2.message);
|
|
8728
8674
|
}
|
|
8729
|
-
function
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
undoneAt: row["undone_at"] ?? null
|
|
8742
|
-
};
|
|
8743
|
-
}
|
|
8744
|
-
async function listThreadMemoryEvents(channelId, threadId) {
|
|
8745
|
-
const { data, error: error2 } = await getSupabaseClient().from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
|
|
8746
|
-
if (error2) throw new Error(`listThreadMemoryEvents(${threadId}): ${error2.message}`);
|
|
8747
|
-
return (data ?? []).map(rowToEvent);
|
|
8748
|
-
}
|
|
8749
|
-
async function undoMemoryEvent(channelId, eventId) {
|
|
8750
|
-
const supabase = getSupabaseClient();
|
|
8751
|
-
const { data, error: error2 } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
|
|
8752
|
-
if (error2) throw new Error(`undoMemoryEvent(${eventId}): ${error2.message}`);
|
|
8753
|
-
if (!data) return "not-found";
|
|
8754
|
-
const event = rowToEvent(data);
|
|
8755
|
-
if (event.undoneAt) return "already-undone";
|
|
8756
|
-
const current = await getChannelMemory(channelId, event.name);
|
|
8757
|
-
const unchangedSince = current && event.after ? sameContent(current, event.after) : !current && !event.after;
|
|
8758
|
-
if (!unchangedSince) return "conflict";
|
|
8759
|
-
const claimed = await supabase.from("memory_events").update({ undone_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", eventId).is("undone_at", null).select("id");
|
|
8760
|
-
if (claimed.error) throw new Error(`undoMemoryEvent(${eventId}): ${claimed.error.message}`);
|
|
8761
|
-
if (!(claimed.data ?? []).length) return "already-undone";
|
|
8762
|
-
const before = event.before;
|
|
8763
|
-
const restore = before ? {
|
|
8764
|
-
description: before.description,
|
|
8765
|
-
type: before.type,
|
|
8766
|
-
body: before.body,
|
|
8767
|
-
source_thread_id: before.sourceThreadId,
|
|
8768
|
-
created_by: before.createdBy
|
|
8769
|
-
} : null;
|
|
8770
|
-
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 });
|
|
8771
|
-
if (writeError) {
|
|
8772
|
-
await supabase.from("memory_events").update({ undone_at: null }).eq("id", eventId);
|
|
8773
|
-
throw new Error(`undoMemoryEvent(${eventId}): ${writeError.message}`);
|
|
8774
|
-
}
|
|
8775
|
-
return "undone";
|
|
8776
|
-
}
|
|
8777
|
-
function isUuid(value) {
|
|
8778
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
8779
|
-
}
|
|
8780
|
-
function formatMemoryPrompt(channelId, memories) {
|
|
8781
|
-
const lines = [];
|
|
8782
|
-
let used = 0;
|
|
8783
|
-
let omitted = 0;
|
|
8784
|
-
for (const m of memories) {
|
|
8785
|
-
const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
|
|
8786
|
-
if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
|
|
8787
|
-
omitted++;
|
|
8788
|
-
continue;
|
|
8789
|
-
}
|
|
8790
|
-
lines.push(line);
|
|
8791
|
-
used += line.length + 1;
|
|
8675
|
+
async function completeMemoryExtraction(req) {
|
|
8676
|
+
const runner = runnerForModel(loadDashboardSettings().agent);
|
|
8677
|
+
const config = resolveChannelConfig(req.channelId);
|
|
8678
|
+
const dir = mkdtempSync2(join18(tmpdir3(), "flyspace-memory-"));
|
|
8679
|
+
try {
|
|
8680
|
+
const inv = buildInvocation(runner, req, dir, {
|
|
8681
|
+
...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
|
|
8682
|
+
...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
|
|
8683
|
+
});
|
|
8684
|
+
return await run3(inv, dir);
|
|
8685
|
+
} finally {
|
|
8686
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
8792
8687
|
}
|
|
8793
|
-
const index = lines.length ? lines.join("\n") + (omitted ? `
|
|
8794
|
-
- (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
|
|
8795
|
-
return [
|
|
8796
|
-
`## Channel memory`,
|
|
8797
|
-
``,
|
|
8798
|
-
`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.`,
|
|
8799
|
-
``,
|
|
8800
|
-
`- Read a memory: \`flyspace memory get --channel ${channelId} <name>\``,
|
|
8801
|
-
`- Search: \`flyspace memory search --channel ${channelId} <words>\``,
|
|
8802
|
-
`- Save or overwrite: \`flyspace memory save --channel ${channelId} --name <kebab-slug> --type <${MEMORY_TYPES.join("|")}> --description "<one line>" --body "<text>" [--thread <thread-id>]\``,
|
|
8803
|
-
`- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
|
|
8804
|
-
``,
|
|
8805
|
-
`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.`,
|
|
8806
|
-
``,
|
|
8807
|
-
`Index:`,
|
|
8808
|
-
index
|
|
8809
|
-
].join("\n");
|
|
8810
8688
|
}
|
|
8811
8689
|
|
|
8812
8690
|
// src/dashboard/rollover-summary.ts
|
|
8813
|
-
import { spawn as
|
|
8814
|
-
import { tmpdir as
|
|
8691
|
+
import { spawn as spawn5 } from "child_process";
|
|
8692
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
8815
8693
|
var HAIKU_MODEL = "haiku";
|
|
8816
8694
|
var CJK_PATTERN = /[ -〿-ヿ㐀-䶿一-鿿豈--]/;
|
|
8817
8695
|
function estimateTokens(text) {
|
|
@@ -8870,11 +8748,11 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
|
|
|
8870
8748
|
if (claudeConfigDir) env["CLAUDE_CONFIG_DIR"] = claudeConfigDir;
|
|
8871
8749
|
if (claudeCodeOauthToken) env["CLAUDE_CODE_OAUTH_TOKEN"] = claudeCodeOauthToken;
|
|
8872
8750
|
else delete env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
8873
|
-
const child =
|
|
8751
|
+
const child = spawn5("claude", args, {
|
|
8874
8752
|
// A stateless rewrite touches no files — tmpdir keeps it that way and
|
|
8875
8753
|
// avoids picking up an unrelated project's own CLAUDE.md/AGENTS.md,
|
|
8876
8754
|
// unlike the channel's own cwd the main turn runs in.
|
|
8877
|
-
cwd:
|
|
8755
|
+
cwd: tmpdir4(),
|
|
8878
8756
|
env,
|
|
8879
8757
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8880
8758
|
});
|
|
@@ -9077,7 +8955,7 @@ ${recent.map((m) => `${m.author}: ${m.body}`).join("\n")}`;
|
|
|
9077
8955
|
|
|
9078
8956
|
// src/dashboard/attachment-downloader.ts
|
|
9079
8957
|
import { writeFile } from "fs/promises";
|
|
9080
|
-
import { join as
|
|
8958
|
+
import { join as join19 } from "path";
|
|
9081
8959
|
import { GetObjectCommand as GetObjectCommand4 } from "@aws-sdk/client-s3";
|
|
9082
8960
|
var debug = process.env["DEBUG"] === "true";
|
|
9083
8961
|
var ATTACHMENT_LINK = /\]\(\/api\/attachments\/([^)?\s]+)(?:\?[^)\s]*)?\)/g;
|
|
@@ -9124,7 +9002,7 @@ async function downloadOne(key) {
|
|
|
9124
9002
|
if (bytes.length > sizeLimit) {
|
|
9125
9003
|
return { kind: "skip", skipped: { name, mimetype, reason: "too_large" } };
|
|
9126
9004
|
}
|
|
9127
|
-
const localPath =
|
|
9005
|
+
const localPath = join19(paths.UPLOADS_DIR, lastSegment);
|
|
9128
9006
|
await writeFile(localPath, bytes);
|
|
9129
9007
|
if (debug)
|
|
9130
9008
|
console.log(`[attachment-downloader] saved ${key} -> ${localPath} (${bytes.length} bytes)`);
|
|
@@ -9296,21 +9174,21 @@ var ChatAgent = class {
|
|
|
9296
9174
|
* kill.
|
|
9297
9175
|
*/
|
|
9298
9176
|
async maybeSupersede(msg) {
|
|
9299
|
-
const
|
|
9300
|
-
if (!
|
|
9177
|
+
const run4 = this.activeRuns.get(msg.threadId);
|
|
9178
|
+
if (!run4 || run4.msg.author !== msg.author || run4.msg.replyId === msg.replyId) return;
|
|
9301
9179
|
try {
|
|
9302
|
-
if (!await this.shouldInterrupt(
|
|
9180
|
+
if (!await this.shouldInterrupt(run4.msg.body, msg.body)) return;
|
|
9303
9181
|
} catch (err) {
|
|
9304
9182
|
console.error("[chat-agent] interrupt classifier failed:", err.message);
|
|
9305
9183
|
return;
|
|
9306
9184
|
}
|
|
9307
|
-
if (this.activeRuns.get(msg.threadId) !==
|
|
9185
|
+
if (this.activeRuns.get(msg.threadId) !== run4) return;
|
|
9308
9186
|
console.log(
|
|
9309
9187
|
`[chat-agent] follow-up supersedes in-flight run \u2014 aborting. thread=${msg.threadId}`
|
|
9310
9188
|
);
|
|
9311
|
-
|
|
9189
|
+
run4.superseded = true;
|
|
9312
9190
|
this.supersedingReplies.add(msg.replyId);
|
|
9313
|
-
|
|
9191
|
+
run4.abort.abort();
|
|
9314
9192
|
}
|
|
9315
9193
|
/**
|
|
9316
9194
|
* Picks up messages nobody answered. `handleMessage` is fire-and-forget and
|
|
@@ -9473,6 +9351,11 @@ var ChatAgent = class {
|
|
|
9473
9351
|
return data.filter((r) => r.id !== excludeReplyId).reverse().slice(-limit).map((r) => ({ author: r.author, body: r.body }));
|
|
9474
9352
|
}
|
|
9475
9353
|
/** Whether a reply run is currently in flight for this thread. */
|
|
9354
|
+
/** One idle memory extraction call (see memory-extraction-runner.ts). Not a
|
|
9355
|
+
* turn: no thread, session, queue slot or usage row. */
|
|
9356
|
+
completeMemoryExtraction(req) {
|
|
9357
|
+
return completeMemoryExtraction(req);
|
|
9358
|
+
}
|
|
9476
9359
|
isThinking(threadId) {
|
|
9477
9360
|
return this.activeRuns.has(threadId);
|
|
9478
9361
|
}
|
|
@@ -9508,9 +9391,9 @@ var ChatAgent = class {
|
|
|
9508
9391
|
* continue with `--resume`. Returns whether a run was actually in flight —
|
|
9509
9392
|
* a queued-but-not-started message is not affected. */
|
|
9510
9393
|
cancel(threadId) {
|
|
9511
|
-
const
|
|
9512
|
-
if (!
|
|
9513
|
-
|
|
9394
|
+
const run4 = this.activeRuns.get(threadId);
|
|
9395
|
+
if (!run4) return false;
|
|
9396
|
+
run4.abort.abort();
|
|
9514
9397
|
return true;
|
|
9515
9398
|
}
|
|
9516
9399
|
/**
|
|
@@ -9672,18 +9555,12 @@ var ChatAgent = class {
|
|
|
9672
9555
|
...docsAccessUnverified ? { docsAccessUnverified } : {}
|
|
9673
9556
|
};
|
|
9674
9557
|
}
|
|
9675
|
-
const
|
|
9676
|
-
|
|
9677
|
-
|
|
9558
|
+
const memory = await loadAgentMemory(owningProfile, thread.channel_id, {
|
|
9559
|
+
readable: docsAccess !== "none",
|
|
9560
|
+
...isHumanAuthor(msg.author) ? { senderHandle: msg.author } : {}
|
|
9678
9561
|
});
|
|
9679
|
-
if (
|
|
9680
|
-
if (
|
|
9681
|
-
const memoryPrompt = await listChannelMemories(thread.channel_id).then((memories) => formatMemoryPrompt(thread.channel_id, memories)).catch((err) => {
|
|
9682
|
-
console.error("[chat-agent] memory lookup failed:", err.message);
|
|
9683
|
-
return void 0;
|
|
9684
|
-
});
|
|
9685
|
-
if (memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt };
|
|
9686
|
-
}
|
|
9562
|
+
if (memory.enabled) channelConfig = { ...channelConfig, disableAutoMemory: true };
|
|
9563
|
+
if (memory.memoryPrompt) channelConfig = { ...channelConfig, memoryPrompt: memory.memoryPrompt };
|
|
9687
9564
|
const store = this.openSessionStore();
|
|
9688
9565
|
try {
|
|
9689
9566
|
const channelKey = `chat:${thread.channel_id}`;
|
|
@@ -10411,7 +10288,7 @@ async function exportDocAsArtifact(channelId, docPath) {
|
|
|
10411
10288
|
);
|
|
10412
10289
|
const abs = resolveDocPath(channelConfig.cwd, docPath);
|
|
10413
10290
|
if (!abs) throw new Error(`doc not found: ${docPath}`);
|
|
10414
|
-
const content =
|
|
10291
|
+
const content = readFileSync13(abs, "utf8");
|
|
10415
10292
|
const title = extractTitle(abs, "md");
|
|
10416
10293
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
10417
10294
|
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).
|
|
@@ -10479,7 +10356,7 @@ var MEDIA_EXTENSIONS = {
|
|
|
10479
10356
|
".csv": "text/csv; charset=utf-8",
|
|
10480
10357
|
".json": "application/json"
|
|
10481
10358
|
};
|
|
10482
|
-
var MAX_RAW_BYTES =
|
|
10359
|
+
var MAX_RAW_BYTES = 100 * 1024 * 1024;
|
|
10483
10360
|
var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
10484
10361
|
".png",
|
|
10485
10362
|
".jpg",
|
|
@@ -10495,7 +10372,7 @@ var ASSET_LIST_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
10495
10372
|
".txt",
|
|
10496
10373
|
".csv"
|
|
10497
10374
|
]);
|
|
10498
|
-
var MAX_UPLOAD_BYTES =
|
|
10375
|
+
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
10499
10376
|
var CODE_EXTENSIONS = {
|
|
10500
10377
|
".js": "javascript",
|
|
10501
10378
|
".jsx": "javascript",
|
|
@@ -10668,7 +10545,7 @@ function probeDir(absDir, showHidden) {
|
|
|
10668
10545
|
return false;
|
|
10669
10546
|
}
|
|
10670
10547
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10671
|
-
const abs =
|
|
10548
|
+
const abs = join20(dir, entry.name);
|
|
10672
10549
|
const kind = entryKind(entry, abs);
|
|
10673
10550
|
if (kind === "dir") {
|
|
10674
10551
|
if (SKIP_DIRS.has(entry.name)) hasOther = true;
|
|
@@ -10688,14 +10565,14 @@ function probeDir(absDir, showHidden) {
|
|
|
10688
10565
|
}
|
|
10689
10566
|
function readGitBranch(dir) {
|
|
10690
10567
|
try {
|
|
10691
|
-
let gitDir =
|
|
10568
|
+
let gitDir = join20(dir, ".git");
|
|
10692
10569
|
if (statSync5(gitDir).isFile()) {
|
|
10693
|
-
const pointer =
|
|
10570
|
+
const pointer = readFileSync14(gitDir, "utf8").trim();
|
|
10694
10571
|
const match = pointer.match(/^gitdir:\s*(.+)$/);
|
|
10695
10572
|
if (!match) return void 0;
|
|
10696
10573
|
gitDir = resolve4(dir, match[1]);
|
|
10697
10574
|
}
|
|
10698
|
-
const head =
|
|
10575
|
+
const head = readFileSync14(join20(gitDir, "HEAD"), "utf8").trim();
|
|
10699
10576
|
const branchMatch = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
10700
10577
|
if (branchMatch) return branchMatch[1];
|
|
10701
10578
|
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 7) : void 0;
|
|
@@ -10717,14 +10594,14 @@ function listDocLevel(root, relDir = "", sort = "newest", showHidden = false) {
|
|
|
10717
10594
|
for (const entry of entries2) {
|
|
10718
10595
|
if (dirs.length + docs.length >= MAX_ENTRIES) break;
|
|
10719
10596
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10720
|
-
const abs =
|
|
10597
|
+
const abs = join20(absDir, entry.name);
|
|
10721
10598
|
const path = relative(root, abs).split(sep).join("/");
|
|
10722
10599
|
const kind = entryKind(entry, abs);
|
|
10723
10600
|
if (kind === "dir") {
|
|
10724
10601
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10725
10602
|
const probe3 = probeDir(abs, showHidden);
|
|
10726
10603
|
if (!probe3.hasDocs && probe3.hasOther) continue;
|
|
10727
|
-
const isRepo = existsSync16(
|
|
10604
|
+
const isRepo = existsSync16(join20(abs, ".git"));
|
|
10728
10605
|
dirs.push({
|
|
10729
10606
|
path,
|
|
10730
10607
|
name: entry.name,
|
|
@@ -10760,7 +10637,7 @@ function flatDocs(root, {
|
|
|
10760
10637
|
for (const entry of entries2) {
|
|
10761
10638
|
if (hits.length >= MAX_FLAT_DOCS) return;
|
|
10762
10639
|
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
10763
|
-
const abs =
|
|
10640
|
+
const abs = join20(dir, entry.name);
|
|
10764
10641
|
const kind = entryKind(entry, abs);
|
|
10765
10642
|
if (kind === "dir") {
|
|
10766
10643
|
if (!SKIP_DIRS.has(entry.name)) walk(abs, depth + 1);
|
|
@@ -10840,7 +10717,7 @@ function uniqueName(absDir, name) {
|
|
|
10840
10717
|
let candidate = name;
|
|
10841
10718
|
for (let n = 1; n < 1e3; n++) {
|
|
10842
10719
|
try {
|
|
10843
|
-
statSync5(
|
|
10720
|
+
statSync5(join20(absDir, candidate));
|
|
10844
10721
|
} catch {
|
|
10845
10722
|
return candidate;
|
|
10846
10723
|
}
|
|
@@ -10862,7 +10739,7 @@ function saveUploadedFile(root, relDir, name, data) {
|
|
|
10862
10739
|
if (!(ext in DOC_EXTENSIONS) && !ASSET_LIST_EXTENSIONS.has(ext)) return { status: "bad-type" };
|
|
10863
10740
|
if (data.byteLength > MAX_UPLOAD_BYTES) return { status: "too-large" };
|
|
10864
10741
|
const final = uniqueName(absDir, safe);
|
|
10865
|
-
const abs =
|
|
10742
|
+
const abs = join20(absDir, final);
|
|
10866
10743
|
writeFileSync9(abs, data);
|
|
10867
10744
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: final };
|
|
10868
10745
|
}
|
|
@@ -10876,13 +10753,13 @@ function createDocFolder(root, relDir, name) {
|
|
|
10876
10753
|
}
|
|
10877
10754
|
const safe = sanitizeUploadName(name);
|
|
10878
10755
|
if (!safe) return { status: "bad-name" };
|
|
10879
|
-
const abs =
|
|
10756
|
+
const abs = join20(absDir, safe);
|
|
10880
10757
|
try {
|
|
10881
10758
|
statSync5(abs);
|
|
10882
10759
|
return { status: "exists" };
|
|
10883
10760
|
} catch {
|
|
10884
10761
|
}
|
|
10885
|
-
|
|
10762
|
+
mkdirSync10(abs);
|
|
10886
10763
|
return { status: "ok", path: relative(root, abs).split(sep).join("/"), name: safe };
|
|
10887
10764
|
}
|
|
10888
10765
|
function createDocFile(root, relDir, name, content) {
|
|
@@ -10899,7 +10776,7 @@ function createDocFile(root, relDir, name, content) {
|
|
|
10899
10776
|
if (!(ext in DOC_EXTENSIONS) && !(ext in CODE_EXTENSIONS)) return { status: "bad-name" };
|
|
10900
10777
|
if (SKIP_FILES.has(safe)) return { status: "bad-name" };
|
|
10901
10778
|
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) return { status: "too-large" };
|
|
10902
|
-
const abs =
|
|
10779
|
+
const abs = join20(absDir, safe);
|
|
10903
10780
|
try {
|
|
10904
10781
|
writeFileSync9(abs, content, { encoding: "utf8", flag: "wx" });
|
|
10905
10782
|
} catch (err) {
|
|
@@ -10937,7 +10814,7 @@ function moveDocEntry(root, from, to) {
|
|
|
10937
10814
|
} catch {
|
|
10938
10815
|
return { status: "bad-target" };
|
|
10939
10816
|
}
|
|
10940
|
-
const finalAbs =
|
|
10817
|
+
const finalAbs = join20(absToDir, name);
|
|
10941
10818
|
if (isDir2 && (finalAbs === absFrom || finalAbs.startsWith(absFrom + sep))) {
|
|
10942
10819
|
return { status: "into-self" };
|
|
10943
10820
|
}
|
|
@@ -10960,7 +10837,7 @@ function deleteDocEntry(root, path) {
|
|
|
10960
10837
|
}
|
|
10961
10838
|
const isDir2 = stat.isDirectory();
|
|
10962
10839
|
if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
|
|
10963
|
-
|
|
10840
|
+
rmSync8(abs, { recursive: isDir2 });
|
|
10964
10841
|
return {
|
|
10965
10842
|
status: "ok",
|
|
10966
10843
|
path: relative(root, abs).split(sep).join("/"),
|
|
@@ -10999,13 +10876,15 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
10999
10876
|
}
|
|
11000
10877
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
11001
10878
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
11002
|
-
const
|
|
11003
|
-
const
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
10879
|
+
const name = basename6(resolved2.abs);
|
|
10880
|
+
const asciiName = name.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
|
|
10881
|
+
const disposition = download ? "attachment" : "inline";
|
|
10882
|
+
const stream = Readable.toWeb(createReadStream(resolved2.abs));
|
|
10883
|
+
return c.body(stream, 200, {
|
|
10884
|
+
"Content-Type": resolved2.contentType,
|
|
10885
|
+
"Content-Length": String(stat.size),
|
|
10886
|
+
"Content-Disposition": `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(name)}`
|
|
10887
|
+
});
|
|
11009
10888
|
});
|
|
11010
10889
|
app.get("/:id/docs/html", (c) => {
|
|
11011
10890
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
@@ -11019,7 +10898,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11019
10898
|
}
|
|
11020
10899
|
if (!stat.isFile()) return c.json({ error: "not found" }, 404);
|
|
11021
10900
|
if (stat.size > MAX_RAW_BYTES) return c.json({ error: "file too large" }, 413);
|
|
11022
|
-
return c.body(new Uint8Array(
|
|
10901
|
+
return c.body(new Uint8Array(readFileSync14(abs)), 200, {
|
|
11023
10902
|
"Content-Type": "text/html; charset=utf-8",
|
|
11024
10903
|
"Content-Security-Policy": HTML_SHARE_CSP,
|
|
11025
10904
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -11204,7 +11083,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11204
11083
|
title: kind ? extractTitle(abs, kind) : basename6(abs),
|
|
11205
11084
|
mtime: stat.mtime.toISOString(),
|
|
11206
11085
|
tooLarge: tooLarge || void 0,
|
|
11207
|
-
content: tooLarge ? "" :
|
|
11086
|
+
content: tooLarge ? "" : readFileSync14(abs, "utf8")
|
|
11208
11087
|
});
|
|
11209
11088
|
});
|
|
11210
11089
|
app.put("/:id/docs/content", async (c) => {
|
|
@@ -11247,7 +11126,7 @@ function createDocsRouter({ gate = requireDocsAccess } = {}) {
|
|
|
11247
11126
|
channel,
|
|
11248
11127
|
rel,
|
|
11249
11128
|
title: extractTitle(abs, "md"),
|
|
11250
|
-
content:
|
|
11129
|
+
content: readFileSync14(abs, "utf8")
|
|
11251
11130
|
};
|
|
11252
11131
|
};
|
|
11253
11132
|
app.get("/:id/docs/translation/:lang", async (c) => {
|
|
@@ -11470,7 +11349,7 @@ function ownSkills(scope, dir) {
|
|
|
11470
11349
|
return [{ scope, name, description, body, path, plugin: null, mtime }];
|
|
11471
11350
|
});
|
|
11472
11351
|
}
|
|
11473
|
-
function
|
|
11352
|
+
function snapshot(scopes) {
|
|
11474
11353
|
const wants = (scope) => !scopes || scopes.has(scope);
|
|
11475
11354
|
const configDir2 = profileClaudeConfigDir();
|
|
11476
11355
|
const rows = [];
|
|
@@ -11505,7 +11384,7 @@ async function pushSkillScopes(scopes) {
|
|
|
11505
11384
|
if (!mirrorEnabled()) return;
|
|
11506
11385
|
const workspace = getProfile();
|
|
11507
11386
|
const supabase = getSupabaseClient();
|
|
11508
|
-
const rows =
|
|
11387
|
+
const rows = snapshot(scopes ? new Set(scopes) : void 0);
|
|
11509
11388
|
let query = supabase.from("skill_files").select("scope, name, hash").eq("workspace", workspace);
|
|
11510
11389
|
if (scopes) query = query.in("scope", scopes);
|
|
11511
11390
|
const { data, error: error2 } = await query;
|
|
@@ -11789,13 +11668,16 @@ function registerMirroredSkillRoutes(api, agent) {
|
|
|
11789
11668
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
11790
11669
|
import { existsSync as existsSync19 } from "fs";
|
|
11791
11670
|
import { homedir as homedir8 } from "os";
|
|
11792
|
-
import { join as
|
|
11671
|
+
import { join as join21, resolve as resolve5 } from "path";
|
|
11793
11672
|
var PROVIDER_LABELS = {
|
|
11794
11673
|
claude: "Claude Code",
|
|
11795
11674
|
codex: "Codex"
|
|
11796
11675
|
};
|
|
11797
11676
|
var TTL_MS2 = 6e4;
|
|
11798
11677
|
var cache6 = /* @__PURE__ */ new Map();
|
|
11678
|
+
function clearProviderStatusCache() {
|
|
11679
|
+
cache6.clear();
|
|
11680
|
+
}
|
|
11799
11681
|
function binaryOnPath(bin) {
|
|
11800
11682
|
try {
|
|
11801
11683
|
execFileSync3(bin, ["--version"], { stdio: "ignore" });
|
|
@@ -11807,7 +11689,7 @@ function binaryOnPath(bin) {
|
|
|
11807
11689
|
function claudeAuthAvailable() {
|
|
11808
11690
|
if (profileEnv("CLAUDE_CODE_OAUTH_TOKEN")?.trim()) return true;
|
|
11809
11691
|
const configDir2 = profileClaudeConfigDir() || resolve5(homedir8(), ".claude");
|
|
11810
|
-
if (existsSync19(
|
|
11692
|
+
if (existsSync19(join21(configDir2, ".credentials.json"))) return true;
|
|
11811
11693
|
if (process.platform !== "darwin") return false;
|
|
11812
11694
|
try {
|
|
11813
11695
|
execFileSync3("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
@@ -11830,7 +11712,7 @@ function probe2(runner) {
|
|
|
11830
11712
|
runner,
|
|
11831
11713
|
label: label2,
|
|
11832
11714
|
connected: false,
|
|
11833
|
-
reason: `${label2} is installed but signed out
|
|
11715
|
+
reason: `${label2} is installed but signed out. Use Sign in below.`
|
|
11834
11716
|
};
|
|
11835
11717
|
}
|
|
11836
11718
|
return { runner, label: label2, connected: true };
|
|
@@ -11840,7 +11722,7 @@ function withAuthFailure(status) {
|
|
|
11840
11722
|
return {
|
|
11841
11723
|
...status,
|
|
11842
11724
|
connected: false,
|
|
11843
|
-
reason:
|
|
11725
|
+
reason: "Sign-in expired and could not be refreshed. Use Sign in again below."
|
|
11844
11726
|
};
|
|
11845
11727
|
}
|
|
11846
11728
|
function providerStatuses() {
|
|
@@ -11854,6 +11736,232 @@ function providerStatuses() {
|
|
|
11854
11736
|
return statuses.map((st) => st.runner === "claude" && st.connected ? withAuthFailure(st) : st);
|
|
11855
11737
|
}
|
|
11856
11738
|
|
|
11739
|
+
// src/dashboard/provider-login.ts
|
|
11740
|
+
import { spawn as spawn6 } from "child_process";
|
|
11741
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11742
|
+
import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
11743
|
+
import { createRequire } from "module";
|
|
11744
|
+
import { dirname as dirname7, join as join22 } from "path";
|
|
11745
|
+
import { Hono as Hono13 } from "hono";
|
|
11746
|
+
var SESSION_TTL_MS = 10 * 6e4;
|
|
11747
|
+
var URL_WAIT_MS = 3e4;
|
|
11748
|
+
var TOKEN_WAIT_MS = 6e4;
|
|
11749
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
11750
|
+
function stripAnsi(raw) {
|
|
11751
|
+
return raw.replace(/\x1b\][^\x07]*\x07/g, "").replace(/\x1b\[\d*G/g, " ").replace(/\x1b\[[0-9;?<>=]*[A-Za-z~]/g, "").replace(/\x1b[78=>]/g, "");
|
|
11752
|
+
}
|
|
11753
|
+
function parseClaudeAuthUrl(raw) {
|
|
11754
|
+
return stripAnsi(raw).match(/https:\/\/\S+\/oauth\/authorize\?\S+/)?.[0];
|
|
11755
|
+
}
|
|
11756
|
+
function parseClaudeToken(raw) {
|
|
11757
|
+
return stripAnsi(raw).match(/sk-ant-oat[A-Za-z0-9_-]+/)?.[0];
|
|
11758
|
+
}
|
|
11759
|
+
function parseCodexDeviceAuth(raw) {
|
|
11760
|
+
const text = stripAnsi(raw);
|
|
11761
|
+
const url = text.match(/https:\/\/\S+\/device\b\S*/)?.[0];
|
|
11762
|
+
const code = text.match(/\b([A-Z0-9]{4,}-[A-Z0-9]{4,})\b/)?.[1];
|
|
11763
|
+
return url && code ? { url, code } : void 0;
|
|
11764
|
+
}
|
|
11765
|
+
function upsertEnvLine(content, key, value) {
|
|
11766
|
+
const line = `${key}=${value}`;
|
|
11767
|
+
const re = new RegExp(`^\\s*(?:export\\s+)?${key}=.*$`, "m");
|
|
11768
|
+
if (re.test(content)) return content.replace(re, line);
|
|
11769
|
+
return `${content}${content && !content.endsWith("\n") ? "\n" : ""}${line}
|
|
11770
|
+
`;
|
|
11771
|
+
}
|
|
11772
|
+
function finish(id, status, error2) {
|
|
11773
|
+
const s = sessions.get(id);
|
|
11774
|
+
if (!s || s.status !== "pending") return;
|
|
11775
|
+
s.status = status;
|
|
11776
|
+
s.error = error2;
|
|
11777
|
+
s.kill();
|
|
11778
|
+
clearTimeout(s.timer);
|
|
11779
|
+
s.timer = setTimeout(() => sessions.delete(id), 6e4);
|
|
11780
|
+
clearProviderStatusCache();
|
|
11781
|
+
}
|
|
11782
|
+
function track(session) {
|
|
11783
|
+
const id = randomUUID2();
|
|
11784
|
+
const s = {
|
|
11785
|
+
...session,
|
|
11786
|
+
status: "pending",
|
|
11787
|
+
output: "",
|
|
11788
|
+
timer: setTimeout(() => finish(id, "failed", "Sign-in timed out. Start again."), SESSION_TTL_MS)
|
|
11789
|
+
};
|
|
11790
|
+
sessions.set(id, s);
|
|
11791
|
+
return [id, s];
|
|
11792
|
+
}
|
|
11793
|
+
function waitFor(s, parse2, ms) {
|
|
11794
|
+
return new Promise((resolve7) => {
|
|
11795
|
+
const started = Date.now();
|
|
11796
|
+
const tick = setInterval(() => {
|
|
11797
|
+
const v = parse2();
|
|
11798
|
+
if (v !== void 0 || s.status !== "pending" || Date.now() - started > ms) {
|
|
11799
|
+
clearInterval(tick);
|
|
11800
|
+
resolve7(v);
|
|
11801
|
+
}
|
|
11802
|
+
}, 200);
|
|
11803
|
+
});
|
|
11804
|
+
}
|
|
11805
|
+
function loadPty() {
|
|
11806
|
+
const require2 = createRequire(import.meta.url);
|
|
11807
|
+
const root = dirname7(require2.resolve("node-pty/package.json"));
|
|
11808
|
+
const helper = join22(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
|
|
11809
|
+
try {
|
|
11810
|
+
if (existsSync20(helper)) chmodSync(helper, 493);
|
|
11811
|
+
} catch {
|
|
11812
|
+
}
|
|
11813
|
+
return require2("node-pty");
|
|
11814
|
+
}
|
|
11815
|
+
function cliEnv() {
|
|
11816
|
+
const configDir2 = profileClaudeConfigDir();
|
|
11817
|
+
const codexHome = profileEnv("CODEX_HOME");
|
|
11818
|
+
return {
|
|
11819
|
+
...process.env,
|
|
11820
|
+
// Never pop a browser on the Host: the person signing in isn't there.
|
|
11821
|
+
BROWSER: "true",
|
|
11822
|
+
...configDir2 ? { CLAUDE_CONFIG_DIR: configDir2 } : {},
|
|
11823
|
+
...codexHome ? { CODEX_HOME: codexHome } : {}
|
|
11824
|
+
};
|
|
11825
|
+
}
|
|
11826
|
+
async function startClaude() {
|
|
11827
|
+
const pty = loadPty().spawn("claude", ["setup-token"], {
|
|
11828
|
+
// Wide enough that the URL and token are never wrapped.
|
|
11829
|
+
cols: 2e3,
|
|
11830
|
+
rows: 50,
|
|
11831
|
+
env: cliEnv()
|
|
11832
|
+
});
|
|
11833
|
+
const [id, s] = track({ runner: "claude", kill: () => pty.kill(), configFile: paths.CONFIG_FILE, pty });
|
|
11834
|
+
pty.onData((d) => {
|
|
11835
|
+
s.output += d;
|
|
11836
|
+
});
|
|
11837
|
+
pty.onExit(() => {
|
|
11838
|
+
if (s.status === "pending" && !parseClaudeToken(s.output)) {
|
|
11839
|
+
finish(id, "failed", "The claude CLI exited before sign-in finished.");
|
|
11840
|
+
}
|
|
11841
|
+
});
|
|
11842
|
+
const url = await waitFor(s, () => parseClaudeAuthUrl(s.output), URL_WAIT_MS);
|
|
11843
|
+
if (!url) {
|
|
11844
|
+
finish(id, "failed");
|
|
11845
|
+
throw new Error("claude setup-token did not print a sign-in URL.");
|
|
11846
|
+
}
|
|
11847
|
+
return { id, url };
|
|
11848
|
+
}
|
|
11849
|
+
async function submitClaudeCode(id, code) {
|
|
11850
|
+
const s = sessions.get(id);
|
|
11851
|
+
if (!s || s.runner !== "claude" || !s.pty) throw new Error("Sign-in session not found. Start again.");
|
|
11852
|
+
if (s.status !== "pending") throw new Error(s.error ?? "Sign-in already finished.");
|
|
11853
|
+
s.output = "";
|
|
11854
|
+
s.pty.write(`${code.trim()}\r`);
|
|
11855
|
+
const token = await waitFor(s, () => parseClaudeToken(s.output), TOKEN_WAIT_MS);
|
|
11856
|
+
if (!token) {
|
|
11857
|
+
finish(id, "failed", "Claude did not accept that code. Start again and paste the full code.");
|
|
11858
|
+
throw new Error("Claude did not accept that code. Start again and paste the full code.");
|
|
11859
|
+
}
|
|
11860
|
+
mkdirSync11(dirname7(s.configFile), { recursive: true });
|
|
11861
|
+
const current = existsSync20(s.configFile) ? readFileSync15(s.configFile, "utf-8") : "";
|
|
11862
|
+
writeFileSync10(s.configFile, upsertEnvLine(current, "CLAUDE_CODE_OAUTH_TOKEN", token), { mode: 384 });
|
|
11863
|
+
const cached = cachedProfileConfig();
|
|
11864
|
+
if (cached) cached.env["CLAUDE_CODE_OAUTH_TOKEN"] = token;
|
|
11865
|
+
clearClaudeAuthFailed();
|
|
11866
|
+
finish(id, "done");
|
|
11867
|
+
}
|
|
11868
|
+
async function startCodex() {
|
|
11869
|
+
const child = spawn6("codex", ["login", "--device-auth"], {
|
|
11870
|
+
env: cliEnv(),
|
|
11871
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
11872
|
+
});
|
|
11873
|
+
const [id, s] = track({ runner: "codex", kill: () => child.kill(), configFile: paths.CONFIG_FILE });
|
|
11874
|
+
const onData = (d) => {
|
|
11875
|
+
s.output += d.toString();
|
|
11876
|
+
};
|
|
11877
|
+
child.stdout?.on("data", onData);
|
|
11878
|
+
child.stderr?.on("data", onData);
|
|
11879
|
+
child.on("error", () => finish(id, "failed", "Could not run the codex CLI on the Host."));
|
|
11880
|
+
child.on(
|
|
11881
|
+
"exit",
|
|
11882
|
+
(code) => finish(id, code === 0 ? "done" : "failed", code === 0 ? void 0 : "Codex sign-in did not complete.")
|
|
11883
|
+
);
|
|
11884
|
+
const auth = await waitFor(s, () => parseCodexDeviceAuth(s.output), URL_WAIT_MS);
|
|
11885
|
+
if (!auth) {
|
|
11886
|
+
finish(id, "failed");
|
|
11887
|
+
throw new Error("codex login did not print a device code.");
|
|
11888
|
+
}
|
|
11889
|
+
return { id, ...auth };
|
|
11890
|
+
}
|
|
11891
|
+
function requireAdmin3(auth) {
|
|
11892
|
+
return auth?.role === "admin" ? null : "admin access required";
|
|
11893
|
+
}
|
|
11894
|
+
function failure2(c, err) {
|
|
11895
|
+
return c.json({ error: err.message }, 500);
|
|
11896
|
+
}
|
|
11897
|
+
function createProviderLoginRouter() {
|
|
11898
|
+
const app = new Hono13();
|
|
11899
|
+
app.use("*", async (c, next) => {
|
|
11900
|
+
const denied = requireAdmin3(c.get("auth"));
|
|
11901
|
+
if (denied) return c.json({ error: denied }, 403);
|
|
11902
|
+
await next();
|
|
11903
|
+
});
|
|
11904
|
+
app.post("/:runner/login", async (c) => {
|
|
11905
|
+
const runner = c.req.param("runner");
|
|
11906
|
+
try {
|
|
11907
|
+
if (runner === "claude") return c.json(await startClaude());
|
|
11908
|
+
if (runner === "codex") return c.json(await startCodex());
|
|
11909
|
+
} catch (err) {
|
|
11910
|
+
return failure2(c, err);
|
|
11911
|
+
}
|
|
11912
|
+
return c.json({ error: "unknown provider" }, 404);
|
|
11913
|
+
});
|
|
11914
|
+
app.post("/claude/login/:id/code", async (c) => {
|
|
11915
|
+
const body = await c.req.json().catch(() => ({}));
|
|
11916
|
+
const code = body["code"];
|
|
11917
|
+
if (typeof code !== "string" || !code.trim()) return c.json({ error: "code is required" }, 400);
|
|
11918
|
+
try {
|
|
11919
|
+
await submitClaudeCode(c.req.param("id"), code);
|
|
11920
|
+
} catch (err) {
|
|
11921
|
+
return c.json({ error: err.message }, 400);
|
|
11922
|
+
}
|
|
11923
|
+
return c.json({ status: "done" });
|
|
11924
|
+
});
|
|
11925
|
+
app.get("/:runner/login/:id", (c) => {
|
|
11926
|
+
const s = sessions.get(c.req.param("id"));
|
|
11927
|
+
if (!s || s.runner !== c.req.param("runner")) return c.json({ error: "session not found" }, 404);
|
|
11928
|
+
return c.json({ status: s.status, ...s.error ? { error: s.error } : {} });
|
|
11929
|
+
});
|
|
11930
|
+
return app;
|
|
11931
|
+
}
|
|
11932
|
+
async function handleProviderLoginRequest(req) {
|
|
11933
|
+
const app = new Hono13();
|
|
11934
|
+
app.use("*", async (c, next) => {
|
|
11935
|
+
c.set("auth", { role: "admin" });
|
|
11936
|
+
await next();
|
|
11937
|
+
});
|
|
11938
|
+
app.route("/", createProviderLoginRouter());
|
|
11939
|
+
const res = await app.request(req.path, {
|
|
11940
|
+
method: req.method,
|
|
11941
|
+
...req.body !== void 0 && {
|
|
11942
|
+
body: JSON.stringify(req.body),
|
|
11943
|
+
headers: { "content-type": "application/json" }
|
|
11944
|
+
}
|
|
11945
|
+
});
|
|
11946
|
+
return { status: res.status, body: await res.json().catch(() => null) };
|
|
11947
|
+
}
|
|
11948
|
+
function registerRelayedProviderLoginRoutes(api, agent) {
|
|
11949
|
+
const relay3 = async (c) => {
|
|
11950
|
+
const denied = requireAdmin3(c.get("auth"));
|
|
11951
|
+
if (denied) return c.json({ error: denied }, 403);
|
|
11952
|
+
const method = c.req.method;
|
|
11953
|
+
const body = method === "GET" ? void 0 : await c.req.json().catch(() => null);
|
|
11954
|
+
const path = c.req.path.slice(c.req.path.indexOf("/settings/providers") + "/settings/providers".length);
|
|
11955
|
+
const result = await agent.providerLoginRequest?.({ method, path, body });
|
|
11956
|
+
if (!result) {
|
|
11957
|
+
return c.json({ error: "Signing in needs the Host machine, which is not connected right now." }, 503);
|
|
11958
|
+
}
|
|
11959
|
+
return c.json(result.body, result.status);
|
|
11960
|
+
};
|
|
11961
|
+
api.all("/settings/providers/:runner/login", relay3);
|
|
11962
|
+
api.all("/settings/providers/:runner/login/*", relay3);
|
|
11963
|
+
}
|
|
11964
|
+
|
|
11857
11965
|
// src/dashboard/host-api.ts
|
|
11858
11966
|
function registerHostRoutes(api) {
|
|
11859
11967
|
api.get("/channels/:id/skills", (c) => {
|
|
@@ -11902,6 +12010,7 @@ function registerHostRoutes(api) {
|
|
|
11902
12010
|
return c.json({ ...await saveWorkspaceBranding(patch), ...local });
|
|
11903
12011
|
});
|
|
11904
12012
|
api.get("/settings/providers", (c) => c.json({ providers: providerStatuses() }));
|
|
12013
|
+
api.route("/settings/providers", createProviderLoginRouter());
|
|
11905
12014
|
api.get(
|
|
11906
12015
|
"/settings/usage-limit",
|
|
11907
12016
|
(c) => c.json({
|
|
@@ -11960,8 +12069,8 @@ function registerHostRoutes(api) {
|
|
|
11960
12069
|
const errors = c.req.query("error") === "true" || c.req.query("error") === "1";
|
|
11961
12070
|
const limit = Math.min(Number(c.req.query("limit")) || 200, 2e3);
|
|
11962
12071
|
const filename = errors ? "custie-error.log" : "custie.log";
|
|
11963
|
-
const file =
|
|
11964
|
-
const exists =
|
|
12072
|
+
const file = join23(paths.LOG_DIR, filename);
|
|
12073
|
+
const exists = existsSync21(file);
|
|
11965
12074
|
return c.json({
|
|
11966
12075
|
file,
|
|
11967
12076
|
exists,
|
|
@@ -11971,18 +12080,18 @@ function registerHostRoutes(api) {
|
|
|
11971
12080
|
});
|
|
11972
12081
|
}
|
|
11973
12082
|
function tailFile(file, limit) {
|
|
11974
|
-
if (!
|
|
11975
|
-
const lines =
|
|
12083
|
+
if (!existsSync21(file)) return [];
|
|
12084
|
+
const lines = readFileSync16(file, "utf-8").split("\n");
|
|
11976
12085
|
if (lines.at(-1) === "") lines.pop();
|
|
11977
12086
|
return lines.slice(-limit);
|
|
11978
12087
|
}
|
|
11979
12088
|
|
|
11980
12089
|
// src/dashboard/data-api.ts
|
|
11981
|
-
import { existsSync as
|
|
12090
|
+
import { existsSync as existsSync23 } from "fs";
|
|
11982
12091
|
import sharp5 from "sharp";
|
|
11983
12092
|
|
|
11984
12093
|
// src/dashboard/automations-routes.ts
|
|
11985
|
-
import { Hono as
|
|
12094
|
+
import { Hono as Hono14 } from "hono";
|
|
11986
12095
|
var TYPES2 = ["schedule", "trigger", "mention-trigger"];
|
|
11987
12096
|
function isType(t) {
|
|
11988
12097
|
return TYPES2.includes(t);
|
|
@@ -11999,7 +12108,7 @@ function statusFor(message) {
|
|
|
11999
12108
|
return 400;
|
|
12000
12109
|
}
|
|
12001
12110
|
function createAutomationWriteRouter() {
|
|
12002
|
-
const app = new
|
|
12111
|
+
const app = new Hono14();
|
|
12003
12112
|
const manager = () => new AutomationManager();
|
|
12004
12113
|
app.get("/:type/:name", (c) => {
|
|
12005
12114
|
const item = manager().get(c.req.param("name"));
|
|
@@ -12055,9 +12164,9 @@ function createAutomationWriteRouter() {
|
|
|
12055
12164
|
}
|
|
12056
12165
|
|
|
12057
12166
|
// src/dashboard/categories-routes.ts
|
|
12058
|
-
import { Hono as
|
|
12167
|
+
import { Hono as Hono15 } from "hono";
|
|
12059
12168
|
function createCategoryWriteRouter() {
|
|
12060
|
-
const app = new
|
|
12169
|
+
const app = new Hono15();
|
|
12061
12170
|
app.get("/", async (c) => {
|
|
12062
12171
|
const { data, error: error2 } = await getSupabaseClient().from("categories").select("*").eq("profile", getProfile()).order("position", { ascending: true });
|
|
12063
12172
|
if (error2) return c.json({ error: error2.message }, 500);
|
|
@@ -12111,7 +12220,7 @@ function createCategoryWriteRouter() {
|
|
|
12111
12220
|
}
|
|
12112
12221
|
|
|
12113
12222
|
// src/dashboard/chat-routes.ts
|
|
12114
|
-
import { Hono as
|
|
12223
|
+
import { Hono as Hono16 } from "hono";
|
|
12115
12224
|
|
|
12116
12225
|
// src/dashboard/fork-summary.ts
|
|
12117
12226
|
var GROQ_API_URL6 = "https://api.groq.com/openai/v1/chat/completions";
|
|
@@ -12531,7 +12640,7 @@ async function withPreviews(supabase, threads) {
|
|
|
12531
12640
|
threads.map((t) => t.id),
|
|
12532
12641
|
THREAD_CHUNK_SIZE
|
|
12533
12642
|
).map(
|
|
12534
|
-
(ids) => supabase.from("thread_replies").select("thread_id, body, author").in("thread_id", ids).is("archived_at", null).order("created_at", { ascending: true })
|
|
12643
|
+
(ids) => supabase.from("thread_replies").select("thread_id, body, author, external_user_id").in("thread_id", ids).is("archived_at", null).order("created_at", { ascending: true })
|
|
12535
12644
|
)
|
|
12536
12645
|
);
|
|
12537
12646
|
const previewByThread = /* @__PURE__ */ new Map();
|
|
@@ -12541,8 +12650,15 @@ async function withPreviews(supabase, threads) {
|
|
|
12541
12650
|
if (entry) {
|
|
12542
12651
|
entry.count += 1;
|
|
12543
12652
|
entry.lastAuthor = r.author;
|
|
12653
|
+
entry.participants.delete(r.author);
|
|
12654
|
+
entry.participants.set(r.author, r.external_user_id ?? null);
|
|
12544
12655
|
} else
|
|
12545
|
-
previewByThread.set(r.thread_id, {
|
|
12656
|
+
previewByThread.set(r.thread_id, {
|
|
12657
|
+
count: 1,
|
|
12658
|
+
firstBody: r.body,
|
|
12659
|
+
lastAuthor: r.author,
|
|
12660
|
+
participants: /* @__PURE__ */ new Map()
|
|
12661
|
+
});
|
|
12546
12662
|
}
|
|
12547
12663
|
}
|
|
12548
12664
|
return threads.map((t) => {
|
|
@@ -12554,7 +12670,9 @@ async function withPreviews(supabase, threads) {
|
|
|
12554
12670
|
// Who spoke last, counting only replies AFTER the opening message —
|
|
12555
12671
|
// the reader's own handle here means there is nothing new for them to
|
|
12556
12672
|
// read, on this browser or any other (see web/src/lib/thread-unread).
|
|
12557
|
-
last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null
|
|
12673
|
+
last_reply_author: entry && entry.count > 1 ? entry.lastAuthor : null,
|
|
12674
|
+
// Everyone who replied, most recent first, for the row's avatar stack.
|
|
12675
|
+
participants: [...entry?.participants ?? []].reverse().map(([author, external_user_id]) => ({ author, external_user_id }))
|
|
12558
12676
|
};
|
|
12559
12677
|
});
|
|
12560
12678
|
}
|
|
@@ -12589,7 +12707,7 @@ async function generateAutoTitle(threadId, channelId, body) {
|
|
|
12589
12707
|
await supabase.from("threads").update({ auto_title: autoTitle }).eq("id", threadId).is("title", null);
|
|
12590
12708
|
}
|
|
12591
12709
|
function createChatRouter(agent = {}) {
|
|
12592
|
-
const app = new
|
|
12710
|
+
const app = new Hono16();
|
|
12593
12711
|
const {
|
|
12594
12712
|
onChatMessage: onMessage,
|
|
12595
12713
|
isAgentThinking,
|
|
@@ -13096,7 +13214,7 @@ function createChatRouter(agent = {}) {
|
|
|
13096
13214
|
}
|
|
13097
13215
|
|
|
13098
13216
|
// src/dashboard/doc-shares-routes.ts
|
|
13099
|
-
import { Hono as
|
|
13217
|
+
import { Hono as Hono17 } from "hono";
|
|
13100
13218
|
|
|
13101
13219
|
// src/dashboard/doc-share-features.ts
|
|
13102
13220
|
import { DeleteObjectCommand as DeleteObjectCommand3 } from "@aws-sdk/client-s3";
|
|
@@ -13280,7 +13398,7 @@ function resolveChannel2(c) {
|
|
|
13280
13398
|
}
|
|
13281
13399
|
var LIST_LIMIT = 500;
|
|
13282
13400
|
function createDocShareListRouter() {
|
|
13283
|
-
const app = new
|
|
13401
|
+
const app = new Hono17();
|
|
13284
13402
|
app.get("/shares", async (c) => {
|
|
13285
13403
|
const viewer = c.get("auth");
|
|
13286
13404
|
const readable = await filterReadableDocsChannels(
|
|
@@ -13324,7 +13442,7 @@ function createDocShareListRouter() {
|
|
|
13324
13442
|
return app;
|
|
13325
13443
|
}
|
|
13326
13444
|
function createDocShareWriteRouter(agent = {}) {
|
|
13327
|
-
const app = new
|
|
13445
|
+
const app = new Hono17();
|
|
13328
13446
|
app.use("/:id/docs/share", requireDocsAccess);
|
|
13329
13447
|
app.use("/:id/docs/share/*", requireDocsAccess);
|
|
13330
13448
|
app.use("/:id/docs/shares", requireDocsAccess);
|
|
@@ -13481,7 +13599,7 @@ function createDocShareWriteRouter(agent = {}) {
|
|
|
13481
13599
|
}
|
|
13482
13600
|
|
|
13483
13601
|
// src/dashboard/search-routes.ts
|
|
13484
|
-
import { Hono as
|
|
13602
|
+
import { Hono as Hono18 } from "hono";
|
|
13485
13603
|
function matchScore(q, fields) {
|
|
13486
13604
|
const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
13487
13605
|
if (terms.length === 0) return 0;
|
|
@@ -13629,7 +13747,7 @@ function searchAutomations(q) {
|
|
|
13629
13747
|
}
|
|
13630
13748
|
var RESULT_LIMIT = 30;
|
|
13631
13749
|
function createSearchRouter() {
|
|
13632
|
-
const app = new
|
|
13750
|
+
const app = new Hono18();
|
|
13633
13751
|
app.get("/", async (c) => {
|
|
13634
13752
|
const q = c.req.query("q")?.trim();
|
|
13635
13753
|
if (!q) return c.json({ results: [] });
|
|
@@ -13645,8 +13763,8 @@ function createSearchRouter() {
|
|
|
13645
13763
|
}
|
|
13646
13764
|
|
|
13647
13765
|
// src/dashboard/uploads-routes.ts
|
|
13648
|
-
import { randomUUID as
|
|
13649
|
-
import { Hono as
|
|
13766
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13767
|
+
import { Hono as Hono19 } from "hono";
|
|
13650
13768
|
import { PutObjectCommand as PutObjectCommand3, GetObjectCommand as GetObjectCommand5 } from "@aws-sdk/client-s3";
|
|
13651
13769
|
import { getSignedUrl as getSignedUrl4 } from "@aws-sdk/s3-request-presigner";
|
|
13652
13770
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
@@ -13661,7 +13779,7 @@ function encodeKeyForUrl(key) {
|
|
|
13661
13779
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
13662
13780
|
}
|
|
13663
13781
|
function createUploadsRouter() {
|
|
13664
|
-
const app = new
|
|
13782
|
+
const app = new Hono19();
|
|
13665
13783
|
app.post("/uploads", async (c) => {
|
|
13666
13784
|
const body = await c.req.parseBody().catch(() => null);
|
|
13667
13785
|
const file = body?.["file"];
|
|
@@ -13669,7 +13787,7 @@ function createUploadsRouter() {
|
|
|
13669
13787
|
if (file.size > MAX_FILE_BYTES) {
|
|
13670
13788
|
return c.json({ error: `file must be ${MAX_FILE_BYTES / (1024 * 1024)}MB or smaller` }, 400);
|
|
13671
13789
|
}
|
|
13672
|
-
const key = `${KEY_PREFIX2}/${getProfile()}/${
|
|
13790
|
+
const key = `${KEY_PREFIX2}/${getProfile()}/${randomUUID3()}-${sanitizeFilename(file.name)}`;
|
|
13673
13791
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
13674
13792
|
await getR2Client().send(
|
|
13675
13793
|
new PutObjectCommand3({
|
|
@@ -13706,14 +13824,14 @@ function createUploadsRouter() {
|
|
|
13706
13824
|
}
|
|
13707
13825
|
|
|
13708
13826
|
// src/dashboard/transcribe-routes.ts
|
|
13709
|
-
import { Hono as
|
|
13827
|
+
import { Hono as Hono20 } from "hono";
|
|
13710
13828
|
var GROQ_TRANSCRIBE_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
13711
13829
|
var MODEL = "whisper-large-v3-turbo";
|
|
13712
13830
|
var MAX_AUDIO_BYTES = 5 * 1024 * 1024;
|
|
13713
13831
|
var MAX_PROMPT_CHARS = 400;
|
|
13714
13832
|
var NO_SPEECH_THRESHOLD = 0.6;
|
|
13715
13833
|
function createTranscribeRouter() {
|
|
13716
|
-
const app = new
|
|
13834
|
+
const app = new Hono20();
|
|
13717
13835
|
app.post("/transcribe", async (c) => {
|
|
13718
13836
|
if (!await isFeatureEnabled(c, "dictation"))
|
|
13719
13837
|
return c.json({ error: "feature not enabled" }, 403);
|
|
@@ -13752,8 +13870,8 @@ function createTranscribeRouter() {
|
|
|
13752
13870
|
}
|
|
13753
13871
|
|
|
13754
13872
|
// src/dashboard/avatars-routes.ts
|
|
13755
|
-
import { createHash as createHash10, randomUUID as
|
|
13756
|
-
import { Hono as
|
|
13873
|
+
import { createHash as createHash10, randomUUID as randomUUID4 } from "crypto";
|
|
13874
|
+
import { Hono as Hono21 } from "hono";
|
|
13757
13875
|
import sharp4 from "sharp";
|
|
13758
13876
|
import { PutObjectCommand as PutObjectCommand4, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
|
|
13759
13877
|
var AVATAR_SIZE = 192;
|
|
@@ -13791,7 +13909,7 @@ async function storeAvatar(file, owner) {
|
|
|
13791
13909
|
if (file.size > MAX_AVATAR_BYTES) return { error: "avatar must be 2MB or smaller" };
|
|
13792
13910
|
const original = Buffer.from(await file.arrayBuffer());
|
|
13793
13911
|
const resized = await sharp4(original).resize({ width: AVATAR_SIZE, height: AVATAR_SIZE, fit: "cover" }).png().toBuffer();
|
|
13794
|
-
const key = `${avatarKeyPrefix()}${owner}-${
|
|
13912
|
+
const key = `${avatarKeyPrefix()}${owner}-${randomUUID4()}.png`;
|
|
13795
13913
|
await getR2Client().send(
|
|
13796
13914
|
new PutObjectCommand4({
|
|
13797
13915
|
Bucket: getR2Bucket(),
|
|
@@ -13806,7 +13924,7 @@ function avatarKeyPrefix() {
|
|
|
13806
13924
|
return `${KEY_PREFIX3}/${getProfile()}/`;
|
|
13807
13925
|
}
|
|
13808
13926
|
function createAvatarsRouter() {
|
|
13809
|
-
const app = new
|
|
13927
|
+
const app = new Hono21();
|
|
13810
13928
|
app.get("/avatars/slack", async (c) => {
|
|
13811
13929
|
const ctx = await resolveAuthContext(c);
|
|
13812
13930
|
if (!ctx) return c.json({ error: "not signed in" }, 401);
|
|
@@ -13855,7 +13973,7 @@ function createAvatarsRouter() {
|
|
|
13855
13973
|
}
|
|
13856
13974
|
|
|
13857
13975
|
// src/dashboard/members-routes.ts
|
|
13858
|
-
import { Hono as
|
|
13976
|
+
import { Hono as Hono22 } from "hono";
|
|
13859
13977
|
function inviteLinkFor(c, token) {
|
|
13860
13978
|
const url = new URL("/auth/invite", callbackUrl(c));
|
|
13861
13979
|
url.searchParams.set("token", token);
|
|
@@ -13863,7 +13981,7 @@ function inviteLinkFor(c, token) {
|
|
|
13863
13981
|
}
|
|
13864
13982
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
13865
13983
|
function createMembersRouter() {
|
|
13866
|
-
const members = new
|
|
13984
|
+
const members = new Hono22();
|
|
13867
13985
|
members.get("/", async (c) => {
|
|
13868
13986
|
const ctx = await resolveAuthContext(c);
|
|
13869
13987
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -13904,7 +14022,7 @@ function createMembersRouter() {
|
|
|
13904
14022
|
return { userId: m.userId, handle: m.handle, role: m.role, avatarUrl };
|
|
13905
14023
|
})
|
|
13906
14024
|
);
|
|
13907
|
-
return c.json({ members: directory });
|
|
14025
|
+
return c.json({ workspaceId: ctx.workspaceId, members: directory });
|
|
13908
14026
|
});
|
|
13909
14027
|
members.post("/:userId/dm", async (c) => {
|
|
13910
14028
|
const ctx = await resolveAuthContext(c);
|
|
@@ -13956,6 +14074,10 @@ function createMembersRouter() {
|
|
|
13956
14074
|
if (avatarKey && !avatarKey.startsWith(`${avatarKeyPrefix()}invite-${ctx.userId}-`)) {
|
|
13957
14075
|
return c.json({ error: "unknown avatar" }, 400);
|
|
13958
14076
|
}
|
|
14077
|
+
const { plan } = await getWorkspaceBilling(ctx.workspaceId);
|
|
14078
|
+
if (plan === "local") {
|
|
14079
|
+
return c.json({ error: "The Solo plan is for one person. Upgrade to invite members." }, 403);
|
|
14080
|
+
}
|
|
13959
14081
|
const existing = await getMembershipByEmail(ctx.workspaceId, email);
|
|
13960
14082
|
if (existing) return c.json({ error: "already a member of this workspace" }, 409);
|
|
13961
14083
|
let membership;
|
|
@@ -14074,7 +14196,7 @@ function createMembersRouter() {
|
|
|
14074
14196
|
}
|
|
14075
14197
|
|
|
14076
14198
|
// src/dashboard/notifications-routes.ts
|
|
14077
|
-
import { Hono as
|
|
14199
|
+
import { Hono as Hono23 } from "hono";
|
|
14078
14200
|
|
|
14079
14201
|
// src/store/notification-store.ts
|
|
14080
14202
|
function rowToNotification(row) {
|
|
@@ -14142,7 +14264,7 @@ async function markAllRead(channelIds, userId) {
|
|
|
14142
14264
|
|
|
14143
14265
|
// src/dashboard/notifications-routes.ts
|
|
14144
14266
|
function createNotificationsRouter() {
|
|
14145
|
-
const notifications = new
|
|
14267
|
+
const notifications = new Hono23();
|
|
14146
14268
|
notifications.get("/", async (c) => {
|
|
14147
14269
|
const auth = c.get("auth");
|
|
14148
14270
|
const channelIds = getProfileChannelRegistry().filter((ch) => canSeeChannel(ch, auth)).map((ch) => ch.id);
|
|
@@ -14169,7 +14291,7 @@ function createNotificationsRouter() {
|
|
|
14169
14291
|
return notifications;
|
|
14170
14292
|
}
|
|
14171
14293
|
function createThreadReadRouter() {
|
|
14172
|
-
const threads = new
|
|
14294
|
+
const threads = new Hono23();
|
|
14173
14295
|
threads.post("/threads/:threadId/read", async (c) => {
|
|
14174
14296
|
const { userId } = c.get("auth");
|
|
14175
14297
|
const threadId = c.req.param("threadId");
|
|
@@ -14181,7 +14303,7 @@ function createThreadReadRouter() {
|
|
|
14181
14303
|
}
|
|
14182
14304
|
|
|
14183
14305
|
// src/dashboard/push-routes.ts
|
|
14184
|
-
import { Hono as
|
|
14306
|
+
import { Hono as Hono24 } from "hono";
|
|
14185
14307
|
|
|
14186
14308
|
// src/push/send.ts
|
|
14187
14309
|
import webpush from "web-push";
|
|
@@ -14284,7 +14406,7 @@ async function pushToUser(userId, payload) {
|
|
|
14284
14406
|
|
|
14285
14407
|
// src/dashboard/push-routes.ts
|
|
14286
14408
|
function createPushRouter() {
|
|
14287
|
-
const push = new
|
|
14409
|
+
const push = new Hono24();
|
|
14288
14410
|
push.get("/key", (c) => c.json({ publicKey: getVapidPublicKey() }));
|
|
14289
14411
|
push.post("/subscribe", async (c) => {
|
|
14290
14412
|
const { userId } = c.get("auth");
|
|
@@ -14315,7 +14437,7 @@ function createPushRouter() {
|
|
|
14315
14437
|
}
|
|
14316
14438
|
|
|
14317
14439
|
// src/dashboard/mcp-oauth-routes.ts
|
|
14318
|
-
import { Hono as
|
|
14440
|
+
import { Hono as Hono25 } from "hono";
|
|
14319
14441
|
var CONNECTIONS_PAGE = "/settings/connections";
|
|
14320
14442
|
function callbackUri(c) {
|
|
14321
14443
|
const proto = c.req.header("x-forwarded-proto") ?? "http";
|
|
@@ -14330,7 +14452,7 @@ function backToDashboard(c, to, params) {
|
|
|
14330
14452
|
return c.redirect(`${target.pathname}${target.search}`);
|
|
14331
14453
|
}
|
|
14332
14454
|
function createMcpOAuthRouter() {
|
|
14333
|
-
const app = new
|
|
14455
|
+
const app = new Hono25();
|
|
14334
14456
|
app.get("/oauth/:provider/start", async (c) => {
|
|
14335
14457
|
const auth = c.get("auth");
|
|
14336
14458
|
if (auth.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -14457,7 +14579,7 @@ function createMcpOAuthRouter() {
|
|
|
14457
14579
|
}
|
|
14458
14580
|
|
|
14459
14581
|
// src/dashboard/mcp-routes.ts
|
|
14460
|
-
import { Hono as
|
|
14582
|
+
import { Hono as Hono26 } from "hono";
|
|
14461
14583
|
|
|
14462
14584
|
// src/mcp/transport.ts
|
|
14463
14585
|
var KINDS = ["inherit", "npx", "local", "http", "sse"];
|
|
@@ -14551,7 +14673,7 @@ function fromServerCommand(command) {
|
|
|
14551
14673
|
|
|
14552
14674
|
// src/dashboard/mcp-routes.ts
|
|
14553
14675
|
var CUSTOM_PROVIDER = "custom";
|
|
14554
|
-
function
|
|
14676
|
+
function requireAdmin4(auth) {
|
|
14555
14677
|
return auth?.role === "admin" ? null : "admin access required";
|
|
14556
14678
|
}
|
|
14557
14679
|
function toResponse4(connection, channelIds, providerCommand = null) {
|
|
@@ -14587,9 +14709,9 @@ function bindingsByConnection() {
|
|
|
14587
14709
|
return map;
|
|
14588
14710
|
}
|
|
14589
14711
|
function createMcpRouter() {
|
|
14590
|
-
const app = new
|
|
14712
|
+
const app = new Hono26();
|
|
14591
14713
|
app.get("/providers", async (c) => {
|
|
14592
|
-
const denied =
|
|
14714
|
+
const denied = requireAdmin4(c.get("auth"));
|
|
14593
14715
|
if (denied) return c.json({ error: denied }, 403);
|
|
14594
14716
|
const providers = await listMcpProviders();
|
|
14595
14717
|
return c.json({
|
|
@@ -14607,7 +14729,7 @@ function createMcpRouter() {
|
|
|
14607
14729
|
});
|
|
14608
14730
|
app.get("/connections", async (c) => {
|
|
14609
14731
|
const auth = c.get("auth");
|
|
14610
|
-
const denied =
|
|
14732
|
+
const denied = requireAdmin4(auth);
|
|
14611
14733
|
if (denied) return c.json({ error: denied }, 403);
|
|
14612
14734
|
const connections = await listMcpConnections(auth.workspaceId);
|
|
14613
14735
|
const commands = new Map(
|
|
@@ -14622,7 +14744,7 @@ function createMcpRouter() {
|
|
|
14622
14744
|
});
|
|
14623
14745
|
app.post("/connections", async (c) => {
|
|
14624
14746
|
const auth = c.get("auth");
|
|
14625
|
-
const denied =
|
|
14747
|
+
const denied = requireAdmin4(auth);
|
|
14626
14748
|
if (denied) return c.json({ error: denied }, 403);
|
|
14627
14749
|
const body = await c.req.json().catch(() => null);
|
|
14628
14750
|
const label2 = typeof body?.label === "string" ? body.label.trim() : "";
|
|
@@ -14659,7 +14781,7 @@ function createMcpRouter() {
|
|
|
14659
14781
|
});
|
|
14660
14782
|
app.patch("/connections/:id", async (c) => {
|
|
14661
14783
|
const auth = c.get("auth");
|
|
14662
|
-
const denied =
|
|
14784
|
+
const denied = requireAdmin4(auth);
|
|
14663
14785
|
if (denied) return c.json({ error: denied }, 403);
|
|
14664
14786
|
const body = await c.req.json().catch(() => null);
|
|
14665
14787
|
if (!body) return c.json({ error: "a body is required" }, 400);
|
|
@@ -14690,7 +14812,7 @@ function createMcpRouter() {
|
|
|
14690
14812
|
});
|
|
14691
14813
|
app.put("/connections/:id/credential", async (c) => {
|
|
14692
14814
|
const auth = c.get("auth");
|
|
14693
|
-
const denied =
|
|
14815
|
+
const denied = requireAdmin4(auth);
|
|
14694
14816
|
if (denied) return c.json({ error: denied }, 403);
|
|
14695
14817
|
const body = await c.req.json().catch(() => null);
|
|
14696
14818
|
const raw = body?.apiKey;
|
|
@@ -14708,7 +14830,7 @@ function createMcpRouter() {
|
|
|
14708
14830
|
});
|
|
14709
14831
|
app.put("/connections/:id/channels", async (c) => {
|
|
14710
14832
|
const auth = c.get("auth");
|
|
14711
|
-
const denied =
|
|
14833
|
+
const denied = requireAdmin4(auth);
|
|
14712
14834
|
if (denied) return c.json({ error: denied }, 403);
|
|
14713
14835
|
const id = c.req.param("id");
|
|
14714
14836
|
const connection = await getMcpConnection(auth.workspaceId, id);
|
|
@@ -14735,7 +14857,7 @@ function createMcpRouter() {
|
|
|
14735
14857
|
});
|
|
14736
14858
|
app.delete("/connections/:id", async (c) => {
|
|
14737
14859
|
const auth = c.get("auth");
|
|
14738
|
-
const denied =
|
|
14860
|
+
const denied = requireAdmin4(auth);
|
|
14739
14861
|
if (denied) return c.json({ error: denied }, 403);
|
|
14740
14862
|
const deleted = await deleteMcpConnection(auth.workspaceId, c.req.param("id"));
|
|
14741
14863
|
if (!deleted) return c.json({ error: "connection not found" }, 404);
|
|
@@ -14846,7 +14968,7 @@ async function sessionUsageForThreads(workspace, threadTsList) {
|
|
|
14846
14968
|
}
|
|
14847
14969
|
|
|
14848
14970
|
// src/dashboard/usage-report.ts
|
|
14849
|
-
import { existsSync as
|
|
14971
|
+
import { existsSync as existsSync22 } from "fs";
|
|
14850
14972
|
function mergeByLabel(buckets) {
|
|
14851
14973
|
const merged = /* @__PURE__ */ new Map();
|
|
14852
14974
|
for (const bucket of buckets) {
|
|
@@ -14892,7 +15014,7 @@ function toReport(days, rows, labelChannel) {
|
|
|
14892
15014
|
};
|
|
14893
15015
|
}
|
|
14894
15016
|
function fromLocalStore(days, labelChannel) {
|
|
14895
|
-
if (!
|
|
15017
|
+
if (!existsSync22(paths.DB_FILE)) return { ...EMPTY, days };
|
|
14896
15018
|
const window = days === 0 ? void 0 : days;
|
|
14897
15019
|
const store = new UsageStore(paths.DB_FILE);
|
|
14898
15020
|
try {
|
|
@@ -14934,7 +15056,7 @@ async function buildChannelUsageReport(channelId, days, labelChannel) {
|
|
|
14934
15056
|
const { totals, byDay } = await channelUsageForWorkspace(workspace, aliasIds, window);
|
|
14935
15057
|
return { days, totals: totals.runs > 0 ? totals : null, byDay };
|
|
14936
15058
|
} catch {
|
|
14937
|
-
if (!
|
|
15059
|
+
if (!existsSync22(paths.DB_FILE)) return { days, totals: null, byDay: [] };
|
|
14938
15060
|
const store = new UsageStore(paths.DB_FILE);
|
|
14939
15061
|
try {
|
|
14940
15062
|
const targetLabel = labelChannel(channelId);
|
|
@@ -14956,7 +15078,7 @@ async function buildThreadUsage(threadTs) {
|
|
|
14956
15078
|
if (totals.runs === 0) return { threadTs, totals: null, byModel: [] };
|
|
14957
15079
|
return { threadTs, totals, byModel };
|
|
14958
15080
|
} catch {
|
|
14959
|
-
if (!
|
|
15081
|
+
if (!existsSync22(paths.DB_FILE)) return { threadTs, totals: null, byModel: [] };
|
|
14960
15082
|
const store = new UsageStore(paths.DB_FILE);
|
|
14961
15083
|
try {
|
|
14962
15084
|
const totals = store.threadTotals(threadTs);
|
|
@@ -14969,7 +15091,7 @@ async function buildThreadUsage(threadTs) {
|
|
|
14969
15091
|
}
|
|
14970
15092
|
|
|
14971
15093
|
// src/dashboard/channel-members-routes.ts
|
|
14972
|
-
import { Hono as
|
|
15094
|
+
import { Hono as Hono27 } from "hono";
|
|
14973
15095
|
async function isChannelAdmin(ctx, channelId) {
|
|
14974
15096
|
if (ctx.role === "admin") return true;
|
|
14975
15097
|
const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
|
|
@@ -14978,7 +15100,7 @@ async function isChannelAdmin(ctx, channelId) {
|
|
|
14978
15100
|
return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
|
|
14979
15101
|
}
|
|
14980
15102
|
function createChannelMembersRouter() {
|
|
14981
|
-
const router = new
|
|
15103
|
+
const router = new Hono27();
|
|
14982
15104
|
router.get("/:id/members", async (c) => {
|
|
14983
15105
|
const ctx = await resolveAuthContext(c);
|
|
14984
15106
|
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
@@ -15026,9 +15148,9 @@ function createChannelMembersRouter() {
|
|
|
15026
15148
|
if (!await isChannelAdmin(ctx, channelId)) {
|
|
15027
15149
|
return c.json({ error: "channel admin access required" }, 403);
|
|
15028
15150
|
}
|
|
15029
|
-
return c.json({
|
|
15151
|
+
return c.json({ memberAccess: await getChannelDocsMemberAccess(channelId) });
|
|
15030
15152
|
});
|
|
15031
|
-
router.put("/:id/docs-access
|
|
15153
|
+
router.put("/:id/docs-access", async (c) => {
|
|
15032
15154
|
const ctx = await resolveAuthContext(c);
|
|
15033
15155
|
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
15034
15156
|
const channelId = c.req.param("id");
|
|
@@ -15036,32 +15158,31 @@ function createChannelMembersRouter() {
|
|
|
15036
15158
|
return c.json({ error: "channel admin access required" }, 403);
|
|
15037
15159
|
}
|
|
15038
15160
|
const body = await c.req.json().catch(() => ({}));
|
|
15039
|
-
if (!
|
|
15040
|
-
return c.json({ error: "
|
|
15161
|
+
if (!isDocsAccess(body.memberAccess)) {
|
|
15162
|
+
return c.json({ error: "memberAccess must be 'none', 'read' or 'write'" }, 400);
|
|
15041
15163
|
}
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
membershipId: c.req.param("membershipId"),
|
|
15045
|
-
access: body.access
|
|
15046
|
-
});
|
|
15047
|
-
return c.json({ grant });
|
|
15048
|
-
});
|
|
15049
|
-
router.delete("/:id/docs-access/:membershipId", async (c) => {
|
|
15050
|
-
const ctx = await resolveAuthContext(c);
|
|
15051
|
-
if (!ctx) return c.json({ error: "workspace access required" }, 403);
|
|
15052
|
-
const channelId = c.req.param("id");
|
|
15053
|
-
if (!await isChannelAdmin(ctx, channelId)) {
|
|
15054
|
-
return c.json({ error: "channel admin access required" }, 403);
|
|
15164
|
+
if (!await setChannelDocsMemberAccess(channelId, body.memberAccess)) {
|
|
15165
|
+
return c.json({ error: "no such channel" }, 404);
|
|
15055
15166
|
}
|
|
15056
|
-
|
|
15057
|
-
if (!removed) return c.json({ error: "no such docs grant" }, 404);
|
|
15058
|
-
return c.body(null, 204);
|
|
15167
|
+
return c.json({ memberAccess: body.memberAccess });
|
|
15059
15168
|
});
|
|
15060
15169
|
return router;
|
|
15061
15170
|
}
|
|
15062
15171
|
|
|
15063
15172
|
// src/dashboard/memories-routes.ts
|
|
15064
|
-
import { Hono as
|
|
15173
|
+
import { Hono as Hono28 } from "hono";
|
|
15174
|
+
async function readPatch(c) {
|
|
15175
|
+
const body = await c.req.json().catch(() => ({}));
|
|
15176
|
+
const patch = {};
|
|
15177
|
+
if (typeof body["description"] === "string") patch.description = body["description"];
|
|
15178
|
+
if (typeof body["body"] === "string") patch.body = body["body"];
|
|
15179
|
+
const type = body["type"];
|
|
15180
|
+
if (type !== void 0) {
|
|
15181
|
+
if (!isMemoryType(type)) return "invalid type";
|
|
15182
|
+
patch.type = type;
|
|
15183
|
+
}
|
|
15184
|
+
return patch;
|
|
15185
|
+
}
|
|
15065
15186
|
var requireChannelVisible = async (c, next) => {
|
|
15066
15187
|
const channel = findChannel(c.req.param("id"));
|
|
15067
15188
|
if (!channel || !canSeeChannel(channel, c.get("auth"))) {
|
|
@@ -15077,7 +15198,7 @@ var requireAgentMemory = async (c, next) => {
|
|
|
15077
15198
|
await next();
|
|
15078
15199
|
};
|
|
15079
15200
|
function createMemoriesRouter() {
|
|
15080
|
-
const router = new
|
|
15201
|
+
const router = new Hono28();
|
|
15081
15202
|
for (const path of [
|
|
15082
15203
|
"/:id/memories",
|
|
15083
15204
|
"/:id/memories/*",
|
|
@@ -15092,15 +15213,8 @@ function createMemoriesRouter() {
|
|
|
15092
15213
|
router.patch("/:id/memories/:memoryId", async (c) => {
|
|
15093
15214
|
const memoryId = c.req.param("memoryId");
|
|
15094
15215
|
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
15095
|
-
const
|
|
15096
|
-
|
|
15097
|
-
if (typeof body["description"] === "string") patch.description = body["description"];
|
|
15098
|
-
if (typeof body["body"] === "string") patch.body = body["body"];
|
|
15099
|
-
const type = body["type"];
|
|
15100
|
-
if (type !== void 0) {
|
|
15101
|
-
if (!isMemoryType(type)) return c.json({ error: "invalid type" }, 400);
|
|
15102
|
-
patch.type = type;
|
|
15103
|
-
}
|
|
15216
|
+
const patch = await readPatch(c);
|
|
15217
|
+
if (typeof patch === "string") return c.json({ error: patch }, 400);
|
|
15104
15218
|
try {
|
|
15105
15219
|
const memory = await updateChannelMemory(c.get("memoryChannelId"), memoryId, patch, "user");
|
|
15106
15220
|
if (!memory) return c.json({ error: "memory not found" }, 404);
|
|
@@ -15116,9 +15230,28 @@ function createMemoriesRouter() {
|
|
|
15116
15230
|
if (!deleted) return c.json({ error: "memory not found" }, 404);
|
|
15117
15231
|
return c.json({ ok: true });
|
|
15118
15232
|
});
|
|
15233
|
+
router.post("/:id/memories/:memoryId/promote", async (c) => {
|
|
15234
|
+
const memoryId = c.req.param("memoryId");
|
|
15235
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
15236
|
+
if (c.get("auth").role !== "admin") {
|
|
15237
|
+
return c.json({ error: "Only a Workspace Admin can move a memory to the workspace." }, 403);
|
|
15238
|
+
}
|
|
15239
|
+
if (findChannel(c.get("memoryChannelId"))?.visibility !== "public") {
|
|
15240
|
+
return c.json({ error: "Memories of a private channel stay in that channel." }, 403);
|
|
15241
|
+
}
|
|
15242
|
+
const result = await promoteChannelMemory(c.get("memoryChannelId"), memoryId);
|
|
15243
|
+
if (!result.ok && result.reason === "not-found") return c.json({ error: "memory not found" }, 404);
|
|
15244
|
+
if (!result.ok) {
|
|
15245
|
+
return c.json({ error: "The workspace already has a memory with this name." }, 409);
|
|
15246
|
+
}
|
|
15247
|
+
return c.json({ memory: result.memory });
|
|
15248
|
+
});
|
|
15119
15249
|
router.get("/:id/memory-events", async (c) => {
|
|
15120
|
-
const threadId = c.req.query("thread")
|
|
15121
|
-
if (
|
|
15250
|
+
const threadId = c.req.query("thread");
|
|
15251
|
+
if (threadId === void 0) {
|
|
15252
|
+
return c.json({ events: await listMemoryEvents(c.get("memoryChannelId"), { limit: 20 }) });
|
|
15253
|
+
}
|
|
15254
|
+
if (!isUuid(threadId)) return c.json({ error: "invalid thread id" }, 400);
|
|
15122
15255
|
return c.json({ events: await listThreadMemoryEvents(c.get("memoryChannelId"), threadId) });
|
|
15123
15256
|
});
|
|
15124
15257
|
router.post("/:id/memory-events/:eventId/undo", async (c) => {
|
|
@@ -15137,9 +15270,68 @@ function createMemoriesRouter() {
|
|
|
15137
15270
|
});
|
|
15138
15271
|
return router;
|
|
15139
15272
|
}
|
|
15273
|
+
function createScopedMemoriesRouter() {
|
|
15274
|
+
const router = new Hono28();
|
|
15275
|
+
router.use("*", requireAgentMemory);
|
|
15276
|
+
const workspace = (c) => ({
|
|
15277
|
+
scope: "workspace",
|
|
15278
|
+
workspaceId: c.get("auth").workspaceId
|
|
15279
|
+
});
|
|
15280
|
+
const mine = (c) => ({
|
|
15281
|
+
scope: "user",
|
|
15282
|
+
workspaceId: c.get("auth").workspaceId,
|
|
15283
|
+
userId: c.get("auth").userId
|
|
15284
|
+
});
|
|
15285
|
+
router.on(["PATCH", "DELETE"], "/workspace/*", async (c, next) => {
|
|
15286
|
+
if (c.get("auth").role !== "admin") {
|
|
15287
|
+
return c.json({ error: "Only a Workspace Admin can change workspace memory." }, 403);
|
|
15288
|
+
}
|
|
15289
|
+
await next();
|
|
15290
|
+
});
|
|
15291
|
+
for (const [path, target] of [
|
|
15292
|
+
["/workspace", workspace],
|
|
15293
|
+
["/me", mine]
|
|
15294
|
+
]) {
|
|
15295
|
+
router.get(path, async (c) => c.json({ memories: await listMemories(target(c)) }));
|
|
15296
|
+
router.patch(`${path}/:memoryId`, async (c) => {
|
|
15297
|
+
const memoryId = c.req.param("memoryId") ?? "";
|
|
15298
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
15299
|
+
const patch = await readPatch(c);
|
|
15300
|
+
if (typeof patch === "string") return c.json({ error: patch }, 400);
|
|
15301
|
+
try {
|
|
15302
|
+
const memory = await updateMemory(target(c), memoryId, patch, "user");
|
|
15303
|
+
if (!memory) return c.json({ error: "memory not found" }, 404);
|
|
15304
|
+
return c.json({ memory });
|
|
15305
|
+
} catch (err) {
|
|
15306
|
+
return c.json({ error: err.message }, 400);
|
|
15307
|
+
}
|
|
15308
|
+
});
|
|
15309
|
+
router.delete(`${path}/:memoryId`, async (c) => {
|
|
15310
|
+
const memoryId = c.req.param("memoryId") ?? "";
|
|
15311
|
+
if (!isUuid(memoryId)) return c.json({ error: "invalid memory id" }, 400);
|
|
15312
|
+
if (!await deleteMemory(target(c), memoryId, "user")) {
|
|
15313
|
+
return c.json({ error: "memory not found" }, 404);
|
|
15314
|
+
}
|
|
15315
|
+
return c.json({ ok: true });
|
|
15316
|
+
});
|
|
15317
|
+
}
|
|
15318
|
+
router.get("/me-settings", async (c) => {
|
|
15319
|
+
const auth = c.get("auth");
|
|
15320
|
+
const membership = await getMembershipByUserId(auth.workspaceId, auth.userId);
|
|
15321
|
+
return c.json({ autoPaused: membership?.memoryAutoPaused ?? false });
|
|
15322
|
+
});
|
|
15323
|
+
router.put("/me-settings", async (c) => {
|
|
15324
|
+
const body = await c.req.json().catch(() => ({}));
|
|
15325
|
+
if (typeof body["autoPaused"] !== "boolean") return c.json({ error: "autoPaused must be a boolean" }, 400);
|
|
15326
|
+
const auth = c.get("auth");
|
|
15327
|
+
await setMemoryAutoPaused(auth.workspaceId, auth.userId, body["autoPaused"]);
|
|
15328
|
+
return c.json({ autoPaused: body["autoPaused"] });
|
|
15329
|
+
});
|
|
15330
|
+
return router;
|
|
15331
|
+
}
|
|
15140
15332
|
|
|
15141
15333
|
// src/dashboard/onboarding-routes.ts
|
|
15142
|
-
import { Hono as
|
|
15334
|
+
import { Hono as Hono29 } from "hono";
|
|
15143
15335
|
|
|
15144
15336
|
// src/dashboard/unique-channel-name.ts
|
|
15145
15337
|
function uniqueChannelName(raw, taken) {
|
|
@@ -15153,7 +15345,7 @@ function uniqueChannelName(raw, taken) {
|
|
|
15153
15345
|
// src/dashboard/workspace-greeting.ts
|
|
15154
15346
|
var GROQ_API_URL8 = "https://api.groq.com/openai/v1/chat/completions";
|
|
15155
15347
|
var GREETING_MODEL = "openai/gpt-oss-20b";
|
|
15156
|
-
var
|
|
15348
|
+
var TIMEOUT_MS2 = 6e3;
|
|
15157
15349
|
var MAX_LISTED_CHANNELS = 8;
|
|
15158
15350
|
var GUIDES = [
|
|
15159
15351
|
"Welcome to Flyspace",
|
|
@@ -15203,7 +15395,7 @@ async function groqGreeting(input2, apiKey) {
|
|
|
15203
15395
|
].join("\n");
|
|
15204
15396
|
const res = await fetch(GROQ_API_URL8, {
|
|
15205
15397
|
method: "POST",
|
|
15206
|
-
signal: AbortSignal.timeout(
|
|
15398
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2),
|
|
15207
15399
|
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
15208
15400
|
body: JSON.stringify({
|
|
15209
15401
|
model: GREETING_MODEL,
|
|
@@ -15240,7 +15432,7 @@ async function buildGreeting(input2) {
|
|
|
15240
15432
|
// src/dashboard/onboarding-routes.ts
|
|
15241
15433
|
var MAX_FOLDERS = 20;
|
|
15242
15434
|
function createOnboardingRouter(agent) {
|
|
15243
|
-
const app = new
|
|
15435
|
+
const app = new Hono29();
|
|
15244
15436
|
app.get("/scan", async (c) => {
|
|
15245
15437
|
const ctx = await resolveAuthContext(c);
|
|
15246
15438
|
if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
|
|
@@ -15406,13 +15598,13 @@ function firstSentence(body) {
|
|
|
15406
15598
|
function threadLabel(row) {
|
|
15407
15599
|
return row.title?.trim() || firstSentence(row.thread_replies?.[0]?.body);
|
|
15408
15600
|
}
|
|
15409
|
-
async function resolveThreadLinks(
|
|
15601
|
+
async function resolveThreadLinks(sessions2) {
|
|
15410
15602
|
const chatIds = [
|
|
15411
|
-
...new Set(
|
|
15603
|
+
...new Set(sessions2.filter((s) => s.channelId.startsWith("chat:")).map((s) => s.threadTs))
|
|
15412
15604
|
];
|
|
15413
15605
|
const externalKeys = [
|
|
15414
15606
|
...new Set(
|
|
15415
|
-
|
|
15607
|
+
sessions2.filter((s) => !s.channelId.startsWith("chat:")).map((s) => `${s.channelId}:${s.threadTs}`)
|
|
15416
15608
|
)
|
|
15417
15609
|
];
|
|
15418
15610
|
const chat = /* @__PURE__ */ new Map();
|
|
@@ -15445,8 +15637,8 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15445
15637
|
api.get("/host/status", async (c) => {
|
|
15446
15638
|
const online = agent.isHostOnline?.(getProfile()) ?? false;
|
|
15447
15639
|
if (!online) {
|
|
15448
|
-
const
|
|
15449
|
-
return c.json({ online: false, reason:
|
|
15640
|
+
const failure3 = agent.getHostOfflineReason?.() ?? null;
|
|
15641
|
+
return c.json({ online: false, reason: failure3?.reason ?? null, since: failure3?.since ?? null });
|
|
15450
15642
|
}
|
|
15451
15643
|
const auth = c.get("auth");
|
|
15452
15644
|
const paused = await getAgentPaused(auth.workspaceId);
|
|
@@ -15497,6 +15689,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15497
15689
|
api.route("/onboarding", createOnboardingRouter(agent));
|
|
15498
15690
|
api.route("/channels", createChannelMembersRouter());
|
|
15499
15691
|
api.route("/channels", createMemoriesRouter());
|
|
15692
|
+
api.route("/memories", createScopedMemoriesRouter());
|
|
15500
15693
|
api.route("/docs", createDocShareListRouter());
|
|
15501
15694
|
api.get("/users", (c) => c.json({ users: listActiveUsers() }));
|
|
15502
15695
|
api.route("/categories", createCategoryWriteRouter());
|
|
@@ -15504,17 +15697,17 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15504
15697
|
const auto = loadEffectiveAutomations();
|
|
15505
15698
|
const runs = /* @__PURE__ */ new Map();
|
|
15506
15699
|
try {
|
|
15507
|
-
for (const [name,
|
|
15508
|
-
runs.set(name, { lastRun:
|
|
15700
|
+
for (const [name, run4] of await lastRunsForWorkspace(getProfile())) {
|
|
15701
|
+
runs.set(name, { lastRun: run4.lastRunAt.toISOString(), lastStatus: run4.lastStatus });
|
|
15509
15702
|
}
|
|
15510
15703
|
} catch {
|
|
15511
|
-
if (
|
|
15704
|
+
if (existsSync23(paths.DB_FILE)) {
|
|
15512
15705
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
15513
15706
|
try {
|
|
15514
|
-
for (const
|
|
15515
|
-
runs.set(
|
|
15516
|
-
lastRun:
|
|
15517
|
-
lastStatus:
|
|
15707
|
+
for (const run4 of runStore.listLastRuns()) {
|
|
15708
|
+
runs.set(run4.name, {
|
|
15709
|
+
lastRun: run4.lastRunAt.toISOString(),
|
|
15710
|
+
lastStatus: run4.lastStatus
|
|
15518
15711
|
});
|
|
15519
15712
|
}
|
|
15520
15713
|
} finally {
|
|
@@ -15570,7 +15763,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15570
15763
|
const runs = (await recentRunsForWorkspace(getProfile(), 200)).map(toResponse5);
|
|
15571
15764
|
return c.json({ runs });
|
|
15572
15765
|
} catch {
|
|
15573
|
-
if (!
|
|
15766
|
+
if (!existsSync23(paths.DB_FILE)) return c.json({ runs: [] });
|
|
15574
15767
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
15575
15768
|
try {
|
|
15576
15769
|
const runs = runStore.listRuns(200).map(toResponse5);
|
|
@@ -15692,7 +15885,7 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15692
15885
|
} catch {
|
|
15693
15886
|
latest = /* @__PURE__ */ new Map();
|
|
15694
15887
|
totals = /* @__PURE__ */ new Map();
|
|
15695
|
-
if (
|
|
15888
|
+
if (existsSync23(paths.DB_FILE)) {
|
|
15696
15889
|
const usageStore = new UsageStore(paths.DB_FILE);
|
|
15697
15890
|
try {
|
|
15698
15891
|
latest = usageStore.latestByThread(threadTsList);
|
|
@@ -15703,25 +15896,25 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15703
15896
|
}
|
|
15704
15897
|
}
|
|
15705
15898
|
const threadLinks = await resolveThreadLinks(raw);
|
|
15706
|
-
const
|
|
15707
|
-
const
|
|
15708
|
-
const turns =
|
|
15709
|
-
const total =
|
|
15899
|
+
const sessions2 = raw.map((s) => {
|
|
15900
|
+
const run4 = latest.get(s.threadTs);
|
|
15901
|
+
const turns = run4?.numTurns ?? 1;
|
|
15902
|
+
const total = run4 ? run4.inputTokens + run4.cacheReadTokens + run4.cacheCreationTokens : void 0;
|
|
15710
15903
|
const link = s.channelId.startsWith("chat:") ? threadLinks.chat.get(s.threadTs) : threadLinks.external.get(`${s.channelId}:${s.threadTs}`);
|
|
15711
15904
|
return {
|
|
15712
15905
|
...s,
|
|
15713
15906
|
channelLabel: channelLabel(s.channelId),
|
|
15714
|
-
model:
|
|
15715
|
-
numTurns:
|
|
15907
|
+
model: run4?.model,
|
|
15908
|
+
numTurns: run4?.numTurns,
|
|
15716
15909
|
contextTokens: total !== void 0 ? Math.round(total / (turns > 0 ? turns : 1)) : void 0,
|
|
15717
|
-
rolledOver:
|
|
15910
|
+
rolledOver: run4?.label === "rollover",
|
|
15718
15911
|
threadTitle: link?.title ?? void 0,
|
|
15719
15912
|
chatChannelId: link?.chatChannelId,
|
|
15720
15913
|
chatThreadId: link?.chatThreadId,
|
|
15721
15914
|
costUsd: totals.get(s.threadTs)?.costUsd
|
|
15722
15915
|
};
|
|
15723
15916
|
});
|
|
15724
|
-
return c.json({ sessions });
|
|
15917
|
+
return c.json({ sessions: sessions2 });
|
|
15725
15918
|
});
|
|
15726
15919
|
const LOGO_SIZE = 256;
|
|
15727
15920
|
const MAX_LOGO_BYTES = 2 * 1024 * 1024;
|
|
@@ -15747,18 +15940,18 @@ function registerDataRoutes(api, agent = {}) {
|
|
|
15747
15940
|
// src/dashboard/device-auth-routes.ts
|
|
15748
15941
|
import { verify } from "crypto";
|
|
15749
15942
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
15750
|
-
import { Hono as
|
|
15943
|
+
import { Hono as Hono31 } from "hono";
|
|
15751
15944
|
|
|
15752
15945
|
// src/dashboard/agent-bridge.ts
|
|
15753
|
-
import { randomBytes as randomBytes8, randomUUID as
|
|
15946
|
+
import { randomBytes as randomBytes8, randomUUID as randomUUID5, sign as signRaw, timingSafeEqual as timingSafeEqual2, verify as verifyRaw } from "crypto";
|
|
15754
15947
|
|
|
15755
15948
|
// src/dashboard/docs-relay.ts
|
|
15756
|
-
import { Hono as
|
|
15949
|
+
import { Hono as Hono30 } from "hono";
|
|
15757
15950
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
15758
15951
|
var FORWARDED_HEADERS = ["content-type", "host", "accept-language"];
|
|
15759
15952
|
var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
|
|
15760
15953
|
async function handleDocsRequest(req) {
|
|
15761
|
-
const app = new
|
|
15954
|
+
const app = new Hono30();
|
|
15762
15955
|
app.use("*", async (c, next) => {
|
|
15763
15956
|
c.set("auth", req.auth);
|
|
15764
15957
|
await next();
|
|
@@ -15817,24 +16010,24 @@ function relay2(agent) {
|
|
|
15817
16010
|
}
|
|
15818
16011
|
|
|
15819
16012
|
// src/dashboard/agent-bridge.ts
|
|
15820
|
-
import { existsSync as
|
|
16013
|
+
import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
|
|
15821
16014
|
import dotenv from "dotenv";
|
|
15822
16015
|
import { WebSocket, WebSocketServer } from "ws";
|
|
15823
16016
|
|
|
15824
16017
|
// src/dashboard/host-identity.ts
|
|
15825
16018
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, generateKeyPairSync as generateKeyPairSync2 } from "crypto";
|
|
15826
|
-
import { existsSync as
|
|
15827
|
-
import { dirname as
|
|
16019
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
|
|
16020
|
+
import { dirname as dirname8, join as join24 } from "path";
|
|
15828
16021
|
function hostPrivateKeyPath() {
|
|
15829
|
-
return process.env["FLYSPACE_HOST_KEY_PATH"] ||
|
|
16022
|
+
return process.env["FLYSPACE_HOST_KEY_PATH"] || join24(paths.BASE_CONFIG_DIR, "host.key");
|
|
15830
16023
|
}
|
|
15831
16024
|
function loadHostPrivateKey(path = hostPrivateKeyPath()) {
|
|
15832
|
-
if (!
|
|
16025
|
+
if (!existsSync24(path)) {
|
|
15833
16026
|
throw new Error(
|
|
15834
16027
|
`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.`
|
|
15835
16028
|
);
|
|
15836
16029
|
}
|
|
15837
|
-
return createPrivateKey2(
|
|
16030
|
+
return createPrivateKey2(readFileSync17(path, "utf8"));
|
|
15838
16031
|
}
|
|
15839
16032
|
function loadHostPublicKey() {
|
|
15840
16033
|
const b64 = process.env["FLYSPACE_HOST_PUBLIC_KEY"];
|
|
@@ -16079,7 +16272,7 @@ async function userEmail(userId) {
|
|
|
16079
16272
|
|
|
16080
16273
|
// src/dashboard/doc-share-publisher.ts
|
|
16081
16274
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
16082
|
-
import { readFileSync as
|
|
16275
|
+
import { readFileSync as readFileSync18, statSync as statSync9 } from "fs";
|
|
16083
16276
|
import { extname as extname2, posix } from "path";
|
|
16084
16277
|
import { CopyObjectCommand, PutObjectCommand as PutObjectCommand5 } from "@aws-sdk/client-s3";
|
|
16085
16278
|
|
|
@@ -16266,7 +16459,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16266
16459
|
}
|
|
16267
16460
|
if (!stat.isFile()) return { status: "not-found" };
|
|
16268
16461
|
if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
|
|
16269
|
-
const source =
|
|
16462
|
+
const source = readFileSync18(abs, "utf8");
|
|
16270
16463
|
const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
|
|
16271
16464
|
if (kind === "excalidraw") {
|
|
16272
16465
|
try {
|
|
@@ -16346,7 +16539,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16346
16539
|
let text = null;
|
|
16347
16540
|
try {
|
|
16348
16541
|
const size = statSync9(docAbs).size;
|
|
16349
|
-
if (size <= budget) text =
|
|
16542
|
+
if (size <= budget) text = readFileSync18(docAbs, "utf8");
|
|
16350
16543
|
} catch {
|
|
16351
16544
|
continue;
|
|
16352
16545
|
}
|
|
@@ -16363,7 +16556,7 @@ async function buildSnapshot(cwd, sourcePath, shareId) {
|
|
|
16363
16556
|
let body;
|
|
16364
16557
|
try {
|
|
16365
16558
|
if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
|
|
16366
|
-
body =
|
|
16559
|
+
body = readFileSync18(resolved2.abs);
|
|
16367
16560
|
} catch {
|
|
16368
16561
|
continue;
|
|
16369
16562
|
}
|
|
@@ -16405,7 +16598,7 @@ async function publishDocShare(input2) {
|
|
|
16405
16598
|
...HTML_FEATURES,
|
|
16406
16599
|
brandingEnabled: resolveFeatures(input2.features, existing).brandingEnabled
|
|
16407
16600
|
} : html ? HTML_FEATURES : resolveFeatures(input2.features, existing);
|
|
16408
|
-
const
|
|
16601
|
+
const snapshot2 = {
|
|
16409
16602
|
...built,
|
|
16410
16603
|
sourceLang: html ? null : await detectSourceLanguage(built.title, built.content)
|
|
16411
16604
|
};
|
|
@@ -16415,8 +16608,8 @@ async function publishDocShare(input2) {
|
|
|
16415
16608
|
const renamed = await setDocShareSlug(existing.id, slug);
|
|
16416
16609
|
if (!renamed) return { status: "not-found" };
|
|
16417
16610
|
}
|
|
16418
|
-
const share2 = await updateDocShareSnapshot(existing.id,
|
|
16419
|
-
const stale = existing.assetKeys.filter((key) => !
|
|
16611
|
+
const share2 = await updateDocShareSnapshot(existing.id, snapshot2, features);
|
|
16612
|
+
const stale = existing.assetKeys.filter((key) => !snapshot2.assetKeys.includes(key));
|
|
16420
16613
|
await deleteShareAssets(stale);
|
|
16421
16614
|
if (!features.audioEnabled && existing.audioEnabled) await dropShareAudio(existing);
|
|
16422
16615
|
void queueShareAudio(share2);
|
|
@@ -16430,7 +16623,7 @@ async function publishDocShare(input2) {
|
|
|
16430
16623
|
channelId: input2.channelId,
|
|
16431
16624
|
sourcePath: input2.sourcePath,
|
|
16432
16625
|
createdBy: input2.createdBy,
|
|
16433
|
-
...
|
|
16626
|
+
...snapshot2,
|
|
16434
16627
|
...features
|
|
16435
16628
|
});
|
|
16436
16629
|
void queueShareAudio(share);
|
|
@@ -16456,10 +16649,14 @@ var METHODS = {
|
|
|
16456
16649
|
threads: ["GET", "HEAD", "POST", "PATCH"],
|
|
16457
16650
|
thread_replies: ["GET", "HEAD", "POST", "PATCH"],
|
|
16458
16651
|
memberships: ["GET", "HEAD"],
|
|
16652
|
+
feature_flags: ["GET", "HEAD"],
|
|
16459
16653
|
channel_members: ["GET", "HEAD"],
|
|
16460
|
-
|
|
16654
|
+
memories: ALL,
|
|
16655
|
+
memory_events: ["GET", "HEAD", "POST", "PATCH"]
|
|
16461
16656
|
};
|
|
16462
|
-
var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members"
|
|
16657
|
+
var CHANNEL_SCOPED = /* @__PURE__ */ new Set(["channel_members"]);
|
|
16658
|
+
var MEMORY_TABLES = /* @__PURE__ */ new Set(["memories", "memory_events"]);
|
|
16659
|
+
var MEMORY_KEYS = ["channel_id", "workspace_id", "user_id", "scope"];
|
|
16463
16660
|
var FORWARDED_HEADERS2 = ["accept", "content-type", "prefer", "range"];
|
|
16464
16661
|
var deny = (message, status = 403) => ({
|
|
16465
16662
|
status,
|
|
@@ -16566,7 +16763,30 @@ async function proxyHostDb(workspaces, req, forward, assigned = workspaces ?? []
|
|
|
16566
16763
|
}
|
|
16567
16764
|
}
|
|
16568
16765
|
params.append(slugColumn, inList(scope));
|
|
16569
|
-
} else if (table
|
|
16766
|
+
} else if (MEMORY_TABLES.has(table)) {
|
|
16767
|
+
const [workspaceIds, channelIds] = await Promise.all([
|
|
16768
|
+
ownedWorkspaceIds(forward, workspaces),
|
|
16769
|
+
ownedChannelIds(forward, workspaces)
|
|
16770
|
+
]);
|
|
16771
|
+
if (!workspaceIds || !channelIds) return deny("Could not resolve the device workspaces.", 502);
|
|
16772
|
+
if (method === "POST") {
|
|
16773
|
+
const ok3 = rows.length > 0 && rows.every((r) => {
|
|
16774
|
+
const channel = r["channel_id"];
|
|
16775
|
+
if (channel !== void 0 && channel !== null) {
|
|
16776
|
+
return typeof channel === "string" && channelIds.includes(channel);
|
|
16777
|
+
}
|
|
16778
|
+
return typeof r["workspace_id"] === "string" && workspaceIds.includes(r["workspace_id"]);
|
|
16779
|
+
});
|
|
16780
|
+
if (!ok3) return deny(`Every ${table} row must belong to a channel or workspace of the device.`);
|
|
16781
|
+
}
|
|
16782
|
+
if (method === "PATCH") {
|
|
16783
|
+
const patch = bodyRows(req.body);
|
|
16784
|
+
if (!patch || patch.some((r) => MEMORY_KEYS.some((k) => k in r))) {
|
|
16785
|
+
return deny(`A ${table} row cannot move to another channel, workspace or person.`);
|
|
16786
|
+
}
|
|
16787
|
+
}
|
|
16788
|
+
params.append("workspace_id", inList(workspaceIds));
|
|
16789
|
+
} else if (table === "memberships" || table === "feature_flags") {
|
|
16570
16790
|
const workspaceIds = await ownedWorkspaceIds(forward, workspaces);
|
|
16571
16791
|
if (!workspaceIds) return deny("Could not resolve the device workspaces.", 502);
|
|
16572
16792
|
params.append("workspace_id", inList(workspaceIds));
|
|
@@ -16648,7 +16868,7 @@ function dataSocketUrl() {
|
|
|
16648
16868
|
return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
|
|
16649
16869
|
}
|
|
16650
16870
|
function listLocalSessions() {
|
|
16651
|
-
if (!
|
|
16871
|
+
if (!existsSync25(paths.DB_FILE)) return [];
|
|
16652
16872
|
const store = new SessionStore(paths.DB_FILE);
|
|
16653
16873
|
try {
|
|
16654
16874
|
return store.listSessions();
|
|
@@ -16664,6 +16884,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
16664
16884
|
cancelAgentRun: (threadId) => chatAgent.cancel(threadId),
|
|
16665
16885
|
regenerateAgentRun: (threadId) => chatAgent.regenerate(threadId),
|
|
16666
16886
|
listAgentRuns: () => chatAgent.runningThreadIds(),
|
|
16887
|
+
completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
|
|
16667
16888
|
listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
|
|
16668
16889
|
listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
|
|
16669
16890
|
getAgentIdentities: () => resolveAgentIdentities(),
|
|
@@ -16674,6 +16895,7 @@ function localAgentCallbacks(chatAgent) {
|
|
|
16674
16895
|
saveLocalDashboardSettings: (patch) => saveDashboardSettings(patch),
|
|
16675
16896
|
writeSkill: (ref, write) => writeSkillAndSync(ref, write),
|
|
16676
16897
|
outputStylesRequest: (req) => handleOutputStylesRequest(req),
|
|
16898
|
+
providerLoginRequest: (req) => handleProviderLoginRequest(req),
|
|
16677
16899
|
docsRequest: (req) => handleDocsRequest(req),
|
|
16678
16900
|
listSessions: () => listLocalSessions(),
|
|
16679
16901
|
scanOnboarding: () => scanOnboarding(),
|
|
@@ -16683,14 +16905,14 @@ function localAgentCallbacks(chatAgent) {
|
|
|
16683
16905
|
}
|
|
16684
16906
|
var HOST_CONFIG_PUSH_INTERVAL_MS = 6e4;
|
|
16685
16907
|
function buildHostConfigSnapshot() {
|
|
16686
|
-
const
|
|
16908
|
+
const snapshot2 = {};
|
|
16687
16909
|
for (const profile of listProfiles()) {
|
|
16688
|
-
|
|
16689
|
-
env:
|
|
16910
|
+
snapshot2[profile] = runWithProfile(profile, () => ({
|
|
16911
|
+
env: existsSync25(paths.CONFIG_FILE) ? dotenv.parse(readFileSync19(paths.CONFIG_FILE, "utf-8")) : {},
|
|
16690
16912
|
agentModel: loadDashboardSettings().agent
|
|
16691
16913
|
}));
|
|
16692
16914
|
}
|
|
16693
|
-
return
|
|
16915
|
+
return snapshot2;
|
|
16694
16916
|
}
|
|
16695
16917
|
function startLocalProfileConfigHydration() {
|
|
16696
16918
|
setProfileConfigCache(buildHostConfigSnapshot());
|
|
@@ -16699,6 +16921,7 @@ function startLocalProfileConfigHydration() {
|
|
|
16699
16921
|
}
|
|
16700
16922
|
var CALL_TIMEOUT_MS = 15e3;
|
|
16701
16923
|
var DOCS_CALL_TIMEOUT_MS = 12e4;
|
|
16924
|
+
var MEMORY_EXTRACTION_TIMEOUT_MS = 165e3;
|
|
16702
16925
|
var AUTH_TIMEOUT_MS = 5e3;
|
|
16703
16926
|
var HEARTBEAT_INTERVAL_MS = 15e3;
|
|
16704
16927
|
var HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 2.5;
|
|
@@ -16898,7 +17121,7 @@ function callHost(method, args, timeoutMs = CALL_TIMEOUT_MS) {
|
|
|
16898
17121
|
return Promise.resolve(null);
|
|
16899
17122
|
}
|
|
16900
17123
|
if (method === "docsRequest") console.log(`[host-socket] docsRequest for "${profile}" -> device ${hosts.get(ws)?.deviceId ?? "legacy"}`);
|
|
16901
|
-
const id =
|
|
17124
|
+
const id = randomUUID5();
|
|
16902
17125
|
return new Promise((resolve7) => {
|
|
16903
17126
|
const timer2 = setTimeout(() => {
|
|
16904
17127
|
pending2.delete(id);
|
|
@@ -16926,6 +17149,9 @@ function remoteAgentCallbacks() {
|
|
|
16926
17149
|
cancelAgentRun: async (threadId) => await callHost("cancelAgentRun", [threadId]) ?? false,
|
|
16927
17150
|
regenerateAgentRun: async (threadId) => await callHost("regenerateAgentRun", [threadId]) ?? false,
|
|
16928
17151
|
listAgentRuns: async () => await callHost("listAgentRuns", []) ?? [],
|
|
17152
|
+
// `null` (no Host, or no answer in time) passes through: the extractor
|
|
17153
|
+
// reads it as a failed call and retries the thread next sweep.
|
|
17154
|
+
completeMemoryExtraction: (req) => callHost("completeMemoryExtraction", [req], MEMORY_EXTRACTION_TIMEOUT_MS),
|
|
16929
17155
|
// Deliberately passes `null` through rather than defaulting to `[]`: with
|
|
16930
17156
|
// no Host connected, claiming every folder is fine would be a guess.
|
|
16931
17157
|
listMissingCwds: () => callHost("listMissingCwds", []),
|
|
@@ -16959,6 +17185,8 @@ function remoteAgentCallbacks() {
|
|
|
16959
17185
|
writeSkill: (ref, write) => callHost("writeSkill", [ref, write]),
|
|
16960
17186
|
// Same posture as writeSkill: `null` becomes a 503.
|
|
16961
17187
|
outputStylesRequest: (req) => callHost("outputStylesRequest", [req]),
|
|
17188
|
+
// Submitting a Claude code waits on the CLI to mint the token.
|
|
17189
|
+
providerLoginRequest: (req) => callHost("providerLoginRequest", [req], DOCS_CALL_TIMEOUT_MS),
|
|
16962
17190
|
// Same posture again. Longer timeout: a first translation of a doc runs a
|
|
16963
17191
|
// model call on the Host before it answers.
|
|
16964
17192
|
docsRequest: (req) => callHost("docsRequest", [req], DOCS_CALL_TIMEOUT_MS),
|
|
@@ -17223,7 +17451,7 @@ async function boundWorkspaceIds(publicKey, userId) {
|
|
|
17223
17451
|
return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
|
|
17224
17452
|
}
|
|
17225
17453
|
function createDeviceAuthRouter() {
|
|
17226
|
-
const app = new
|
|
17454
|
+
const app = new Hono31();
|
|
17227
17455
|
app.post("/code", async (c) => {
|
|
17228
17456
|
const body = await readBody(c);
|
|
17229
17457
|
const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
|
|
@@ -17387,7 +17615,7 @@ function createDeviceAuthRouter() {
|
|
|
17387
17615
|
|
|
17388
17616
|
// src/dashboard/api.ts
|
|
17389
17617
|
function createApiRouter(agent = {}, mode = "all") {
|
|
17390
|
-
const api = new
|
|
17618
|
+
const api = new Hono32();
|
|
17391
17619
|
api.use("*", async (c, next) => {
|
|
17392
17620
|
const known = await listKnownWorkspaces();
|
|
17393
17621
|
const fromHost = profileFromHost(trustedHost(c), known);
|
|
@@ -17411,6 +17639,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
17411
17639
|
const resolved2 = requested && known.includes(requested) ? requested : profileFromHost(trustedHost(c), known);
|
|
17412
17640
|
if (!resolved2) {
|
|
17413
17641
|
return c.json({
|
|
17642
|
+
unknownWorkspace: isUnknownWorkspaceHost(trustedHost(c), known),
|
|
17414
17643
|
theme: BRANDING_DEFAULTS.theme,
|
|
17415
17644
|
font: BRANDING_DEFAULTS.font,
|
|
17416
17645
|
displayName: "Flyspace",
|
|
@@ -17442,6 +17671,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
17442
17671
|
});
|
|
17443
17672
|
registerMirroredSkillRoutes(api, agent);
|
|
17444
17673
|
registerRelayedOutputStylesRoutes(api, agent);
|
|
17674
|
+
registerRelayedProviderLoginRoutes(api, agent);
|
|
17445
17675
|
api.get("/channels/:id/docs/flat", requireDocsAccess, async (c) => {
|
|
17446
17676
|
const { cwd } = resolveChannelConfig(c.req.param("id"));
|
|
17447
17677
|
const query = c.req.query("q") ?? "";
|
|
@@ -17499,7 +17729,7 @@ function createApiRouter(agent = {}, mode = "all") {
|
|
|
17499
17729
|
}
|
|
17500
17730
|
|
|
17501
17731
|
// src/dashboard/app-icon-routes.ts
|
|
17502
|
-
import { Hono as
|
|
17732
|
+
import { Hono as Hono33 } from "hono";
|
|
17503
17733
|
var NEUTRAL_MANIFEST = {
|
|
17504
17734
|
id: "/",
|
|
17505
17735
|
name: "Flyspace",
|
|
@@ -17537,7 +17767,7 @@ async function withResolvedProfile2(c, next) {
|
|
|
17537
17767
|
return profile ? runWithProfile(profile, () => next()) : next();
|
|
17538
17768
|
}
|
|
17539
17769
|
function createAppIconRouter() {
|
|
17540
|
-
const app = new
|
|
17770
|
+
const app = new Hono33();
|
|
17541
17771
|
app.get("/manifest.json", withResolvedProfile2, async (c) => {
|
|
17542
17772
|
if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
|
|
17543
17773
|
const profile = getProfile();
|
|
@@ -17577,7 +17807,7 @@ function pngResponse(c, png) {
|
|
|
17577
17807
|
}
|
|
17578
17808
|
|
|
17579
17809
|
// src/dashboard/stripe-routes.ts
|
|
17580
|
-
import { Hono as
|
|
17810
|
+
import { Hono as Hono34 } from "hono";
|
|
17581
17811
|
function planStatusFor(status) {
|
|
17582
17812
|
const known = ["trialing", "active", "paused", "canceled"];
|
|
17583
17813
|
return known.includes(status) ? status : null;
|
|
@@ -17620,7 +17850,7 @@ async function handleCardAdded(session) {
|
|
|
17620
17850
|
}
|
|
17621
17851
|
}
|
|
17622
17852
|
function createStripeWebhookRouter() {
|
|
17623
|
-
const stripeRouter = new
|
|
17853
|
+
const stripeRouter = new Hono34();
|
|
17624
17854
|
stripeRouter.post("/webhook", async (c) => {
|
|
17625
17855
|
const secret = process.env["STRIPE_WEBHOOK_SECRET"];
|
|
17626
17856
|
const signature = c.req.header("stripe-signature");
|
|
@@ -17731,7 +17961,7 @@ function startHostStatusBroadcast() {
|
|
|
17731
17961
|
|
|
17732
17962
|
// src/dashboard/server.ts
|
|
17733
17963
|
var DEFAULT_DASHBOARD_PORT = 4747;
|
|
17734
|
-
var WEB_DIST =
|
|
17964
|
+
var WEB_DIST = join25(paths.PACKAGE_ROOT, "web", "dist");
|
|
17735
17965
|
var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
|
|
17736
17966
|
async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
|
|
17737
17967
|
for (let port = start; port < start + attempts; port++) {
|
|
@@ -17780,7 +18010,8 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17780
18010
|
const mirrorsSkills = mode === "host" || process.env["FLYSPACE_SKILL_SYNC"] === "1";
|
|
17781
18011
|
const stopSkillSync = mirrorsSkills ? installSkillSync(listProfiles()) : () => {
|
|
17782
18012
|
};
|
|
17783
|
-
const app = new
|
|
18013
|
+
const app = new Hono35();
|
|
18014
|
+
app.onError(reportingErrorHandler);
|
|
17784
18015
|
app.use("*", async (c, next) => {
|
|
17785
18016
|
await next();
|
|
17786
18017
|
if (c.req.path.startsWith("/assets/")) {
|
|
@@ -17819,14 +18050,14 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
|
|
|
17819
18050
|
} else {
|
|
17820
18051
|
app.route("/share", createPublicShareAssetRouter(WEB_DIST));
|
|
17821
18052
|
app.route("/", createAppIconRouter());
|
|
17822
|
-
if (
|
|
18053
|
+
if (existsSync26(WEB_DIST) || existsSync26(join25(paths.PACKAGE_ROOT, "web", "package.json"))) {
|
|
17823
18054
|
app.use("/*", serveStatic({ root: WEB_DIST }));
|
|
17824
18055
|
}
|
|
17825
18056
|
app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
|
|
17826
|
-
const spaFallback = serveStatic({ path:
|
|
18057
|
+
const spaFallback = serveStatic({ path: join25(WEB_DIST, "index.html") });
|
|
17827
18058
|
app.get("*", async (c, next) => {
|
|
17828
|
-
if (!
|
|
17829
|
-
const isCheckout =
|
|
18059
|
+
if (!existsSync26(join25(WEB_DIST, "index.html"))) {
|
|
18060
|
+
const isCheckout = existsSync26(join25(paths.PACKAGE_ROOT, "web", "package.json"));
|
|
17830
18061
|
return c.text(
|
|
17831
18062
|
isCheckout ? `Workspace UI build missing (${WEB_DIST}).
|
|
17832
18063
|
Run \`pnpm --dir web build\` (or \`pnpm --dir web dev\`).
|
|
@@ -18264,7 +18495,7 @@ async function threadReplies(client2, nameOrId, ts2) {
|
|
|
18264
18495
|
async function postMessage(client2, args) {
|
|
18265
18496
|
const channelArg = getArg2(args, "--channel");
|
|
18266
18497
|
const textFilePath = getArg2(args, "--text-file");
|
|
18267
|
-
const text = textFilePath ?
|
|
18498
|
+
const text = textFilePath ? readFileSync20(textFilePath, "utf8") : getArg2(args, "--text");
|
|
18268
18499
|
const threadTs = getArg2(args, "--thread");
|
|
18269
18500
|
const markdown = args.includes("--markdown");
|
|
18270
18501
|
const broadcast = args.includes("--broadcast") && Boolean(threadTs);
|
|
@@ -18362,7 +18593,7 @@ function defaultClaudeConfigDir() {
|
|
|
18362
18593
|
return process.env.CLAUDE_CONFIG_DIR || resolve6(homedir9(), ".claude");
|
|
18363
18594
|
}
|
|
18364
18595
|
function hasClaudeCredentials(configDir2) {
|
|
18365
|
-
if (
|
|
18596
|
+
if (existsSync27(resolve6(configDir2, ".credentials.json"))) {
|
|
18366
18597
|
return true;
|
|
18367
18598
|
}
|
|
18368
18599
|
if (process.platform === "darwin") {
|
|
@@ -18419,7 +18650,7 @@ async function ensureClaudeCodeReady() {
|
|
|
18419
18650
|
}
|
|
18420
18651
|
function getManifestYaml(botName) {
|
|
18421
18652
|
const manifestPath = resolve6(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
18422
|
-
const raw =
|
|
18653
|
+
const raw = readFileSync20(manifestPath, "utf-8");
|
|
18423
18654
|
return raw.replace(/name: Custie/g, `name: ${botName}`);
|
|
18424
18655
|
}
|
|
18425
18656
|
async function stepPersonalise() {
|
|
@@ -18632,7 +18863,7 @@ async function stepBrowserTokens(botName) {
|
|
|
18632
18863
|
})
|
|
18633
18864
|
));
|
|
18634
18865
|
const DEBUG = process.argv.includes("--debug");
|
|
18635
|
-
const USER_DATA_DIR = resolve6(
|
|
18866
|
+
const USER_DATA_DIR = resolve6(tmpdir5(), "custie-playwright-profile");
|
|
18636
18867
|
let context;
|
|
18637
18868
|
try {
|
|
18638
18869
|
log4("Opening browser \u2014 a Slack login page will appear.\n");
|
|
@@ -18726,7 +18957,7 @@ async function stepBrowserTokens(botName) {
|
|
|
18726
18957
|
let settled = false;
|
|
18727
18958
|
let interval;
|
|
18728
18959
|
let timeout;
|
|
18729
|
-
const
|
|
18960
|
+
const finish2 = (action) => {
|
|
18730
18961
|
if (settled) return;
|
|
18731
18962
|
settled = true;
|
|
18732
18963
|
clearInterval(interval);
|
|
@@ -18735,7 +18966,7 @@ async function stepBrowserTokens(botName) {
|
|
|
18735
18966
|
};
|
|
18736
18967
|
const checkUrl = (p) => {
|
|
18737
18968
|
if (p.url().includes("install-on-team?success=1")) {
|
|
18738
|
-
|
|
18969
|
+
finish2(() => resolve7(p));
|
|
18739
18970
|
}
|
|
18740
18971
|
};
|
|
18741
18972
|
context.on("page", (newPage) => {
|
|
@@ -18746,7 +18977,7 @@ async function stepBrowserTokens(botName) {
|
|
|
18746
18977
|
for (const p of context.pages()) checkUrl(p);
|
|
18747
18978
|
}, 1e3);
|
|
18748
18979
|
timeout = setTimeout(() => {
|
|
18749
|
-
|
|
18980
|
+
finish2(() => reject(new Error("Timed out waiting for the app to be installed.")));
|
|
18750
18981
|
}, 12e4);
|
|
18751
18982
|
});
|
|
18752
18983
|
success4("App installed! Extracting Bot Token...");
|
|
@@ -18836,7 +19067,7 @@ function writeConfigFile(config) {
|
|
|
18836
19067
|
`OWNER_USER_ID=${config.ownerUserId}`,
|
|
18837
19068
|
``
|
|
18838
19069
|
].join("\n");
|
|
18839
|
-
|
|
19070
|
+
writeFileSync12(paths.CONFIG_FILE, envContent);
|
|
18840
19071
|
success4(`Config written to ${paths.CONFIG_FILE}`);
|
|
18841
19072
|
}
|
|
18842
19073
|
function printNextSteps() {
|
|
@@ -18848,11 +19079,11 @@ function printNextSteps() {
|
|
|
18848
19079
|
console.log(" Run \x1B[1mflyspace service install\x1B[0m\n");
|
|
18849
19080
|
}
|
|
18850
19081
|
function copyDefaultPrompt(botName) {
|
|
18851
|
-
if (!
|
|
19082
|
+
if (!existsSync27(paths.PROMPT_FILE)) {
|
|
18852
19083
|
const defaultPrompt = resolve6(paths.PACKAGE_ROOT, "system.default.md");
|
|
18853
|
-
if (
|
|
18854
|
-
const content =
|
|
18855
|
-
|
|
19084
|
+
if (existsSync27(defaultPrompt)) {
|
|
19085
|
+
const content = readFileSync20(defaultPrompt, "utf-8").replace(/\{\{botName\}\}/g, botName);
|
|
19086
|
+
writeFileSync12(paths.PROMPT_FILE, content);
|
|
18856
19087
|
success4(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
18857
19088
|
}
|
|
18858
19089
|
}
|
|
@@ -18864,7 +19095,7 @@ async function runSlackSetup(args) {
|
|
|
18864
19095
|
const prereq = await ensureClaudeCodeReady();
|
|
18865
19096
|
if (!prereq) return;
|
|
18866
19097
|
const { claudeConfigDir } = prereq;
|
|
18867
|
-
if (
|
|
19098
|
+
if (existsSync27(paths.CONFIG_FILE)) {
|
|
18868
19099
|
const reconfigure = await confirm4({
|
|
18869
19100
|
message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
|
|
18870
19101
|
default: false
|
|
@@ -19146,6 +19377,7 @@ async function runPost(args) {
|
|
|
19146
19377
|
}
|
|
19147
19378
|
async function runChatCmd(args) {
|
|
19148
19379
|
loadEnvFiles();
|
|
19380
|
+
useHostDbWithoutCredentials();
|
|
19149
19381
|
const subcommand = args[0];
|
|
19150
19382
|
const rest = args.slice(1);
|
|
19151
19383
|
switch (subcommand) {
|
|
@@ -19174,21 +19406,21 @@ async function runChatCmd(args) {
|
|
|
19174
19406
|
}
|
|
19175
19407
|
|
|
19176
19408
|
// src/commands/memory.ts
|
|
19177
|
-
import { readFileSync as
|
|
19409
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
19178
19410
|
|
|
19179
19411
|
// src/store/claude-memory-import.ts
|
|
19180
|
-
import { existsSync as
|
|
19412
|
+
import { existsSync as existsSync28, readdirSync as readdirSync5, readFileSync as readFileSync21 } from "fs";
|
|
19181
19413
|
import { homedir as homedir10 } from "os";
|
|
19182
|
-
import { basename as basename8, join as
|
|
19414
|
+
import { basename as basename8, join as join26 } from "path";
|
|
19183
19415
|
import dotenv2 from "dotenv";
|
|
19184
19416
|
function claudeMemoryDir(cwd, configDir2) {
|
|
19185
|
-
return
|
|
19417
|
+
return join26(configDir2, "projects", cwd.replaceAll(/[^a-zA-Z0-9]/g, "-"), "memory");
|
|
19186
19418
|
}
|
|
19187
19419
|
function profileClaudeConfigDirFor(profile) {
|
|
19188
|
-
const dir = profile === "default" ? paths.BASE_CONFIG_DIR :
|
|
19189
|
-
const file =
|
|
19190
|
-
const env =
|
|
19191
|
-
return env["CLAUDE_CONFIG_DIR"]?.trim() ||
|
|
19420
|
+
const dir = profile === "default" ? paths.BASE_CONFIG_DIR : join26(paths.PROFILES_DIR, profile);
|
|
19421
|
+
const file = join26(dir, "config.env");
|
|
19422
|
+
const env = existsSync28(file) ? dotenv2.parse(readFileSync21(file)) : {};
|
|
19423
|
+
return env["CLAUDE_CONFIG_DIR"]?.trim() || join26(homedir10(), ".claude");
|
|
19192
19424
|
}
|
|
19193
19425
|
var TYPE_MAP = {
|
|
19194
19426
|
user: "preference",
|
|
@@ -19196,7 +19428,7 @@ var TYPE_MAP = {
|
|
|
19196
19428
|
project: "fact",
|
|
19197
19429
|
reference: "reference"
|
|
19198
19430
|
};
|
|
19199
|
-
var
|
|
19431
|
+
var DESCRIPTION_MAX = 200;
|
|
19200
19432
|
function unquote2(value) {
|
|
19201
19433
|
const v = value.trim();
|
|
19202
19434
|
return /^(["']).*\1$/.test(v) ? v.slice(1, -1) : v;
|
|
@@ -19214,8 +19446,8 @@ function parseClaudeMemory(fileName, text, channelId) {
|
|
|
19214
19446
|
}
|
|
19215
19447
|
const name = slugify(fields["name"] || basename8(fileName, ".md"));
|
|
19216
19448
|
let description = (fields["description"] ?? "").replaceAll(/\s+/g, " ").trim();
|
|
19217
|
-
if (description.length >
|
|
19218
|
-
description = `${description.slice(0,
|
|
19449
|
+
if (description.length > DESCRIPTION_MAX) {
|
|
19450
|
+
description = `${description.slice(0, DESCRIPTION_MAX - 3).trimEnd()}...`;
|
|
19219
19451
|
}
|
|
19220
19452
|
const input2 = {
|
|
19221
19453
|
channelId,
|
|
@@ -19233,11 +19465,11 @@ function parseClaudeMemory(fileName, text, channelId) {
|
|
|
19233
19465
|
return input2;
|
|
19234
19466
|
}
|
|
19235
19467
|
async function importClaudeMemories(dir, channelId, opts = {}) {
|
|
19236
|
-
if (!
|
|
19468
|
+
if (!existsSync28(dir)) throw new Error(`No Claude memory directory at ${dir}`);
|
|
19237
19469
|
const files = readdirSync5(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md").sort();
|
|
19238
19470
|
const results = [];
|
|
19239
19471
|
for (const file of files) {
|
|
19240
|
-
const parsed = parseClaudeMemory(file,
|
|
19472
|
+
const parsed = parseClaudeMemory(file, readFileSync21(join26(dir, file), "utf-8"), channelId);
|
|
19241
19473
|
if ("skip" in parsed) {
|
|
19242
19474
|
results.push({ file, outcome: "skipped", reason: parsed.skip });
|
|
19243
19475
|
continue;
|
|
@@ -19272,6 +19504,12 @@ var USAGE5 = `
|
|
|
19272
19504
|
Defaults to the channel profile's CLAUDE_CONFIG_DIR
|
|
19273
19505
|
(or ~/.claude)/projects/<cwd slug>/memory
|
|
19274
19506
|
|
|
19507
|
+
Scope (list, get, search, save, delete; default: the channel's memory):
|
|
19508
|
+
--scope user --user <user-id> That member's personal memory, in the channel's
|
|
19509
|
+
workspace. Only ever the current sender's.
|
|
19510
|
+
--scope workspace The workspace's memory. Read-only here: only a
|
|
19511
|
+
Workspace Admin changes it, from Settings.
|
|
19512
|
+
|
|
19275
19513
|
Types: ${MEMORY_TYPES.join(", ")}
|
|
19276
19514
|
`;
|
|
19277
19515
|
function getArg4(args, flag) {
|
|
@@ -19291,6 +19529,7 @@ function indexLine(m) {
|
|
|
19291
19529
|
}
|
|
19292
19530
|
async function runMemoryCmd(args) {
|
|
19293
19531
|
loadEnvFiles();
|
|
19532
|
+
useHostDbWithoutCredentials();
|
|
19294
19533
|
const subcommand = args[0];
|
|
19295
19534
|
const rest = args.slice(1);
|
|
19296
19535
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
@@ -19301,17 +19540,23 @@ async function runMemoryCmd(args) {
|
|
|
19301
19540
|
if (!channelArg) throw new Error("--channel <name-or-id> is required");
|
|
19302
19541
|
const channel = await resolveChannel(channelArg, getArg4(rest, "--profile-name"));
|
|
19303
19542
|
const words = positionals(rest);
|
|
19543
|
+
const target = await resolveTarget(rest, channel);
|
|
19544
|
+
const where = target.scope === "channel" ? channel.name : `${target.scope} memory`;
|
|
19545
|
+
if (target.scope === "workspace" && (subcommand === "save" || subcommand === "delete")) {
|
|
19546
|
+
throw new Error("Workspace memory is read-only to the agent; a Workspace Admin manages it.");
|
|
19547
|
+
}
|
|
19304
19548
|
switch (subcommand) {
|
|
19305
19549
|
case "list": {
|
|
19306
|
-
const memories = await
|
|
19550
|
+
const memories = await listMemories(target);
|
|
19307
19551
|
console.log(memories.length ? memories.map(indexLine).join("\n") : "No memories yet.");
|
|
19308
19552
|
break;
|
|
19309
19553
|
}
|
|
19310
19554
|
case "get": {
|
|
19311
19555
|
const name = words[0];
|
|
19312
19556
|
if (!name) throw new Error("Usage: flyspace memory get --channel <c> <name>");
|
|
19313
|
-
const memory = await
|
|
19314
|
-
if (!memory) throw new Error(`No memory named ${name} in ${
|
|
19557
|
+
const memory = await getMemory(target, name);
|
|
19558
|
+
if (!memory) throw new Error(`No memory named ${name} in ${where}.`);
|
|
19559
|
+
await markMemoryUsed(memory);
|
|
19315
19560
|
console.log(`# ${memory.name} (${memory.type})
|
|
19316
19561
|
|
|
19317
19562
|
${memory.description}
|
|
@@ -19321,7 +19566,7 @@ ${memory.body}`);
|
|
|
19321
19566
|
}
|
|
19322
19567
|
case "search": {
|
|
19323
19568
|
if (!words.length) throw new Error("Usage: flyspace memory search --channel <c> <words...>");
|
|
19324
|
-
const hits = await
|
|
19569
|
+
const hits = await searchMemories(target, words.join(" "));
|
|
19325
19570
|
console.log(hits.length ? hits.map(indexLine).join("\n") : "No matching memories.");
|
|
19326
19571
|
break;
|
|
19327
19572
|
}
|
|
@@ -19336,11 +19581,11 @@ ${memory.body}`);
|
|
|
19336
19581
|
);
|
|
19337
19582
|
}
|
|
19338
19583
|
if (!isMemoryType(type)) throw new Error(`--type must be one of ${MEMORY_TYPES.join(", ")}`);
|
|
19339
|
-
const body = bodyArg === "-" ?
|
|
19584
|
+
const body = bodyArg === "-" ? readFileSync22(0, "utf-8") : bodyArg;
|
|
19340
19585
|
const thread = getArg4(rest, "--thread");
|
|
19341
|
-
const { created, changed } = await
|
|
19586
|
+
const { created, changed } = await saveMemory(
|
|
19587
|
+
target,
|
|
19342
19588
|
{
|
|
19343
|
-
channelId: channel.id,
|
|
19344
19589
|
name,
|
|
19345
19590
|
type,
|
|
19346
19591
|
description,
|
|
@@ -19351,18 +19596,19 @@ ${memory.body}`);
|
|
|
19351
19596
|
"agent"
|
|
19352
19597
|
);
|
|
19353
19598
|
const verb = created ? "Saved" : changed ? "Updated" : "Unchanged:";
|
|
19354
|
-
console.log(`${verb} memory ${name} in ${
|
|
19599
|
+
console.log(`${verb} memory ${name} in ${where}.`);
|
|
19355
19600
|
break;
|
|
19356
19601
|
}
|
|
19357
19602
|
case "delete": {
|
|
19358
19603
|
const ref = words[0];
|
|
19359
19604
|
if (!ref) throw new Error("Usage: flyspace memory delete --channel <c> <name-or-id>");
|
|
19360
|
-
const deleted = await
|
|
19361
|
-
if (!deleted) throw new Error(`No memory ${ref} in ${
|
|
19605
|
+
const deleted = await deleteMemory(target, ref, "agent", getArg4(rest, "--thread"));
|
|
19606
|
+
if (!deleted) throw new Error(`No memory ${ref} in ${where}.`);
|
|
19362
19607
|
console.log(`Deleted memory ${ref}.`);
|
|
19363
19608
|
break;
|
|
19364
19609
|
}
|
|
19365
19610
|
case "import": {
|
|
19611
|
+
if (target.scope !== "channel") throw new Error("import only fills a channel memory");
|
|
19366
19612
|
const dir = getArg4(rest, "--dir") ?? claudeMemoryDir(channel.cwd, profileClaudeConfigDirFor(channel.profile));
|
|
19367
19613
|
const dryRun = rest.includes("--dry-run");
|
|
19368
19614
|
const results = await importClaudeMemories(dir, channel.id, { dryRun });
|
|
@@ -19382,6 +19628,19 @@ ${memory.body}`);
|
|
|
19382
19628
|
process.exit(1);
|
|
19383
19629
|
}
|
|
19384
19630
|
}
|
|
19631
|
+
async function resolveTarget(args, channel) {
|
|
19632
|
+
const scope = getArg4(args, "--scope") ?? "channel";
|
|
19633
|
+
if (scope === "channel") return { scope: "channel", channelId: channel.id };
|
|
19634
|
+
if (scope !== "user" && scope !== "workspace") {
|
|
19635
|
+
throw new Error("--scope must be channel, user or workspace");
|
|
19636
|
+
}
|
|
19637
|
+
const workspace = await getWorkspaceBySlug(channel.profile);
|
|
19638
|
+
if (!workspace) throw new Error(`No workspace for ${channel.profile}.`);
|
|
19639
|
+
if (scope === "workspace") return { scope, workspaceId: workspace.id };
|
|
19640
|
+
const userId = getArg4(args, "--user");
|
|
19641
|
+
if (!userId) throw new Error("--scope user needs --user <user-id>");
|
|
19642
|
+
return { scope, workspaceId: workspace.id, userId };
|
|
19643
|
+
}
|
|
19385
19644
|
|
|
19386
19645
|
// src/commands/mcp.ts
|
|
19387
19646
|
var USAGE6 = `
|
|
@@ -19420,10 +19679,10 @@ async function runMcpCmd(args) {
|
|
|
19420
19679
|
}
|
|
19421
19680
|
|
|
19422
19681
|
// src/commands/dashboard.ts
|
|
19423
|
-
import { spawn as
|
|
19424
|
-
import { existsSync as
|
|
19682
|
+
import { spawn as spawn7, spawnSync } from "child_process";
|
|
19683
|
+
import { existsSync as existsSync29 } from "fs";
|
|
19425
19684
|
import { platform as platform7 } from "os";
|
|
19426
|
-
import { join as
|
|
19685
|
+
import { join as join27 } from "path";
|
|
19427
19686
|
function flagValue(args, name) {
|
|
19428
19687
|
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
19429
19688
|
if (eq) return eq.slice(`--${name}=`.length);
|
|
@@ -19433,13 +19692,13 @@ function flagValue(args, name) {
|
|
|
19433
19692
|
function openBrowser2(url) {
|
|
19434
19693
|
const cmd = platform7() === "darwin" ? "open" : platform7() === "win32" ? "start" : "xdg-open";
|
|
19435
19694
|
try {
|
|
19436
|
-
|
|
19695
|
+
spawn7(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
19437
19696
|
} catch {
|
|
19438
19697
|
}
|
|
19439
19698
|
}
|
|
19440
19699
|
function ensureWebBuild() {
|
|
19441
|
-
if (
|
|
19442
|
-
if (!
|
|
19700
|
+
if (existsSync29(join27(paths.PACKAGE_ROOT, "web", "dist", "index.html"))) return;
|
|
19701
|
+
if (!existsSync29(join27(paths.PACKAGE_ROOT, "web", "package.json"))) return;
|
|
19443
19702
|
console.log(" web/dist missing \u2014 building the dashboard UI (pnpm --dir web build)...");
|
|
19444
19703
|
const result = spawnSync("pnpm", ["--dir", "web", "build"], {
|
|
19445
19704
|
cwd: paths.PACKAGE_ROOT,
|
|
@@ -19489,7 +19748,7 @@ async function runDashboard(args) {
|
|
|
19489
19748
|
}
|
|
19490
19749
|
|
|
19491
19750
|
// src/commands/workspace.ts
|
|
19492
|
-
import { existsSync as
|
|
19751
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "fs";
|
|
19493
19752
|
var USAGE7 = `
|
|
19494
19753
|
Usage: flyspace workspace <subcommand> [options]
|
|
19495
19754
|
|
|
@@ -19575,10 +19834,10 @@ function provisionLocal(botName) {
|
|
|
19575
19834
|
ensureDirs();
|
|
19576
19835
|
console.log(` config dir ready (${paths.CONFIG_DIR})`);
|
|
19577
19836
|
console.log(` data dir ready (${paths.DATA_DIR})`);
|
|
19578
|
-
if (
|
|
19837
|
+
if (existsSync30(paths.CONFIG_FILE)) {
|
|
19579
19838
|
console.log(" config.env exists");
|
|
19580
19839
|
} else {
|
|
19581
|
-
|
|
19840
|
+
writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
|
|
19582
19841
|
`);
|
|
19583
19842
|
console.log(` config.env written (${paths.CONFIG_FILE})`);
|
|
19584
19843
|
}
|
|
@@ -19592,8 +19851,8 @@ async function provisionChannels(specs) {
|
|
|
19592
19851
|
continue;
|
|
19593
19852
|
}
|
|
19594
19853
|
const cwd = expandPath(spec.cwd);
|
|
19595
|
-
if (!
|
|
19596
|
-
|
|
19854
|
+
if (!existsSync30(cwd)) {
|
|
19855
|
+
mkdirSync13(cwd, { recursive: true });
|
|
19597
19856
|
console.log(` created cwd ${cwd}`);
|
|
19598
19857
|
}
|
|
19599
19858
|
await createChannel({ name: spec.name, cwd, position: index });
|
|
@@ -19802,7 +20061,7 @@ async function main() {
|
|
|
19802
20061
|
break;
|
|
19803
20062
|
case "-v":
|
|
19804
20063
|
case "--version":
|
|
19805
|
-
printRaw("0.1.
|
|
20064
|
+
printRaw("0.1.16");
|
|
19806
20065
|
break;
|
|
19807
20066
|
case "-h":
|
|
19808
20067
|
case "--help":
|