@hanzo/rooms 0.1.72 → 0.1.74

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.
Files changed (110) hide show
  1. package/assets/personas/ada.md +187 -0
  2. package/assets/personas/adi_shamir.md +155 -0
  3. package/assets/personas/al_khwarizmi.md +168 -0
  4. package/assets/personas/al_kindi.md +172 -0
  5. package/assets/personas/alan_kay.md +154 -0
  6. package/assets/personas/analyst.md +138 -0
  7. package/assets/personas/andrew_yao.md +155 -0
  8. package/assets/personas/auguste_kerckhoffs.md +168 -0
  9. package/assets/personas/barbara_liskov.md +154 -0
  10. package/assets/personas/butler_lampson.md +156 -0
  11. package/assets/personas/charles_babbage.md +175 -0
  12. package/assets/personas/creative.md +141 -0
  13. package/assets/personas/curry.md +169 -0
  14. package/assets/personas/cynthia_dwork.md +158 -0
  15. package/assets/personas/david_chaum.md +157 -0
  16. package/assets/personas/david_parnas.md +153 -0
  17. package/assets/personas/dennis_ritchie.md +153 -0
  18. package/assets/personas/dijkstra.md +187 -0
  19. package/assets/personas/donald_knuth.md +156 -0
  20. package/assets/personas/douglas_engelbart.md +175 -0
  21. package/assets/personas/edgar_codd.md +151 -0
  22. package/assets/personas/engineer.md +140 -0
  23. package/assets/personas/feynman.md +178 -0
  24. package/assets/personas/finney.md +187 -0
  25. package/assets/personas/fred_brooks.md +152 -0
  26. package/assets/personas/geoffrey_hinton.md +194 -0
  27. package/assets/personas/herbert_simon.md +190 -0
  28. package/assets/personas/hickey.md +169 -0
  29. package/assets/personas/hopper.md +191 -0
  30. package/assets/personas/hubert_dreyfus.md +184 -0
  31. package/assets/personas/humberto_maturana.md +156 -0
  32. package/assets/personas/index.json +1 -0
  33. package/assets/personas/jcr_licklider.md +178 -0
  34. package/assets/personas/jobs.md +177 -0
  35. package/assets/personas/joe_armstrong.md +155 -0
  36. package/assets/personas/john_backus.md +179 -0
  37. package/assets/personas/john_mccarthy.md +177 -0
  38. package/assets/personas/jon_postel.md +149 -0
  39. package/assets/personas/joseph_weizenbaum.md +185 -0
  40. package/assets/personas/judea_pearl.md +192 -0
  41. package/assets/personas/karen_sparck_jones.md +154 -0
  42. package/assets/personas/konrad_zuse.md +175 -0
  43. package/assets/personas/kunihiko_fukushima.md +152 -0
  44. package/assets/personas/leslie_lamport.md +156 -0
  45. package/assets/personas/markov.md +171 -0
  46. package/assets/personas/mentor.md +141 -0
  47. package/assets/personas/nancy_lynch.md +154 -0
  48. package/assets/personas/niklaus_wirth.md +154 -0
  49. package/assets/personas/norbert_wiener.md +179 -0
  50. package/assets/personas/north-star.md +138 -0
  51. package/assets/personas/peter_naur.md +151 -0
  52. package/assets/personas/pike.md +165 -0
  53. package/assets/personas/radia_perlman.md +156 -0
  54. package/assets/personas/raj_reddy.md +154 -0
  55. package/assets/personas/ralph_merkle.md +152 -0
  56. package/assets/personas/richard_sutton.md +156 -0
  57. package/assets/personas/robin_milner.md +149 -0
  58. package/assets/personas/shafi_goldwasser.md +156 -0
  59. package/assets/personas/shannon.md +184 -0
  60. package/assets/personas/stafford_beer.md +157 -0
  61. package/assets/personas/taher_elgamal.md +180 -0
  62. package/assets/personas/tesla.md +181 -0
  63. package/assets/personas/tony_hoare.md +153 -0
  64. package/assets/personas/torvalds.md +188 -0
  65. package/assets/personas/tpope.md +163 -0
  66. package/assets/personas/turing.md +188 -0
  67. package/assets/personas/whitfield_diffie.md +159 -0
  68. package/assets/personas/zeekay.md +151 -0
  69. package/dist/App.js +26 -22
  70. package/dist/Chat.js +51 -38
  71. package/dist/Column.d.ts +3 -3
  72. package/dist/Column.js +5 -27
  73. package/dist/Me.d.ts +8 -3
  74. package/dist/Me.js +79 -108
  75. package/dist/Palette.d.ts +18 -0
  76. package/dist/Palette.js +131 -0
  77. package/dist/Rail.js +3 -15
  78. package/dist/Recents.d.ts +1 -2
  79. package/dist/Recents.js +9 -9
  80. package/dist/Shell.d.ts +7 -0
  81. package/dist/Shell.js +65 -46
  82. package/dist/Top.d.ts +17 -0
  83. package/dist/Top.js +58 -0
  84. package/dist/agents.d.ts +64 -0
  85. package/dist/agents.js +62 -0
  86. package/dist/crew.d.ts +26 -5
  87. package/dist/crew.js +208 -83
  88. package/dist/help.d.ts +43 -0
  89. package/dist/help.js +58 -0
  90. package/dist/lib/session.d.ts +5 -10
  91. package/dist/lib/session.js +6 -10
  92. package/dist/lib/web.d.ts +6 -0
  93. package/dist/lib/web.js +39 -7
  94. package/dist/recent.d.ts +2 -2
  95. package/dist/recent.js +3 -5
  96. package/dist/seek.d.ts +37 -0
  97. package/dist/seek.js +55 -0
  98. package/dist/team.js +3 -11
  99. package/dist/thread.js +7 -2
  100. package/dist/view.d.ts +44 -0
  101. package/dist/view.js +98 -0
  102. package/package.json +7 -7
  103. package/dist/Account.d.ts +0 -12
  104. package/dist/Account.js +0 -65
  105. package/dist/Find.d.ts +0 -7
  106. package/dist/Find.js +0 -82
  107. package/dist/Profile.d.ts +0 -4
  108. package/dist/Profile.js +0 -67
  109. package/dist/Standing.d.ts +0 -4
  110. package/dist/Standing.js +0 -16
