@hanzo/build 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -70,6 +70,7 @@ hanzo.id token for `Host.admin`. One left column, never two.
70
70
  | `''` | New — the empty state and the composer |
71
71
  | `sess_<32 hex>` | one run, live |
72
72
  | `-/automations` | repeating work, read from `/v1/auto/flows` |
73
+ | `-/mcp` | the fleet's native MCP servers, from `POST /v1/mcp` `tools/list` |
73
74
  | `-/codebases` | the forge's repositories |
74
75
  | `-/sync` | bring granted repositories onto the forge |
75
76
  | `-/projects` | the forge's boards |
@@ -83,11 +84,16 @@ collide.
83
84
 
84
85
  ## What is on the screen
85
86
 
86
- **The rail** (hanzo.build). New, then Automations, then the forge — Codebase, Projects, Issues —
87
- then Artifacts, Customize (the platform's plugins), More (Templates, Machines,
88
- Docs), then the org's coding runs newest first with a live status dot, and the
89
- account. Collapse is an explicit toggle kept in this browser. A host with its
90
- own rail draws `DevSection` there instead.
87
+ **The rail** (hanzo.build). At the top, the mark. Signed in to an organization,
88
+ that spot is the organization switcher: the org, and the projects under it.
89
+ New, then Automations, then the forge — Codebase, Projects, Issues — then MCP,
90
+ Artifacts, More (Templates, Machines, Docs), then the org's coding runs newest
91
+ first with a live status dot. The account stays at the foot. Settings opens
92
+ that account on this page. Collapse is an explicit toggle kept in this browser.
93
+ A host with its own rail draws `DevSection` there instead.
94
+
95
+ **MCP.** The native servers `POST /v1/mcp` lists, each with the operations it
96
+ names. A run starts a server when it calls it, and the server stays up afterwards.
91
97
 
92
98
  **New.** "What's up next?", and at the foot the composer: where the run runs
93
99
  (Default is the platform's sandbox; the org's machines follow), the codebase
@@ -123,6 +129,7 @@ picker and open-in-tab; Share and Publish; the console dock under it.
123
129
  | `GET /v1/projects` · `POST /v1/projects/fork` · `GET /v1/templates` | artifacts, templates |
124
130
  | `GET /v1/git/repos/{name}/tree` · `/blob` | Files and Code |
125
131
  | `POST /v1/platform/apps` · `GET /v1/platform/builds` | Add to project, Publish |
132
+ | `POST /v1/mcp` | the native MCP servers, `tools/list` |
126
133
  | `GET /v1/models` · `POST /v1/event` | the model list, a verdict |
127
134
 
128
135
  `mode`, `model` and `effort` are sent with a run as asked; the platform
package/lib/account.js CHANGED
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * finding a run by name (the search icon).
5
5
  */
6
6
  import { SizableText, XStack, YStack } from '@hanzo/gui';
7
- import { LogOut, Settings } from '@hanzogui/lucide-icons-2';
7
+ import { LogOut } from '@hanzogui/lucide-icons-2';
8
8
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
9
9
  import { useMemo, useState } from 'react';
10
10
  import { useHost } from './host.js';
@@ -20,21 +20,10 @@ export function onAccount(fn) {
20
20
  export function revealAccount() {
21
21
  reveal?.();
22
22
  }
23
- /** A settings address on another host. This page's own account is the dialog itself. */
24
- function accountSettings(href) {
25
- if (typeof window === 'undefined')
26
- return false;
27
- try {
28
- return new URL(href, window.location.origin).origin !== window.location.origin;
29
- }
30
- catch {
31
- return false;
32
- }
33
- }
34
23
  export function Account({ open, onOpenChange }) {
35
24
  const host = useHost();
36
25
  const who = host.person;
37
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: [accountSettings(host.links.settings) ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.open(host.links.settings), children: [_jsx(Settings, { size: 14 }), " Settings"] })) : null, host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null] })] }) }));
26
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 360, children: [_jsx(DialogTitle, { children: "Account" }), _jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", numberOfLines: 1, children: who?.name || who?.email || 'Signed out' }), who?.email && who.name ? (_jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })) : null, host.org ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["Organization: ", host.org] })) : null] }), _jsx(XStack, { gap: "$2", justify: "flex-end", flexWrap: "wrap", children: host.signOut ? (_jsxs(Button, { variant: "outline", size: "sm", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Sign out"] })) : null })] }) }));
38
27
  }
