flyspace-cli 0.1.15 → 0.1.18
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-6PJ73XYW.js → chunk-B3KW3N5Y.js} +1 -1
- package/dist/{chunk-ZBMYF5LX.js → chunk-L3I3BPLP.js} +5 -1
- package/dist/{chunk-KFT2YP5W.js → chunk-SWLIZNEI.js} +241 -77
- package/dist/cli.js +13489 -13815
- package/dist/index.js +2 -2
- package/dist/{threads-PAG4PKHR.js → threads-EWR6YAZY.js} +2 -2
- package/package.json +6 -4
|
@@ -157,6 +157,9 @@ function setSupabaseClient(replacement) {
|
|
|
157
157
|
function hasSupabaseCredentials() {
|
|
158
158
|
return !!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_SECRET_KEY"];
|
|
159
159
|
}
|
|
160
|
+
function databaseReachable() {
|
|
161
|
+
return !!client || hasSupabaseCredentials();
|
|
162
|
+
}
|
|
160
163
|
function getSupabaseClient() {
|
|
161
164
|
if (client) return client;
|
|
162
165
|
const url = process.env["SUPABASE_URL"];
|
|
@@ -319,7 +322,7 @@ function deriveThreadTitle(text) {
|
|
|
319
322
|
}
|
|
320
323
|
async function syncPostToDashboard(opts) {
|
|
321
324
|
const { channelId, ts, threadTs, text } = opts;
|
|
322
|
-
if (!
|
|
325
|
+
if (!databaseReachable()) {
|
|
323
326
|
console.warn("[dashboard-sync] Supabase not configured \u2014 skipping dashboard sync");
|
|
324
327
|
return;
|
|
325
328
|
}
|
|
@@ -430,6 +433,7 @@ export {
|
|
|
430
433
|
sweepScratchDir,
|
|
431
434
|
setSupabaseClient,
|
|
432
435
|
hasSupabaseCredentials,
|
|
436
|
+
databaseReachable,
|
|
433
437
|
getSupabaseClient,
|
|
434
438
|
AGENT_AUTHOR,
|
|
435
439
|
SYNCED_BOT_AUTHOR,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AGENT_AUTHOR,
|
|
4
4
|
SYNCED_BOT_AUTHOR,
|
|
5
|
+
databaseReachable,
|
|
5
6
|
ensureDirs,
|
|
6
7
|
getProfile,
|
|
7
8
|
getR2Bucket,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
runWithProfile,
|
|
13
14
|
sweepScratchDir,
|
|
14
15
|
syncPostToDashboard
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-L3I3BPLP.js";
|
|
16
17
|
|
|
17
18
|
// src/index.ts
|
|
18
19
|
import { execFile, spawn as spawn4 } from "child_process";
|
|
@@ -263,7 +264,7 @@ function stopAutomationRegistryPolling() {
|
|
|
263
264
|
}
|
|
264
265
|
|
|
265
266
|
// src/channels.ts
|
|
266
|
-
import { existsSync as existsSync2, mkdirSync, statSync } from "fs";
|
|
267
|
+
import { existsSync as existsSync2, mkdirSync, realpathSync, statSync } from "fs";
|
|
267
268
|
import { homedir } from "os";
|
|
268
269
|
import { isAbsolute, resolve as resolve2, sep } from "path";
|
|
269
270
|
function isLogicalCwd(p) {
|
|
@@ -272,11 +273,22 @@ function isLogicalCwd(p) {
|
|
|
272
273
|
function logicalRoot() {
|
|
273
274
|
return process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace");
|
|
274
275
|
}
|
|
276
|
+
var workspaceRoots = /* @__PURE__ */ new Map();
|
|
277
|
+
async function refreshWorkspaceRoots() {
|
|
278
|
+
const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
|
|
279
|
+
if (error) return;
|
|
280
|
+
workspaceRoots = new Map(
|
|
281
|
+
(data ?? []).filter((w) => w.root_path).map((w) => [w.slug, expandPath(w.root_path)])
|
|
282
|
+
);
|
|
283
|
+
}
|
|
275
284
|
function resolveLogicalCwd(p) {
|
|
276
|
-
|
|
285
|
+
const [slug, name] = p.split("/");
|
|
286
|
+
const root = workspaceRoots.get(slug);
|
|
287
|
+
return root ? resolve2(root, name) : resolve2(logicalRoot(), p);
|
|
277
288
|
}
|
|
278
289
|
function ensureLogicalCwdDir(cwd) {
|
|
279
|
-
|
|
290
|
+
const roots = [logicalRoot(), ...workspaceRoots.values()];
|
|
291
|
+
if (!roots.some((r) => cwd.startsWith(r + sep)) || existsSync2(cwd)) return;
|
|
280
292
|
mkdirSync(cwd, { recursive: true });
|
|
281
293
|
}
|
|
282
294
|
function expandPath(p) {
|
|
@@ -358,6 +370,9 @@ function indexRegistry(list) {
|
|
|
358
370
|
);
|
|
359
371
|
}
|
|
360
372
|
async function refreshChannelRegistry() {
|
|
373
|
+
const rootsBefore = JSON.stringify([...workspaceRoots]);
|
|
374
|
+
await refreshWorkspaceRoots();
|
|
375
|
+
if (JSON.stringify([...workspaceRoots]) !== rootsBefore) lastSnapshot2 = void 0;
|
|
361
376
|
const { data, error } = await getSupabaseClient().from("channels").select("*").order("position", { ascending: true });
|
|
362
377
|
if (error) {
|
|
363
378
|
console.error("[channels] refreshChannelRegistry failed:", error.message);
|
|
@@ -522,7 +537,44 @@ function toRow(input) {
|
|
|
522
537
|
if (input.archivedAt !== void 0) row["archived_at"] = input.archivedAt;
|
|
523
538
|
return row;
|
|
524
539
|
}
|
|
540
|
+
var ChannelCwdError = class extends Error {
|
|
541
|
+
constructor(message) {
|
|
542
|
+
super(message);
|
|
543
|
+
this.name = "ChannelCwdError";
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
function realpathOrSelf(p) {
|
|
547
|
+
try {
|
|
548
|
+
return realpathSync(p);
|
|
549
|
+
} catch {
|
|
550
|
+
return p;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function assertCwdInWorkspace(profile, cwd) {
|
|
554
|
+
if (isLogicalCwd(cwd)) {
|
|
555
|
+
if (cwd.split("/")[0] !== profile) {
|
|
556
|
+
throw new ChannelCwdError(`Folder "${cwd}" belongs to another workspace.`);
|
|
557
|
+
}
|
|
558
|
+
return cwd;
|
|
559
|
+
}
|
|
560
|
+
const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
|
|
561
|
+
if (error) throw new Error(error.message);
|
|
562
|
+
const workspaces = data ?? [];
|
|
563
|
+
const ownRoot = workspaces.find((w) => w.slug === profile)?.root_path;
|
|
564
|
+
const home = ownRoot?.match(/^(\/Users\/[^/]+|\/home\/[^/]+|\/root)(?=\/|$)/)?.[1];
|
|
565
|
+
const absolute = home && (cwd === "~" || cwd.startsWith("~/")) ? resolve2(home, cwd.slice(2)) : expandPath(cwd);
|
|
566
|
+
const path = realpathOrSelf(absolute);
|
|
567
|
+
const roots = workspaces.filter((w) => w.root_path).map((w) => ({ slug: w.slug, root: realpathOrSelf(expandPath(w.root_path)) }));
|
|
568
|
+
const owner = roots.filter(({ root }) => path === root || path.startsWith(root + sep)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
569
|
+
if (owner ? owner.slug !== profile : roots.some((r) => r.slug === profile)) {
|
|
570
|
+
throw new ChannelCwdError(
|
|
571
|
+
owner ? `Folder "${cwd}" belongs to the "${owner.slug}" workspace.` : `Folder "${cwd}" is outside this workspace's folder.`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return absolute;
|
|
575
|
+
}
|
|
525
576
|
async function createChannel(input) {
|
|
577
|
+
input = { ...input, cwd: await assertCwdInWorkspace(getProfile(), input.cwd) };
|
|
526
578
|
const { data, error } = await getSupabaseClient().from("channels").insert({ ...toRow(input), profile: getProfile() }).select().single();
|
|
527
579
|
if (error) throw new Error(error.message);
|
|
528
580
|
const channel = rowToChannel(data);
|
|
@@ -547,6 +599,9 @@ async function updateChannelById(id, patch) {
|
|
|
547
599
|
if (existing?.isDefault && (patch.visibility === "private" || patch.archivedAt || patch.isDefault === false || patch.name !== void 0 && patch.name.trim() !== existing.name)) {
|
|
548
600
|
throw new DefaultChannelError();
|
|
549
601
|
}
|
|
602
|
+
if (patch.cwd !== void 0) {
|
|
603
|
+
patch = { ...patch, cwd: await assertCwdInWorkspace(existing?.profile ?? getProfile(), patch.cwd) };
|
|
604
|
+
}
|
|
550
605
|
const { data, error } = await getSupabaseClient().from("channels").update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select().single();
|
|
551
606
|
if (error) throw new Error(error.message);
|
|
552
607
|
await refreshChannelRegistry();
|
|
@@ -1543,9 +1598,9 @@ function isAuthError(text) {
|
|
|
1543
1598
|
}
|
|
1544
1599
|
function authErrorMessage(raw) {
|
|
1545
1600
|
return `The agent can't sign in to Claude right now (the Host's login expired and could not be refreshed).
|
|
1546
|
-
|
|
1601
|
+
To fix it, a workspace admin can open Settings \u2192 Agent \u2192 Providers and click Sign in next to Claude Code.
|
|
1547
1602
|
|
|
1548
|
-
Original error: ${raw}`;
|
|
1603
|
+
> Original error: ${raw}`;
|
|
1549
1604
|
}
|
|
1550
1605
|
function isTransientError(text, apiErrorStatus) {
|
|
1551
1606
|
if (apiErrorStatus == null) {
|
|
@@ -2111,7 +2166,7 @@ var featureFlags = {
|
|
|
2111
2166
|
dictation: {
|
|
2112
2167
|
title: "Dictation",
|
|
2113
2168
|
description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
|
|
2114
|
-
stage: "
|
|
2169
|
+
stage: "beta"
|
|
2115
2170
|
},
|
|
2116
2171
|
"agent-memory": {
|
|
2117
2172
|
title: "Agent memory",
|
|
@@ -2161,6 +2216,7 @@ async function disableFeatureFlag(workspaceId, flagId) {
|
|
|
2161
2216
|
|
|
2162
2217
|
// src/store/memory-store.ts
|
|
2163
2218
|
var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
|
|
2219
|
+
var channelTarget = (channelId) => ({ scope: "channel", channelId });
|
|
2164
2220
|
var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
2165
2221
|
var DESCRIPTION_MAX = 200;
|
|
2166
2222
|
var BODY_MAX = 16e3;
|
|
@@ -2184,7 +2240,10 @@ function validateMemoryInput(input) {
|
|
|
2184
2240
|
function rowToMemory(row) {
|
|
2185
2241
|
return {
|
|
2186
2242
|
id: row["id"],
|
|
2187
|
-
|
|
2243
|
+
scope: row["scope"] ?? "channel",
|
|
2244
|
+
workspaceId: row["workspace_id"],
|
|
2245
|
+
channelId: row["channel_id"] ?? null,
|
|
2246
|
+
userId: row["user_id"] ?? null,
|
|
2188
2247
|
name: row["name"],
|
|
2189
2248
|
description: row["description"],
|
|
2190
2249
|
type: row["type"],
|
|
@@ -2192,23 +2251,40 @@ function rowToMemory(row) {
|
|
|
2192
2251
|
sourceThreadId: row["source_thread_id"] ?? null,
|
|
2193
2252
|
createdBy: row["created_by"],
|
|
2194
2253
|
createdAt: row["created_at"],
|
|
2195
|
-
updatedAt: row["updated_at"]
|
|
2254
|
+
updatedAt: row["updated_at"],
|
|
2255
|
+
lastUsedAt: row["last_used_at"] ?? null
|
|
2196
2256
|
};
|
|
2197
2257
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2258
|
+
function targetRow(target) {
|
|
2259
|
+
switch (target.scope) {
|
|
2260
|
+
case "channel":
|
|
2261
|
+
return { scope: "channel", channel_id: target.channelId };
|
|
2262
|
+
case "workspace":
|
|
2263
|
+
return { scope: "workspace", workspace_id: target.workspaceId };
|
|
2264
|
+
case "user":
|
|
2265
|
+
return { scope: "user", workspace_id: target.workspaceId, user_id: target.userId };
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function inTarget(query, target) {
|
|
2269
|
+
let q = query;
|
|
2270
|
+
for (const [column, value] of Object.entries(targetRow(target))) q = q.eq(column, value);
|
|
2271
|
+
return q;
|
|
2272
|
+
}
|
|
2273
|
+
var describeTarget = (t) => t.scope === "channel" ? t.channelId : t.scope === "workspace" ? `workspace ${t.workspaceId}` : `user ${t.userId}`;
|
|
2274
|
+
async function listMemories(target) {
|
|
2275
|
+
const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).order("updated_at", { ascending: false });
|
|
2276
|
+
if (error) throw new Error(`listMemories(${describeTarget(target)}): ${error.message}`);
|
|
2201
2277
|
return (data ?? []).map(rowToMemory);
|
|
2202
2278
|
}
|
|
2203
|
-
async function
|
|
2204
|
-
const { data, error } = await getSupabaseClient().from("memories").select("*")
|
|
2205
|
-
if (error) throw new Error(`
|
|
2279
|
+
async function getMemory(target, name) {
|
|
2280
|
+
const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).eq("name", name).maybeSingle();
|
|
2281
|
+
if (error) throw new Error(`getMemory(${describeTarget(target)}, ${name}): ${error.message}`);
|
|
2206
2282
|
return data ? rowToMemory(data) : null;
|
|
2207
2283
|
}
|
|
2208
|
-
async function
|
|
2284
|
+
async function saveMemory(target, input, actor) {
|
|
2209
2285
|
validateMemoryInput(input);
|
|
2210
2286
|
const supabase = getSupabaseClient();
|
|
2211
|
-
const existing = await
|
|
2287
|
+
const existing = await getMemory(target, input.name);
|
|
2212
2288
|
const fields = {
|
|
2213
2289
|
description: input.description.trim(),
|
|
2214
2290
|
type: input.type,
|
|
@@ -2219,9 +2295,9 @@ async function saveChannelMemory(input, actor) {
|
|
|
2219
2295
|
if (existing && sameContent(existing, fields)) {
|
|
2220
2296
|
return { memory: existing, created: false, changed: false };
|
|
2221
2297
|
}
|
|
2222
|
-
const { data, error } = 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,
|
|
2298
|
+
const { data, error } = 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, ...targetRow(target), name: input.name }).select("*").single();
|
|
2223
2299
|
if (error)
|
|
2224
|
-
throw new Error(`
|
|
2300
|
+
throw new Error(`saveMemory(${describeTarget(target)}, ${input.name}): ${error.message}`);
|
|
2225
2301
|
const memory = rowToMemory(data);
|
|
2226
2302
|
await logMemoryEvent({
|
|
2227
2303
|
memory,
|
|
@@ -2233,12 +2309,12 @@ async function saveChannelMemory(input, actor) {
|
|
|
2233
2309
|
});
|
|
2234
2310
|
return { memory, created: !existing, changed: true };
|
|
2235
2311
|
}
|
|
2236
|
-
async function
|
|
2237
|
-
const existing = (await
|
|
2312
|
+
async function updateMemory(target, memoryId, patch, actor) {
|
|
2313
|
+
const existing = (await listMemories(target)).find((m) => m.id === memoryId);
|
|
2238
2314
|
if (!existing) return null;
|
|
2239
|
-
const { memory } = await
|
|
2315
|
+
const { memory } = await saveMemory(
|
|
2316
|
+
target,
|
|
2240
2317
|
{
|
|
2241
|
-
channelId,
|
|
2242
2318
|
name: existing.name,
|
|
2243
2319
|
description: patch.description ?? existing.description,
|
|
2244
2320
|
type: patch.type ?? existing.type,
|
|
@@ -2249,20 +2325,28 @@ async function updateChannelMemory(channelId, memoryId, patch, actor) {
|
|
|
2249
2325
|
);
|
|
2250
2326
|
return memory;
|
|
2251
2327
|
}
|
|
2252
|
-
async function
|
|
2328
|
+
async function searchMemories(target, query) {
|
|
2253
2329
|
const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
|
|
2254
2330
|
if (!words.length) return [];
|
|
2255
|
-
let request = getSupabaseClient().from("memories").select("*")
|
|
2331
|
+
let request = inTarget(getSupabaseClient().from("memories").select("*"), target);
|
|
2256
2332
|
for (const word of words) {
|
|
2257
2333
|
request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
|
|
2258
2334
|
}
|
|
2259
2335
|
const { data, error } = await request.order("updated_at", { ascending: false }).limit(20);
|
|
2260
|
-
if (error) throw new Error(`
|
|
2336
|
+
if (error) throw new Error(`searchMemories(${describeTarget(target)}): ${error.message}`);
|
|
2261
2337
|
return (data ?? []).map(rowToMemory);
|
|
2262
2338
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2339
|
+
var USED_GRANULARITY_MS = 24 * 60 * 6e4;
|
|
2340
|
+
async function markMemoryUsed(memory) {
|
|
2341
|
+
const cutoff = new Date(Date.now() - USED_GRANULARITY_MS).toISOString();
|
|
2342
|
+
if (memory.lastUsedAt && memory.lastUsedAt > cutoff) return;
|
|
2343
|
+
const { error } = await getSupabaseClient().from("memories").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memory.id).or(`last_used_at.is.null,last_used_at.lt.${cutoff}`);
|
|
2344
|
+
if (error) console.error(`[memory] markMemoryUsed(${memory.id}) failed:`, error.message);
|
|
2345
|
+
}
|
|
2346
|
+
async function deleteMemory(target, idOrName, actor, threadId) {
|
|
2347
|
+
const { data, error } = await inTarget(getSupabaseClient().from("memories").delete(), target).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
|
|
2348
|
+
if (error)
|
|
2349
|
+
throw new Error(`deleteMemory(${describeTarget(target)}, ${idOrName}): ${error.message}`);
|
|
2266
2350
|
const deleted = (data ?? []).map(rowToMemory);
|
|
2267
2351
|
for (const memory of deleted) {
|
|
2268
2352
|
await logMemoryEvent({
|
|
@@ -2276,6 +2360,25 @@ async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
|
|
|
2276
2360
|
}
|
|
2277
2361
|
return deleted.length > 0;
|
|
2278
2362
|
}
|
|
2363
|
+
async function promoteChannelMemory(channelId, memoryId) {
|
|
2364
|
+
const supabase = getSupabaseClient();
|
|
2365
|
+
const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
|
|
2366
|
+
if (!existing) return { ok: false, reason: "not-found" };
|
|
2367
|
+
if (await getMemory({ scope: "workspace", workspaceId: existing.workspaceId }, existing.name)) {
|
|
2368
|
+
return { ok: false, reason: "name-taken" };
|
|
2369
|
+
}
|
|
2370
|
+
const { data, error } = await supabase.from("memories").update({ scope: "workspace", channel_id: null, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memoryId).eq("scope", "channel").select("*").maybeSingle();
|
|
2371
|
+
if (error) throw new Error(`promoteChannelMemory(${memoryId}): ${error.message}`);
|
|
2372
|
+
if (!data) return { ok: false, reason: "not-found" };
|
|
2373
|
+
const memory = rowToMemory(data);
|
|
2374
|
+
await logMemoryEvent({ memory, action: "create", before: null, after: snapshot(memory), actor: "user", threadId: null });
|
|
2375
|
+
return { ok: true, memory };
|
|
2376
|
+
}
|
|
2377
|
+
var listChannelMemories = (channelId) => listMemories(channelTarget(channelId));
|
|
2378
|
+
var getChannelMemory = (channelId, name) => getMemory(channelTarget(channelId), name);
|
|
2379
|
+
var saveChannelMemory = ({ channelId, ...fields }, actor) => saveMemory(channelTarget(channelId), fields, actor);
|
|
2380
|
+
var updateChannelMemory = (channelId, memoryId, patch, actor) => updateMemory(channelTarget(channelId), memoryId, patch, actor);
|
|
2381
|
+
var deleteChannelMemory = (channelId, idOrName, actor, threadId) => deleteMemory(channelTarget(channelId), idOrName, actor, threadId);
|
|
2279
2382
|
function unchangedSince(current, after) {
|
|
2280
2383
|
return current && after ? sameContent(current, after) : !current && !after;
|
|
2281
2384
|
}
|
|
@@ -2294,6 +2397,10 @@ function sameContent(m, other) {
|
|
|
2294
2397
|
async function logMemoryEvent(e) {
|
|
2295
2398
|
const { error } = await getSupabaseClient().from("memory_events").insert({
|
|
2296
2399
|
channel_id: e.memory.channelId,
|
|
2400
|
+
// A channel event's workspace comes from its channel (0100 trigger).
|
|
2401
|
+
...e.memory.channelId ? {} : { workspace_id: e.memory.workspaceId },
|
|
2402
|
+
scope: e.memory.scope,
|
|
2403
|
+
user_id: e.memory.userId,
|
|
2297
2404
|
memory_id: e.action === "delete" ? null : e.memory.id,
|
|
2298
2405
|
thread_id: e.threadId,
|
|
2299
2406
|
name: e.memory.name,
|
|
@@ -2319,15 +2426,18 @@ function rowToEvent(row) {
|
|
|
2319
2426
|
undoneAt: row["undone_at"] ?? null
|
|
2320
2427
|
};
|
|
2321
2428
|
}
|
|
2322
|
-
async function
|
|
2429
|
+
async function listMemoryEvents(channelId, filter = {}) {
|
|
2323
2430
|
const supabase = getSupabaseClient();
|
|
2324
|
-
|
|
2325
|
-
if (
|
|
2431
|
+
let query = supabase.from("memory_events").select("*").eq("channel_id", channelId);
|
|
2432
|
+
if (filter.threadId) query = query.eq("thread_id", filter.threadId);
|
|
2433
|
+
if (filter.actor) query = query.eq("actor", filter.actor);
|
|
2434
|
+
const { data, error } = await query.order("created_at", { ascending: false }).limit(filter.limit ?? 50);
|
|
2435
|
+
if (error) throw new Error(`listMemoryEvents(${channelId}): ${error.message}`);
|
|
2326
2436
|
const events = (data ?? []).map(rowToEvent);
|
|
2327
2437
|
if (!events.length) return [];
|
|
2328
2438
|
const names = [...new Set(events.map((e) => e.name))];
|
|
2329
2439
|
const live = await supabase.from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).in("name", names);
|
|
2330
|
-
if (live.error) throw new Error(`
|
|
2440
|
+
if (live.error) throw new Error(`listMemoryEvents(${channelId}): ${live.error.message}`);
|
|
2331
2441
|
const byName = new Map(
|
|
2332
2442
|
(live.data ?? []).map((r) => [
|
|
2333
2443
|
r["name"],
|
|
@@ -2344,6 +2454,7 @@ async function listThreadMemoryEvents(channelId, threadId) {
|
|
|
2344
2454
|
};
|
|
2345
2455
|
});
|
|
2346
2456
|
}
|
|
2457
|
+
var listThreadMemoryEvents = (channelId, threadId) => listMemoryEvents(channelId, { threadId, actor: "agent" });
|
|
2347
2458
|
async function undoMemoryEvent(channelId, eventId) {
|
|
2348
2459
|
const supabase = getSupabaseClient();
|
|
2349
2460
|
const { data, error } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
|
|
@@ -2374,21 +2485,29 @@ async function undoMemoryEvent(channelId, eventId) {
|
|
|
2374
2485
|
function isUuid(value) {
|
|
2375
2486
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
2376
2487
|
}
|
|
2377
|
-
function formatMemoryPrompt(channelId, memories) {
|
|
2378
|
-
const lines = [];
|
|
2488
|
+
function formatMemoryPrompt(channelId, memories, extras = {}) {
|
|
2379
2489
|
let used = 0;
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2490
|
+
const indexOf = (list, searchHint) => {
|
|
2491
|
+
const lines = [];
|
|
2492
|
+
let omitted = 0;
|
|
2493
|
+
for (const m of list) {
|
|
2494
|
+
const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
|
|
2495
|
+
if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
|
|
2496
|
+
omitted++;
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
lines.push(line);
|
|
2500
|
+
used += line.length + 1;
|
|
2386
2501
|
}
|
|
2387
|
-
lines.
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2502
|
+
if (!lines.length && !omitted) return "(none yet)";
|
|
2503
|
+
return lines.join("\n") + (omitted ? `
|
|
2504
|
+
- (${omitted} older memories not listed; find them with \`${searchHint}\`)` : "");
|
|
2505
|
+
};
|
|
2506
|
+
const user = extras.user;
|
|
2507
|
+
const userFlags2 = user ? `--scope user --user ${user.userId}` : "";
|
|
2508
|
+
const userIndex = user ? indexOf(user.memories, `memory search --channel ${channelId} ${userFlags2}`) : "";
|
|
2509
|
+
const channelIndex = indexOf(memories, "memory search");
|
|
2510
|
+
const workspaceIndex = extras.workspace?.length ? indexOf(extras.workspace, `memory search --channel ${channelId} --scope workspace`) : "";
|
|
2392
2511
|
return [
|
|
2393
2512
|
`## Channel memory`,
|
|
2394
2513
|
``,
|
|
@@ -2400,26 +2519,22 @@ function formatMemoryPrompt(channelId, memories) {
|
|
|
2400
2519
|
`- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
|
|
2401
2520
|
``,
|
|
2402
2521
|
`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.`,
|
|
2522
|
+
...user ? [
|
|
2523
|
+
``,
|
|
2524
|
+
`A personal preference of @${user.handle} (language, tone, reply length, time zone, how they like to work) belongs in their personal memory instead, which only their own turns see: add \`${userFlags2}\` to the same commands. Never save someone else's preferences there, and never put project knowledge there.`
|
|
2525
|
+
] : [],
|
|
2526
|
+
...workspaceIndex ? [
|
|
2527
|
+
``,
|
|
2528
|
+
`Workspace memories below apply to every channel. They are read-only to you: read one with \`--scope workspace\` on \`get\`; a Workspace Admin manages them.`
|
|
2529
|
+
] : [],
|
|
2403
2530
|
``,
|
|
2531
|
+
...user ? [`Personal memory of @${user.handle}:`, userIndex, ``] : [],
|
|
2404
2532
|
`Index:`,
|
|
2405
|
-
|
|
2533
|
+
channelIndex,
|
|
2534
|
+
...workspaceIndex ? [``, `Workspace memory:`, workspaceIndex] : []
|
|
2406
2535
|
].join("\n");
|
|
2407
2536
|
}
|
|
2408
2537
|
|
|
2409
|
-
// src/store/memory-context.ts
|
|
2410
|
-
async function loadAgentMemory(profile, channelId, opts = {}) {
|
|
2411
|
-
const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
|
|
2412
|
-
console.error("[memory] feature flag lookup failed:", err.message);
|
|
2413
|
-
return false;
|
|
2414
|
-
});
|
|
2415
|
-
if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
|
|
2416
|
-
const memoryPrompt = await listChannelMemories(channelId).then((memories) => formatMemoryPrompt(channelId, memories)).catch((err) => {
|
|
2417
|
-
console.error("[memory] memory lookup failed:", err.message);
|
|
2418
|
-
return void 0;
|
|
2419
|
-
});
|
|
2420
|
-
return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
2538
|
// src/store/workspace-store.ts
|
|
2424
2539
|
function rowToMembership(row) {
|
|
2425
2540
|
return {
|
|
@@ -2431,7 +2546,8 @@ function rowToMembership(row) {
|
|
|
2431
2546
|
role: row["role"],
|
|
2432
2547
|
status: row["status"],
|
|
2433
2548
|
invitedAt: row["invited_at"],
|
|
2434
|
-
acceptedAt: row["accepted_at"] ?? null
|
|
2549
|
+
acceptedAt: row["accepted_at"] ?? null,
|
|
2550
|
+
memoryAutoPaused: row["memory_auto_paused"] === true
|
|
2435
2551
|
};
|
|
2436
2552
|
}
|
|
2437
2553
|
var WORKSPACE_CACHE_TTL_MS = 6e4;
|
|
@@ -2504,9 +2620,6 @@ async function setWorkspaceStripeSubscription(workspaceId, fields) {
|
|
|
2504
2620
|
}).eq("id", workspaceId);
|
|
2505
2621
|
if (error) throw new Error(`setWorkspaceStripeSubscription(${workspaceId}): ${error.message}`);
|
|
2506
2622
|
}
|
|
2507
|
-
async function markWorkspaceAgentConnected(workspaceId) {
|
|
2508
|
-
await stampOnce(workspaceId, "agent_connected_at");
|
|
2509
|
-
}
|
|
2510
2623
|
async function markWorkspaceProjectsSetup(workspaceId) {
|
|
2511
2624
|
await stampOnce(workspaceId, "projects_setup_at");
|
|
2512
2625
|
}
|
|
@@ -2587,6 +2700,10 @@ async function listMembershipsByEmail(email) {
|
|
|
2587
2700
|
if (error) throw new Error(`listMembershipsByEmail: ${error.message}`);
|
|
2588
2701
|
return (data ?? []).map(rowToMembership);
|
|
2589
2702
|
}
|
|
2703
|
+
async function setMemoryAutoPaused(workspaceId, userId, paused) {
|
|
2704
|
+
const { error } = await getSupabaseClient().from("memberships").update({ memory_auto_paused: paused }).eq("workspace_id", workspaceId).eq("user_id", userId);
|
|
2705
|
+
if (error) throw new Error(`setMemoryAutoPaused(${workspaceId}): ${error.message}`);
|
|
2706
|
+
}
|
|
2590
2707
|
async function listMemberships(workspaceId) {
|
|
2591
2708
|
const { data, error } = await getSupabaseClient().from("memberships").select("*").eq("workspace_id", workspaceId).order("invited_at", { ascending: true });
|
|
2592
2709
|
if (error) throw new Error(`listMemberships(${workspaceId}): ${error.message}`);
|
|
@@ -2710,6 +2827,37 @@ async function setAgentPaused(workspaceId, paused) {
|
|
|
2710
2827
|
if (error) throw new Error(`setAgentPaused(${workspaceId}): ${error.message}`);
|
|
2711
2828
|
}
|
|
2712
2829
|
|
|
2830
|
+
// src/store/memory-context.ts
|
|
2831
|
+
async function loadAgentMemory(profile, channelId, opts = {}) {
|
|
2832
|
+
const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
|
|
2833
|
+
console.error("[memory] feature flag lookup failed:", err.message);
|
|
2834
|
+
return false;
|
|
2835
|
+
});
|
|
2836
|
+
if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
|
|
2837
|
+
const memoryPrompt = await buildPrompt(profile, channelId, opts).catch((err) => {
|
|
2838
|
+
console.error("[memory] memory lookup failed:", err.message);
|
|
2839
|
+
return void 0;
|
|
2840
|
+
});
|
|
2841
|
+
return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
|
|
2842
|
+
}
|
|
2843
|
+
async function buildPrompt(profile, channelId, opts) {
|
|
2844
|
+
const workspace = await getWorkspaceBySlug(profile);
|
|
2845
|
+
const handle = opts.senderHandle?.toLowerCase();
|
|
2846
|
+
const sender = workspace && handle ? (await listMemberships(workspace.id)).find(
|
|
2847
|
+
(m) => m.userId && m.handle.toLowerCase() === handle
|
|
2848
|
+
) : void 0;
|
|
2849
|
+
const [channel, shared, own] = await Promise.all([
|
|
2850
|
+
listMemories({ scope: "channel", channelId }),
|
|
2851
|
+
workspace ? listMemories({ scope: "workspace", workspaceId: workspace.id }) : [],
|
|
2852
|
+
workspace && sender ? listMemories({ scope: "user", workspaceId: workspace.id, userId: sender.userId }) : []
|
|
2853
|
+
]);
|
|
2854
|
+
const extras = {
|
|
2855
|
+
workspace: shared,
|
|
2856
|
+
...sender ? { user: { userId: sender.userId, handle: sender.handle, memories: own } } : {}
|
|
2857
|
+
};
|
|
2858
|
+
return formatMemoryPrompt(channelId, channel, extras);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2713
2861
|
// src/dashboard/settings.ts
|
|
2714
2862
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
|
|
2715
2863
|
import { dirname, resolve as resolve5 } from "path";
|
|
@@ -4427,6 +4575,11 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
4427
4575
|
var Scheduler = class {
|
|
4428
4576
|
jobs = /* @__PURE__ */ new Map();
|
|
4429
4577
|
caughtUp = /* @__PURE__ */ new Set();
|
|
4578
|
+
// A run is only recorded when it finishes, so while one is in flight the
|
|
4579
|
+
// store still shows the previous tick. Without this guard, a registry
|
|
4580
|
+
// reload during a long run re-registers the schedule and catchup starts a
|
|
4581
|
+
// second, overlapping copy (sah-budget-fetch ran twice a day this way).
|
|
4582
|
+
running = /* @__PURE__ */ new Set();
|
|
4430
4583
|
store;
|
|
4431
4584
|
constructor(store) {
|
|
4432
4585
|
this.store = store;
|
|
@@ -4478,6 +4631,11 @@ var Scheduler = class {
|
|
|
4478
4631
|
wrappedRun();
|
|
4479
4632
|
}
|
|
4480
4633
|
async runAndRecord(name, runFn) {
|
|
4634
|
+
if (this.running.has(name)) {
|
|
4635
|
+
console.log(`[scheduler] ${name} is still running, skipping overlapping run`);
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
this.running.add(name);
|
|
4481
4639
|
const startedAt = /* @__PURE__ */ new Date();
|
|
4482
4640
|
const elapsed = () => Date.now() - startedAt.getTime();
|
|
4483
4641
|
try {
|
|
@@ -4486,6 +4644,8 @@ var Scheduler = class {
|
|
|
4486
4644
|
} catch (err) {
|
|
4487
4645
|
console.error(`[scheduler] Error in ${name}:`, err);
|
|
4488
4646
|
this.store?.recordRun(name, "error", startedAt, elapsed());
|
|
4647
|
+
} finally {
|
|
4648
|
+
this.running.delete(name);
|
|
4489
4649
|
}
|
|
4490
4650
|
}
|
|
4491
4651
|
unregister(name) {
|
|
@@ -5352,7 +5512,7 @@ async function ownIdentityFor(client) {
|
|
|
5352
5512
|
return own;
|
|
5353
5513
|
}
|
|
5354
5514
|
async function mirrorIncomingMessage(client, msg) {
|
|
5355
|
-
if (!
|
|
5515
|
+
if (!databaseReachable()) return;
|
|
5356
5516
|
if (msg.subtype && SKIP_SUBTYPES.has(msg.subtype)) return;
|
|
5357
5517
|
if (msg.subtype === "message_changed" || msg.subtype === "message_deleted") return;
|
|
5358
5518
|
try {
|
|
@@ -8382,7 +8542,7 @@ function startUsageSync(store, workspace, options = {}) {
|
|
|
8382
8542
|
|
|
8383
8543
|
// src/store/usage-recording.ts
|
|
8384
8544
|
function installUsageRecording(dbPath, options = {}) {
|
|
8385
|
-
const { sync =
|
|
8545
|
+
const { sync = databaseReachable() } = options;
|
|
8386
8546
|
const stores = /* @__PURE__ */ new Map();
|
|
8387
8547
|
const stopSyncs = [];
|
|
8388
8548
|
const storeFor = (profile) => {
|
|
@@ -8642,10 +8802,7 @@ async function startServer() {
|
|
|
8642
8802
|
const store = new SessionStore(paths.DB_FILE);
|
|
8643
8803
|
const runStore = new AutomationRunStore(paths.DB_FILE);
|
|
8644
8804
|
installUsageRecording();
|
|
8645
|
-
const
|
|
8646
|
-
process.env["SUPABASE_URL"] && process.env["SUPABASE_SECRET_KEY"]
|
|
8647
|
-
);
|
|
8648
|
-
const stopRunSync = hasSupabaseCreds ? startAutomationRunSync(runStore, getProfile(), {
|
|
8805
|
+
const stopRunSync = databaseReachable() ? startAutomationRunSync(runStore, getProfile(), {
|
|
8649
8806
|
onError: (error) => console.error(`[${ts2()}] [flyspace] automation run sync:`, error)
|
|
8650
8807
|
}) : void 0;
|
|
8651
8808
|
const reactionStore = new ReactionStore(paths.DB_FILE);
|
|
@@ -8710,7 +8867,6 @@ export {
|
|
|
8710
8867
|
getChannelRegistry,
|
|
8711
8868
|
getProfileChannelRegistry,
|
|
8712
8869
|
findChannelById,
|
|
8713
|
-
findChannelByAgentName,
|
|
8714
8870
|
findChannel,
|
|
8715
8871
|
startChannelRegistryPolling,
|
|
8716
8872
|
isCwdUsable,
|
|
@@ -8718,6 +8874,7 @@ export {
|
|
|
8718
8874
|
warnOnBrokenChannelCwds,
|
|
8719
8875
|
resolveCwd,
|
|
8720
8876
|
resolveChannelPrompt,
|
|
8877
|
+
ChannelCwdError,
|
|
8721
8878
|
createChannel,
|
|
8722
8879
|
DefaultChannelError,
|
|
8723
8880
|
updateChannelById,
|
|
@@ -8745,6 +8902,7 @@ export {
|
|
|
8745
8902
|
modelLabel,
|
|
8746
8903
|
usageLimitedUntil,
|
|
8747
8904
|
usageLimitedModels,
|
|
8905
|
+
clearClaudeAuthFailed,
|
|
8748
8906
|
claudeAuthFailed,
|
|
8749
8907
|
buildSystemPrompt,
|
|
8750
8908
|
docsAccessPromptNote,
|
|
@@ -8761,16 +8919,23 @@ export {
|
|
|
8761
8919
|
MEMORY_TYPES,
|
|
8762
8920
|
isMemoryType,
|
|
8763
8921
|
validateMemoryInput,
|
|
8922
|
+
listMemories,
|
|
8923
|
+
getMemory,
|
|
8924
|
+
saveMemory,
|
|
8925
|
+
updateMemory,
|
|
8926
|
+
searchMemories,
|
|
8927
|
+
markMemoryUsed,
|
|
8928
|
+
deleteMemory,
|
|
8929
|
+
promoteChannelMemory,
|
|
8764
8930
|
listChannelMemories,
|
|
8765
8931
|
getChannelMemory,
|
|
8766
8932
|
saveChannelMemory,
|
|
8767
8933
|
updateChannelMemory,
|
|
8768
|
-
searchChannelMemories,
|
|
8769
8934
|
deleteChannelMemory,
|
|
8935
|
+
listMemoryEvents,
|
|
8770
8936
|
listThreadMemoryEvents,
|
|
8771
8937
|
undoMemoryEvent,
|
|
8772
8938
|
isUuid,
|
|
8773
|
-
loadAgentMemory,
|
|
8774
8939
|
getWorkspaceBySlug,
|
|
8775
8940
|
getWorkspaceById,
|
|
8776
8941
|
listWorkspaces,
|
|
@@ -8778,7 +8943,6 @@ export {
|
|
|
8778
8943
|
createWorkspace,
|
|
8779
8944
|
deleteWorkspace,
|
|
8780
8945
|
setWorkspaceStripeSubscription,
|
|
8781
|
-
markWorkspaceAgentConnected,
|
|
8782
8946
|
markWorkspaceProjectsSetup,
|
|
8783
8947
|
markWorkspaceGreeted,
|
|
8784
8948
|
getWorkspaceGreetedAt,
|
|
@@ -8790,6 +8954,7 @@ export {
|
|
|
8790
8954
|
getMembershipByEmail,
|
|
8791
8955
|
getMembershipByUserId,
|
|
8792
8956
|
listMembershipsByEmail,
|
|
8957
|
+
setMemoryAutoPaused,
|
|
8793
8958
|
listMemberships,
|
|
8794
8959
|
createMembership,
|
|
8795
8960
|
HANDLE_PATTERN,
|
|
@@ -8806,12 +8971,11 @@ export {
|
|
|
8806
8971
|
setAllowGuestAccess,
|
|
8807
8972
|
getAgentPaused,
|
|
8808
8973
|
setAgentPaused,
|
|
8809
|
-
|
|
8974
|
+
loadAgentMemory,
|
|
8810
8975
|
extractCategories,
|
|
8811
8976
|
buildAutoTitle,
|
|
8812
8977
|
BRANDING_DEFAULTS,
|
|
8813
8978
|
getWorkspaceBranding,
|
|
8814
|
-
localDefaults,
|
|
8815
8979
|
loadDashboardSettings,
|
|
8816
8980
|
saveDashboardSettings,
|
|
8817
8981
|
loadWorkspaceBranding,
|