package/dist/crew.js CHANGED
@@ -1,31 +1,32 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // The crew, under the composer: who you can talk to, and a place to make one more.
3
+ // Who a conversation can hold, and the sheet that makes one more.
4
4
  //
5
- // The first disc is an outline with a plus — the shape of a face that is not
6
- // drawn yet — and it opens the sheet that makes an agent: a name, what it does,
7
- // which mind, and a likeness you upload or describe. The rest are the org's
8
- // agents, the house crew first in the order the cast lists them, each drawn by
9
- // the one `Face` every surface draws with. The house crew is the core team
10
- // hanzoai/personas publishes (team.ts).
5
+ // The composer's `+` › Agents (`useAgentShelf`) lists the org's agents, the
6
+ // default cast first: the core team hanzoai/personas publishes (team.ts), drawn
7
+ // for an org that keeps no row of its own for a member. Picking one adds it to
8
+ // the conversation that is open, and the reply comes back in each voice in the
9
+ // room; picking it again lets it out. The page does not move. Create agent and
10
+ // Browse personas open the sheet (`Hire`, mounted once by `AgentMaker`); Manage
11
+ // agents is Dev's Customize › Agents.
11
12
  //
12
- // Under a chat, pressing a face adds that character to the conversation that is
13
- // open, and the reply comes back in each voice in the room; pressing one again
14
- // lets them out. The page does not move: the organization, the thread and the
15
- // address stay as they are. On a page that is not a chat (`to`), pressing a
16
- // face opens a chat with that character there. The sidebar's roster row opens a
17
- // chat with one character alone.
18
- import { useEffect, useRef, useState } from 'react';
13
+ // The crew strip (`Crew`) is the same list as faces, on Home: pressing one opens
14
+ // a chat with that character there (`to`).
15
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
19
16
  import { useRooms } from './host.js';
20
- import { Camera, Check, Plus, Upload } from 'lucide-react';
17
+ import { ArrowLeft, Camera, Check, Plus, Sparkles, Upload, Users } from 'lucide-react';
18
+ import { generateImages } from '@hanzo/ai';
21
19
  import { useAgents, useModels } from '@hanzo/ai/react';
22
20
  import { Box, Button, Input, Text, Textarea, View, XStack, YStack } from '@hanzo/ui';
23
21
  import { useAi } from './lib/ai.js';
24
22
  import { mark } from './lib/mark.js';
25
23
  import { reach } from './lib/reach.js';
26
- import { CAST, Face } from './cast.js';
24
+ import { Face } from './cast.js';
27
25
  import { useOpen } from './open.js';
28
- import { TEAM, called } from './team.js';
26
+ import { TEAM, called, member } from './team.js';
27
+ import { FIGURES, MEMOJI, PAINTER, draft, line, ordered, title } from './agents.js';
28
+ import { app, go } from './lib/host.js';
29
+ import { site } from './where.js';
29
30
  import { bare } from './lib/bare.js';
30
31
  import { BAD, mix } from './lib/mix.js';
31
32
  /** One disc's edge, and the name under it. */
@@ -62,24 +63,64 @@ export function useCrew() {
62
63
  }
63
64
  /** The house crew's names, which a reply may be split by even after one has left the room. */
64
65
  export const HOUSE = TEAM.map((one) => one.name);