39
28
  export function Find({ open, onOpenChange, recents, onOpen, }) {
40
29
  const [q, setQ] = useState('');
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The fleet's native MCP servers, as the MCP server itself lists them.
3
+ *
4
+ * POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
5
+ *
6
+ * One tool is one native server. Its operations are the names in `op`. A run
7
+ * starts a server the first time it calls it; the server stays up afterwards.
8
+ * `describe` is how a caller reads one operation's schema, so it is not a server.
9
+ */
10
+ import { type Target } from './call.ts';
11
+ export interface Native {
12
+ name: string;
13
+ description: string;
14
+ ops: string[];
15
+ }
16
+ /** The servers in one tools/list answer. A missing tool array is a refusal. */
17
+ export declare function nativesOf(body: unknown): Native[];
18
+ /** Every native server the fleet MCP server is serving. */
19
+ export declare function natives(t: Target): Promise<Native[]>;
package/lib/api/mcp.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The fleet's native MCP servers, as the MCP server itself lists them.
3
+ *
4
+ * POST /v1/mcp {"method":"tools/list"} → {result:{tools:[…]}}
5
+ *
6
+ * One tool is one native server. Its operations are the names in `op`. A run
7
+ * starts a server the first time it calls it; the server stays up afterwards.
8
+ * `describe` is how a caller reads one operation's schema, so it is not a server.
9
+ */
10
+ import { call, Refusal } from './call.js';
11
+ const words = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x !== '') : []);
12
+ /** The servers in one tools/list answer. A missing tool array is a refusal. */
13
+ export function nativesOf(body) {
14
+ const root = body && typeof body === 'object' ? body : {};
15
+ const err = root.error;
16
+ if (err && typeof err === 'object') {
17
+ const message = err.message;
18
+ throw new Refusal(200, typeof message === 'string' && message ? message : 'The MCP server refused the list');
19
+ }
20
+ const result = root.result && typeof root.result === 'object' ? root.result : null;
21
+ const tools = result && Array.isArray(result.tools) ? result.tools : null;
22
+ if (!tools)
23
+ throw new Refusal(502, 'The MCP server did not list its tools');
24
+ const out = [];
25
+ for (const raw of tools) {
26
+ if (!raw || typeof raw !== 'object')
27
+ continue;
28
+ const t = raw;
29
+ const name = typeof t.name === 'string' ? t.name : '';
30
+ if (!name || name === 'describe')
31
+ continue;
32
+ const schema = t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : {};
33
+ const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : {};
34
+ const op = props.op && typeof props.op === 'object' ? props.op : {};
35
+ out.push({
36
+ name,
37
+ description: typeof t.description === 'string' ? t.description : '',
38
+ ops: words(op.enum),
39
+ });
40
+ }
41
+ return out;
42
+ }
43
+ /** Every native server the fleet MCP server is serving. */
44
+ export async function natives(t) {
45
+ return nativesOf(await call(t, 'POST', '/v1/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' }));
46
+ }
package/lib/builder.js CHANGED
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  * /-/issues the forge's issues
12
12
  * /-/artifacts what this org has built
13
13
  * /-/templates the public starters
14
+ * /-/mcp the fleet's native MCP servers
14
15
  * /<slug> a deployed project's workspace, in this same window
15
16
  *
16
17
  * The rail is the sessions rail every Hanzo surface shares: New, the builder's
@@ -41,6 +42,7 @@ import { Run } from './run.js';
41
42
  import { DOTS } from './section.js';
42
43
  import { Artifacts, Templates } from './shelf.js';
43
44
  import { Account, Find, onAccount } from './account.js';
45
+ import { Where } from './switch.js';
44
46
  function Shell() {
45
47
  const host = useHost();
46
48
  const t = useTarget();
@@ -78,7 +80,7 @@ function Shell() {
78
80
  active: screen === 'issues',
79
81
  },
80
82
  { id: 'artifacts', label: 'Artifacts', icon: _jsx(Blocks, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), active: screen === 'artifacts' },
81
- { id: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16 }), onPress: () => host.open(host.links.customize) },
83
+ { id: 'mcp', label: 'MCP', icon: _jsx(Puzzle, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), active: screen === 'mcp' },
82
84
  ];
