flyspace-cli 0.1.35 → 0.1.37

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.
@@ -343,6 +343,7 @@ function rowToChannel(row) {
343
343
  model: row.model,
344
344
  effort: row.effort && isEffort(row.effort) ? row.effort : null,
345
345
  prompt: row.prompt?.trim() || null,
346
+ topicCategories: normaliseTopicCategories(row.topic_categories),
346
347
  // A uuid[] arrives as a real array; anything else (an older row read
347
348
  // through a cached client, a hand-edited value) reads as "no MCP", which
348
349
  // is the safe direction for a list that decides which credentials a run
@@ -368,7 +369,7 @@ function rowToChannel(row) {
368
369
  agentName: row.agent_name,
369
370
  dmKey: row.dm_key,
370
371
  isDefault: row.is_default === true,
371
- profile: row.profile ?? "default",
372
+ profile: row.profile,
372
373
  archivedAt: row.archived_at,
373
374
  greetedAt: row.greeted_at,
374
375
  createdAt: row.created_at,
@@ -528,6 +529,15 @@ function resolveEffort(explicitEffort, channelId, fallback) {
528
529
  if (explicitEffort) return explicitEffort;
529
530
  return resolveChannelEffort(channelId) ?? fallback;
530
531
  }
532
+ function normaliseTopicCategories(value) {
533
+ if (!Array.isArray(value)) return [];
534
+ const out = [];
535
+ for (const v of value) {
536
+ const t = typeof v === "string" ? v.trim() : "";
537
+ if (t && !out.includes(t)) out.push(t);
538
+ }
539
+ return out;
540
+ }
531
541
  function toRow(input) {
532
542
  const row = {};
533
543
  if (input.name !== void 0) row["name"] = input.name;
@@ -537,6 +547,10 @@ function toRow(input) {
537
547
  if (input.model !== void 0) row["model"] = input.model;
538
548
  if (input.effort !== void 0) row["effort"] = input.effort;
539
549
  if (input.prompt !== void 0) row["prompt"] = input.prompt?.trim() || null;
550
+ if (input.topicCategories !== void 0) {
551
+ const list = normaliseTopicCategories(input.topicCategories);
552
+ row["topic_categories"] = list.length ? list : null;
553
+ }
540
554
  if (input.access !== void 0) row["access"] = input.access;
541
555
  if (input.visibility !== void 0) row["visibility"] = input.visibility;
542
556
  if (input.memberEmails !== void 0) {
@@ -2625,7 +2639,7 @@ async function listWorkspaceSummaries() {
2625
2639
  const [workspaces, settings, channels, memberships, activity] = await Promise.all([
2626
2640
  db.from("workspaces").select("id, slug, created_at, agent_connected_at, plan, plan_status, archived_at"),
2627
2641
  db.from("workspace_settings").select("profile, display_name, logo"),
2628
- db.from("channels").select("profile").is("archived_at", null),
2642
+ db.from("channels").select("profile, is_default").is("archived_at", null),
2629
2643
  db.from("memberships").select("workspace_id, user_id, email, handle, role, accepted_at").eq("status", "active").eq("is_support", false).order("invited_at", { ascending: true }),
2630
2644
  db.rpc("workspace_last_activity")
2631
2645
  ]);
@@ -2646,6 +2660,9 @@ async function listWorkspaceSummaries() {
2646
2660
  );
2647
2661
  const tally = (keys) => keys.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), /* @__PURE__ */ new Map());
2648
2662
  const channelCounts = tally((channels.data ?? []).map((c) => c.profile));
2663
+ const ownChannelCounts = tally(
2664
+ (channels.data ?? []).filter((c) => !c.is_default).map((c) => c.profile)
2665
+ );
2649
2666
  const memberCounts = tally((memberships.data ?? []).map((m) => m.workspace_id));
2650
2667
  const owners = /* @__PURE__ */ new Map();
2651
2668
  for (const m of memberships.data ?? []) {
@@ -2672,6 +2689,7 @@ async function listWorkspaceSummaries() {
2672
2689
  displayName: displayNames.get(w.slug) || null,
2673
2690
  logo: logos.get(w.slug) || null,
2674
2691
  channelCount: channelCounts.get(w.slug) ?? 0,
2692
+ ownChannelCount: ownChannelCounts.get(w.slug) ?? 0,
2675
2693
  memberCount: memberCounts.get(w.id) ?? 0,
2676
2694
  lastActivityAt: lastActivity.get(w.slug) ?? null,
2677
2695
  owner: owners.get(w.id) ?? null
@@ -2961,8 +2979,8 @@ var MAX_BODY_CHARS = 4e3;
2961
2979
  var MAX_THREAD_CHARS = 12e3;
2962
2980
  var THREAD_TIMEOUT_MS = 15e3;
2963
2981
  var SUBJECT_MAX_CHARS = 30;
2964
- var CATEGORY_MAX_CHARS = 12;
2965
- var DEFAULT_TITLE_RULES = `- category: ONE short word or phrase, e.g. Bug, Feature, Question, Design, Ops, Docs, Idea. Reuse a category from the provided list whenever one fits; invent a new one only if none does.
2982
+ var CATEGORY_MAX_CHARS = 16;
2983
+ var DEFAULT_TITLE_RULES = `- category: ONE short phrase naming the component or feature area the thread is about (e.g. Docs pane, Auto Topic, Login, Billing), not the kind of request. Pick from Channel categories whenever one fits, otherwise reuse one of the Existing categories; invent a new area name only if none fits. Generic types such as Bug, Feature, Idea or Question are a last resort, only when no area can be named.
2966
2984
  - subject: the most concise noun phrase that names the topic, as short as possible (aim for 3 to 6 words, or under 15 Chinese characters). Keep only the core topic: drop filler, background, verbs like "help me" or "how to", and details already implied by the category or channel. Not a copied sentence, no quotes, no Markdown, no trailing punctuation.
2967
2985
  - If the Workspace or Channel instructions contain title or naming rules, follow them over the defaults above.
2968
2986
  - Keep the message's own language. If it is Traditional Chinese (Taiwan), use Taiwan wording and keep technical terms in English.
@@ -3003,6 +3021,7 @@ async function buildAutoTitle(input) {
3003
3021
  ${input.workspacePrompt.slice(0, 1500)}` : "",
3004
3022
  input.channelPrompt ? `Channel instructions:
3005
3023
  ${input.channelPrompt.slice(0, 1500)}` : "",
3024
+ input.channelCategories.length ? `Channel categories (pick one of these whenever it fits): ${input.channelCategories.join(", ")}` : "",
3006
3025
  `Existing categories: ${input.existingCategories.length ? input.existingCategories.join(", ") : "(none yet)"}`,
3007
3026
  whole ? `Conversation (latest last):
3008
3027
  ${input.body.slice(-MAX_THREAD_CHARS)}` : `Opening message:
package/dist/cli.js CHANGED
@@ -187,7 +187,7 @@ import {
187
187
  warnOnBrokenChannelCwds,
188
188
  workspaceSkillsDir,
189
189
  writeSkillFile
190
- } from "./chunk-KTMAS4TP.js";
190
+ } from "./chunk-JTZN44U4.js";
191
191
  import {
192
192
  firstReplyBody,
193
193
  getThread,
@@ -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.35" : null;
1864
+ var CLI_VERSION = true ? "0.1.37" : 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,7 +2471,7 @@ 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.35";
2474
+ const current = "0.1.37";
2475
2475
  console.log(`Current version: ${current}`);
2476
2476
  console.log("Checking for updates...\n");
2477
2477
  try {
@@ -3331,19 +3331,31 @@ function hashInviteToken(token) {
3331
3331
  }
3332
3332
  async function issueInvite(membershipId, message) {
3333
3333
  const db = getSupabaseClient();
3334
- const { error: clearError } = await db.from("workspace_invites").delete().eq("membership_id", membershipId).is("accepted_at", null);
3335
- if (clearError) throw new Error(`issueInvite(${membershipId}): ${clearError.message}`);
3336
3334
  const token = randomBytes2(32).toString("hex");
3337
- const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3338
- const { error: error2 } = await db.from("workspace_invites").insert({
3335
+ const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3).toISOString();
3336
+ const { data: pending2, error: pendingError } = await db.from("workspace_invites").select("id, token_hash, previous_token_hashes").eq("membership_id", membershipId).is("accepted_at", null).order("created_at", { ascending: false }).limit(1).maybeSingle();
3337
+ if (pendingError) throw new Error(`issueInvite(${membershipId}): ${pendingError.message}`);
3338
+ const { error: error2 } = pending2 ? await db.from("workspace_invites").update({
3339
+ token_hash: hashInviteToken(token),
3340
+ previous_token_hashes: [
3341
+ ...pending2.previous_token_hashes ?? [],
3342
+ pending2.token_hash
3343
+ ],
3344
+ message,
3345
+ expires_at: expiresAt
3346
+ }).eq("id", pending2.id) : await db.from("workspace_invites").insert({
3339
3347
  membership_id: membershipId,
3340
3348
  token_hash: hashInviteToken(token),
3341
3349
  message,
3342
- expires_at: expiresAt.toISOString()
3350
+ expires_at: expiresAt
3343
3351
  });
3344
3352
  if (error2) throw new Error(`issueInvite(${membershipId}): ${error2.message}`);
3345
3353
  return token;
3346
3354
  }
3355
+ function tokenHashFilter(token) {
3356
+ const hash = hashInviteToken(token);
3357
+ return `token_hash.eq.${hash},previous_token_hashes.cs.{${hash}}`;
3358
+ }
3347
3359
  async function latestInviteMessage(membershipId) {
3348
3360
  const { data, error: error2 } = await getSupabaseClient().from("workspace_invites").select("message").eq("membership_id", membershipId).order("created_at", { ascending: false }).limit(1).maybeSingle();
3349
3361
  if (error2) throw new Error(`latestInviteMessage(${membershipId}): ${error2.message}`);
@@ -3352,7 +3364,7 @@ async function latestInviteMessage(membershipId) {
3352
3364
  async function lookupInviteByToken(token) {
3353
3365
  const { data, error: error2 } = await getSupabaseClient().from("workspace_invites").select(
3354
3366
  "id, message, expires_at, accepted_at, memberships!inner(workspace_id, user_id, email)"
3355
- ).eq("token_hash", hashInviteToken(token)).maybeSingle();
3367
+ ).or(tokenHashFilter(token)).maybeSingle();
3356
3368
  if (error2) throw new Error(`lookupInviteByToken: ${error2.message}`);
3357
3369
  return toInviteLookup(data);
3358
3370
  }
@@ -3817,12 +3829,19 @@ async function revokeSetupInvite(id) {
3817
3829
  }
3818
3830
  async function reissueSetupInvite(id) {
3819
3831
  const db = getSupabaseClient();
3820
- const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message").eq("id", id).is("accepted_at", null).maybeSingle();
3832
+ const { data, error: error2 } = await db.from("workspace_setup_invites").select("email, owner_name, message, token_hash, previous_token_hashes").eq("id", id).is("accepted_at", null).maybeSingle();
3821
3833
  if (error2) throw new Error(`reissueSetupInvite(${id}): ${error2.message}`);
3822
3834
  if (!data) throw new Error("no pending invite with that id");
3823
3835
  const token = randomBytes3(32).toString("hex");
3824
3836
  const expiresAt = new Date(Date.now() + INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3);
3825
- const { error: updateError } = await db.from("workspace_setup_invites").update({ token_hash: hashInviteToken(token), expires_at: expiresAt.toISOString() }).eq("id", id);
3837
+ const { error: updateError } = await db.from("workspace_setup_invites").update({
3838
+ token_hash: hashInviteToken(token),
3839
+ previous_token_hashes: [
3840
+ ...data.previous_token_hashes ?? [],
3841
+ data.token_hash
3842
+ ],
3843
+ expires_at: expiresAt.toISOString()
3844
+ }).eq("id", id);
3826
3845
  if (updateError) throw new Error(`reissueSetupInvite(${id}): ${updateError.message}`);
3827
3846
  return {
3828
3847
  token,
@@ -3834,7 +3853,7 @@ async function reissueSetupInvite(id) {
3834
3853
  };
3835
3854
  }
3836
3855
  async function lookupSetupInvite(token) {
3837
- const { data, error: error2 } = await getSupabaseClient().from("workspace_setup_invites").select("id, email, owner_name, plan, message, expires_at, accepted_at, workspace_id").eq("token_hash", hashInviteToken(token)).maybeSingle();
3856
+ const { data, error: error2 } = await getSupabaseClient().from("workspace_setup_invites").select("id, email, owner_name, plan, message, expires_at, accepted_at, workspace_id").or(tokenHashFilter(token)).maybeSingle();
3838
3857
  if (error2) throw new Error(`lookupSetupInvite: ${error2.message}`);
3839
3858
  if (!data) return { state: "unknown" };
3840
3859
  if (data.accepted_at) return { state: "used", workspaceId: data.workspace_id ?? null };
@@ -6591,7 +6610,7 @@ var requireDocsAccess = async (c, next) => {
6591
6610
  };
6592
6611
 
6593
6612
  // ../api/src/dashboard/api.ts
6594
- import { Hono as Hono31 } from "hono";
6613
+ import { Hono as Hono32 } from "hono";
6595
6614
 
6596
6615
  // ../api/src/dashboard/public-share-routes.ts
6597
6616
  import { existsSync as existsSync17, readFileSync as readFileSync16, statSync as statSync3 } from "fs";
@@ -10596,6 +10615,7 @@ async function labelThread(channelId, body, scope) {
10596
10615
  return buildAutoTitle({
10597
10616
  channelName: channel.name,
10598
10617
  channelPrompt: channel.prompt,
10618
+ channelCategories: channel.topicCategories,
10599
10619
  workspacePrompt: loadWorkspacePrompt().prompt,
10600
10620
  titlePrompt: (await loadWorkspaceBranding()).autoTitlePrompt,
10601
10621
  existingCategories,
@@ -13757,8 +13777,89 @@ async function ensureIntroThread(ch) {
13757
13777
  return error2?.code === "23505" ? (await existing()).data?.id ?? null : null;
13758
13778
  }
13759
13779
 
13760
- // ../api/src/dashboard/channel-members-routes.ts
13780
+ // ../api/src/dashboard/channel-suggest.ts
13761
13781
  import { Hono as Hono26 } from "hono";
13782
+ var CATEGORY_MAX_CHARS = 16;
13783
+ var CATEGORY_MAX_ITEMS = 30;
13784
+ var PROMPT_MAX_CHARS = 4e3;
13785
+ var READ_FOLDER = "Read-only: inspect the current folder first (top-level file listing, CLAUDE.md / AGENTS.md, README.md, docs/), but do not create, modify or delete anything.";
13786
+ var SYSTEMS = {
13787
+ topicCategories: [
13788
+ "You propose the preferred topic categories for a chat channel. Each new thread in it is titled `[category] subject`, so the categories should name this project's concrete components or feature areas (e.g. Docs pane, Auto Topic, Login, Billing).",
13789
+ READ_FOLDER,
13790
+ `Rules: roughly 15 to 30 categories; each at most ${CATEGORY_MAX_CHARS} characters; no generic types such as Bug, Feature, Idea, Question or Task; no duplicates. Keep useful ones from the current list and from the recent thread titles. Use the language the project is worked in; for Chinese use Traditional Chinese (Taiwan) and keep technical terms in English. Never use em dashes or en dashes.`,
13791
+ "Output only the categories, one per line, with no numbering, bullets or commentary."
13792
+ ].join("\n\n"),
13793
+ prompt: [
13794
+ "You draft the channel prompt for a chat channel: instructions appended to the workspace prompt for every agent run in it. A prompt is about the conversation (who is talking here, the audience, tone, language, and what is off limits), not project knowledge: architecture, conventions and commands already live in the folder's CLAUDE.md / AGENTS.md, so never repeat them.",
13795
+ READ_FOLDER,
13796
+ "Rules: short (at most about 10 lines), concrete, grounded in what the folder and threads show. Improve on the current prompt if there is one rather than starting over. Write in the language the project is worked in; for Chinese use Traditional Chinese (Taiwan), full-width punctuation, technical terms in English. Never use em dashes or en dashes.",
13797
+ "Output only the prompt text itself, with no preamble, quotes or code fences."
13798
+ ].join("\n\n")
13799
+ };
13800
+ function buildSuggestContext(ch, field, threadTitles, memories) {
13801
+ return [
13802
+ `Channel: #${ch.name}`,
13803
+ field === "prompt" ? `Current prompt:
13804
+ ${ch.prompt?.trim() || "(none)"}` : `Current categories:
13805
+ ${ch.topicCategories.join("\n") || "(none)"}`,
13806
+ field === "topicCategories" && ch.prompt ? `Channel prompt:
13807
+ ${ch.prompt.slice(0, 1500)}` : "",
13808
+ `Recent thread titles:
13809
+ ${threadTitles.join("\n") || "(none yet)"}`,
13810
+ memories.length ? `Channel memory:
13811
+ ${memories.map((m) => `- ${m.name}: ${m.description}`).join("\n")}` : ""
13812
+ ].filter(Boolean).join("\n\n");
13813
+ }
13814
+ function parseSuggestion(field, raw) {
13815
+ const text = raw.replace(/^```[^\n]*\n?|\n?```\s*$/g, "").replace(/\s*[\u2013\u2014]\s*/g, ", ").trim();
13816
+ if (field === "prompt") return text ? text.slice(0, PROMPT_MAX_CHARS) : null;
13817
+ const seen = /* @__PURE__ */ new Set();
13818
+ const items = [];
13819
+ for (const line of text.split("\n")) {
13820
+ const item = line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "").replace(/[`*"[\]]/g, "").trim();
13821
+ const key = item.toLowerCase();
13822
+ if (!item || item.length > CATEGORY_MAX_CHARS || seen.has(key)) continue;
13823
+ seen.add(key);
13824
+ items.push(item);
13825
+ }
13826
+ return items.length ? items.slice(0, CATEGORY_MAX_ITEMS).join("\n") : null;
13827
+ }
13828
+ function createChannelSuggestRouter(agent) {
13829
+ const router = new Hono26();
13830
+ router.post("/:id/suggest", async (c) => {
13831
+ const channel = findChannel(c.req.param("id"));
13832
+ if (!channel || !canSeeChannel(channel, c.get("auth"))) {
13833
+ return c.json({ error: "channel not found" }, 404);
13834
+ }
13835
+ const body = await c.req.json().catch(() => ({}));
13836
+ const field = body.field;
13837
+ if (field !== "prompt" && field !== "topicCategories") return c.json({ error: "invalid field" }, 400);
13838
+ if (!agent.suggestChannelField) return c.json({ error: "not supported here" }, 501);
13839
+ const [{ data: threads }, memories] = await Promise.all([
13840
+ getSupabaseClient().from("threads").select("auto_title, title").eq("channel_id", channel.id).order("created_at", { ascending: false }).limit(50),
13841
+ // Memory is a Labs feature; a channel without it just has none to offer.
13842
+ listChannelMemories(channel.id).catch(() => [])
13843
+ ]);
13844
+ const titles = (threads ?? []).map((t) => t.title ?? t.auto_title).filter((t) => !!t);
13845
+ const result = await agent.suggestChannelField({
13846
+ channelId: channel.id,
13847
+ system: SYSTEMS[field],
13848
+ user: buildSuggestContext(channel, field, titles, memories)
13849
+ });
13850
+ if (!result) {
13851
+ const failure3 = agent.getHostCallFailure?.("suggestChannelField");
13852
+ return failure3 ? c.json({ error: "The Host is offline or did not answer in time." }, 503) : c.json({ error: "The Host needs an upgrade for suggestions: run flyspace upgrade." }, 501);
13853
+ }
13854
+ const value = result.ok ? parseSuggestion(field, result.raw) : null;
13855
+ if (value === null) return c.json({ error: "Could not generate a suggestion." }, 502);
13856
+ return c.json({ value });
13857
+ });
13858
+ return router;
13859
+ }
13860
+
13861
+ // ../api/src/dashboard/channel-members-routes.ts
13862
+ import { Hono as Hono27 } from "hono";
13762
13863
  async function isChannelAdmin(ctx, channelId) {
13763
13864
  if (ctx.role === "admin") return true;
13764
13865
  const membership = await getMembershipByUserId(ctx.workspaceId, ctx.userId);
@@ -13767,7 +13868,7 @@ async function isChannelAdmin(ctx, channelId) {
13767
13868
  return members.some((m) => m.membershipId === membership.id && m.role === "channel_admin");
13768
13869
  }
13769
13870
  function createChannelMembersRouter() {
13770
- const router = new Hono26();
13871
+ const router = new Hono27();
13771
13872
  router.get("/:id/members", async (c) => {
13772
13873
  const ctx = await resolveAuthContext(c);
13773
13874
  if (!ctx) return c.json({ error: "workspace access required" }, 403);
@@ -13837,7 +13938,7 @@ function createChannelMembersRouter() {
13837
13938
  }
13838
13939
 
13839
13940
  // ../api/src/dashboard/memories-routes.ts
13840
- import { Hono as Hono27 } from "hono";
13941
+ import { Hono as Hono28 } from "hono";
13841
13942
  async function readPatch(c) {
13842
13943
  const body = await c.req.json().catch(() => ({}));
13843
13944
  const patch = {};
@@ -13865,7 +13966,7 @@ var requireAgentMemory = async (c, next) => {
13865
13966
  await next();
13866
13967
  };
13867
13968
  function createMemoriesRouter() {
13868
- const router = new Hono27();
13969
+ const router = new Hono28();
13869
13970
  for (const path of [
13870
13971
  "/:id/memories",
13871
13972
  "/:id/memories/*",
@@ -13938,7 +14039,7 @@ function createMemoriesRouter() {
13938
14039
  return router;
13939
14040
  }
13940
14041
  function createScopedMemoriesRouter() {
13941
- const router = new Hono27();
14042
+ const router = new Hono28();
13942
14043
  router.use("*", requireAgentMemory);
13943
14044
  const workspace = (c) => ({
13944
14045
  scope: "workspace",
@@ -13998,7 +14099,7 @@ function createScopedMemoriesRouter() {
13998
14099
  }
13999
14100
 
14000
14101
  // ../api/src/dashboard/session-import-routes.ts
14001
- import { Hono as Hono28 } from "hono";
14102
+ import { Hono as Hono29 } from "hono";
14002
14103
  var MAX_FOLDERS2 = 20;
14003
14104
  var DAY_MS = 24 * 60 * 60 * 1e3;
14004
14105
  var IMPORT_RANGES = ["all", "7d", "latest5"];
@@ -14104,7 +14205,7 @@ async function syncSession(agent, channelId, session, author, existing) {
14104
14205
  return messages.length;
14105
14206
  }
14106
14207
  function createSessionImportRouter(agent) {
14107
- const app = new Hono28();
14208
+ const app = new Hono29();
14108
14209
  const scanFolders = async () => {
14109
14210
  const scan = await ask(agent, { op: "scan", slug: getProfile() });
14110
14211
  return scan && importableFolders(scan.folders, getProfileChannelRegistry());
@@ -14374,6 +14475,7 @@ function registerDataRoutes(api, agent = {}) {
14374
14475
  model: ch.model ?? void 0,
14375
14476
  effort: ch.effort ?? void 0,
14376
14477
  prompt: ch.prompt ?? void 0,
14478
+ topicCategories: ch.topicCategories,
14377
14479
  access: ch.access,
14378
14480
  visibility: ch.visibility,
14379
14481
  memberEmails: ch.memberEmails,
@@ -14401,6 +14503,7 @@ function registerDataRoutes(api, agent = {}) {
14401
14503
  api.route("/onboarding", createOnboardingRouter(agent));
14402
14504
  api.route("/session-import", createSessionImportRouter(agent));
14403
14505
  api.route("/channels", createChannelGreetingRouter(agent));
14506
+ api.route("/channels", createChannelSuggestRouter(agent));
14404
14507
  api.route("/channels", createChannelMembersRouter());
14405
14508
  api.route("/channels", createMemoriesRouter());
14406
14509
  api.route("/memories", createScopedMemoriesRouter());
@@ -14654,7 +14757,7 @@ function registerDataRoutes(api, agent = {}) {
14654
14757
  // ../api/src/dashboard/device-auth-routes.ts
14655
14758
  import { verify } from "crypto";
14656
14759
  import { getConnInfo } from "@hono/node-server/conninfo";
14657
- import { Hono as Hono29 } from "hono";
14760
+ import { Hono as Hono30 } from "hono";
14658
14761
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14659
14762
  function cleanText(value, max) {
14660
14763
  if (typeof value !== "string") return null;
@@ -14702,7 +14805,7 @@ async function boundWorkspaceIds(publicKey, userId) {
14702
14805
  return new Set(existing ? (await listDeviceWorkspaces(existing)).map((w) => w.id) : []);
14703
14806
  }
14704
14807
  function createDeviceAuthRouter() {
14705
- const app = new Hono29();
14808
+ const app = new Hono30();
14706
14809
  app.post("/code", async (c) => {
14707
14810
  const body = await readBody(c);
14708
14811
  const publicKey = typeof body["public_key"] === "string" ? body["public_key"] : "";
@@ -15021,7 +15124,7 @@ var workspaceRef = () => ({ kind: "workspace" });
15021
15124
  var channelRef = (c) => ({ kind: "channel", channelId: c.req.param("id") });
15022
15125
 
15023
15126
  // ../api/src/dashboard/output-styles-routes.ts
15024
- import { Hono as Hono30 } from "hono";
15127
+ import { Hono as Hono31 } from "hono";
15025
15128
  function requireAdmin3(auth) {
15026
15129
  return auth?.role === "admin" ? null : "admin access required";
15027
15130
  }
@@ -15041,7 +15144,7 @@ function failure(c, err) {
15041
15144
  return c.json({ error: message }, err instanceof OutputStyleConflictError ? 409 : 500);
15042
15145
  }
15043
15146
  function createOutputStylesRouter() {
15044
- const app = new Hono30();
15147
+ const app = new Hono31();
15045
15148
  app.get("/", (c) => {
15046
15149
  const auth = c.get("auth");
15047
15150
  const denied = requireAdmin3(auth);
@@ -15133,7 +15236,7 @@ function createOutputStylesRouter() {
15133
15236
  return app;
15134
15237
  }
15135
15238
  async function handleOutputStylesRequest(req) {
15136
- const app = new Hono30();
15239
+ const app = new Hono31();
15137
15240
  app.use("*", async (c, next) => {
15138
15241
  c.set("auth", { role: "admin" });
15139
15242
  await next();
@@ -15154,7 +15257,7 @@ import { streamSSE } from "hono/streaming";
15154
15257
 
15155
15258
  // ../api/src/dashboard/api.ts
15156
15259
  function createGatedRouter(agent, register) {
15157
- const api = new Hono31();
15260
+ const api = new Hono32();
15158
15261
  api.use("*", async (c, next) => {
15159
15262
  const known = await listKnownWorkspaces();
15160
15263
  const fromHost = profileFromHost(trustedHost(c), known);
@@ -15207,7 +15310,7 @@ import sharp7 from "sharp";
15207
15310
 
15208
15311
  // src/dashboard/channels-routes.ts
15209
15312
  import { existsSync as existsSync22, mkdirSync as mkdirSync10 } from "fs";
15210
- import { Hono as Hono32 } from "hono";
15313
+ import { Hono as Hono33 } from "hono";
15211
15314
  function coerceAccess(raw) {
15212
15315
  if (raw === "open" || raw === "*" || raw === "all") return raw;
15213
15316
  if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) return raw;
@@ -15222,6 +15325,9 @@ function coerceRuntime(raw) {
15222
15325
  function coerceEffort(raw) {
15223
15326
  return typeof raw === "string" && isEffort(raw) ? raw : void 0;
15224
15327
  }
15328
+ function coerceTopicCategories(raw) {
15329
+ return Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : void 0;
15330
+ }
15225
15331
  function coerceMounts(raw) {
15226
15332
  const list = typeof raw === "string" ? raw.split("\n") : raw;
15227
15333
  if (!Array.isArray(list)) return void 0;
@@ -15258,6 +15364,7 @@ function toResponse3(channel) {
15258
15364
  model: channel.model ?? void 0,
15259
15365
  effort: channel.effort ?? void 0,
15260
15366
  prompt: channel.prompt ?? void 0,
15367
+ topicCategories: channel.topicCategories,
15261
15368
  access: channel.access,
15262
15369
  visibility: channel.visibility,
15263
15370
  memberEmails: channel.memberEmails,
@@ -15270,7 +15377,7 @@ function toResponse3(channel) {
15270
15377
  };
15271
15378
  }
15272
15379
  function createChannelWriteRouter() {
15273
- const app = new Hono32();
15380
+ const app = new Hono33();
15274
15381
  app.post("/", async (c) => {
15275
15382
  const body = await c.req.json().catch(() => ({}));
15276
15383
  const name = typeof body["name"] === "string" ? body["name"].trim() : "";
@@ -15289,6 +15396,7 @@ function createChannelWriteRouter() {
15289
15396
  model: model || null,
15290
15397
  effort: coerceEffort(body["effort"]) ?? null,
15291
15398
  prompt: typeof body["prompt"] === "string" ? body["prompt"] : null,
15399
+ topicCategories: coerceTopicCategories(body["topicCategories"]),
15292
15400
  access: coerceAccess(body["access"]),
15293
15401
  visibility: coerceVisibility(body["visibility"]),
15294
15402
  memberEmails: coerceMemberEmails(body["memberEmails"]),
@@ -15318,6 +15426,7 @@ function createChannelWriteRouter() {
15318
15426
  model: model || null,
15319
15427
  effort: coerceEffort(body["effort"]) ?? null,
15320
15428
  prompt: typeof body["prompt"] === "string" ? body["prompt"] : null,
15429
+ topicCategories: coerceTopicCategories(body["topicCategories"]),
15321
15430
  access: coerceAccess(body["access"]),
15322
15431
  visibility: coerceVisibility(body["visibility"]),
15323
15432
  memberEmails: coerceMemberEmails(body["memberEmails"]),
@@ -15360,6 +15469,7 @@ function createChannelWriteRouter() {
15360
15469
  if (typeof body["position"] === "number") patch.position = body["position"];
15361
15470
  if (typeof body["model"] === "string") patch.model = body["model"] || null;
15362
15471
  if ("effort" in body) patch.effort = coerceEffort(body["effort"]) ?? null;
15472
+ if ("topicCategories" in body) patch.topicCategories = coerceTopicCategories(body["topicCategories"]);
15363
15473
  if ("prompt" in body) {
15364
15474
  patch.prompt = typeof body["prompt"] === "string" ? body["prompt"] : null;
15365
15475
  }
@@ -15410,7 +15520,7 @@ import {
15410
15520
  } from "fs";
15411
15521
  import { Readable } from "stream";
15412
15522
  import { basename as basename8, dirname as dirname8, extname, join as join23, relative, resolve as resolve5, sep } from "path";
15413
- import { Hono as Hono33 } from "hono";
15523
+ import { Hono as Hono34 } from "hono";
15414
15524
  import { streamSSE as streamSSE2 } from "hono/streaming";
15415
15525
 
15416
15526
  // src/dashboard/docs-watcher.ts
@@ -15988,7 +16098,8 @@ var CLAUDE_MODEL = "sonnet";
15988
16098
  var CODEX_MODEL = "gpt-6-astra";
15989
16099
  var CODEX_EFFORT = "low";
15990
16100
  var TIMEOUT_MS2 = 15e4;
15991
- function buildInvocation(runner, req, dir, auth, baseEnv = process.env) {
16101
+ var SUGGEST_TOOLS = "Read,Glob,Grep";
16102
+ function buildInvocation(runner, req, dir, auth, baseEnv = process.env, tools = "") {
15992
16103
  const env = { ...baseEnv, CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" };
15993
16104
  if (runner === "codex") {
15994
16105
  const outputFile = join21(dir, "last-message.txt");
@@ -16034,7 +16145,7 @@ ${req.user}`,
16034
16145
  CLAUDE_MODEL,
16035
16146
  "--no-session-persistence",
16036
16147
  "--tools",
16037
- "",
16148
+ tools,
16038
16149
  "--strict-mcp-config",
16039
16150
  "--setting-sources",
16040
16151
  "",
@@ -16083,20 +16194,33 @@ function run3(inv, cwd) {
16083
16194
  child.stdin.end(inv.stdin);
16084
16195
  });
16085
16196
  }
16086
- async function completeMemoryExtraction(req) {
16197
+ async function complete(req, tools = "", cwd) {
16087
16198
  const runner = runnerForModel(loadDashboardSettings().agent);
16088
16199
  const config = resolveChannelConfig(req.channelId);
16089
16200
  const dir = mkdtempSync2(join21(tmpdir4(), "flyspace-memory-"));
16090
16201
  try {
16091
- const inv = buildInvocation(runner, req, dir, {
16092
- ...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
16093
- ...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
16094
- });
16095
- return await run3(inv, dir);
16202
+ const inv = buildInvocation(
16203
+ runner,
16204
+ req,
16205
+ dir,
16206
+ {
16207
+ ...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
16208
+ ...config.claudeCodeOauthToken ? { claudeCodeOauthToken: config.claudeCodeOauthToken } : {}
16209
+ },
16210
+ process.env,
16211
+ tools
16212
+ );
16213
+ return await run3(inv, cwd ?? dir);
16096
16214
  } finally {
16097
16215
  rmSync7(dir, { recursive: true, force: true });
16098
16216
  }
16099
16217
  }
16218
+ function completeMemoryExtraction(req) {
16219
+ return complete(req);
16220
+ }
16221
+ function suggestChannelField(req) {
16222
+ return complete(req, SUGGEST_TOOLS, resolveChannelConfig(req.channelId).cwd);
16223
+ }
16100
16224
 
16101
16225
  // src/dashboard/rollover-summary.ts
16102
16226
  import { spawn as spawn5 } from "child_process";
@@ -18300,7 +18424,7 @@ function deleteDocEntry(root, path) {
18300
18424
  };
18301
18425
  }
18302
18426
  function createDocsRouter({ gate = requireDocsAccess } = {}) {
18303
- const app = new Hono33();
18427
+ const app = new Hono34();
18304
18428
  app.use("/:id/docs", gate);
18305
18429
  app.use("/:id/docs/*", gate);
18306
18430
  app.get("/:id/docs", (c) => {
@@ -18638,7 +18762,7 @@ function withDocLinkBridge(html) {
18638
18762
  // src/dashboard/skills-routes-host.ts
18639
18763
  import { existsSync as existsSync27, statSync as statSync7 } from "fs";
18640
18764
  import { dirname as dirname9 } from "path";
18641
- import { Hono as Hono34 } from "hono";
18765
+ import { Hono as Hono35 } from "hono";
18642
18766
  function workspaceScope() {
18643
18767
  const claudeConfigDir = profileClaudeConfigDir();
18644
18768
  const cwd = defaultClaudeCwd();
@@ -18744,12 +18868,12 @@ var diskBackend = {
18744
18868
  write: writeSkillAndSync
18745
18869
  };
18746
18870
  function createWorkspaceSkillsRouter() {
18747
- const app = new Hono34();
18871
+ const app = new Hono35();
18748
18872
  mountSkillRoutes(app, "/skills", workspaceRef, diskBackend);
18749
18873
  return app;
18750
18874
  }
18751
18875
  function createChannelSkillsRouter() {
18752
- const app = new Hono34();
18876
+ const app = new Hono35();
18753
18877
  mountSkillRoutes(app, "/:id/skill-files", channelRef, diskBackend);
18754
18878
  return app;
18755
18879
  }
@@ -18884,7 +19008,7 @@ import { randomUUID as randomUUID5 } from "crypto";
18884
19008
  import { chmodSync, existsSync as existsSync29, mkdirSync as mkdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
18885
19009
  import { createRequire } from "module";
18886
19010
  import { dirname as dirname10, join as join25 } from "path";
18887
- import { Hono as Hono35 } from "hono";
19011
+ import { Hono as Hono36 } from "hono";
18888
19012
  var SESSION_TTL_MS = 10 * 6e4;
18889
19013
  var URL_WAIT_MS = 3e4;
18890
19014
  var TOKEN_WAIT_MS = 6e4;
@@ -19041,7 +19165,7 @@ function failure2(c, err) {
19041
19165
  return c.json({ error: err.message }, 500);
19042
19166
  }
19043
19167
  function createProviderLoginRouter() {
19044
- const app = new Hono35();
19168
+ const app = new Hono36();
19045
19169
  app.use("*", async (c, next) => {
19046
19170
  const denied = requireAdmin4(c.get("auth"));
19047
19171
  if (denied) return c.json({ error: denied }, 403);
@@ -19085,7 +19209,7 @@ function createProviderLoginRouter() {
19085
19209
  return app;
19086
19210
  }
19087
19211
  async function handleProviderLoginRequest(req) {
19088
- const app = new Hono35();
19212
+ const app = new Hono36();
19089
19213
  app.use("*", async (c, next) => {
19090
19214
  c.set("auth", { role: "admin" });
19091
19215
  await next();
@@ -19477,13 +19601,13 @@ function handleSessionImportRequest(req) {
19477
19601
  }
19478
19602
 
19479
19603
  // src/dashboard/docs-relay-host.ts
19480
- import { Hono as Hono36 } from "hono";
19604
+ import { Hono as Hono37 } from "hono";
19481
19605
  var RETURNED_HEADERS = ["content-type", "content-disposition", "content-security-policy", "cache-control"];
19482
19606
  async function handleDocsRequest(req) {
19483
19607
  return runRelayed(req, (app) => app.route("/channels", createDocsRouter({ gate: async (_c, next) => next() })));
19484
19608
  }
19485
19609
  async function runRelayed(req, register) {
19486
- const app = new Hono36();
19610
+ const app = new Hono37();
19487
19611
  app.use("*", async (c, next) => {
19488
19612
  c.set("auth", req.auth);
19489
19613
  await next();
@@ -19781,6 +19905,7 @@ function localAgentCallbacks(chatAgent) {
19781
19905
  regenerateAgentRun: (threadId, stoppedReplyId) => chatAgent.regenerate(threadId, stoppedReplyId),
19782
19906
  listAgentRuns: () => chatAgent.runningThreadIds(),
19783
19907
  completeMemoryExtraction: (req) => chatAgent.completeMemoryExtraction(req),
19908
+ suggestChannelField: (req) => suggestChannelField(req),
19784
19909
  listMissingCwds: () => auditChannelCwds().map((ch) => ch.id),
19785
19910
  listRunningProfiles: () => listProfiles().filter(isServiceRunning2),
19786
19911
  getAgentIdentities: () => resolveAgentIdentities(),
@@ -19993,7 +20118,7 @@ var MAX_AGE_MINUTES = 30;
19993
20118
  var GRACE_MS = 6e4;
19994
20119
  var BODY_LIMIT = 140;
19995
20120
  function preview(body) {
19996
- const flat = body.replace(/```[\s\S]*?```/g, "[code]").replace(/\s+/g, " ").trim();
20121
+ const flat = body.replace(/```[\s\S]*?```/g, "[code]").replace(/!\[([^\]]*)\]\([^)]*\)/g, (_, alt) => alt || "[image]").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/<(https?:[^|>]+)\|([^>]+)>/g, "$2").replace(/^\s{0,3}(?:#{1,6}\s+|>\s?|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+)/gm, "").replace(/^\s*(?:[-*_]\s*){3,}$/gm, "").replace(/(\*\*|__|~~)(.+?)\1/g, "$2").replace(/(^|[^\w*])[*_](\S(?:.*?\S)?)[*_](?=[^\w*]|$)/g, "$1$2").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
19997
20122
  return flat.length > BODY_LIMIT ? `${flat.slice(0, BODY_LIMIT - 1)}\u2026` : flat;
19998
20123
  }
19999
20124
  async function channelNames(ids) {
@@ -20067,7 +20192,7 @@ import { join as join28 } from "path";
20067
20192
  import { serveStatic } from "@hono/node-server/serve-static";
20068
20193
 
20069
20194
  // src/dashboard/app-icon-routes.ts
20070
- import { Hono as Hono37 } from "hono";
20195
+ import { Hono as Hono38 } from "hono";
20071
20196
  var NEUTRAL_MANIFEST = {
20072
20197
  id: "/",
20073
20198
  name: "Flyspace",
@@ -20105,7 +20230,7 @@ async function withResolvedProfile2(c, next) {
20105
20230
  return profile ? runWithProfile(profile, () => next()) : next();
20106
20231
  }
20107
20232
  function createAppIconRouter() {
20108
- const app = new Hono37();
20233
+ const app = new Hono38();
20109
20234
  app.get("/manifest.json", withResolvedProfile2, async (c) => {
20110
20235
  if (!await resolvedProfileHost(c)) return c.json(NEUTRAL_MANIFEST);
20111
20236
  const profile = getProfile();
@@ -20679,7 +20804,7 @@ async function main() {
20679
20804
  break;
20680
20805
  case "-v":
20681
20806
  case "--version":
20682
- printRaw("0.1.35");
20807
+ printRaw("0.1.37");
20683
20808
  break;
20684
20809
  case "-h":
20685
20810
  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-KTMAS4TP.js";
4
+ } from "./chunk-JTZN44U4.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.35",
3
+ "version": "0.1.37",
4
4
  "description": "AI-native headless community OS",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,