65
- /** The core team, drawn for an org that does not keep a member under its own row. */
66
- const DEFAULT_HOUSE_AGENTS = TEAM.map((one) => ({ id: `house-${one.id}`, name: one.name, avatar: `/agents/${one.id}.png` }));
67
- /** The org's agents, the cast first in its own order, then the rest as listed. */
68
- function ordered(agents) {
69
- const list = Array.isArray(agents) ? agents : [];
70
- const names = new Set(list.map((a) => a.name?.toLowerCase()));
71
- const combined = [...list];
72
- for (const house of DEFAULT_HOUSE_AGENTS) {
73
- if (!names.has(house.name.toLowerCase())) {
74
- combined.push(house);
75
- }
76
- }
77
- const rank = new Map(Object.keys(CAST).map((name, i) => [name, i]));
78
- return combined.sort((a, b) => {
79
- const ra = rank.get(a.name?.toLowerCase() ?? '') ?? Infinity;
80
- const rb = rank.get(b.name?.toLowerCase() ?? '') ?? Infinity;
81
- return ra - rb;
82
- });
66
+ /** Two names for one agent differ only in case ("Dev" and "dev"). */
67
+ const same = (a, b) => a.toLowerCase() === b.toLowerCase();
68
+ // THE SHEET IS OPENED FROM A MENU AND DRAWN BESIDE THE COMPOSER, two places
69
+ // that share no parent, so whether it is open lives here, outside React, the
70
+ // way open.ts keeps the conversation.
71
+ let making = null;
72
+ const heard = new Set();
73
+ const listen = (f) => {
74
+ heard.add(f);
75
+ return () => void heard.delete(f);
76
+ };
77
+ /** Opens the New agent sheet, blank or on its persona list; null shuts it. */
78
+ export function make(how) {
79
+ making = how;
80
+ for (const f of heard)
81
+ f();
82
+ }
83
+ /**
84
+ * The New agent sheet, wherever the host mounts this once (beside the
85
+ * composer). It is drawn while `make` holds it open, and an agent it makes joins
86
+ * the conversation that is open.
87
+ */
88
+ export function AgentMaker() {
89
+ const how = useSyncExternalStore(listen, () => making, () => null);
90
+ const { agents: room, openAgent } = useOpen();
91
+ if (!how)
92
+ return null;
93
+ return (_jsx(Hire, { start: how === 'persona', onClose: () => make(null), onMade: (name) => {
94
+ if (!room.some((there) => same(there, name)))
95
+ openAgent(name);
96
+ } }));
97
+ }
98
+ /**
99
+ * The composer's `+` › Agents: the org's agents, the default cast first, each
100
+ * by its face; on is in the open conversation, and a pick toggles it there.
101
+ */
102
+ export function useAgentShelf() {
103
+ const { agents, loading, error } = useCrew();
104
+ const { agents: room, openAgent } = useOpen();
105
+ const pathed = useRooms().path !== null;
106
+ return {
107
+ rows: ordered(agents).map((one) => ({
108
+ id: one.name,
109
+ label: called(one.name),
110
+ icon: _jsx(Face, { src: one.avatar, emoji: one.emoji, name: one.name, size: 16 }),
111
+ note: member(one.name)?.role ?? one.description,
112
+ on: room.some((there) => same(there, one.name)),
113
+ })),
114
+ pick: (id) => openAgent(called(id)),
115
+ loading,
116
+ error: error ? reach(error) : null,
117
+ search: 'Search agents',
118
+ create: { label: 'Create agent', onPress: () => make('new') },
119
+ // Dev's Customize › Agents: a move inside the app where the host reads the
120
+ // path (hanzo.ai), else Dev on hanzo.ai, as Chat's Create file or site goes.
121
+ manage: { label: 'Manage agents', onPress: () => (pathed ? go(app('-/customize/agents')) : window.location.assign(site(app('-/customize/agents')))) },
122
+ browse: { label: 'Browse personas', onPress: () => make('persona') },
123
+ };
83
124
  }
84
125
  /**
85
126
  * `to` is the chat a press opens, for a strip on a page that is not one. Absent,
@@ -146,23 +187,72 @@ function Said({ id, text }) {
146
187
  return null;
147
188
  return (_jsx(Text, { id: id, role: "alert", fontSize: "$2", color: BAD, children: text }));
148
189
  }
190
+ /** A file the host serves, as text; a refusal says which and why. */
191
+ async function text(path) {
192
+ const r = await fetch(path);
193
+ if (!r.ok)
194
+ throw new Error(`${path} did not load (${r.status}).`);
195
+ return r.text();
196
+ }
197
+ /** The roster, read once a page when the list first opens; a failed read is asked again next time. */
198
+ let roster = null;
199
+ const figures = () => (roster ??= text(`${FIGURES}/index.json`)
200
+ .then((t) => JSON.parse(t))
201
+ .catch((e) => {
202
+ roster = null;
203
+ throw e;
204
+ }));
205
+ /** How many matches the list draws; typing narrows the rest. */
206
+ const SHOWN = 50;
207
+ /**
208
+ * Start from a persona: @hanzo/personas' figures by name and one line, the ones
209
+ * with a written PERSONA.md first, searched by either.
210
+ */
211
+ function Figures({ onPick }) {
212
+ const [all, setAll] = useState(null);
213
+ const [failed, setFailed] = useState(null);
214
+ const [q, setQ] = useState('');
215
+ useEffect(() => {
216
+ let live = true;
217
+ figures().then((list) => live && setAll([...list].sort((a, b) => Number(!a.called) - Number(!b.called))), (e) => live && setFailed(reach(e)));
218
+ return () => {
219
+ live = false;
220
+ };
221
+ }, []);
222
+ const needle = q.trim().toLowerCase();
223
+ const found = (all ?? []).filter((f) => !needle || `${title(f)} ${line(f)}`.toLowerCase().includes(needle));
224
+ return (_jsxs(YStack, { gap: "$2", children: [_jsx(Input, { value: q, onChange: (e) => setQ(e.target.value), placeholder: all ? `Search ${all.length.toLocaleString()} figures` : 'Search figures', "aria-label": "Search personas", autoFocus: true }), _jsx(YStack, { maxH: 360, overflowY: "auto", overflowX: "hidden", role: "group", "aria-label": "Personas", children: found.slice(0, SHOWN).map((f) => (_jsxs(YStack, { render: "button", onClick: () => onPick(f), items: "flex-start", gap: 2, px: "$2", py: "$2", rounded: "$3", cursor: "pointer", hoverStyle: { bg: '$raised' }, focusVisibleStyle: { bg: '$raised' }, children: [_jsx(Text, { fontSize: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, children: title(f) }), _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: line(f) })] }, f.slug))) }), _jsx(Text, { fontSize: "$2", color: failed ? BAD : '$soft', role: failed ? 'alert' : undefined, children: failed ??
225
+ (!all
226
+ ? 'Reading the personas…'
227
+ : !found.length
228
+ ? 'No figure matches.'
229
+ : found.length > SHOWN
230
+ ? `${SHOWN} of ${found.length.toLocaleString()} shown. Type to narrow them.`
231
+ : null) })] }));
232
+ }
149
233
  /**
150
234
  * The sheet that makes an agent.
151
235
  *
152
236
  * A name, a mind and a spending limit are what the agents API requires; what it
153
237
  * does becomes its instructions; the likeness is whichever the maker gives — a
154
238
  * photo uploaded onto the round picture (pressed, or dropped on), or a face
155
- * described and drawn by the image model. Both go through `mark`, the one crop
156
- * every saved face takes, so a drawn face and an uploaded one are the same kind
157
- * of picture. The row keeps at most one of a picture and an emoji, so choosing
158
- * one clears the other here too.
239
+ * described and drawn by Hanzo's image model (`PAINTER`), one call per press of
240
+ * Draw or Regenerate. An agent created with neither a picture nor an emoji has
241
+ * one drawn then, once, from the description or else its name. Every face goes
242
+ * through `mark`, the one crop every saved face takes, and is kept as the row's
243
+ * `avatar`. The row keeps at most one of a picture and an emoji, so choosing one
244
+ * clears the other here too.
245
+ *
246
+ * Start from a persona fills the name, the instructions and the face's
247
+ * description from a figure of @hanzo/personas' roster (agents.ts `draft`);
248
+ * `start` opens the sheet on that list.
159
249
  *
160
250
  * Create stays off until the required fields are filled in the API's own
161
251
  * terms, and a refusal is said in plain words under the field it concerns.
162
252
  *
163
253
  * EXPORTED, because Contacts adds agents too and there is one way to make one.
164
254
  */