83
85
  const more = [
84
86
  { id: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16 }), onPress: () => go(path({ kind: 'screen', screen: 'templates' })), active: screen === 'templates' },
@@ -86,37 +88,13 @@ function Shell() {
86
88
  id: 'machines',
87
89
  label: 'Machines',
88
90
  icon: _jsx(Cpu, { size: 16 }),
89
- onPress: () => {
90
- const home = host.links.home.replace(/\/+$/, '');
91
- const computers = `${home}/platform/computers`;
92
- try {
93
- if (new URL(computers, window.location.origin).origin === window.location.origin) {
94
- host.go('');
95
- return;
96
- }
97
- }
98
- catch {
99
- return;
100
- }
101
- host.open(computers);
102
- },
91
+ onPress: () => host.go(''),
103
92
  },
104
93
  { id: 'docs', label: 'Docs', icon: _jsx(BookOpen, { size: 16 }), onPress: () => window.open('https://docs.hanzo.ai/docs/dev', '_blank', 'noopener,noreferrer') },
105
94
  ];
106
95
  return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsxs(YStack, { position: "relative", shrink: 0, children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
107
96
  ? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
108
- : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => {
109
- try {
110
- if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
111
- setAccount(true);
112
- return;
113
- }
114
- }
115
- catch {
116
- return;
117
- }
118
- host.open(host.links.settings);
119
- }, onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : 44, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsxs(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", gap: "$2", render: "button", "aria-label": host.org ? `Organization ${host.org}` : 'Hanzo', onPress: () => go(''), children: [_jsx(Brand, { org: host.org }), _jsx(SizableText, { size: "$2", color: "$ink", numberOfLines: 1, children: host.org || 'Hanzo' })] }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
97
+ : { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => setAccount(true), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(Brand, { org: host.org }), pt: collapsed ? undefined : host.person && (host.org || (host.memberships?.length ?? 0) > 0) ? 80 : 40, open: drawer, onOpenChange: setDrawer, label: "Runs" }), collapsed ? null : (_jsx(XStack, { position: "absolute", t: 8, l: 8, r: 8, z: 2, items: "center", children: host.person && (host.org || (host.memberships?.length ?? 0) > 0) ? (_jsx(Where, {})) : (_jsx(XStack, { render: "button", "aria-label": "Hanzo", items: "center", onPress: () => go(''), children: _jsx(HanzoMark, { size: 18 }) })) }))] }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
120
98
  }
121
99
  /** Whichever pane the address names, beside a rail — the builder's or the host's. */
