flyspace-cli 0.1.33 → 0.1.34

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.
@@ -295,6 +295,15 @@ function resolveLogicalCwd(p) {
295
295
  function isUnderLogicalRoot(cwd) {
296
296
  return [logicalRoot(), ...workspaceRoots.values()].some((r) => cwd.startsWith(r + sep));
297
297
  }
298
+ function toLogicalCwd(cwd) {
299
+ const fromRoot = relative(logicalRoot(), cwd).split(sep).join("/");
300
+ if (isLogicalCwd(fromRoot)) return fromRoot;
301
+ for (const [slug, root] of workspaceRoots) {
302
+ const name = relative(root, cwd);
303
+ if (isLogicalCwd(`${slug}/${name}`)) return `${slug}/${name}`;
304
+ }
305
+ return cwd;
306
+ }
298
307
  function ensureLogicalCwdDir(cwd) {
299
308
  if (!isUnderLogicalRoot(cwd) || existsSync2(cwd)) return;
300
309
  mkdirSync(cwd, { recursive: true });
@@ -522,7 +531,7 @@ function resolveEffort(explicitEffort, channelId, fallback) {
522
531
  function toRow(input) {
523
532
  const row = {};
524
533
  if (input.name !== void 0) row["name"] = input.name;
525
- if (input.cwd !== void 0) row["cwd"] = isLogicalCwd(input.cwd) ? input.cwd : expandPath(input.cwd);
534
+ if (input.cwd !== void 0) row["cwd"] = isLogicalCwd(input.cwd) ? input.cwd : toLogicalCwd(expandPath(input.cwd));
526
535
  if (input.categoryId !== void 0) row["category_id"] = input.categoryId;
527
536
  if (input.externalChannelId !== void 0) row["external_channel_id"] = input.externalChannelId;
528
537
  if (input.model !== void 0) row["model"] = input.model;
package/dist/cli.js CHANGED
@@ -187,7 +187,7 @@ import {
187
187
  warnOnBrokenChannelCwds,
188
188
  workspaceSkillsDir,
189
189
  writeSkillFile
190
- } from "./chunk-KGX44L3V.js";
190
+ } from "./chunk-KTMAS4TP.js";
191
191
  import {
192
192
  firstReplyBody,
193
193
  getThread,
@@ -621,14 +621,14 @@ function handleHostDbResult(id, res) {
621
621
  }
622
622
  function whenConnected() {
623
623
  if (sender) return Promise.resolve(sender);
624
- return new Promise((resolve7, reject) => {
624
+ return new Promise((resolve8, reject) => {
625
625
  const timer2 = setTimeout(() => {
626
626
  onSender = onSender.filter((w) => w !== wake);
627
627
  reject(new Error("Host is not connected to the Data API"));
628
628
  }, REQUEST_TIMEOUT_MS);
629
629
  const wake = () => {
630
630
  clearTimeout(timer2);
631
- resolve7(sender);
631
+ resolve8(sender);
632
632
  };
633
633
  onSender.push(wake);
634
634
  });
@@ -636,12 +636,12 @@ function whenConnected() {
636
636
  async function requestHostDb(req) {
637
637
  const send2 = await whenConnected();
638
638
  const id = randomUUID();
639
- return new Promise((resolve7, reject) => {
639
+ return new Promise((resolve8, reject) => {
640
640
  const timer2 = setTimeout(() => {
641
641
  pending.delete(id);
642
642
  reject(new Error("Data API did not answer in time"));
643
643
  }, REQUEST_TIMEOUT_MS);
644
- pending.set(id, { resolve: resolve7, reject, timer: timer2 });
644
+ pending.set(id, { resolve: resolve8, reject, timer: timer2 });
645
645
  send2({ type: "db", id, req });
646
646
  });
647
647
  }
@@ -653,14 +653,14 @@ function handleAttachmentUrlResult(id, url) {
653
653
  async function requestAttachmentUrl(key) {
654
654
  const send2 = await whenConnected();
655
655
  const id = randomUUID();
656
- return new Promise((resolve7, reject) => {
656
+ return new Promise((resolve8, reject) => {
657
657
  const timer2 = setTimeout(() => {
658
658
  pendingUrls.delete(id);
659
659
  reject(new Error("Data API did not answer in time"));
660
660
  }, REQUEST_TIMEOUT_MS);
661
661
  pendingUrls.set(id, (url) => {
662
662
  clearTimeout(timer2);
663
- resolve7(url);
663
+ resolve8(url);
664
664
  });
665
665
  send2({ type: "attachment-url", id, key });
666
666
  });
@@ -807,10 +807,10 @@ function editConfig() {
807
807
  const editor = process.env["EDITOR"] || "vi";
808
808
  console.log(`[flyspace] Opening ${paths.CONFIG_FILE} in ${editor}...`);
809
809
  const child = spawn(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
810
- return new Promise((resolve7, reject) => {
810
+ return new Promise((resolve8, reject) => {
811
811
  child.on("close", (code) => {
812
812
  if (code === 0) {
813
- resolve7();
813
+ resolve8();
814
814
  } else {
815
815
  reject(new Error(`Editor exited with code ${code}`));
816
816
  }
@@ -1861,7 +1861,7 @@ async function runAuthCmd(args) {
1861
1861
  }
1862
1862
 
1863
1863
  // ../api/src/version.ts
1864
- var CLI_VERSION = true ? "0.1.33" : null;
1864
+ var CLI_VERSION = true ? "0.1.34" : null;
1865
1865
  function isOlderVersion(a, b) {
1866
1866
  const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1867
1867
  const [pa, pb] = [parse(a), parse(b)];
@@ -2471,14 +2471,19 @@ function npmEnv(target) {
2471
2471
  return nodeBin ? { ...process.env, PATH: `${nodeBin}:${process.env.PATH ?? ""}` } : process.env;
2472
2472
  }
2473
2473
  async function runUpgrade() {
2474
- const current = "0.1.33";
2474
+ const current = "0.1.34";
2475
2475
  console.log(`Current version: ${current}`);
2476
2476
  console.log("Checking for updates...\n");
2477
2477
  try {
2478
2478
  const latest2 = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", env: npmEnv() }).trim();
2479
2479
  const stale = installTargets().filter((t) => installedVersion(t) !== latest2);
2480
2480
  if (stale.length === 0) {
2481
- console.log(`Already on the latest version (${current}).`);
2481
+ if (current === latest2) {
2482
+ console.log(`Already on the latest version (${current}).`);
2483
+ return;
2484
+ }
2485
+ console.log(`${latest2} is installed but this process runs ${current}. Restarting...`);
2486
+ restartInstalledProfiles();
2482
2487
  return;
2483
2488
  }
2484
2489
  console.log(`New version available: ${latest2}`);
@@ -4795,10 +4800,10 @@ var DEFAULT_DASHBOARD_PORT = 4747;
4795
4800
  var DEFAULT_DASHBOARD_HOST = "127.0.0.1";
4796
4801
  async function findFreePort(start, host = DEFAULT_DASHBOARD_HOST, attempts = 50) {
4797
4802
  for (let port = start; port < start + attempts; port++) {
4798
- const free = await new Promise((resolve7) => {
4803
+ const free = await new Promise((resolve8) => {
4799
4804
  const probe3 = createServer();
4800
- probe3.once("error", () => resolve7(false));
4801
- probe3.once("listening", () => probe3.close(() => resolve7(true)));
4805
+ probe3.once("error", () => resolve8(false));
4806
+ probe3.once("listening", () => probe3.close(() => resolve8(true)));
4802
4807
  probe3.listen(port, host);
4803
4808
  });
4804
4809
  if (free) return port;
@@ -5626,14 +5631,14 @@ async function stepBrowserTokens(botName) {
5626
5631
  const appLink = page2.locator('a[href*="/apps/A"]').first();
5627
5632
  const hasExistingApp = await appLink.isVisible({ timeout: 3e3 }).catch(() => false);
5628
5633
  if (hasExistingApp) {
5629
- const tokenPromise = new Promise((resolve7) => {
5634
+ const tokenPromise = new Promise((resolve8) => {
5630
5635
  page2.on("request", (req) => {
5631
5636
  if (token) return;
5632
5637
  const postData = req.postData() || "";
5633
5638
  const match = postData.match(/xoxc-[a-f0-9-]+/);
5634
5639
  if (match) {
5635
5640
  token = match[0];
5636
- resolve7(token);
5641
+ resolve8(token);
5637
5642
  }
5638
5643
  });
5639
5644
  });
@@ -5684,7 +5689,7 @@ async function stepBrowserTokens(botName) {
5684
5689
  log4("Almost done! The browser will now open an install page.\n");
5685
5690
  console.log(' \x1B[1mPlease click "Install to Workspace", then click "Allow".\x1B[0m\n');
5686
5691
  await page2.goto(installUrl);
5687
- const installPage = await new Promise((resolve7, reject) => {
5692
+ const installPage = await new Promise((resolve8, reject) => {
5688
5693
  let settled = false;
5689
5694
  let interval;
5690
5695
  let timeout;
@@ -5697,7 +5702,7 @@ async function stepBrowserTokens(botName) {
5697
5702
  };
5698
5703
  const checkUrl = (p) => {
5699
5704
  if (p.url().includes("install-on-team?success=1")) {
5700
- finish2(() => resolve7(p));
5705
+ finish2(() => resolve8(p));
5701
5706
  }
5702
5707
  };
5703
5708
  context.on("page", (newPage) => {
@@ -6411,9 +6416,9 @@ async function runMcpCmd(args) {
6411
6416
 
6412
6417
  // src/commands/dashboard.ts
6413
6418
  import { spawn as spawn8, spawnSync } from "child_process";
6414
- import { existsSync as existsSync33 } from "fs";
6419
+ import { existsSync as existsSync34 } from "fs";
6415
6420
  import { platform as platform7 } from "os";
6416
- import { join as join28 } from "path";
6421
+ import { join as join29 } from "path";
6417
6422
 
6418
6423
  // src/reconcile.ts
6419
6424
  import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
@@ -6586,7 +6591,7 @@ var requireDocsAccess = async (c, next) => {
6586
6591
  };
6587
6592
 
6588
6593
  // ../api/src/dashboard/api.ts
6589
- import { Hono as Hono30 } from "hono";
6594
+ import { Hono as Hono31 } from "hono";
6590
6595
 
6591
6596
  // ../api/src/dashboard/public-share-routes.ts
6592
6597
  import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
@@ -7651,7 +7656,7 @@ function truncationNotice(lang, voice) {
7651
7656
  if (lang === "en" || lang.startsWith("en-")) return TRUNCATION_NOTICE.en;
7652
7657
  return null;
7653
7658
  }
7654
- var sleep2 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
7659
+ var sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
7655
7660
  async function synthesizeWithRetry(script, voice) {
7656
7661
  let lastError;
7657
7662
  for (let attempt = 1; attempt <= SYNTH_ATTEMPTS; attempt += 1) {
@@ -13992,14 +13997,244 @@ function createScopedMemoriesRouter() {
13992
13997
  return router;
13993
13998
  }
13994
13999
 
14000
+ // ../api/src/dashboard/session-import-routes.ts
14001
+ import { Hono as Hono28 } from "hono";
14002
+ var MAX_FOLDERS2 = 20;
14003
+ var DAY_MS = 24 * 60 * 60 * 1e3;
14004
+ var IMPORT_RANGES = ["all", "7d", "latest5"];
14005
+ async function ask(agent, req) {
14006
+ const res = await agent.sessionImportRequest?.(req);
14007
+ return res && res.op === req.op ? res : null;
14008
+ }
14009
+ function hostUnavailable2(c, agent) {
14010
+ return c.json(
14011
+ {
14012
+ error: "host offline",
14013
+ reason: agent.getHostCallFailure?.("sessionImportRequest") ?? "offline"
14014
+ },
14015
+ 503
14016
+ );
14017
+ }
14018
+ function pickSessions(sessions2, range, now = Date.now()) {
14019
+ if (range === "latest5") return sessions2.slice(0, 5);
14020
+ if (range === "7d") return sessions2.filter((s) => s.lastUsed >= now - 7 * DAY_MS);
14021
+ return sessions2;
14022
+ }
14023
+ function channelForFolder(folder, channels) {
14024
+ return folder.linkedAs ? channels.find((ch) => ch.name === folder.linkedAs && !ch.archivedAt) : void 0;
14025
+ }
14026
+ async function importRows(channelIds) {
14027
+ if (!channelIds.length) return [];
14028
+ const { data, error: error2 } = await getSupabaseClient().from("session_imports").select("channel_id, agent, session_id, thread_id, last_message_uuid, message_count").in("channel_id", channelIds);
14029
+ if (error2) throw new Error(error2.message);
14030
+ return data ?? [];
14031
+ }
14032
+ async function importableFolders(folders, channels) {
14033
+ const mapped = folders.map((f) => ({ folder: f, channel: channelForFolder(f, channels) }));
14034
+ const rows = await importRows(mapped.flatMap((m) => m.channel ? [m.channel.id] : []));
14035
+ const byKey = new Map(rows.map((r) => [rowKey(r.channel_id, r.agent, r.session_id), r]));
14036
+ return mapped.map(({ folder, channel }) => {
14037
+ const sessions2 = folder.sessions.flatMap((s) => {
14038
+ const row = channel ? byKey.get(rowKey(channel.id, s.agent, s.sessionId)) : void 0;
14039
+ return s.bound && !row ? [] : [{ ...s, row }];
14040
+ });
14041
+ return { folder: { ...folder, sessions: sessions2 }, channel };
14042
+ }).filter((m) => m.folder.sessions.length);
14043
+ }
14044
+ var rowKey = (channelId, agent, sessionId) => `${channelId}:${agent}:${sessionId}`;
14045
+ function replyRows(threadId, messages, author, fallbackAt) {
14046
+ return messages.map((m, i) => ({
14047
+ thread_id: threadId,
14048
+ author: m.role === "user" ? author : AGENT_AUTHOR,
14049
+ body: m.text,
14050
+ created_at: m.at ?? new Date(fallbackAt + i).toISOString()
14051
+ }));
14052
+ }
14053
+ async function syncSession(agent, channelId, session, author, existing) {
14054
+ const supabase = getSupabaseClient();
14055
+ const read = await ask(agent, {
14056
+ op: "read",
14057
+ agent: session.agent,
14058
+ sessionId: session.sessionId,
14059
+ ...existing?.last_message_uuid ? { afterUuid: existing.last_message_uuid } : {}
14060
+ });
14061
+ if (!read) return null;
14062
+ const messages = read.messages ?? [];
14063
+ if (!messages.length) return 0;
14064
+ const last = messages[messages.length - 1];
14065
+ if (existing) {
14066
+ const { error: error3 } = await supabase.from("thread_replies").insert(replyRows(existing.thread_id, messages, author, Date.now()));
14067
+ if (error3) throw new Error(error3.message);
14068
+ await supabase.from("session_imports").update({
14069
+ last_message_uuid: last.uuid,
14070
+ message_count: existing.message_count + messages.length,
14071
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
14072
+ }).eq("channel_id", channelId).eq("agent", session.agent).eq("session_id", session.sessionId);
14073
+ return messages.length;
14074
+ }
14075
+ const startedAt = messages[0].at ?? (/* @__PURE__ */ new Date()).toISOString();
14076
+ const { data: thread, error: threadError } = await supabase.from("threads").insert({
14077
+ channel_id: channelId,
14078
+ title: null,
14079
+ author,
14080
+ imported_agent: session.agent,
14081
+ created_at: startedAt
14082
+ }).select("id").single();
14083
+ if (threadError || !thread) throw new Error(threadError?.message ?? "could not create thread");
14084
+ const threadId = thread.id;
14085
+ const { error: claimError } = await supabase.from("session_imports").insert({
14086
+ channel_id: channelId,
14087
+ agent: session.agent,
14088
+ session_id: session.sessionId,
14089
+ thread_id: threadId,
14090
+ last_message_uuid: last.uuid,
14091
+ message_count: messages.length
14092
+ });
14093
+ if (claimError) {
14094
+ await supabase.from("threads").delete().eq("id", threadId);
14095
+ if (claimError.code === "23505") return 0;
14096
+ throw new Error(claimError.message);
14097
+ }
14098
+ const { error: error2 } = await supabase.from("thread_replies").insert(replyRows(threadId, messages, author, Date.parse(startedAt)));
14099
+ if (error2) throw new Error(error2.message);
14100
+ await ask(agent, {
14101
+ op: "bind",
14102
+ items: [{ channelId, threadId, agent: session.agent, sessionId: session.sessionId }]
14103
+ });
14104
+ return messages.length;
14105
+ }
14106
+ function createSessionImportRouter(agent) {
14107
+ const app = new Hono28();
14108
+ const scanFolders = async () => {
14109
+ const scan = await ask(agent, { op: "scan", slug: getProfile() });
14110
+ return scan && importableFolders(scan.folders, getProfileChannelRegistry());
14111
+ };
14112
+ const channelParam = (c) => getProfileChannelRegistry().find((ch) => ch.id === c.req.param("channelId"));
14113
+ app.get("/scan", async (c) => {
14114
+ const ctx = await resolveAuthContext(c);
14115
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14116
+ const folders = await scanFolders();
14117
+ if (!folders) return hostUnavailable2(c, agent);
14118
+ return c.json({
14119
+ folders: folders.map(({ folder, channel }) => ({
14120
+ ...folder,
14121
+ channel: channel ? { id: channel.id, name: channel.name } : null,
14122
+ sessions: folder.sessions.map(({ row, bound: _bound, ...s }) => ({
14123
+ ...s,
14124
+ imported: !!row
14125
+ }))
14126
+ }))
14127
+ });
14128
+ });
14129
+ app.post("/import", async (c) => {
14130
+ const ctx = await resolveAuthContext(c);
14131
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14132
+ const body = await c.req.json().catch(() => ({}));
14133
+ const picks = (Array.isArray(body.folders) ? body.folders : []).filter((f) => typeof f?.path === "string").map((f) => ({
14134
+ path: f.path,
14135
+ range: IMPORT_RANGES.includes(f.range ?? "") ? f.range : "all"
14136
+ }));
14137
+ if (!picks.length) return c.json({ error: "no folders chosen" }, 400);
14138
+ if (picks.length > MAX_FOLDERS2) return c.json({ error: `at most ${MAX_FOLDERS2} folders` }, 400);
14139
+ const folders = await scanFolders();
14140
+ if (!folders) return hostUnavailable2(c, agent);
14141
+ const offered = new Map(folders.map((m) => [m.folder.path, m]));
14142
+ if (picks.some((p) => !offered.has(p.path)))
14143
+ return c.json({ error: "folder was not offered by the scan" }, 400);
14144
+ const needLink = picks.filter((p) => !offered.get(p.path).channel);
14145
+ if (needLink.length && !isWorkspaceOwner(ctx)) {
14146
+ return c.json({ error: "workspace owner access required to add new folders" }, 403);
14147
+ }
14148
+ const taken = new Set(getProfileChannelRegistry().map((ch) => ch.name));
14149
+ const items = needLink.map((p) => ({
14150
+ name: uniqueChannelName(offered.get(p.path).folder.name, taken),
14151
+ path: p.path
14152
+ }));
14153
+ const linked = items.length ? await agent.linkProjectFolders?.(getProfile(), items) : [];
14154
+ if (!linked) return hostUnavailable2(c, agent);
14155
+ const created = /* @__PURE__ */ new Map();
14156
+ for (const [i, l] of linked.entries())
14157
+ created.set(items[i].path, await createChannel({ name: l.name, cwd: l.cwd }));
14158
+ const results = [];
14159
+ for (const p of picks) {
14160
+ const { folder, channel: existing } = offered.get(p.path);
14161
+ const channel = existing ?? created.get(p.path);
14162
+ let threads = 0;
14163
+ let messages = 0;
14164
+ for (const s of pickSessions(folder.sessions, p.range)) {
14165
+ if (s.row) continue;
14166
+ const n = await syncSession(agent, channel.id, s, ctx.handle, void 0);
14167
+ if (n === null) return hostUnavailable2(c, agent);
14168
+ if (n > 0) threads += 1;
14169
+ messages += n;
14170
+ }
14171
+ results.push({
14172
+ path: p.path,
14173
+ channel: { id: channel.id, name: channel.name },
14174
+ created: !existing,
14175
+ threads,
14176
+ messages
14177
+ });
14178
+ }
14179
+ return c.json({ results }, 201);
14180
+ });
14181
+ app.get("/channels/:channelId", async (c) => {
14182
+ const ctx = await resolveAuthContext(c);
14183
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14184
+ const channel = channelParam(c);
14185
+ if (!channel) return c.json({ error: "channel not found" }, 404);
14186
+ const folders = await scanFolders();
14187
+ if (!folders) return hostUnavailable2(c, agent);
14188
+ const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
14189
+ const sessions2 = [];
14190
+ for (const { row, bound: _bound, ...s } of folder?.sessions ?? []) {
14191
+ let newMessages = 0;
14192
+ if (row) {
14193
+ const read = await ask(agent, {
14194
+ op: "read",
14195
+ agent: s.agent,
14196
+ sessionId: s.sessionId,
14197
+ ...row.last_message_uuid ? { afterUuid: row.last_message_uuid } : {}
14198
+ });
14199
+ newMessages = read?.messages?.length ?? 0;
14200
+ }
14201
+ const status = !row ? "not-imported" : newMessages ? "new" : "synced";
14202
+ sessions2.push({ ...s, threadId: row?.thread_id ?? null, status, newMessages });
14203
+ }
14204
+ return c.json({ folder: folder?.path ?? null, sessions: sessions2 });
14205
+ });
14206
+ app.post("/channels/:channelId/sync", async (c) => {
14207
+ const ctx = await resolveAuthContext(c);
14208
+ if (ctx?.role !== "admin") return c.json({ error: "admin access required" }, 403);
14209
+ const channel = channelParam(c);
14210
+ if (!channel) return c.json({ error: "channel not found" }, 404);
14211
+ const body = await c.req.json().catch(() => ({}));
14212
+ const only = Array.isArray(body.sessionIds) ? new Set(body.sessionIds.filter((s) => typeof s === "string")) : null;
14213
+ const folders = await scanFolders();
14214
+ if (!folders) return hostUnavailable2(c, agent);
14215
+ const folder = folders.find((m) => m.channel?.id === channel.id)?.folder;
14216
+ let threads = 0;
14217
+ let messages = 0;
14218
+ for (const s of folder?.sessions ?? []) {
14219
+ if (only && !only.has(s.sessionId)) continue;
14220
+ const n = await syncSession(agent, channel.id, s, ctx.handle, s.row);
14221
+ if (n === null) return hostUnavailable2(c, agent);
14222
+ if (!s.row && n > 0) threads += 1;
14223
+ messages += n;
14224
+ }
14225
+ return c.json({ threads, messages });
14226
+ });
14227
+ return app;
14228
+ }
14229
+
13995
14230
  // ../api/src/dashboard/host-status-signal.ts
13996
14231
  var HOST_STATUS_CHANNEL = "host-status";
13997
14232
  var channelReady = null;
13998
14233
  function getHostStatusChannel() {
13999
- channelReady ??= new Promise((resolve7) => {
14234
+ channelReady ??= new Promise((resolve8) => {
14000
14235
  const channel = getSupabaseClient().channel(HOST_STATUS_CHANNEL);
14001
14236
  channel.subscribe((state) => {
14002
- if (state === "SUBSCRIBED") resolve7(channel);
14237
+ if (state === "SUBSCRIBED") resolve8(channel);
14003
14238
  });
14004
14239
  });
14005
14240
  return channelReady;
@@ -14164,6 +14399,7 @@ function registerDataRoutes(api, agent = {}) {
14164
14399
  });
14165
14400
  api.route("/channels", createDocShareWriteRouter(agent));
14166
14401
  api.route("/onboarding", createOnboardingRouter(agent));
14402
+ api.route("/session-import", createSessionImportRouter(agent));
14167
14403
  api.route("/channels", createChannelGreetingRouter(agent));
14168
14404
  api.route("/channels", createChannelMembersRouter());
14169
14405
  api.route("/channels", createMemoriesRouter());
@@ -14418,7 +14654,7 @@ function registerDataRoutes(api, agent = {}) {
14418
14654
  // ../api/src/dashboard/device-auth-routes.ts
14419
14655
  import { verify } from "crypto";
14420
14656
  import { getConnInfo } from "@hono/node-server/conninfo";
14421
- import { Hono as Hono28 } from "hono";
14657
+ import { Hono as Hono29 } from "hono";
14422
14658
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14423
14659
  function cleanText(value, max) {
14424
14660
  if (typeof value !== "string") return null;
@@ -14466,7 +14702,7 @@ async function boundWorkspaceIds(publicKey, userId) {
14466
14702
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
14467
14703
  }
14468
14704
  function createDeviceAuthRouter() {
14469
- const app = new Hono28();
14705
+ const app = new Hono29();
14470
14706
  app.post("/code", async (c) => {
14471
14707
  const body = await readBody(c);
14472
14708
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -14785,7 +15021,7 @@ var workspaceRef = () => ({ kind: "workspace" });
14785
15021
  var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
14786
15022
 
14787
15023
  // ../api/src/dashboard/output-styles-routes.ts
14788
- import { Hono as Hono29 } from "hono";
15024
+ import { Hono as Hono30 } from "hono";
14789
15025
  function requireAdmin3(auth) {
14790
15026
  return auth?.role === "admin" ? null : "admin access required";
14791
15027
  }
@@ -14805,7 +15041,7 @@ function failure(c, err) {
14805
15041
  return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
14806
15042
  }
14807
15043
  function createOutputStylesRouter() {
14808
- const app = new Hono29();
15044
+ const app = new Hono30();
14809
15045
  app.get("/", (c) => {
14810
15046
  const auth = c.get("auth");
14811
15047
  const denied = requireAdmin3(auth);
@@ -14897,7 +15133,7 @@ function createOutputStylesRouter() {
14897
15133
  return app;
14898
15134
  }
14899
15135
  async function handleOutputStylesRequest(req) {
14900
- const app = new Hono29();
15136
+ const app = new Hono30();
14901
15137
  app.use("*", async (c, next) => {
14902
15138
  c.set("auth", { role: "admin" });
14903
15139
  await next();
@@ -14918,7 +15154,7 @@ import { streamSSE } from "hono/streaming";
14918
15154
 
14919
15155
  // ../api/src/dashboard/api.ts
14920
15156
  function createGatedRouter(agent, register) {
14921
- const api = new Hono30();
15157
+ const api = new Hono31();
14922
15158
  api.use("*", async (c, next) => {
14923
15159
  const known = await listKnownWorkspaces();
14924
15160
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -14971,7 +15207,7 @@ import sharp7 from "sharp";
14971
15207
 
14972
15208
  // src/dashboard/channels-routes.ts
14973
15209
  import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
14974
- import { Hono as Hono31 } from "hono";
15210
+ import { Hono as Hono32 } from "hono";
14975
15211
  function coerceAccess(raw) {
14976
15212
  if (raw === "open" || raw === "*" || raw === "all") return raw;
14977
15213
  if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
@@ -15034,7 +15270,7 @@ function toResponse3(channel) {
15034
15270
  };
15035
15271
  }
15036
15272
  function createChannelWriteRouter() {
15037
- const app = new Hono31();
15273
+ const app = new Hono32();
15038
15274
  app.post("/", async (c) => {
15039
15275
  const body = await c.req.json().catch(() => ({}));
15040
15276
  const name = typeof body["name"] === "string" ? body["name"].trim() : "";
@@ -15174,7 +15410,7 @@ import {
15174
15410
  } from "fs";
15175
15411
  import { Readable } from "stream";
15176
15412
  import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
15177
- import { Hono as Hono32 } from "hono";
15413
+ import { Hono as Hono33 } from "hono";
15178
15414
  import { streamSSE as streamSSE2 } from "hono/streaming";
15179
15415
 
15180
15416
  // src/dashboard/docs-watcher.ts
@@ -15476,7 +15712,7 @@ function needsRefresh(expiresAt, now = Date.now()) {
15476
15712
  if (!expiresAt) return false;
15477
15713
  return new Date(expiresAt).getTime() - now < REFRESH_SKEW_MS;
15478
15714
  }
15479
- var sleep3 = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
15715
+ var sleep3 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
15480
15716
  async function refreshConnection(params) {
15481
15717
  const { workspaceId, connection, provider } = params;
15482
15718
  const deps = params.deps ?? {};
@@ -15810,7 +16046,7 @@ ${req.user}`,
15810
16046
  };
15811
16047
  }
15812
16048
  function run3(inv, cwd) {
15813
- return new Promise((resolve7) => {
16049
+ return new Promise((resolve8) => {
15814
16050
  const child = spawn4(inv.command, inv.args, {
15815
16051
  cwd,
15816
16052
  env: inv.env,
@@ -15824,14 +16060,14 @@ function run3(inv, cwd) {
15824
16060
  child.on("error", (err) => {
15825
16061
  clearTimeout(timer2);
15826
16062
  console.error(`[memory-extraction] ${inv.command} failed to start:`, err.message);
15827
- resolve7({ ok: false });
16063
+ resolve8({ ok: false });
15828
16064
  });
15829
16065
  child.on("close", (code) => {
15830
16066
  clearTimeout(timer2);
15831
16067
  if (code !== 0) {
15832
16068
  const detail = (stderr || stdout).trim().split("\n").at(-1) ?? "";
15833
16069
  console.error(`[memory-extraction] ${inv.command} exited ${code}: ${detail.slice(0, 200)}`);
15834
- resolve7({ ok: false });
16070
+ resolve8({ ok: false });
15835
16071
  return;
15836
16072
  }
15837
16073
  let raw = stdout;
@@ -15842,7 +16078,7 @@ function run3(inv, cwd) {
15842
16078
  raw = "";
15843
16079
  }
15844
16080
  }
15845
- resolve7({ ok: true, raw });
16081
+ resolve8({ ok: true, raw });
15846
16082
  });
15847
16083
  child.stdin.end(inv.stdin);
15848
16084
  });
@@ -15904,7 +16140,7 @@ Rules:
15904
16140
  - No preamble like "Summary:" \u2014 start directly with the summary text.`;
15905
16141
  var HAIKU_TIMEOUT_MS = 6e4;
15906
16142
  function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
15907
- return new Promise((resolve7, reject) => {
16143
+ return new Promise((resolve8, reject) => {
15908
16144
  const args = [
15909
16145
  "--print",
15910
16146
  "--model",
@@ -15955,7 +16191,7 @@ function runHaikuPrint(input2, claudeConfigDir, claudeCodeOauthToken) {
15955
16191
  reject(new Error("claude -p --model haiku returned empty output"));
15956
16192
  return;
15957
16193
  }
15958
- resolve7(text);
16194
+ resolve8(text);
15959
16195
  });
15960
16196
  });
15961
16197
  }
@@ -16351,6 +16587,15 @@ var ChatAgent = class {
16351
16587
  * restart picks up edits. */
16352
16588
  workspaceRoots = null;
16353
16589
  /** Fire-and-forget entry point, called by chat-routes after every write. */
16590
+ /** The CLI of the session already bound to this thread, if any. */
16591
+ boundRunner(channelId, threadId) {
16592
+ const store = this.openSessionStore();
16593
+ try {
16594
+ return store.getSession(`chat:${channelId}`, threadId)?.runner;
16595
+ } finally {
16596
+ store.close();
16597
+ }
16598
+ }
16354
16599
  handleMessage(msg) {
16355
16600
  if (msg.author === AGENT_AUTHOR) return;
16356
16601
  this.seenReplies.add(msg.replyId);
@@ -16731,7 +16976,7 @@ var ChatAgent = class {
16731
16976
  );
16732
16977
  ensureLogicalCwdDir(baseConfig.cwd);
16733
16978
  const pickedModel = msg.model?.trim();
16734
- let activeRunner = pickedModel ? runnerForModel(pickedModel) : defaultRunner;
16979
+ let activeRunner = pickedModel ? runnerForModel(pickedModel) : this.boundRunner(thread.channel_id, msg.threadId) ?? defaultRunner;
16735
16980
  let channelConfig = pickedModel ? {
16736
16981
  ...baseConfig,
16737
16982
  ...activeRunner === "codex" ? { model: pickedModel, codexModel: pickedModel } : { model: pickedModel }
@@ -17037,22 +17282,22 @@ ${msg.body}` : msg.body;
17037
17282
  let ready;
17038
17283
  try {
17039
17284
  channel = this.supabase().channel(`agent-thinking:${threadId}`);
17040
- ready = new Promise((resolve7) => {
17285
+ ready = new Promise((resolve8) => {
17041
17286
  const timeout = setTimeout(() => {
17042
17287
  console.error(`[chat-agent] thinking-channel ${threadId}: subscribe timed out after 10s`);
17043
- resolve7(false);
17288
+ resolve8(false);
17044
17289
  }, 1e4);
17045
17290
  channel.subscribe((status, err) => {
17046
17291
  if (status === "SUBSCRIBED") {
17047
17292
  clearTimeout(timeout);
17048
- resolve7(true);
17293
+ resolve8(true);
17049
17294
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
17050
17295
  console.error(
17051
17296
  `[chat-agent] thinking-channel ${threadId}: ${status}`,
17052
17297
  err?.message ?? ""
17053
17298
  );
17054
17299
  clearTimeout(timeout);
17055
- resolve7(false);
17300
+ resolve8(false);
17056
17301
  }
17057
17302
  });
17058
17303
  });
@@ -17099,19 +17344,19 @@ ${msg.body}` : msg.body;
17099
17344
  let ready;
17100
17345
  try {
17101
17346
  channel = this.supabase().channel("channel-activity");
17102
- ready = new Promise((resolve7) => {
17347
+ ready = new Promise((resolve8) => {
17103
17348
  const timeout = setTimeout(() => {
17104
17349
  console.error("[chat-agent] channel-activity: subscribe timed out after 10s");
17105
- resolve7(false);
17350
+ resolve8(false);
17106
17351
  }, 1e4);
17107
17352
  channel.subscribe((status, err) => {
17108
17353
  if (status === "SUBSCRIBED") {
17109
17354
  clearTimeout(timeout);
17110
- resolve7(true);
17355
+ resolve8(true);
17111
17356
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
17112
17357
  console.error(`[chat-agent] channel-activity: ${status}`, err?.message ?? "");
17113
17358
  clearTimeout(timeout);
17114
- resolve7(false);
17359
+ resolve8(false);
17115
17360
  }
17116
17361
  });
17117
17362
  });
@@ -18055,7 +18300,7 @@ function deleteDocEntry(root, path) {
18055
18300
  };
18056
18301
  }
18057
18302
  function createDocsRouter({ gate = requireDocsAccess } = {}) {
18058
- const app = new Hono32();
18303
+ const app = new Hono33();
18059
18304
  app.use("/:id/docs", gate);
18060
18305
  app.use("/:id/docs/*", gate);
18061
18306
  app.get("/:id/docs", (c) => {
@@ -18393,7 +18638,7 @@ function withDocLinkBridge(html) {
18393
18638
  // src/dashboard/skills-routes-host.ts
18394
18639
  import { existsSync as existsSync27, statSync as statSync7 } from "fs";
18395
18640
  import { dirname as dirname9 } from "path";
18396
- import { Hono as Hono33 } from "hono";
18641
+ import { Hono as Hono34 } from "hono";
18397
18642
  function workspaceScope() {
18398
18643
  const claudeConfigDir = profileClaudeConfigDir();
18399
18644
  const cwd = defaultClaudeCwd();
@@ -18499,12 +18744,12 @@ var diskBackend = {
18499
18744
  write: writeSkillAndSync
18500
18745
  };
18501
18746
  function createWorkspaceSkillsRouter() {
18502
- const app = new Hono33();
18747
+ const app = new Hono34();
18503
18748
  mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
18504
18749
  return app;
18505
18750
  }
18506
18751
  function createChannelSkillsRouter() {
18507
- const app = new Hono33();
18752
+ const app = new Hono34();
18508
18753
  mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
18509
18754
  return app;
18510
18755
  }
@@ -18639,7 +18884,7 @@ import { randomUUID as randomUUID5 } from "crypto";
18639
18884
  import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
18640
18885
  import { createRequire } from "module";
18641
18886
  import { dirname as dirname10, join as join25 } from "path";
18642
- import { Hono as Hono34 } from "hono";
18887
+ import { Hono as Hono35 } from "hono";
18643
18888
  var SESSION_TTL_MS = 10 * 6e4;
18644
18889
  var URL_WAIT_MS = 3e4;
18645
18890
  var TOKEN_WAIT_MS = 6e4;
@@ -18688,13 +18933,13 @@ function track(session) {
18688
18933
  return [id, s];
18689
18934
  }
18690
18935
  function waitFor(s, parse, ms) {
18691
- return new Promise((resolve7) => {
18936
+ return new Promise((resolve8) => {
18692
18937
  const started = Date.now();
18693
18938
  const tick = setInterval(() => {
18694
18939
  const v = parse();
18695
18940
  if (v !== void 0 || s.status !== "pending" || Date.now() - started > ms) {
18696
18941
  clearInterval(tick);
18697
- resolve7(v);
18942
+ resolve8(v);
18698
18943
  }
18699
18944
  }, 200);
18700
18945
  });
@@ -18796,7 +19041,7 @@ function failure2(c, err) {
18796
19041
  return c.json({ error: err.message }, 500);
18797
19042
  }
18798
19043
  function createProviderLoginRouter() {
18799
- const app = new Hono34();
19044
+ const app = new Hono35();
18800
19045
  app.use("*", async (c, next) => {
18801
19046
  const denied = requireAdmin4(c.get("auth"));
18802
19047
  if (denied) return c.json({ error: denied }, 403);
@@ -18840,7 +19085,7 @@ function createProviderLoginRouter() {
18840
19085
  return app;
18841
19086
  }
18842
19087
  async function handleProviderLoginRequest(req) {
18843
- const app = new Hono34();
19088
+ const app = new Hono35();
18844
19089
  app.use("*", async (c, next) => {
18845
19090
  c.set("auth", { role: "admin" });
18846
19091
  await next();
@@ -18992,18 +19237,253 @@ function createHostApiRouter(agent = {}) {
18992
19237
 
18993
19238
  // src/dashboard/agent-bridge-host.ts
18994
19239
  import { sign as signRaw, timingSafeEqual as timingSafeEqual2 } from "crypto";
18995
- import { existsSync as existsSync31, readFileSync as readFileSync26 } from "fs";
19240
+ import { existsSync as existsSync32, readFileSync as readFileSync27 } from "fs";
18996
19241
  import dotenv2 from "dotenv";
18997
19242
  import { WebSocket as WebSocket2 } from "ws";
18998
19243
 
19244
+ // src/dashboard/session-import-host.ts
19245
+ import {
19246
+ closeSync as closeSync3,
19247
+ existsSync as existsSync31,
19248
+ openSync as openSync3,
19249
+ readdirSync as readdirSync6,
19250
+ readFileSync as readFileSync25,
19251
+ readSync as readSync3,
19252
+ realpathSync as realpathSync5
19253
+ } from "fs";
19254
+ import { basename as basename9, join as join26, resolve as resolve7 } from "path";
19255
+ var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
19256
+ var HEAD_BYTES = 64 * 1024;
19257
+ var TITLE_MAX = 80;
19258
+ function readHead2(file) {
19259
+ let fd;
19260
+ try {
19261
+ fd = openSync3(file, "r");
19262
+ const buf = Buffer.alloc(HEAD_BYTES);
19263
+ return buf.toString("utf-8", 0, readSync3(fd, buf, 0, buf.length, 0));
19264
+ } catch {
19265
+ return "";
19266
+ } finally {
19267
+ if (fd !== void 0) closeSync3(fd);
19268
+ }
19269
+ }
19270
+ function parseLines(text) {
19271
+ const out = [];
19272
+ for (const line of text.split("\n")) {
19273
+ if (!line) continue;
19274
+ try {
19275
+ out.push(JSON.parse(line));
19276
+ } catch {
19277
+ }
19278
+ }
19279
+ return out;
19280
+ }
19281
+ function cleanText2(text) {
19282
+ const t = text.trim();
19283
+ if (!t || t.startsWith("<")) return null;
19284
+ return t;
19285
+ }
19286
+ function blocksText(content, kinds) {
19287
+ if (typeof content === "string") return cleanText2(content);
19288
+ if (!Array.isArray(content)) return null;
19289
+ const parts = content.filter(
19290
+ (b) => kinds.includes(b?.type) && typeof b.text === "string"
19291
+ ).map((b) => b.text);
19292
+ return parts.length ? cleanText2(parts.join("\n\n")) : null;
19293
+ }
19294
+ function boundSessionIds() {
19295
+ if (!existsSync31(paths.DB_FILE)) return /* @__PURE__ */ new Set();
19296
+ const store = new SessionStore(paths.DB_FILE);
19297
+ try {
19298
+ return new Set(store.listSessions().map((s) => s.sessionId));
19299
+ } finally {
19300
+ store.close();
19301
+ }
19302
+ }
19303
+ function claudeMessages(lines) {
19304
+ const out = [];
19305
+ for (const o of lines) {
19306
+ if (o["type"] !== "user" && o["type"] !== "assistant" || o["isMeta"] || o["isSidechain"])
19307
+ continue;
19308
+ if (o["entrypoint"] === "sdk-cli") continue;
19309
+ const role = o["type"];
19310
+ const text = blocksText(o["message"]?.content, ["text"]);
19311
+ if (!text || typeof o["uuid"] !== "string") continue;
19312
+ const at = typeof o["timestamp"] === "string" ? o["timestamp"] : null;
19313
+ const last = out[out.length - 1];
19314
+ if (role === "assistant" && last?.role === "assistant") {
19315
+ last.text += `
19316
+
19317
+ ${text}`;
19318
+ last.uuid = o["uuid"];
19319
+ } else {
19320
+ out.push({ uuid: o["uuid"], role, text, at });
19321
+ }
19322
+ }
19323
+ return out;
19324
+ }
19325
+ function codexMessages(lines) {
19326
+ const out = [];
19327
+ lines.forEach((o, i) => {
19328
+ const p = o["payload"];
19329
+ if (o["type"] !== "response_item" || p?.type !== "message") return;
19330
+ if (p.role !== "user" && p.role !== "assistant") return;
19331
+ const text = blocksText(p.content, ["input_text", "output_text"]);
19332
+ if (!text) return;
19333
+ out.push({
19334
+ uuid: `line:${i}`,
19335
+ role: p.role,
19336
+ text,
19337
+ at: typeof o["timestamp"] === "string" ? o["timestamp"] : null
19338
+ });
19339
+ });
19340
+ return out;
19341
+ }
19342
+ function claudeSummary(file, mtime) {
19343
+ const head = parseLines(readHead2(file));
19344
+ if (head.some((o) => o["entrypoint"] === "sdk-cli")) return null;
19345
+ const first = claudeMessages(head).find((m) => m.role === "user");
19346
+ if (!first) return null;
19347
+ return {
19348
+ agent: "claude",
19349
+ sessionId: basename9(file, ".jsonl"),
19350
+ title: first.text.slice(0, TITLE_MAX),
19351
+ lastUsed: mtime
19352
+ };
19353
+ }
19354
+ function codexSummary(file, mtime) {
19355
+ const head = parseLines(readHead2(file));
19356
+ const meta = head.find((o) => o["type"] === "session_meta")?.["payload"];
19357
+ if (!meta?.id || !meta.cwd || meta.originator === "codex_exec") return null;
19358
+ const first = codexMessages(head).find((m) => m.role === "user");
19359
+ if (!first) return null;
19360
+ return {
19361
+ cwd: meta.cwd,
19362
+ session: {
19363
+ agent: "codex",
19364
+ sessionId: meta.id,
19365
+ title: first.text.slice(0, TITLE_MAX),
19366
+ lastUsed: mtime
19367
+ }
19368
+ };
19369
+ }
19370
+ function enabledAgents() {
19371
+ return new Set(
19372
+ detectRunners().filter((r) => r.installed && r.loggedIn).map((r) => r.id)
19373
+ );
19374
+ }
19375
+ function scanSessionFolders(slug) {
19376
+ const agents = enabledAgents();
19377
+ const bound = boundSessionIds();
19378
+ const since2 = Date.now() - MAX_AGE_MS;
19379
+ const byPath = /* @__PURE__ */ new Map();
19380
+ const add = (cwd, s) => {
19381
+ const path = resolve7(cwd);
19382
+ if (isExcluded(path)) return;
19383
+ byPath.set(path, [
19384
+ ...byPath.get(path) ?? [],
19385
+ bound.has(s.sessionId) ? { ...s, bound: true } : s
19386
+ ]);
19387
+ };
19388
+ if (agents.has("claude")) {
19389
+ const root = join26(claudeDir(), "projects");
19390
+ let dirs = [];
19391
+ try {
19392
+ dirs = readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
19393
+ } catch {
19394
+ }
19395
+ for (const dir of dirs) {
19396
+ const files = jsonlFiles(join26(root, dir), 0).filter((f) => f.mtime >= since2);
19397
+ let cwd = null;
19398
+ for (const f of files) {
19399
+ cwd ??= cwdFromTranscript(f.file);
19400
+ if (cwd) break;
19401
+ }
19402
+ if (!cwd) continue;
19403
+ for (const f of files) {
19404
+ const s = claudeSummary(f.file, f.mtime);
19405
+ if (s) add(cwd, s);
19406
+ }
19407
+ }
19408
+ }
19409
+ if (agents.has("codex")) {
19410
+ for (const f of jsonlFiles(join26(codexDir(), "sessions"), 4)) {
19411
+ if (f.mtime < since2) continue;
19412
+ const s = codexSummary(f.file, f.mtime);
19413
+ if (s) add(s.cwd, s.session);
19414
+ }
19415
+ }
19416
+ const linked = linkedTargets(slug);
19417
+ const folders = [...byPath].map(([path, sessions2]) => {
19418
+ let real = path;
19419
+ try {
19420
+ real = realpathSync5(path);
19421
+ } catch {
19422
+ }
19423
+ sessions2.sort((a, b) => b.lastUsed - a.lastUsed);
19424
+ return {
19425
+ path,
19426
+ name: basename9(path),
19427
+ lastUsed: sessions2[0].lastUsed,
19428
+ sessions: sessions2,
19429
+ ...linked.has(real) ? { linkedAs: linked.get(real) } : {}
19430
+ };
19431
+ });
19432
+ return folders.sort((a, b) => b.lastUsed - a.lastUsed);
19433
+ }
19434
+ function findTranscript(agent, sessionId) {
19435
+ if (!/^[A-Za-z0-9-]+$/.test(sessionId)) return null;
19436
+ if (agent === "claude") {
19437
+ const root = join26(claudeDir(), "projects");
19438
+ try {
19439
+ for (const e of readdirSync6(root, { withFileTypes: true })) {
19440
+ const file = join26(root, e.name, `${sessionId}.jsonl`);
19441
+ if (e.isDirectory() && existsSync31(file)) return file;
19442
+ }
19443
+ } catch {
19444
+ }
19445
+ return null;
19446
+ }
19447
+ return jsonlFiles(join26(codexDir(), "sessions"), 4).find((f) => f.file.endsWith(`${sessionId}.jsonl`))?.file ?? null;
19448
+ }
19449
+ function readSessionMessages(agent, sessionId, afterUuid) {
19450
+ const file = findTranscript(agent, sessionId);
19451
+ if (!file) return null;
19452
+ const lines = parseLines(readFileSync25(file, "utf-8"));
19453
+ const messages = agent === "claude" ? claudeMessages(lines) : codexMessages(lines);
19454
+ if (!afterUuid) return messages;
19455
+ const idx = messages.findIndex((m) => m.uuid === afterUuid);
19456
+ return idx === -1 ? [] : messages.slice(idx + 1);
19457
+ }
19458
+ function bindSessions(items) {
19459
+ const store = new SessionStore(paths.DB_FILE);
19460
+ try {
19461
+ for (const i of items)
19462
+ store.saveSession(`chat:${i.channelId}`, i.threadId, i.sessionId, i.agent);
19463
+ } finally {
19464
+ store.close();
19465
+ }
19466
+ return items.length;
19467
+ }
19468
+ function handleSessionImportRequest(req) {
19469
+ switch (req.op) {
19470
+ case "scan":
19471
+ return { op: "scan", folders: scanSessionFolders(req.slug) };
19472
+ case "read":
19473
+ return { op: "read", messages: readSessionMessages(req.agent, req.sessionId, req.afterUuid) };
19474
+ case "bind":
19475
+ return { op: "bind", bound: bindSessions(req.items) };
19476
+ }
19477
+ }
19478
+
18999
19479
  // src/dashboard/docs-relay-host.ts
19000
- import { Hono as Hono35 } from "hono";
19480
+ import { Hono as Hono36 } from "hono";
19001
19481
  var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
19002
19482
  async function handleDocsRequest(req) {
19003
19483
  return runRelayed(req, (app) => app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() })));
19004
19484
  }
19005
19485
  async function runRelayed(req, register) {
19006
- const app = new Hono35();
19486
+ const app = new Hono36();
19007
19487
  app.use("*", async (c, next) => {
19008
19488
  c.set("auth", req.auth);
19009
19489
  await next();
@@ -19026,7 +19506,7 @@ async function handleHostRequest(req) {
19026
19506
  }
19027
19507
 
19028
19508
  // src/dashboard/doc-share-publisher.ts
19029
- import { readFileSync as readFileSync25, statSync as statSync9 } from "fs";
19509
+ import { readFileSync as readFileSync26, statSync as statSync9 } from "fs";
19030
19510
  import { extname as extname2, posix } from "path";
19031
19511
  var MAX_ASSET_BYTES = 20 * 1024 * 1024;
19032
19512
  var MAX_CONTENT_BYTES2 = 2 * 1024 * 1024;
@@ -19141,7 +19621,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
19141
19621
  }
19142
19622
  if (!stat.isFile()) return { status: "not-found" };
19143
19623
  if (stat.size > MAX_CONTENT_BYTES2) return { status: "too-large" };
19144
- const source = readFileSync25(abs, "utf8");
19624
+ const source = readFileSync26(abs, "utf8");
19145
19625
  const title = extractTitle(abs, kind === "markdown" ? "md" : kind);
19146
19626
  if (kind === "excalidraw") {
19147
19627
  try {
@@ -19212,7 +19692,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
19212
19692
  let text = null;
19213
19693
  try {
19214
19694
  const size = statSync9(docAbs).size;
19215
- if (size <= budget) text = readFileSync25(docAbs, "utf8");
19695
+ if (size <= budget) text = readFileSync26(docAbs, "utf8");
19216
19696
  } catch {
19217
19697
  continue;
19218
19698
  }
@@ -19229,7 +19709,7 @@ function readDocShareBundle({ cwd, sourcePath }) {
19229
19709
  let body;
19230
19710
  try {
19231
19711
  if (statSync9(resolved2.abs).size > MAX_ASSET_BYTES) continue;
19232
- body = readFileSync25(resolved2.abs);
19712
+ body = readFileSync26(resolved2.abs);
19233
19713
  } catch {
19234
19714
  continue;
19235
19715
  }
@@ -19257,13 +19737,13 @@ function readDocShareBundle({ cwd, sourcePath }) {
19257
19737
 
19258
19738
  // src/dashboard/host-upgrade.ts
19259
19739
  import { spawn as spawn7 } from "child_process";
19260
- import { mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
19261
- import { join as join26 } from "path";
19740
+ import { mkdirSync as mkdirSync13, openSync as openSync4 } from "fs";
19741
+ import { join as join27 } from "path";
19262
19742
  var running = false;
19263
19743
  function startHostUpgrade() {
19264
19744
  if (running) return { started: false };
19265
19745
  mkdirSync13(paths.LOG_DIR, { recursive: true });
19266
- const log5 = openSync3(join26(paths.LOG_DIR, "host-upgrade.log"), "a");
19746
+ const log5 = openSync4(join27(paths.LOG_DIR, "host-upgrade.log"), "a");
19267
19747
  const child = spawn7(process.execPath, [process.argv[1] ?? "", "upgrade"], {
19268
19748
  detached: true,
19269
19749
  stdio: ["ignore", log5, log5]
@@ -19284,7 +19764,7 @@ function dataSocketUrl() {
19284
19764
  return `ws://127.0.0.1:${process.env["FLYSPACE_DATA_API_PORT"] ?? DEFAULT_DATA_API_PORT}${HOST_SOCKET_PATH}`;
19285
19765
  }
19286
19766
  function listLocalSessions() {
19287
- if (!existsSync31(paths.DB_FILE)) return [];
19767
+ if (!existsSync32(paths.DB_FILE)) return [];
19288
19768
  const store = new SessionStore(paths.DB_FILE);
19289
19769
  try {
19290
19770
  return store.listSessions();
@@ -19321,6 +19801,7 @@ function localAgentCallbacks(chatAgent) {
19321
19801
  upgradeHost: () => startHostUpgrade(),
19322
19802
  scanOnboarding: (slug) => scanOnboarding(slug),
19323
19803
  linkProjectFolders: (slug, items) => linkProjectFolders(slug, items),
19804
+ sessionImportRequest: (req) => handleSessionImportRequest(req),
19324
19805
  isHostOnline: () => true
19325
19806
  };
19326
19807
  }
@@ -19329,7 +19810,7 @@ function buildHostConfigSnapshot() {
19329
19810
  const snapshot2 = {};
19330
19811
  for (const profile of listProfiles()) {
19331
19812
  snapshot2[profile] = runWithProfile(profile, () => ({
19332
- env: existsSync31(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync26(paths.CONFIG_FILE, "utf-8")) : {},
19813
+ env: existsSync32(paths.CONFIG_FILE) ? dotenv2.parse(readFileSync27(paths.CONFIG_FILE, "utf-8")) : {},
19333
19814
  agentModel: loadDashboardSettings().agent
19334
19815
  }));
19335
19816
  }
@@ -19579,12 +20060,12 @@ function startPushWatcher() {
19579
20060
  }
19580
20061
 
19581
20062
  // src/dashboard/machine-server.ts
19582
- import { existsSync as existsSync32 } from "fs";
19583
- import { join as join27 } from "path";
20063
+ import { existsSync as existsSync33 } from "fs";
20064
+ import { join as join28 } from "path";
19584
20065
  import { serveStatic } from "@hono/node-server/serve-static";
19585
20066
 
19586
20067
  // src/dashboard/app-icon-routes.ts
19587
- import { Hono as Hono36 } from "hono";
20068
+ import { Hono as Hono37 } from "hono";
19588
20069
  var NEUTRAL_MANIFEST = {
19589
20070
  id: "/",
19590
20071
  name: "Flyspace",
@@ -19622,7 +20103,7 @@ async function withResolvedProfile2(c, next) {
19622
20103
  return profile ? runWithProfile(profile, () => next()) : next();
19623
20104
  }
19624
20105
  function createAppIconRouter() {
19625
- const app = new Hono36();
20106
+ const app = new Hono37();
19626
20107
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
19627
20108
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
19628
20109
  const profile = getProfile();
@@ -19662,7 +20143,7 @@ function pngResponse(c, png) {
19662
20143
  }
19663
20144
 
19664
20145
  // src/dashboard/machine-server.ts
19665
- var WEB_DIST = join27(paths.WEB_ROOT, "dist");
20146
+ var WEB_DIST = join28(paths.WEB_ROOT, "dist");
19666
20147
  async function bootMachine() {
19667
20148
  await reconcileAll().catch(
19668
20149
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -19682,14 +20163,14 @@ async function bootMachine() {
19682
20163
  function mountSpa(app) {
19683
20164
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
19684
20165
  app.route("/", createAppIconRouter());
19685
- if (existsSync32(WEB_DIST) || existsSync32(join27(paths.WEB_ROOT, "package.json"))) {
20166
+ if (existsSync33(WEB_DIST) || existsSync33(join28(paths.WEB_ROOT, "package.json"))) {
19686
20167
  app.use("/*", serveStatic({ root: WEB_DIST }));
19687
20168
  }
19688
20169
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
19689
- const spaFallback = serveStatic({ path: join27(WEB_DIST, "index.html") });
20170
+ const spaFallback = serveStatic({ path: join28(WEB_DIST, "index.html") });
19690
20171
  app.get("*", async (c, next) => {
19691
- if (!existsSync32(join27(WEB_DIST, "index.html"))) {
19692
- const isCheckout = existsSync32(join27(paths.WEB_ROOT, "package.json"));
20172
+ if (!existsSync33(join28(WEB_DIST, "index.html"))) {
20173
+ const isCheckout = existsSync33(join28(paths.WEB_ROOT, "package.json"));
19693
20174
  return c.text(
19694
20175
  isCheckout ? `Workspace UI build missing (${WEB_DIST}).
19695
20176
  Run \`pnpm build:web\` from the repo root.
@@ -19823,8 +20304,8 @@ function openBrowser2(url) {
19823
20304
  }
19824
20305
  }
19825
20306
  function ensureWebBuild() {
19826
- if (existsSync33(join28(paths.WEB_ROOT, "dist", "index.html"))) return;
19827
- if (!existsSync33(join28(paths.WEB_ROOT, "package.json"))) return;
20307
+ if (existsSync34(join29(paths.WEB_ROOT, "dist", "index.html"))) return;
20308
+ if (!existsSync34(join29(paths.WEB_ROOT, "package.json"))) return;
19828
20309
  console.log(" packages/ui/dist missing, building the dashboard UI...");
19829
20310
  const result = spawnSync("pnpm", ["build"], {
19830
20311
  cwd: paths.WEB_ROOT,
@@ -19874,7 +20355,7 @@ async function runDashboard(args) {
19874
20355
  }
19875
20356
 
19876
20357
  // src/commands/workspace.ts
19877
- import { existsSync as existsSync34, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
20358
+ import { existsSync as existsSync35, mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "fs";
19878
20359
  var USAGE7 = `
19879
20360
  Usage: flyspace workspace <subcommand> [options]
19880
20361
 
@@ -19960,7 +20441,7 @@ function provisionLocal(botName) {
19960
20441
  ensureDirs();
19961
20442
  console.log(` config dir ready (${paths.CONFIG_DIR})`);
19962
20443
  console.log(` data dir ready (${paths.DATA_DIR})`);
19963
- if (existsSync34(paths.CONFIG_FILE)) {
20444
+ if (existsSync35(paths.CONFIG_FILE)) {
19964
20445
  console.log(" config.env exists");
19965
20446
  } else {
19966
20447
  writeFileSync13(paths.CONFIG_FILE, `BOT_NAME=${botName}
@@ -19977,7 +20458,7 @@ async function provisionChannels(specs) {
19977
20458
  continue;
19978
20459
  }
19979
20460
  const cwd = expandPath(spec.cwd);
19980
- if (!existsSync34(cwd)) {
20461
+ if (!existsSync35(cwd)) {
19981
20462
  mkdirSync14(cwd, { recursive: true });
19982
20463
  console.log(` created cwd ${cwd}`);
19983
20464
  }
@@ -20055,10 +20536,10 @@ async function runReconcileCmd(args) {
20055
20536
  }
20056
20537
 
20057
20538
  // src/cli.ts
20058
- import { join as join29 } from "path";
20539
+ import { join as join30 } from "path";
20059
20540
  var home = flyspaceHome();
20060
20541
  if (home) {
20061
- const bin = join29(home, "bin");
20542
+ const bin = join30(home, "bin");
20062
20543
  const path = process.env.PATH ?? "";
20063
20544
  if (!path.split(":").includes(bin)) process.env.PATH = path ? `${bin}:${path}` : bin;
20064
20545
  }
@@ -20196,7 +20677,7 @@ async function main() {
20196
20677
  break;
20197
20678
  case "-v":
20198
20679
  case "--version":
20199
- printRaw("0.1.33");
20680
+ printRaw("0.1.34");
20200
20681
  break;
20201
20682
  case "-h":
20202
20683
  case "--help":
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startServer
4
- } from "./chunk-KGX44L3V.js";
4
+ } from "./chunk-KTMAS4TP.js";
5
5
  import "./chunk-GYHCSGMJ.js";
6
6
  export {
7
7
  startServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flyspace-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "AI-native headless community OS",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,