165
- export function Hire({ onClose, onMade }) {
255
+ export function Hire({ onClose, onMade, start = false }) {
166
256
  const { create } = useAgents();
167
257
  const { models } = useModels();
168
258
  const { client } = useAi();
@@ -179,15 +269,22 @@ export function Hire({ onClose, onMade }) {
179
269
  const [wrong, setWrong] = useState(null);
180
270
  const [over, setOver] = useState(false);
181
271
  const [lit, setLit] = useState(false);
272
+ const [choosing, setChoosing] = useState(start);
273
+ /** A drawing failed on Create, so the next press makes the agent without one. */
274
+ const [faceless, setFaceless] = useState(false);
182
275
  const picking = useRef(null);
183
276
  useEffect(() => {
184
277
  const key = (e) => {
185
- if (e.key === 'Escape')
278
+ if (e.key !== 'Escape')
279
+ return;
280
+ if (choosing)
281
+ setChoosing(false);
282
+ else
186
283
  onClose();
187
284
  };
188
285
  window.addEventListener('keydown', key);
189
286
  return () => window.removeEventListener('keydown', key);
190
- }, [onClose]);
287
+ }, [onClose, choosing]);
191
288
  // WHAT CREATE WAITS FOR, in the API's own terms, so a request it would refuse
192
289
  // is never sent. Each rule names the field it belongs to.
193
290
  const called = name.trim();
@@ -215,30 +312,28 @@ export function Hire({ onClose, onMade }) {
215
312
  setWrong({ field: 'picture', text: reach(e) });
216
313
  }
217
314
  };