122
100
  function Pane({ onStarted }) {
package/lib/forge.js CHANGED
@@ -18,6 +18,7 @@ import { boardKey, pinBoard, pinCodebase, readPending, writePending } from './ch
18
18
  import { useKept, useRead } from './data.js';
19
19
  import { useHost, useTarget } from './host.js';
20
20
  import { path } from './route.js';
21
+ import { Servers } from './mcp.js';
21
22
  import { Sync } from './sync.js';
22
23
  import { grants } from './api/github.js';
23
24
  function Head({ title, says }) {
@@ -41,6 +42,8 @@ export function Forge({ screen }) {
41
42
  return _jsx(Automations, {});
42
43
  if (screen === 'sync')
43
44
  return _jsx(Sync, {});
45
+ if (screen === 'mcp')
46
+ return _jsx(Servers, {});
44
47
  if (screen === 'codebases')
45
48
  return _jsx(Codebases, {});
46
49
  if (screen === 'projects')
@@ -170,18 +173,7 @@ function Codebases() {
170
173
  pinCodebase(host.org, c);
171
174
  host.go('');
172
175
  };
173
- return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => {
174
- try {
175
- if (new URL(host.links.settings, window.location.origin).origin === window.location.origin) {
176
- revealAccount();
177
- return;
178
- }
179
- }
180
- catch {
181
- return;
182
- }
183
- host.open(host.links.settings);
184
- }, children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
176
+ return (_jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: [_jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(XStack, { justify: "space-between", items: "flex-start", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$6", fontWeight: "500", color: "$ink", children: host.org || 'Forge' }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Create and browse this organization\u2019s repositories." })] }), _jsxs(XStack, { render: "button", "aria-label": "Settings", items: "center", gap: "$1.5", px: "$2", py: "$1", rounded: "$3", hoverStyle: { bg: '$hover' }, onPress: () => revealAccount(), children: [_jsx(Settings, { size: 14 }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Settings" })] })] }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { px: "$2.5", py: "$1.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$2", color: "$ink", children: "All repos" }) }), _jsx(YStack, { flex: 1 }), _jsx(YStack, { width: 240, minW: 0, children: _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find repo\u2026", "aria-label": "Find a repository", disabled: !signed }) }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'sync' })), children: "Sync" }), _jsxs(Button, { size: "sm", disabled: !signed, onPress: () => setMaking(true), children: [_jsx(Plus, { size: 14 }), "New"] })] }), list.error ? (_jsx(Note, { children: list.error.message })) : !signed ? (_jsx(Note, { children: "Sign in to see this organization's repositories." })) : list.loading && list.value.length === 0 ? (_jsx(Note, { children: "Reading the forge\u2026" })) : shown.length === 0 ? (_jsx(Note, { children: list.value.length === 0 ? 'No repositories yet.' : 'Nothing matches.' })) : (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", items: "center", children: [_jsxs(XStack, { flex: 1, render: "button", "aria-label": "Sort by name", items: "center", gap: "$1", onPress: () => setSort('name'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Name" }), sort === 'name' ? _jsx(ChevronDown, { size: 12 }) : null] }), _jsxs(XStack, { render: "button", "aria-label": "Sort by last updated", items: "center", justify: "flex-end", gap: "$1", width: 140, onPress: () => setSort('updated'), children: [_jsx(SizableText, { size: "$1", color: "$soft", children: "Last updated" }), sort === 'updated' ? _jsx(ChevronDown, { size: 12 }) : null] })] }), slice.map((c) => (_jsxs(XStack, { render: "button", "aria-label": `Work on ${c.name}`, onPress: () => open(c), items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, children: [_jsx(GitBranch, { size: 14 }), _jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: c.name }), _jsx(SizableText, { size: "$1", color: "$soft", width: 140, style: { textAlign: 'right' }, children: syncing.has(c.name) ? 'Syncing…' : when(c.updated) })] }, `${c.org}/${c.name}`))), _jsxs(XStack, { px: "$3", py: "$2", justify: "space-between", items: "center", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: `Showing ${from}–${to} of ${shown.length}` }), _jsxs(XStack, { gap: "$3", items: "center", children: [_jsx(XStack, { render: "button", "aria-label": "Previous page", disabled: safePage === 0, onPress: () => setPage(safePage - 1), children: _jsx(SizableText, { size: "$1", color: safePage === 0 ? '$soft' : '$ink', children: "Prev" }) }), _jsx(SizableText, { size: "$1", color: "$soft", children: `${safePage + 1} / ${pages}` }), _jsx(XStack, { render: "button", "aria-label": "Next page", disabled: safePage >= pages - 1, onPress: () => setPage(safePage + 1), children: _jsx(SizableText, { size: "$1", color: safePage >= pages - 1 ? '$soft' : '$ink', children: "Next" }) })] })] })] }))] }), _jsx(NewRepo, { open: making, onOpenChange: setMaking, onCreate: async (name, description) => {
185
177
  const row = await create(t, name, description);
186
178
  pinCodebase(host.org, row);
187
179
  setMaking(false);
package/lib/host.d.ts CHANGED
@@ -22,6 +22,10 @@ export interface Host {
22
22
  token: () => string | null;
23
23
  /** The org every call is scoped to. */
24
24
  org: string | null;
25
+ /** The organizations this person belongs to. The switcher lists these and no others. */
26
+ memberships?: readonly string[];
27
+ /** Stay on this page and scope every read to `org`. */
28
+ chooseOrg?: (org: string) => void;
25
29
  /** Who is signed in, or null. */
26
30
  person: Person | null;
27
31
  /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
package/lib/mcp.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function Servers(): import("react").JSX.Element;
package/lib/mcp.js ADDED
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The fleet's native MCP servers.
4
+ *
5
+ * The list is the MCP server's own tools/list. A server starts when a run
6
+ * calls it and stays up afterwards, so this pane lists them rather than
7
+ * starting them.
8
+ */
9
+ import { SizableText, XStack, YStack } from '@hanzo/gui';
10
+ import { ChevronDown } from '@hanzogui/lucide-icons-2';
11
+ import { Input } from '@hanzo/ui';
12
+ import { useState } from 'react';
13
+ import { natives } from './api/mcp.js';
14
+ import { useRead } from './data.js';
15
+ import { useTarget } from './host.js';
16
+ export function Servers() {
17
+ const t = useTarget();
18
+ const list = useRead(() => natives(t), [], [t]);
19
+ const [q, setQ] = useState('');
20
+ const [open, setOpen] = useState(null);
21
+ const needle = q.trim().toLowerCase();
22
+ const shown = list.value.filter((s) => !needle || s.name.toLowerCase().includes(needle) || s.ops.some((op) => op.toLowerCase().includes(needle)));
23
+ const ops = shown.reduce((n, s) => n + s.ops.length, 0);
24
+ return (_jsx(YStack, { flex: 1, minH: 0, overflow: "scroll", px: "$6", py: "$6", children: _jsxs(YStack, { width: "100%", maxW: 1040, mx: "auto", gap: "$4", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { render: "h1", size: "$6", color: "$ink", children: "MCP" }), _jsx(SizableText, { size: "$2", color: "$soft", children: "Native servers. A run starts one when it calls it, and the server stays up afterwards." })] }), _jsx(Input, { value: q, onChangeText: setQ, placeholder: "Find a server or an operation\u2026", "aria-label": "Find a server" }), list.loading && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: "Reading the servers\u2026" })) : list.error && list.value.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.error.message })) : shown.length === 0 ? (_jsx(SizableText, { size: "$2", color: "$soft", children: list.value.length === 0 ? 'The MCP server listed no native servers.' : 'No server matches.' })) : (_jsxs(YStack, { gap: "$1", children: [_jsxs(SizableText, { size: "$1", color: "$soft", children: [shown.length, " servers \u00B7 ", ops, " operations"] }), shown.map((s) => {
25
+ const on = open === s.name;
26
+ return (_jsxs(YStack, { borderWidth: 1, borderColor: "$borderColor", rounded: "$3", overflow: "hidden", children: [_jsxs(XStack, { render: "button", "aria-expanded": on, "aria-label": s.name, items: "center", gap: "$3", px: "$3", py: "$2.5", hoverStyle: { bg: '$hover' }, onPress: () => setOpen(on ? null : s.name), children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: s.name }), s.description ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: on ? undefined : 2, children: s.description })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: s.ops.length }), _jsx(ChevronDown, { size: 14, style: { transform: on ? 'rotate(180deg)' : undefined } })] }), on ? (_jsx(YStack, { px: "$3", py: "$2", gap: "$1", borderTopWidth: 1, borderColor: "$borderColor", children: s.ops.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "This server named no operations." })) : (s.ops.map((op) => (_jsx(SizableText, { size: "$1", color: "$ink", children: op }, op)))) })) : null] }, s.name));
27
+ })] }))] }) }));
28
+ }
package/lib/route.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * so it can hold neither the `_` a session id carries nor start with `-`.
11
11
  * Anything else is not an address here and reads as the empty state.
