@hanzo/rooms 0.1.76 → 0.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Chat.js CHANGED
@@ -33,7 +33,7 @@ import { Prose } from "./Prose.js";
33
33
  import { useAgent, useAi, useChat, } from "@hanzo/ai/react";
34
34
  import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
35
35
  import { ASK, Chat as ChatSurface, Failure, Parts, words } from "@hanzo/ui/chat";
36
- import { carriedIn, compose, partsOf, said as told, weigh } from "./lib/attach.js";
36
+ import { carriedIn, carrying, compose, partsOf, said as told, weigh } from "./lib/attach.js";
37
37
  import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
38
38
  import { channel, drop, hold, landed, localOf, spend, take, usePane, useWorkFile } from "./pane.js";
39
39
  import { Face, brief as roomBrief, join, roleOf, roomTurn, roster as rosterLine, rosterOf, speakers, speaksOf, voiceOf } from "./cast.js";
@@ -49,7 +49,7 @@ import { app, dev, go } from "./lib/host.js";
49
49
  import { useRooms } from "./host.js";
50
50
  import { chats, house, paidPlan, paused, setCreditsAfterAllowance, useLimits } from "./lib/limits.js";
51
51
  import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
52
- import { RESEARCH, WEB } from "./lib/web.js";
52
+ import { WEB } from "./lib/web.js";
53
53
  import { useHydrated } from "./lib/hydrated.js";
54
54
  import { base, served } from "./lib/ai.js";
55
55
  import { onServed } from "./lib/served.js";
@@ -516,7 +516,17 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
516
516
  // What the conversation carries beside its words (@hanzo/build `useCarry`):
517
517
  // the skills picked from `+`, what Hanzo remembers and the mode, said at the
518
518
  // end of the room's system turn so every request carries them.
519
- const carry = useCarry(target, open ?? "", (tab) => go(app(`-/customize/${tab}`)));
519
+ // What the plugins act on — the draft and the files held for the next message —
520
+ // read when they act, so it is lent through a ref filled further down.
521
+ const lent = useRef({ draft: "", setDraft: () => { }, attach: () => { } });
522
+ const carry = useCarry(target, open ?? "", (tab) => go(app(`-/customize/${tab}`)), {
523
+ get draft() {
524
+ return lent.current.draft;
525
+ },
526
+ setDraft: (x) => lent.current.setDraft(x),
527
+ attach: (f) => lent.current.attach(f),
528
+ go: (href) => (/^https?:/.test(href) ? void window.open(href, "_blank", "noopener,noreferrer") : go(href)),
529
+ });
520
530
  const agentShelf = useAgentShelf();
521
531
  // Up to three things worth doing next, from the person's own activity (@hanzo/build).
522
532
  const hints = useSuggestions(target);
@@ -716,7 +726,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
716
726
  // world, in any language, is answered from what the web says now and streams.
717
727
  params: {
718
728
  reasoning_effort: mind.effort,
719
- ...(!anonymous && house(rowOf(model)?.family) ? { tools: carry.gear.gear.research ? [...WEB, RESEARCH] : WEB } : {}),
729
+ ...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}),
720
730
  },
721
731
  ...(open ? { thread: open } : {}),