218
- // The image model draws the face; `mark` makes it the same 256px square every
219
- // saved picture is, so a drawn face weighs what an uploaded one weighs.
315
+ /** What a drawing is of: the description, else the name. */
316
+ const subject = look.trim() || called;
317
+ // One call to the image model; `mark` makes the answer the same 256px square
318
+ // every saved picture is, so a drawn face weighs what an uploaded one weighs.
319
+ const paint = async () => {
320
+ const [image] = await generateImages(client, `${subject}, ${MEMOJI}`, { model: PAINTER, n: 1, size: '512x512', response_format: 'b64_json' });
321
+ if (!image?.b64_json)
322
+ throw new Error('The image model answered with no picture.');
323
+ const bytes = Uint8Array.from(atob(image.b64_json), (c) => c.charCodeAt(0));
324
+ const face = await mark(new File([bytes], 'face.png', { type: 'image/png' }));
325
+ setAvatar(face);
326
+ setEmoji('');
327
+ return face;
328
+ };
220
329
  const draw = async () => {
221
- if (!client || !look.trim())
330
+ if (!client || !subject || busy)
222
331
  return;
223
332
  setBusy('draw');
224
333
  setWrong(null);
225
334
  try {
226
- const out = await client.http.json({
227
- method: 'POST',
228
- path: '/v1/images/generations',
229
- body: {
230
- prompt: `${look.trim()}, memoji style 3D emoji portrait, head and shoulders, centered, plain background`,
231
- n: 1,
232
- size: '512x512',
233
- response_format: 'b64_json',
234
- },
235
- });
236
- const b64 = out?.data?.[0]?.b64_json;
237
- if (!b64)
238
- throw new Error('The image model answered with no picture.');
239
- const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
240
- setAvatar(await mark(new File([bytes], 'face.png', { type: 'image/png' })));
241
- setEmoji('');
335
+ await paint();
336
+ setFaceless(false);
242
337
  }
243
338
  catch (e) {
244
339
  setWrong({ field: 'picture', text: reach(e) });
@@ -247,18 +342,48 @@ export function Hire({ onClose, onMade }) {
247
342
  setBusy(null);
248
343
  }
249
344
  };
345
+ // A figure fills the sheet: its PERSONA.md is read where one is written.
346
+ const become = async (f) => {
347
+ setChoosing(false);
348
+ setWrong(null);
349
+ try {
350
+ const md = f.called ? await text(`${FIGURES}/${f.slug}.md`) : null;
351
+ const d = draft(f, md);
352
+ setName(d.name);
353
+ setDoes(d.does);
354
+ setLook(d.look);
355
+ setFaceless(false);
356
+ }
357
+ catch (e) {
358
+ setWrong({ field: 'form', text: reach(e) });
359
+ }
360
+ };
250
361
  const make = async () => {
251
362
  if (!ready || busy)
252
363
  return;
253
364
  setBusy('make');
254
365
  setWrong(null);
366
+ // NO PICTURE CHOSEN, SO ONE IS DRAWN, once. A drawing that fails says why,
367
+ // and the next press makes the agent without one.
368
+ let face = avatar;
369
+ if (!face && !emoji.trim() && !faceless && client) {
370
+ try {
371
+ face = await paint();
372
+ }
373
+ catch (e) {
374
+ setWrong({ field: 'picture', text: `${reach(e).replace(/[.!?]?$/, '.')} Press Create again to make ${called} without a picture.` });
375
+ setFaceless(true);
376
+ setBusy(null);
377
+ return;
378
+ }
379
+ }
255
380
  // The budget fields are the agents API's (cloud apps/agents createAgentIn);
256
381
  // @hanzo/ai 0.6.16's AgentCreateParams predates them.
257
382
  const body = {
258
383
  name: called,
259
384
  model,
260
385
  ...(does.trim() ? { instructions: does.trim() } : {}),
261
- ...(avatar ? { avatar } : emoji.trim() ? { emoji: emoji.trim() } : {}),
386
+ ...(face ? { avatar: face } : emoji.trim() ? { emoji: emoji.trim() } : {}),
262
387
  cap_micro_usd: micro(cap),
263
388
  max_task_micro_usd: micro(task),
264
389
  period,
@@ -280,25 +405,25 @@ export function Hire({ onClose, onMade }) {
280
405
  return (_jsx(Box, { position: "fixed", inset: 0, z: "var(--z-modal)", display: "flex", items: "center", justify: "center", p: "$4", bg: "$sunken", overflowY: "auto", onClick: (e) => {
281
406
  if (e.target === e.currentTarget)
282
407
  onClose();
283
- }, children: _jsxs(YStack, { role: "dialog", "aria-modal": true, "aria-labelledby": "hire-title", width: "100%", maxW: 460, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Text, { id: "hire-title", render: "h2", fontSize: "$5", fontWeight: "600", color: "$ink", children: "New agent" }), _jsxs(XStack, { items: "center", gap: "$4", flexWrap: "wrap", children: [_jsxs(Box, { render: "button", onClick: () => picking.current?.click(), onMouseEnter: () => setLit(true), onMouseLeave: () => setLit(false), onFocus: () => setLit(true), onBlur: () => setLit(false), onDragOver: (e) => {
284
- e.preventDefault();
285
- setOver(true);
286
- }, onDragLeave: () => setOver(false), onDrop: (e) => {
287
- e.preventDefault();
288
- setOver(false);
289
- void pick(e.dataTransfer.files?.[0]);
290
- }, "aria-label": avatar ? 'Change photo' : 'Upload photo', "aria-describedby": "hire-picture-hint", position: "relative", width: 88, height: 88, rounded: 9999, overflow: "hidden", shrink: 0, items: "center", justify: "center", bg: "$raised", borderWidth: 2, borderStyle: avatar || emoji ? 'solid' : 'dashed', borderColor: over || lit ? '$ink' : '$soft', children: [face, !face || lit || over ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", inset: 0, items: "center", justify: "center", gap: "$1", bg: face ? VEIL : undefined, children: _jsx(Camera, { size: 22, color: face ? 'var(--pure-white)' : 'var(--soft, var(--muted-foreground))' }) })) : null] }), _jsx("input", { ref: picking, type: "file", hidden: true, accept: "image/png,image/jpeg,image/gif,image/webp", onChange: (e) => {
291
- void pick(e.target.files?.[0]);
292
- e.target.value = '';
293
- } }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1.5", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => picking.current?.click(), children: [_jsx(Upload, { size: 14, "aria-hidden": true }), " ", avatar ? 'Change photo' : 'Upload photo'] }), avatar ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setAvatar(''), children: "Remove" })) : null] }), _jsx(Text, { id: "hire-picture-hint", fontSize: "$2", color: "$soft", children: over ? 'Drop the photo to use it.' : 'Press the circle or drop a photo on it. PNG, JPEG, GIF or WebP.' })] })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-look", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or describe a face and press Draw" }), _jsxs(XStack, { gap: "$1.5", items: "center", children: [_jsx(Box, { flex: 1, children: _jsx(Input, { id: "hire-look", value: look, onChange: (e) => setLook(e.target.value), onKeyDown: (e) => {
294
- if (e.key === 'Enter')
295
- void draw();
296
- }, placeholder: "A calm engineer with round glasses" }) }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy !== null || !look.trim(), onClick: () => void draw(), children: busy === 'draw' ? 'Drawing…' : 'Draw' })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Text, { render: "label", htmlFor: "hire-emoji", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or one emoji" }), _jsx(Box, { width: 64, children: _jsx(Input, { id: "hire-emoji", value: emoji, onChange: (e) => {
297
- setEmoji(e.target.value);
298
- if (e.target.value)
299
- setAvatar('');
300
- }, "aria-describedby": "hire-picture-wrong" }) })] }), _jsx(Said, { id: "hire-picture-wrong", text: shown('picture') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-name", fontSize: "$2", fontWeight: "600", children: "Name" }), _jsx(Input, { id: "hire-name", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
301
- if (e.key === 'Enter')
302
- void make();
303
- }, placeholder: "Michael", autoFocus: true, "aria-required": "true", "aria-invalid": Boolean(nameWrong || shown('name')), "aria-describedby": "hire-name-wrong" }), _jsx(Said, { id: "hire-name-wrong", text: nameWrong ?? shown('name') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-model", fontSize: "$2", fontWeight: "600", children: "Model" }), _jsxs(Text, { render: _jsx("select", { id: "hire-model", value: model, onChange: (e) => setModel(e.target.value), "aria-describedby": "hire-model-wrong" }), ...FIELD, children: [_jsx("option", { value: "enso", children: "enso" }), models.map((one) => (_jsx("option", { value: one.id, children: one.id }, one.id)))] }), _jsx(Said, { id: "hire-model-wrong", text: shown('model') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsxs(Text, { render: "label", htmlFor: "hire-does", fontSize: "$2", fontWeight: "600", children: ["What it does", ' ', _jsx(Text, { fontWeight: "400", color: "var(--soft, var(--muted-foreground))", children: "\u2014 optional" })] }), _jsx(Textarea, { id: "hire-does", value: does, onChangeText: setDoes, placeholder: "The work it takes and the voice it answers in", rows: 3, "aria-describedby": "hire-does-wrong" }), _jsx(Said, { id: "hire-does-wrong", text: shown('does') })] }), _jsxs(View, { render: _jsx("fieldset", { "aria-describedby": "hire-budget-wrong" }), display: "block", borderWidth: 0, p: 0, m: 0, children: [_jsx(Text, { render: "legend", display: "block", fontSize: "$2", fontWeight: "600", mb: 6, children: "Spending limit" }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Up to $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Spending limit in dollars", inputMode: "decimal", value: cap, onChange: (e) => setCap(e.target.value), "aria-invalid": !capOk }) }), _jsx(Text, { render: _jsx("select", { "aria-label": "Spending limit period", value: period, onChange: (e) => setPeriod(e.target.value) }), ...FIELD, width: "auto", children: PERIODS.map(([value, words]) => (_jsx("option", { value: value, children: words }, value))) })] }), _jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "At most $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Most one task may spend, in dollars", inputMode: "decimal", value: task, onChange: (e) => setTask(e.target.value), "aria-invalid": !taskOk }) }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "on one task" })] })] }), _jsx(Box, { mt: "$1.5", children: _jsx(Said, { id: "hire-budget-wrong", text: budgetWrong ?? shown('budget') }) })] }), _jsx(Said, { id: "hire-form-wrong", text: shown('form') }), _jsxs(XStack, { justify: "flex-end", gap: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy !== null || !ready, onClick: () => void make(), children: busy === 'make' ? 'Creating…' : 'Create' })] })] }) }));
408
+ }, children: _jsxs(YStack, { role: "dialog", "aria-modal": true, "aria-labelledby": "hire-title", width: "100%", maxW: 460, gap: "$3", p: "$5", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { id: "hire-title", render: "h2", fontSize: "$5", fontWeight: "600", color: "$ink", children: choosing ? 'Start from a persona' : 'New agent' }), choosing ? (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => setChoosing(false), children: [_jsx(ArrowLeft, { size: 14, "aria-hidden": true }), " Back"] })) : (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setChoosing(true), children: [_jsx(Users, { size: 14, "aria-hidden": true }), " Start from a persona"] }))] }), choosing ? (_jsx(Figures, { onPick: (f) => void become(f) })) : (_jsxs(_Fragment, { children: [_jsxs(XStack, { items: "center", gap: "$4", flexWrap: "wrap", children: [_jsxs(Box, { render: "button", onClick: () => picking.current?.click(), onMouseEnter: () => setLit(true), onMouseLeave: () => setLit(false), onFocus: () => setLit(true), onBlur: () => setLit(false), onDragOver: (e) => {
409
+ e.preventDefault();
410
+ setOver(true);
411
+ }, onDragLeave: () => setOver(false), onDrop: (e) => {
412
+ e.preventDefault();
413
+ setOver(false);
414
+ void pick(e.dataTransfer.files?.[0]);
415
+ }, "aria-label": avatar ? 'Change photo' : 'Upload photo', "aria-describedby": "hire-picture-hint", position: "relative", width: 88, height: 88, rounded: 9999, overflow: "hidden", shrink: 0, items: "center", justify: "center", bg: "$raised", borderWidth: 2, borderStyle: avatar || emoji ? 'solid' : 'dashed', borderColor: over || lit ? '$ink' : '$soft', children: [face, !face || lit || over ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", inset: 0, items: "center", justify: "center", gap: "$1", bg: face ? VEIL : undefined, children: _jsx(Camera, { size: 22, color: face ? 'var(--pure-white)' : 'var(--soft, var(--muted-foreground))' }) })) : null] }), _jsx("input", { ref: picking, type: "file", hidden: true, accept: "image/png,image/jpeg,image/gif,image/webp", onChange: (e) => {
416
+ void pick(e.target.files?.[0]);
417
+ e.target.value = '';
418
+ } }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1.5", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => picking.current?.click(), children: [_jsx(Upload, { size: 14, "aria-hidden": true }), " ", avatar ? 'Change photo' : 'Upload photo'] }), avatar ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setAvatar(''), children: "Remove" })) : null] }), _jsx(Text, { id: "hire-picture-hint", fontSize: "$2", color: "$soft", children: over ? 'Drop the photo to use it.' : 'Press the circle or drop a photo on it, or Create draws one.' })] })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-look", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or describe a face and press Draw" }), _jsxs(XStack, { gap: "$1.5", items: "center", children: [_jsx(Box, { flex: 1, children: _jsx(Input, { id: "hire-look", value: look, onChange: (e) => setLook(e.target.value), onKeyDown: (e) => {
419
+ if (e.key === 'Enter')
420
+ void draw();
421
+ }, placeholder: "A calm engineer with round glasses" }) }), _jsxs(Button, { size: "sm", variant: "outline", disabled: busy !== null || !subject || !client, onClick: () => void draw(), children: [_jsx(Sparkles, { size: 14, "aria-hidden": true }), " ", busy === 'draw' ? 'Drawing…' : avatar ? 'Regenerate' : 'Draw'] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(Text, { render: "label", htmlFor: "hire-emoji", fontSize: "$2", color: "var(--soft, var(--muted-foreground))", children: "Or one emoji" }), _jsx(Box, { width: 64, children: _jsx(Input, { id: "hire-emoji", value: emoji, onChange: (e) => {
422
+ setEmoji(e.target.value);
423
+ if (e.target.value)
424
+ setAvatar('');
425
+ }, "aria-describedby": "hire-picture-wrong" }) })] }), _jsx(Said, { id: "hire-picture-wrong", text: shown('picture') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-name", fontSize: "$2", fontWeight: "600", children: "Name" }), _jsx(Input, { id: "hire-name", value: name, onChange: (e) => setName(e.target.value), onKeyDown: (e) => {
426
+ if (e.key === 'Enter')
427
+ void make();
428
+ }, placeholder: "Michael", autoFocus: true, "aria-required": "true", "aria-invalid": Boolean(nameWrong || shown('name')), "aria-describedby": "hire-name-wrong" }), _jsx(Said, { id: "hire-name-wrong", text: nameWrong ?? shown('name') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsx(Text, { render: "label", htmlFor: "hire-model", fontSize: "$2", fontWeight: "600", children: "Model" }), _jsxs(Text, { render: _jsx("select", { id: "hire-model", value: model, onChange: (e) => setModel(e.target.value), "aria-describedby": "hire-model-wrong" }), ...FIELD, children: [_jsx("option", { value: "enso", children: "enso" }), models.map((one) => (_jsx("option", { value: one.id, children: one.id }, one.id)))] }), _jsx(Said, { id: "hire-model-wrong", text: shown('model') })] }), _jsxs(YStack, { gap: "$1.5", children: [_jsxs(Text, { render: "label", htmlFor: "hire-does", fontSize: "$2", fontWeight: "600", children: ["What it does", ' ', _jsx(Text, { fontWeight: "400", color: "var(--soft, var(--muted-foreground))", children: "\u2014 optional" })] }), _jsx(Textarea, { id: "hire-does", value: does, onChangeText: setDoes, placeholder: "The work it takes and the voice it answers in", rows: 3, "aria-describedby": "hire-does-wrong" }), _jsx(Said, { id: "hire-does-wrong", text: shown('does') })] }), _jsxs(View, { render: _jsx("fieldset", { "aria-describedby": "hire-budget-wrong" }), display: "block", borderWidth: 0, p: 0, m: 0, children: [_jsx(Text, { render: "legend", display: "block", fontSize: "$2", fontWeight: "600", mb: 6, children: "Spending limit" }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "Up to $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Spending limit in dollars", inputMode: "decimal", value: cap, onChange: (e) => setCap(e.target.value), "aria-invalid": !capOk }) }), _jsx(Text, { render: _jsx("select", { "aria-label": "Spending limit period", value: period, onChange: (e) => setPeriod(e.target.value) }), ...FIELD, width: "auto", children: PERIODS.map(([value, words]) => (_jsx("option", { value: value, children: words }, value))) })] }), _jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$soft", children: "At most $" }), _jsx(Box, { width: 84, children: _jsx(Input, { "aria-label": "Most one task may spend, in dollars", inputMode: "decimal", value: task, onChange: (e) => setTask(e.target.value), "aria-invalid": !taskOk }) }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "on one task" })] })] }), _jsx(Box, { mt: "$1.5", children: _jsx(Said, { id: "hire-budget-wrong", text: budgetWrong ?? shown('budget') }) })] }), _jsx(Said, { id: "hire-form-wrong", text: shown('form') }), _jsxs(XStack, { justify: "flex-end", gap: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: busy !== null || !ready, onClick: () => void make(), children: busy === 'make' ? 'Creating…' : 'Create' })] })] }))] }) }));
304
429
  }