12
12
  */
13
- export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync';
13
+ export type Screen = 'artifacts' | 'templates' | 'codebases' | 'projects' | 'issues' | 'automations' | 'sync' | 'mcp';
14
14
  export type Route = {
15
15
  kind: 'new';
16
16
  } | {
package/lib/route.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const SESSION = /^sess_[0-9a-f]{32}$/;
2
2
  export const SLUG = /^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/;
3
- const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync'];
3
+ const SCREENS = ['artifacts', 'templates', 'codebases', 'projects', 'issues', 'automations', 'sync', 'mcp'];
4
4
  export function route(path) {
5
5
  const p = path.replace(/^\/+|\/+$/g, '');
6
6
  if (SESSION.test(p))
package/lib/section.js CHANGED
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * host's own `go`.
12
12
  */
13
13
  import { SizableText } from '@hanzo/gui';
14
- import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
14
+ import { Blocks, CircleDot, FolderGit2, Kanban, LayoutTemplate, Puzzle, SquarePen, Workflow } from '@hanzogui/lucide-icons-2';
15
15
  import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
16
16
  import { useMemo } from 'react';
17
17
  import { pinBoard } from './choice.js';
@@ -48,5 +48,5 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
48
48
  return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Workflow, { size: 16, "aria-hidden": true }), active: screen === 'automations', onPress: () => go(path({ kind: 'screen', screen: 'automations' })), children: "Automations" }), _jsx(SidebarItem, { icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), active: screen === 'codebases', onPress: () => go(path({ kind: 'screen', screen: 'codebases' })), children: "Codebase" }), _jsx(SidebarItem, { icon: _jsx(Kanban, { size: 16, "aria-hidden": true }), active: screen === 'projects', onPress: () => go(path({ kind: 'screen', screen: 'projects' })), children: "Projects" }), _jsx(SidebarItem, { icon: _jsx(CircleDot, { size: 16, "aria-hidden": true }), active: screen === 'issues', onPress: () => {
49
49
  pinBoard(host.org, '');
50
50
  go(path({ kind: 'screen', screen: 'issues' }));
51
- }, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
51
+ }, children: "Issues" }), _jsx(SidebarItem, { icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), active: screen === 'mcp', onPress: () => go(path({ kind: 'screen', screen: 'mcp' })), children: "MCP" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
52
52
  }
