@hanzo/build 0.2.0

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 (57) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/NOTICE +37 -0
  4. package/README.md +120 -0
  5. package/lib/account.d.ts +11 -0
  6. package/lib/account.js +26 -0
  7. package/lib/api/call.d.ts +38 -0
  8. package/lib/api/call.js +75 -0
  9. package/lib/api/coding.d.ts +56 -0
  10. package/lib/api/coding.js +58 -0
  11. package/lib/api/git.d.ts +27 -0
  12. package/lib/api/git.js +39 -0
  13. package/lib/api/github.d.ts +78 -0
  14. package/lib/api/github.js +73 -0
  15. package/lib/api/models.d.ts +17 -0
  16. package/lib/api/models.js +28 -0
  17. package/lib/api/places.d.ts +25 -0
  18. package/lib/api/places.js +31 -0
  19. package/lib/api/platform.d.ts +59 -0
  20. package/lib/api/platform.js +69 -0
  21. package/lib/api/projects.d.ts +52 -0
  22. package/lib/api/projects.js +97 -0
  23. package/lib/api/sessions.d.ts +74 -0
  24. package/lib/api/sessions.js +120 -0
  25. package/lib/api/sse.d.ts +24 -0
  26. package/lib/api/sse.js +68 -0
  27. package/lib/api/turn.d.ts +51 -0
  28. package/lib/api/turn.js +121 -0
  29. package/lib/api/verdict.d.ts +12 -0
  30. package/lib/api/verdict.js +20 -0
  31. package/lib/builder.d.ts +5 -0
  32. package/lib/builder.js +81 -0
  33. package/lib/data.d.ts +42 -0
  34. package/lib/data.js +178 -0
  35. package/lib/host.d.ts +57 -0
  36. package/lib/host.js +26 -0
  37. package/lib/index.d.ts +11 -0
  38. package/lib/index.js +10 -0
  39. package/lib/landing.d.ts +13 -0
  40. package/lib/landing.js +132 -0
  41. package/lib/out.d.ts +10 -0
  42. package/lib/out.js +4 -0
  43. package/lib/project.d.ts +3 -0
  44. package/lib/project.js +250 -0
  45. package/lib/publish.d.ts +15 -0
  46. package/lib/publish.js +94 -0
  47. package/lib/route.d.ts +30 -0
  48. package/lib/route.js +28 -0
  49. package/lib/run.d.ts +3 -0
  50. package/lib/run.js +67 -0
  51. package/lib/shelf.d.ts +2 -0
  52. package/lib/shelf.js +60 -0
  53. package/lib/tools.d.ts +25 -0
  54. package/lib/tools.js +64 -0
  55. package/lib/voice.d.ts +17 -0
  56. package/lib/voice.js +86 -0
  57. package/package.json +70 -0