package/dist/help.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /** One Help row that leaves for a page. */
2
+ export interface Page {
3
+ key: string;
4
+ label: string;
5
+ href: string;
6
+ }
7
+ /**
8
+ * The Help menu's pages, read at call time: `site()` is the app's origin on
9
+ * hanzo.ai and hanzo.ai itself on hanzo.team. Help center is /support, the
10
+ * page that leads to the help center, the docs, status and a person.
11
+ */
12
+ export declare const pages: () => Page[];
13
+ /** The agreements under the Help menu's rule. */
14
+ export declare const legal: () => Page[];
15
+ /** One of the app's keys: what it does, and how a Mac and every other platform spell it. */
16
+ export interface Key {
17
+ does: string;
18
+ mac: string;
19
+ pc: string;
20
+ }
21
+ /**
22
+ * Every key the app answers, in the order a reader looks for them. Each is
23
+ * handled where it acts: App.tsx (⌘K, ⌘, and, in a window, Back and Forward),
24
+ * Me.tsx (⌘/), @hanzo/build's panel (⌘.), the composer (Enter, ⌘U), chord.ts
25
+ * (the conversation's chords, spelled by `said` so the two cannot drift).
26
+ */
27
+ export declare const KEYS: Key[];
28
+ /** The part of a keydown `asked` reads. */
29
+ export interface Press {
30
+ key?: string;
31
+ ctrlKey: boolean;
32
+ metaKey: boolean;
33
+ altKey: boolean;
34
+ repeat?: boolean;
35
+ }
36
+ /** Whether a keydown asks for the keyboard shortcuts: ⌘/ or Ctrl+/, once a press. */
37
+ export declare const asked: (e: Press) => boolean;
38
+ /**
39
+ * Opens an address in a tab of its own, through an anchor, so a host that
40
+ * sends links out of its window (the desktop's system browser) hears it as the
41
+ * link it is.
42
+ */
43
+ export declare function out(href: string): void;
package/dist/help.js ADDED
@@ -0,0 +1,58 @@
1
+ // The Help menu's pages and the app's keys, as data: the account menu (Me.tsx)
2
+ // draws both, its shortcuts dialog renders KEYS, and a spec reads them as they
3
+ // are. No React, so a spec reads it as it is.
4
+ import { said } from './chord.js';
5
+ import { site } from './where.js';
6
+ /** Where the release notes are published: the site's own nav names the same. */
7
+ const RELEASES = 'https://github.com/hanzoai/cli/releases';
8
+ /**
9
+ * The Help menu's pages, read at call time: `site()` is the app's origin on
10
+ * hanzo.ai and hanzo.ai itself on hanzo.team. Help center is /support, the
11
+ * page that leads to the help center, the docs, status and a person.
12
+ */
13
+ export const pages = () => [
14
+ { key: 'help', label: 'Help center', href: site('/support') },
15
+ { key: 'center', label: 'Privacy center', href: site('/legal') },
16
+ { key: 'releases', label: 'Release notes', href: RELEASES },
17
+ { key: 'download', label: 'Download apps', href: site('/download') },
18
+ ];
19
+ /** The agreements under the Help menu's rule. */
20
+ export const legal = () => [
21
+ { key: 'terms', label: 'Terms of Service', href: site('/legal/terms') },
22
+ { key: 'privacy', label: 'Privacy Policy', href: site('/legal/privacy') },
23
+ ];
24
+ /**
25
+ * Every key the app answers, in the order a reader looks for them. Each is
26
+ * handled where it acts: App.tsx (⌘K, ⌘, and, in a window, Back and Forward),
27
+ * Me.tsx (⌘/), @hanzo/build's panel (⌘.), the composer (Enter, ⌘U), chord.ts
28
+ * (the conversation's chords, spelled by `said` so the two cannot drift).
29
+ */
30
+ export const KEYS = [
31
+ { does: 'Search chats', mac: '⌘K', pc: 'Ctrl+K' },
32
+ { does: 'Settings', mac: '⌘,', pc: 'Ctrl+,' },
33
+ { does: 'Keyboard shortcuts', mac: '⌘/', pc: 'Ctrl+/' },
34
+ { does: 'Side panel', mac: '⌘.', pc: 'Ctrl+.' },
35
+ { does: 'Attach files', mac: '⌘U', pc: 'Ctrl+U' },
36
+ { does: 'Send', mac: 'Enter', pc: 'Enter' },
37
+ { does: 'New line', mac: '⇧Enter', pc: 'Shift+Enter' },
38
+ { does: 'Rename chat', mac: said('rename', true), pc: said('rename', false) },
39
+ { does: 'Pin or unpin chat', mac: said('pin', true), pc: said('pin', false) },
40
+ { does: 'Back', mac: '⌘[', pc: 'Alt+←' },
41
+ { does: 'Forward', mac: '⌘]', pc: 'Alt+→' },
42
+ ];
43
+ /** Whether a keydown asks for the keyboard shortcuts: ⌘/ or Ctrl+/, once a press. */
44
+ export const asked = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.repeat && e.key === '/';
45
+ /**
46
+ * Opens an address in a tab of its own, through an anchor, so a host that
47
+ * sends links out of its window (the desktop's system browser) hears it as the
48
+ * link it is.
49
+ */
50
+ export function out(href) {
51
+ const a = document.createElement('a');
52
+ a.href = href;
53
+ a.target = '_blank';
54
+ a.rel = 'noopener noreferrer';
55
+ document.body.appendChild(a);
56
+ a.click();
57
+ a.remove();
58
+ }
@@ -180,16 +180,11 @@ export declare const PICKED = "hanzo:org:picked";
180
180
  * app's account menu — is one the gate does not ask for again.