@@ -0,0 +1 @@
1
+ export declare function Where(): import("react").JSX.Element;
package/lib/switch.js ADDED
@@ -0,0 +1,40 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The rail's top left: the organization, and the projects under it.
4
+ *
5
+ * The account stays at the foot of the rail. Settings opens that account on
6
+ * this page. Choosing an organization reloads this origin so every read is
7
+ * scoped to it.
8
+ */
9
+ import { YStack } from '@hanzo/gui';
10
+ import { MenuLabel, MenuRow, MenuRule, OrgSwitcher } from '@hanzo/ui/product';
11
+ import { revealAccount } from './account.js';
12
+ import { projects } from './api/projects.js';
13
+ import { useRead } from './data.js';
14
+ import { useHost, useTarget } from './host.js';
15
+ import { route } from './route.js';
16
+ export function Where() {
17
+ const host = useHost();
18
+ const t = useTarget();
19
+ const signed = Boolean(host.person);
20
+ const list = useRead(signed ? () => projects(t) : null, [], [t, signed]);
21
+ const here = route(host.path);
22
+ const current = host.org || '';
23
+ const scope = {
24
+ currentOrg: () => current,
25
+ setCurrentOrg: (next) => host.chooseOrg?.(next),
26
+ isScopedAway: () => false,
27
+ hasSelectedOrg: () => Boolean(current),
28
+ enterOrg: (next) => host.chooseOrg?.(next),
29
+ leaveOrg: () => undefined,
30
+ switchOrg: (next) => host.chooseOrg?.(next),
31
+ };
32
+ const rows = () => Promise.resolve((host.memberships ?? (current ? [current] : [])).map((name) => ({ name, displayName: name })));
33
+ return (_jsx(OrgSwitcher, { scope: scope, orgs: (page) => (page === 0 ? rows() : Promise.resolve([])), pageSize: 50, search: false, heading: "Organization", lead: true, sub: here.kind === 'project' ? here.slug : undefined, current: current ? { name: current, displayName: current } : undefined, aria: current ? `Organization ${current}` : 'Organization', footer: (close) => (_jsxs(YStack, { gap: "$1", children: [list.value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Project" }), _jsx(YStack, { role: "radiogroup", "aria-label": "Projects", gap: "$1", children: list.value.slice(0, 12).map((p) => (_jsx(MenuRow, { label: p.name || p.slug, active: here.kind === 'project' && here.slug === p.slug, onPress: () => {
34
+ close();
35
+ host.go(p.slug);
36
+ } }, p.slug))) })] })) : null, _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Settings", onPress: () => {
37
+ close();
38
+ revealAccount();
39
+ } })] })) }));
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a page (hanzo.build) and as a component (hanzo.ai/dev).",