722
732
  onError: (e) => {
@@ -854,6 +864,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
854
864
  // read from it for the question.
855
865
  const { held: files, refused: turnedAway } = usePane(at);
856
866
  const via = useMemo(() => ({ api: ai, org: anonymous ? null : org() }), [ai, anonymous]);
867
+ lent.current = { draft, setDraft, attach: (f) => void hold(at, f, via) };
857
868
  // "Use in Chat" from the Library: `?attach=<item>` holds it for the next message.
858
869
  useAttach(at, via);
859
870
  // "Schedule this": the prompt the dialog opens on, or null while it is shut.
@@ -1140,15 +1151,55 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1140
1151
  * while; past it the turn says the file is not readable yet rather than
1141
1152
  * answering as though it had been read.
1142
1153
  */
1154
+ // WHAT THE PLUGINS THAT ARE ON ADD, asked just before a message goes: words
1155
+ // the model reads with it (carried in the turn's own block, as files are), or
1156
+ // an answer of the plugin's own, drawn as the reply without asking a model.
1157
+ const [prepWrong, setPrepWrong] = useState(null);
1158
+ const answered = (text, reply) => {
1159
+ apply.current((held) => [
1160
+ ...held,
1161
+ { id: `u-${Date.now()}`, role: "user", content: text },
1162
+ { id: `a-${Date.now()}`, role: "assistant", content: reply },
1163
+ ]);
1164
+ if (!anonymous)
1165
+ void ai.threads
1166
+ .record([{ role: "user", content: text }, { role: "assistant", content: reply }], open ?? undefined)
1167
+ .then((id) => (!open && id ? reopen(id) : undefined))
1168
+ .catch(() => { });
1169
+ };
1143
1170
  const submit = () => {
1144
1171
  const text = draft.trim();
1145
1172
  if (preparing || (!text && !files.length))
1146
1173
  return;
1174
+ if (!carry.preparing)
1175
+ return deliver(text, "");
1176
+ setDraft("");
1177
+ setPrepWrong(null);
1178
+ void (async () => {
1179
+ setPreparing("Preparing");
1180
+ try {
1181
+ // The files held for this message, as bytes, for a plugin that reads them (PDF).
1182
+ const bytes = await Promise.all(files.filter((h) => h.href).map(async (h) => new File([await (await fetch(h.href)).blob()], h.name, { type: h.type })));
1183
+ const got = await carry.prep({ text, files: bytes, thread: open ?? "" }, new AbortController().signal);
1184
+ if (got.reply && !got.system)
1185
+ return answered(text, got.reply);
1186
+ deliver(text, got.system ?? "");
1187
+ }
1188
+ catch (e) {
1189
+ setPrepWrong(e instanceof Error ? e.message : String(e));
1190
+ setDraft(text);
1191
+ }
1192
+ finally {
1193
+ setPreparing(null);
1194
+ }
1195
+ })();
1196
+ };
1197
+ const deliver = (text, extra) => {
1147
1198
  const holding = files.slice();
1148
1199
  const earlier = carriedIn(chat.messages);
1149
1200
  if (!holding.length && !earlier.length) {
1150
1201
  setDraft("");
1151
- guarded(text);
1202
+ guarded(extra ? carrying(text, extra) : text);
1152
1203
  return;
1153
1204
  }
1154
1205
  setDraft("");
@@ -1163,7 +1214,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1163
1214
  const all = [...attached, ...reused];
1164
1215
  if (!all.length) {
1165
1216
  if (text)
1166
- guarded(text);
1217
+ guarded(extra ? carrying(text, extra) : text);
1167
1218
  else
1168
1219
  setDraft(text);
1169
1220
  return;
@@ -1180,7 +1231,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1180
1231
  }
1181
1232
  if (!grounds?.passages.length)
1182
1233
  why = why ?? unread(now);
1183
- guarded(compose(text, { attached, reused }, grounds, why, partly(now)));
1234
+ guarded(compose(text, { attached, reused }, grounds, why, partly(now), extra));
1184
1235
  }
1185
1236
  finally {
1186
1237
  setPreparing(null);
@@ -1257,7 +1308,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1257
1308
  // just typed left the screen, and the first thing a stranger saw of the
1258
1309
  // product was a page about training data. The room stays; the sheet sits on
1259
1310
  // top of it, and the question is still there behind it.
1260
- const consent = asking === null ? null : (_jsx(Consent, { question: asking, onAgree: () => {
1311
+ const consent = asking === null ? null : (_jsx(Consent, { question: told(asking).text, onAgree: () => {
1261
1312
  const s = store();
1262
1313
  // Nothing to persist into is not a reason to refuse the turn the
1263
1314
  // reader just agreed to; it means the question is asked again next
@@ -1426,7 +1477,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1426
1477
  },
1427
1478
  // WHAT GOES WITH THE MESSAGE, above the frame and at its width: the
1428
1479
  // pause and its ways past, one chip a file, and talk mode's words.
1429
- head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(_Fragment, { children: [banner ? (_jsx(LimitedBanner, { message: banner.message, actions: banner.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onOpen: openFile, onRemove: () => drop(at, one.id), onSave: anonymous ? undefined : (url, id) => keepOne(url, id, one.name, one.type) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
1480
+ head: files.length || turnedAway || prepWrong || preparing || talk.open || talk.refusal || pause ? (_jsxs(_Fragment, { children: [banner ? (_jsx(LimitedBanner, { message: banner.message, actions: banner.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onOpen: openFile, onRemove: () => drop(at, one.id), onSave: anonymous ? undefined : (url, id) => keepOne(url, id, one.name, one.type) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway || prepWrong ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway || prepWrong })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
1430
1481
  ? worded(talk.refusal)
1431
1482
  : talk.reply
1432
1483
  ? `Hanzo: ${talk.reply}`
@@ -1441,6 +1492,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1441
1492
  upload: () => take(at, via),
1442
1493
  agents: agentShelf,
1443
1494
  skills: carry.skills,
1495
+ plugins: carry.plugins,
1444
1496
  research: { on: carry.gear.gear.research, set: (on) => carry.gear.set({ research: on }) },
1445
1497
  memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
1446
1498
  },
@@ -1455,6 +1507,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1455
1507
  ...(anonymous ? [] : carry.on),
1456
1508
  ],
1457
1509
  slash: anonymous ? undefined : carry.skills,
1510
+ at: anonymous ? undefined : carry.plugins,
1458
1511
  mode: anonymous ? null : { value: carry.gear.gear.mode, set: (m) => carry.gear.set({ mode: m }) },
1459
1512
  // The model, the effort and the mode under the frame (@hanzo/build).
1460
1513
  mind,
@@ -1466,7 +1519,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1466
1519
  talk,
1467
1520
  // While nothing has been said: what is worth doing next.
1468
1521
  below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onPick: setDraft, onDismiss: hints.dismiss }) : undefined,
1469
- }) }), _jsx(AgentMaker, {}), isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1522
+ }) }), _jsx(AgentMaker, {}), carry.dialog, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1470
1523
  }
1471
1524
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1472
1525
  function Consent({ question, onAgree, onCancel, }) {
@@ -28,7 +28,14 @@ export declare function weigh(bytes: number): string;
28
28
  * stored without indexing, or the read failed — so the model says so rather
29
29
  * than answering as though it had read the files.
30
30
  */
31
- export declare function compose(text: string, carried: Carried, grounds: Grounds | null, why?: string, partial?: string): string;
31
+ export declare function compose(text: string, carried: Carried, grounds: Grounds | null, why?: string, partial?: string, extra?: string): string;
32
+ /**
33
+ * The message as sent with what a plugin found for it (research, a PDF's text):
34
+ * the words, then the same block files ride in, holding no files, so it is
35
+ * drawn as the words alone and `lookup` reads the turn as carried, never
36
+ * searching it a second time.
37
+ */
38
+ export declare function carrying(text: string, extra: string): string;
32
39
  /** A turn read back: the words a person wrote, and the files it carried. */
33
40
  export interface Said {
34
41
  text: string;
@@ -38,7 +38,7 @@ const inert = (s) => s.split(CLOSE).join('</workspace files>');
38
38
  * stored without indexing, or the read failed — so the model says so rather
39
39
  * than answering as though it had read the files.
40
40
  */
41
- export function compose(text, carried, grounds, why, partial) {
41
+ export function compose(text, carried, grounds, why, partial, extra) {
42
42
  const words = text.trim();
43
43
  const all = [...carried.attached, ...carried.reused];
44
44
  if (!all.length)
@@ -62,10 +62,23 @@ export function compose(text, carried, grounds, why, partial) {
62
62
  else {
63
63
  lines.push(`No passages could be read from them${why ? ` (${why})` : ''}: say so, and answer only what you can without them.`);
64
64
  }
65
+ if (extra)
66
+ lines.push('', inert(extra));
65
67
  lines.push(CLOSE);
66
68
  const block = lines.join('\n');
67
69
  return words ? `${words}\n\n${block}` : block;
68
70
  }
71
+ /**
72
+ * The message as sent with what a plugin found for it (research, a PDF's text):
73
+ * the words, then the same block files ride in, holding no files, so it is
74
+ * drawn as the words alone and `lookup` reads the turn as carried, never
75
+ * searching it a second time.
76
+ */
77
+ export function carrying(text, extra) {
78
+ const words = text.trim();
79
+ const block = [OPEN, JSON.stringify({ attached: [], reused: [] }), inert(extra), CLOSE].join('\n');
80
+ return words ? `${words}\n\n${block}` : block;
81
+ }
69
82
  /** Takes the files block back off a turn, for drawing. */
70
83
  export function said(content) {
71
84
  const at = content.startsWith(`${OPEN}\n`) ? 0 : content.lastIndexOf(`\n${OPEN}\n`);
package/dist/lib/web.d.ts CHANGED
@@ -37,12 +37,6 @@
37
37
  import { type ChatCompletionTool } from '@hanzo/ai';
38
38
  /** Pages a turn that names them reads. */
39
39
  export declare const PAGES = 3;
40
- /**
41
- * Research: beside `WEB`, a turn that carries this reads deeper — ten results
42
- * listed, five pages read — whatever its shape, and the model is asked for a
43
- * report that cites them.
44
- */
45
- export declare const RESEARCH: ChatCompletionTool;
46
40
  /** The two tools that mark a turn as one that may use the web. */
47
41
  export declare const WEB: ChatCompletionTool[];
48
42
  /**
package/dist/lib/web.js CHANGED
@@ -41,9 +41,6 @@ export const PAGES = 3;
41
41
  /** Results a search lists, and how many of them it reads. */
42
42
  const LISTED = 6;
43
43
  const READ = 2;
44
- /** What Research lists and reads instead. */
45
- const DEEP_LISTED = 10;
46
- const DEEP_READ = 5;
47
44
  /** Characters of a page a search quotes, and a named page quotes. */
48
45
  const PAGE_CHARS = 3500;
49
46
  const CRAWL_CHARS = 6000;
@@ -56,21 +53,6 @@ const GATE = 8192;
56
53
  const SEARCH_MS = 20_000;
57
54
  const SKIM_MS = 10_000;
58
55
  const READ_MS = 25_000;
59
- /**
60
- * Research: beside `WEB`, a turn that carries this reads deeper — ten results
61
- * listed, five pages read — whatever its shape, and the model is asked for a
62
- * report that cites them.
63
- */
64
- export const RESEARCH = {
65
- type: 'function',
66
- function: {
67
- name: 'research',
68
- description: 'Research the question in depth on the live web and answer with a cited report.',
69
- parameters: { type: 'object', properties: {}, required: [] },
70
- },
71
- };
72
- /** The word a Research turn's model is given. */
73
- const REPORT = 'Research mode: answer with a detailed report — a short direct answer first, then the evidence under headings — citing each claim with the numbered sources above as [n](url). Say plainly where the sources disagree or fall short.';
74
56
  /** The two tools that mark a turn as one that may use the web. */
75
57
  export const WEB = [
76
58
  {
@@ -545,7 +527,7 @@ function pointed(text) {
545
527
  /** What the model is told of a page it was not given. */
546
528
  const withheld = (url) => `The person's message names ${label(url)}. It was not opened: opening an address like it can sign in, confirm, unsubscribe or spend a one-time link for them.`;
547
529
  /** Runs one search: the results, best first, and the text of the best pages. */
548
- async function search(call, q, lang, now, deep = false) {
530
+ async function search(call, q, lang, now) {
549
531
  let found;
550
532
  try {
551
533
  found = await call('/v1/websearch', lang ? { q, language: lang } : { q }, SEARCH_MS);
@@ -554,11 +536,11 @@ async function search(call, q, lang, now, deep = false) {
554
536
  return unavailable(e);
555
537
  }
556
538
  const words = keywords(q);
557
- const ranked = rank(found?.results ?? [], words).slice(0, deep ? DEEP_LISTED : LISTED);
539
+ const ranked = rank(found?.results ?? [], words).slice(0, LISTED);
558
540
  const head = `Web search for "${q}", ${stamp(now)}.`;
559
541
  if (!ranked.length)
560
542
  return `${head}\nNothing was found.`;
561
- const pages = await Promise.all(ranked.slice(0, deep ? DEEP_READ : READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
543
+ const pages = await Promise.all(ranked.slice(0, READ).map((r) => call('/v1/crawl', { url: r.url }, SKIM_MS)
562
544
  .then((d) => (d?.success ? excerpt(d.data?.markdown ?? '', words, PAGE_CHARS) : ''))
563
545
  .catch(() => '')));
564
546
  const items = ranked.map((r, i) => {
@@ -581,19 +563,6 @@ async function read(call, url, words) {
581
563
  return `Could not read ${url}: ${e instanceof Error ? e.message : String(e)}.`;
582
564
  }
583
565
  }
584
- /** A Research turn is looked up whatever its shape: its own words, searched. Files and pictures still stay home. */
585
- function forced(messages, spoken) {
586
- const last = messages.at(-1);
587
- if (last?.role !== 'user')
588
- return null;
589
- const { text: said, carried } = told(textOf(last.content));
590
- if (carried || pictured(last.content))
591
- return null;
592
- const plain = spell(said.trim().slice(0, GATE)).trim();
593
- if (!plain)
594
- return null;
595
- return { pages: [], held: [], q: plain.slice(0, QUERY), lang: language(plain, spoken), about: keywords(plain) };
596
- }
597
566
  /**
598
567
  * The lookup the last turn needs, or null. A short follow-up ("and in
599
568
  * Moscow?") is read with the question before it, the way the person meant it.
@@ -698,8 +667,7 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
698
667
  if (!asked.stream || !Array.isArray(asked.messages) || !WEB.every((t) => names.has(t.function.name)))
699
668
  return base(input, init);
700
669
  const { tools: _tools, tool_choice: _choice, ...bare } = asked;
701
- const deep = names.has(RESEARCH.function.name);
702
- const look = lookup(asked.messages, spoken()) ?? (deep ? forced(asked.messages, spoken()) : null);
670
+ const look = lookup(asked.messages, spoken());
703
671
  if (!look)
704
672
  return base(input, { ...init, body: JSON.stringify(bare) });
705
673
  // The reader's stop, and the stream's own cancel, end every request below.
@@ -727,10 +695,10 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
727
695
  };
728
696
  const found = (await Promise.all([
729
697
  ...look.pages.map((u) => read(call, u, look.about)),
730
- ...(look.q ? [search(call, look.q, look.lang, now, deep)] : []),
698
+ ...(look.q ? [search(call, look.q, look.lang, now)] : []),
731
699
  ...look.held.map(async (u) => withheld(u)),
732
700
  ])).join('\n\n');
733
- const messages = primed(asked.messages, found, now, deep ? REPORT : '');
701
+ const messages = primed(asked.messages, found, now);
734
702
  const first = await send(messages);
735
703
  if (!streams(first))
736
704
  return first;
@@ -741,7 +709,7 @@ export function researched(base, clock = () => new Date(), spoken = reader) {
741
709
  // results: asked once more, it answers. One that failed is not asked again.
742
710
  const { said, failed } = await relay(first, out);
743
711
  if (!said.trim() && !failed) {
744
- const again = await send(primed(asked.messages, found, now, deep ? `${REPORT}\n\n${SILENT}` : SILENT));
712
+ const again = await send(primed(asked.messages, found, now, SILENT));
745
713
  if (streams(again))
746
714
  await relay(again, out);
747
715
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.63",
52
+ "@hanzo/build": ">=0.2.64",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
@@ -67,7 +67,7 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.63",
70
+ "@hanzo/build": "0.2.64",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",