181
181
  */
182
182
  export declare function pick(o: string): void;
183
- /** The reserved org whose own people are the platform's operators. */
184
- export declare const ADMIN_ORG = "admin";
185
- /**
186
- * Whether the signed-in person is a SUPERADMIN, as IAM signs it: the org of the
187
- * person's own row — the first entry of the token's `orgs`, which IAM always puts
188
- * first (hanzoai/iam internal/store/membership.go `MemberOrgRefs`) — is the
189
- * reserved `admin` org. It is the equality IAM decides `assume` and the
190
- * organization list on (pkg/schema/user.go `SuperAdmin`) and cloud's `Super`.
191
- * Not the `owner` claim: a sign-in token carries the APPLICATION's org there. A
192
- * membership of `admin` is not it either, and neither is being an org's admin.
183
+ /**
184
+ * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
185
+ * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
186
+ * org, the first entry of the token's `orgs`, is the reserved `admin` org, compared
187
+ * verbatim. Not the `owner` claim, not a membership of `admin`, not an org's admin.
193
188
  * It decides only what is drawn: IAM refuses everyone else the list of every
194
189
  * organization and the step into one.
195
190
  */
@@ -14,6 +14,7 @@
14
14
  * who was signed in, which reads as a permissions problem rather than a typo.
15
15
  * Two derivations of one name never agree; this is the one derivation.