package/lib/tools.js ADDED
@@ -0,0 +1,64 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The composer's quiet controls, once, for every composer the builder draws:
4
+ * attach files (which ride the prompt as text — nothing is uploaded anywhere
5
+ * else) and dictate (recorded here, transcribed by the platform).
6
+ */
7
+ import { SizableText, XStack } from '@hanzo/gui';
8
+ import { ChevronDown, Mic, MicOff, Paperclip, Plus, X } from '@hanzogui/lucide-icons-2';
9
+ import { ComposerTool } from '@hanzo/ui/chat';
10
+ import { ChipSelect } from '@hanzo/ui/product';
11
+ import { useRef } from 'react';
12
+ import { useTarget } from './host.js';
13
+ import { useDictation } from './voice.js';
14
+ const EACH = 100_000;
15
+ const ALL = 200_000;
16
+ /** The ask with its attachments appended as fenced text. */
17
+ export function compose(prompt, files) {
18
+ if (!files.length)
19
+ return prompt;
20
+ return prompt + files.map((f) => `\n\n\`${f.name}\`:\n\`\`\`\n${f.text}\n\`\`\``).join('');
21
+ }
22
+ /** Read `list` into `have`, within the caps; answers the new list and what was refused. */
23
+ export async function read(have, list) {
24
+ const files = [...have];
25
+ let total = files.reduce((n, f) => n + f.text.length, 0);
26
+ let note = '';
27
+ for (const file of Array.from(list)) {
28
+ if (file.size > EACH) {
29
+ note = `${file.name} is over 100 KB; attach a smaller file or point the run at the repository.`;
30
+ continue;
31
+ }
32
+ const text = await file.text();
33
+ if (total + text.length > ALL) {
34
+ note = 'Attachments are capped at 200 KB in all.';
35
+ break;
36
+ }
37
+ total += text.length;
38
+ files.push({ name: file.name, text });
39
+ }
40
+ return { files, note };
41
+ }
42
+ export function Attach({ files, onFiles, onNote }) {
43
+ const picker = useRef(null);
44
+ return (_jsxs(_Fragment, { children: [_jsx(ComposerTool, { label: "Attach files", icon: _jsx(Plus, { size: 14 }), onPress: () => picker.current?.click() }), _jsx("input", { ref: picker, type: "file", multiple: true, hidden: true,
45
+ // `hidden` alone loses to a reset that sets `display` on inputs.
46
+ style: { display: 'none' }, tabIndex: -1, "aria-hidden": true, onChange: (e) => {
47
+ const input = e.currentTarget;
48
+ if (!input.files)
49
+ return;
50
+ void read(files, input.files).then(({ files: next, note }) => {
51
+ onFiles(next);
52
+ onNote(note);
53
+ input.value = '';
54
+ });
55
+ } })] }));
56
+ }
57
+ /** The attached files as small removable marks. */
58
+ export function Files({ files, onFiles }) {
59
+ return (_jsx(_Fragment, { children: files.map((f) => (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", height: 24, rounded: "$2", bg: "$raised", children: [_jsx(Paperclip, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", numberOfLines: 1, maxW: 140, children: f.name }), _jsx(ComposerTool, { label: `Remove ${f.name}`, icon: _jsx(X, { size: 12 }), onPress: () => onFiles(files.filter((x) => x !== f)) })] }, f.name))) }));
60
+ }
61
+ export function Dictate({ onText, onNote }) {
62
+ const voice = useDictation(useTarget(), onText, onNote);
63
+ return (_jsxs(XStack, { items: "center", children: [_jsx(ComposerTool, { label: !voice.able ? 'Dictation needs a microphone this page can record' : voice.on ? 'Stop and transcribe' : voice.busy ? 'Transcribing…' : 'Dictate', icon: voice.on ? _jsx(MicOff, { size: 14 }) : _jsx(Mic, { size: 14 }), onPress: () => void voice.toggle(), disabled: !voice.able || voice.busy, "aria-pressed": voice.on }), _jsx(ChipSelect, { quiet: true, name: "Dictation language", icon: _jsx(ChevronDown, { size: 12 }), label: "", chosen: voice.languages.find((l) => l.id === voice.language) ?? null, items: voice.languages, onChange: (l) => voice.setLanguage(l.id), placeholder: "Search languages\u2026", disabled: !voice.able })] }));
64
+ }
package/lib/voice.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Target } from './api/call.ts';
2
+ export declare const LANGUAGES: {
3
+ id: string;
4
+ label: string;
5
+ }[];
6
+ export declare function useDictation(t: Target, onText: (text: string) => void, onNote: (note: string) => void): {
7
+ on: boolean;
8
+ busy: boolean;
9
+ able: boolean;
10
+ toggle: () => Promise<void>;
11
+ language: string;
12
+ setLanguage: import("react").Dispatch<import("react").SetStateAction<string>>;
13
+ languages: {
14
+ id: string;
15
+ label: string;
16
+ }[];
17
+ };
package/lib/voice.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Dictation into the draft, heard by the platform.
3
+ *
4
+ * The microphone is recorded in this browser and the recording is transcribed
5
+ * by `POST /v1/audio/transcriptions` on the platform, through `@hanzo/voice` —
6
+ * never by a browser's built-in recognizer, which ships the audio to its
7
+ * vendor. Press to record, press again to stop; the words land in the draft.
8
+ * Where there is no microphone or no recorder, `able` is false and the control
9
+ * says so rather than recording into nothing.
10
+ */
11
+ import { speech } from '@hanzo/voice';
12
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
+ export const LANGUAGES = [
14
+ { id: 'en', label: 'English' },
15
+ { id: 'es', label: 'Español' },
16
+ { id: 'fr', label: 'Français' },
17
+ { id: 'de', label: 'Deutsch' },
18
+ { id: 'ja', label: '日本語' },
19
+ { id: 'zh', label: '中文' },
20
+ ];
21
+ /** Whether this browser can record the microphone at all. */
22
+ function recordable() {
23
+ if (typeof window === 'undefined' || typeof navigator === 'undefined')
24
+ return false;
25
+ return typeof window.MediaRecorder === 'function' && Boolean(navigator.mediaDevices?.getUserMedia);
26
+ }
27
+ export function useDictation(t, onText, onNote) {
28
+ const [on, setOn] = useState(false);
29
+ const [busy, setBusy] = useState(false);
30
+ const [able, setAble] = useState(false);
31
+ const [language, setLanguage] = useState(() => {
32
+ const nav = typeof navigator !== 'undefined' ? navigator.language.split('-')[0] : 'en';
33
+ return LANGUAGES.some((l) => l.id === nav) ? nav : 'en';
34
+ });
35
+ const rec = useRef(null);
36
+ const said = useRef(onText);
37
+ said.current = onText;
38
+ const note = useRef(onNote);
39
+ note.current = onNote;
40
+ const ear = useMemo(() => speech({ baseUrl: t.api, token: t.token, ear: 'whisper' }), [t]);
41
+ useEffect(() => setAble(recordable()), []);
42
+ const toggle = useCallback(async () => {
43
+ if (rec.current) {
44
+ rec.current.stop();
45
+ return;
46
+ }
47
+ let stream;
48
+ try {
49
+ stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
50
+ }
51
+ catch {
52
+ note.current('The microphone is not available to this page.');
53
+ return;
54
+ }
55
+ const chunks = [];
56
+ const r = new MediaRecorder(stream);
57
+ r.ondataavailable = (e) => {
58
+ if (e.data.size)
59
+ chunks.push(e.data);
60
+ };
61
+ r.onstop = () => {
62
+ rec.current = null;
63
+ setOn(false);
64
+ for (const track of stream.getTracks())
65
+ track.stop();
66
+ const audio = new Blob(chunks, { type: r.mimeType || 'audio/webm' });
67
+ if (!audio.size)
68
+ return;
69
+ setBusy(true);
70
+ ear
71
+ .transcribe(audio, { language })
72
+ .then((text) => {
73
+ const words = text.trim();
74
+ if (words)
75
+ said.current(words);
76
+ })
77
+ .catch((e) => note.current(e instanceof Error ? e.message : 'The platform could not transcribe that.'))
78
+ .finally(() => setBusy(false));
79
+ };
80
+ rec.current = r;
81
+ r.start();
82
+ setOn(true);
83
+ }, [ear, language]);
84
+ useEffect(() => () => rec.current?.stop(), []);
85
+ return { on, busy, able, toggle, language, setLanguage, languages: LANGUAGES };
86
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@hanzo/build",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0 OR MIT",
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 (platform.hanzo.ai/dev).",
7
+ "engines": {
8
+ "node": ">=22.18"
9
+ },
10
+ "devDependencies": {
11
+ "@hanzo/font": "1.9.3",
12
+ "@hanzo/gui": "8.3.5",
13
+ "@hanzo/iam": "0.21.18",
14
+ "@hanzo/ui": "8.27.16",
15
+ "@hanzo/vite": "0.1.4",
16
+ "@hanzo/voice": "0.1.8",
17
+ "@hanzogui/lucide-icons-2": "8.3.5",
18
+ "@hanzogui/next-theme": "8.3.5",
19
+ "@types/node": "26.5.0",
20
+ "@types/react": "19.2.18",
21
+ "@types/react-dom": "19.2.7",
22
+ "@vitejs/plugin-react": "^6.1.1",
23
+ "next-themes": "^0.4.6",
24
+ "react": "19.2.8",
25
+ "react-dom": "19.2.8",
26
+ "react-hook-form": "^7.87.0",
27
+ "react-native-web": "^0.21.2",
28
+ "react-router": "^8.3.1",
29
+ "typescript": "7.0.2",
30
+ "vite": "^8.2.2",
31
+ "vitest": "^4.1.11"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./lib/index.d.ts",
36
+ "import": "./lib/index.js",
37
+ "default": "./lib/index.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "lib",
43
+ "LICENSE-APACHE",
44
+ "LICENSE-MIT",
45
+ "NOTICE"
46
+ ],
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/hanzoai/build.git"
54
+ },
55
+ "peerDependencies": {
56
+ "@hanzo/gui": ">=8.3.5",
57
+ "@hanzo/ui": ">=8.27.16",
58
+ "@hanzogui/lucide-icons-2": ">=8.3.5",
59
+ "react": ">=19.2.8",
60
+ "@hanzo/voice": ">=0.1.8"
61
+ },
62
+ "scripts": {
63
+ "dev": "vite",
64
+ "build": "tsc --noEmit && vite build",
65
+ "build:lib": "rm -rf lib && tsc -p tsconfig.lib.json",
66
+ "typecheck": "tsc --noEmit",
67
+ "preview": "vite preview",
68
+ "test": "vitest run"
69
+ }
70
+ }