16
16
  */
17
+ import { superAdmin as sudo } from '@hanzo/iam/browser';
17
18
  import { api } from './api.js';
18
19
  import { where } from '../where.js';
19
20
  /** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
@@ -436,21 +437,16 @@ export function pick(o) {
436
437
  }
437
438
  window.dispatchEvent(new Event(ORG));
438
439
  }
439
- /** The reserved org whose own people are the platform's operators. */
440
- export const ADMIN_ORG = 'admin';
441
440
  /**
442
- * Whether the signed-in person is a SUPERADMIN, as IAM signs it: the org of the
443
- * person's own row — the first entry of the token's `orgs`, which IAM always puts
444
- * first (hanzoai/iam internal/store/membership.go `MemberOrgRefs`) — is the
445
- * reserved `admin` org. It is the equality IAM decides `assume` and the
446
- * organization list on (pkg/schema/user.go `SuperAdmin`) and cloud's `Super`.
447
- * Not the `owner` claim: a sign-in token carries the APPLICATION's org there. A
448
- * membership of `admin` is not it either, and neither is being an org's admin.
441
+ * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
442
+ * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
443
+ * org, the first entry of the token's `orgs`, is the reserved `admin` org, compared
444
+ * verbatim. Not the `owner` claim, not a membership of `admin`, not an org's admin.
449
445
  * It decides only what is drawn: IAM refuses everyone else the list of every
450
446
  * organization and the step into one.
451
447
  */
452
448
  export function superAdmin() {
453
- return home() === ADMIN_ORG;
449
+ return sudo(claims());
454
450
  }
455
451
  /** The organization a SuperAdmin stepped into through IAM, from the token's `assumed` claim, or null. */
456
452
  export function assumed() {
package/dist/lib/web.d.ts CHANGED
@@ -37,6 +37,12 @@
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;
40
46
  /** The two tools that mark a turn as one that may use the web. */
41
47
  export declare const WEB: ChatCompletionTool[];
42
48
  /**