@hanzo/build 0.2.80 → 0.2.81

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 (165) hide show
  1. package/README.md +13 -0
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +1 -20
  5. package/lib/api/agents.js +16 -8
  6. package/lib/api/auto.d.ts +3 -23
  7. package/lib/api/auto.js +39 -22
  8. package/lib/api/billing.d.ts +0 -35
  9. package/lib/api/billing.js +6 -5
  10. package/lib/api/call.d.ts +16 -8
  11. package/lib/api/call.js +32 -1
  12. package/lib/api/capabilities.d.ts +0 -11
  13. package/lib/api/capabilities.js +2 -1
  14. package/lib/api/codebases.d.ts +0 -10
  15. package/lib/api/codebases.js +3 -2
  16. package/lib/api/coding.d.ts +3 -32
  17. package/lib/api/coding.js +13 -7
  18. package/lib/api/connectors.d.ts +1 -17
  19. package/lib/api/connectors.js +5 -4
  20. package/lib/api/environment.d.ts +6 -20
  21. package/lib/api/environment.js +22 -19
  22. package/lib/api/github.d.ts +0 -22
  23. package/lib/api/github.js +3 -2
  24. package/lib/api/keys.d.ts +0 -14
  25. package/lib/api/keys.js +4 -3
  26. package/lib/api/library.d.ts +3 -17
  27. package/lib/api/library.js +23 -22
  28. package/lib/api/machines.d.ts +0 -15
  29. package/lib/api/machines.js +6 -5
  30. package/lib/api/mcp.d.ts +0 -9
  31. package/lib/api/mcp.js +3 -2
  32. package/lib/api/members.d.ts +0 -15
  33. package/lib/api/members.js +4 -3
  34. package/lib/api/memory.d.ts +0 -11
  35. package/lib/api/memory.js +2 -1
  36. package/lib/api/places.d.ts +1 -9
  37. package/lib/api/places.js +12 -1
  38. package/lib/api/plugins.d.ts +1 -16
  39. package/lib/api/plugins.js +4 -3
  40. package/lib/api/profile.d.ts +0 -10
  41. package/lib/api/profile.js +5 -4
  42. package/lib/api/projects.d.ts +0 -14
  43. package/lib/api/projects.js +2 -1
  44. package/lib/api/provider.d.ts +2 -14
  45. package/lib/api/provider.js +12 -10
  46. package/lib/api/sandbox.d.ts +0 -17
  47. package/lib/api/sandbox.js +5 -4
  48. package/lib/api/sessions.d.ts +0 -17
  49. package/lib/api/sessions.js +5 -4
  50. package/lib/api/skills.d.ts +1 -15
  51. package/lib/api/skills.js +5 -4
  52. package/lib/api/studio.d.ts +0 -14
  53. package/lib/api/studio.js +6 -5
  54. package/lib/api/turn.d.ts +6 -28
  55. package/lib/api/turn.js +105 -49
  56. package/lib/api/verdict.d.ts +0 -9
  57. package/lib/api/verdict.js +2 -1
  58. package/lib/api/webhooks.d.ts +1 -14
  59. package/lib/api/webhooks.js +5 -4
  60. package/lib/ask.js +7 -4
  61. package/lib/automations.d.ts +5 -4
  62. package/lib/automations.js +95 -74
  63. package/lib/back.js +10 -4
  64. package/lib/builder.js +7 -5
  65. package/lib/carry.js +20 -16
  66. package/lib/customize/agents.js +78 -31
  67. package/lib/customize/connectors.js +62 -48
  68. package/lib/customize/index.js +43 -10
  69. package/lib/customize/plugins.js +34 -22
  70. package/lib/customize/say.d.ts +13 -1
  71. package/lib/customize/say.js +15 -21
  72. package/lib/customize/skills.js +31 -22
  73. package/lib/customize/ui.d.ts +5 -9
  74. package/lib/customize/ui.js +26 -16
  75. package/lib/data.d.ts +1 -1
  76. package/lib/data.js +3 -2
  77. package/lib/desk.js +45 -33
  78. package/lib/door.js +35 -10
  79. package/lib/environment.js +25 -19
  80. package/lib/files.js +43 -34
  81. package/lib/find.js +12 -9
  82. package/lib/foot.js +7 -4
  83. package/lib/forge.js +61 -40
  84. package/lib/gear.d.ts +2 -1
  85. package/lib/gear.js +6 -4
  86. package/lib/git.js +29 -19
  87. package/lib/landing.d.ts +5 -5
  88. package/lib/landing.js +52 -44
  89. package/lib/library.js +73 -51
  90. package/lib/meter.d.ts +1 -1
  91. package/lib/meter.js +14 -5
  92. package/lib/mic.js +12 -5
  93. package/lib/mind.d.ts +1 -0
  94. package/lib/mind.js +5 -3
  95. package/lib/nav.d.ts +6 -5
  96. package/lib/nav.js +37 -30
  97. package/lib/panel.d.ts +2 -1
  98. package/lib/panel.js +52 -36
  99. package/lib/plan.d.ts +4 -20
  100. package/lib/plan.js +26 -10
  101. package/lib/plans.js +49 -30
  102. package/lib/plugins/design.js +3 -2
  103. package/lib/plugins/index.d.ts +23 -4
  104. package/lib/plugins/index.js +11 -26
  105. package/lib/plugins/pdf.js +2 -1
  106. package/lib/plugins/project.js +6 -3
  107. package/lib/plugins/research.js +3 -2
  108. package/lib/plugins/sketch.js +6 -3
  109. package/lib/plugins/slides.js +3 -2
  110. package/lib/plugins/template.js +6 -3
  111. package/lib/plus.js +25 -13
  112. package/lib/prefs.d.ts +0 -14
  113. package/lib/prefs.js +2 -1
  114. package/lib/project.js +69 -62
  115. package/lib/prompt.d.ts +12 -11
  116. package/lib/prompt.js +38 -20
  117. package/lib/publish.js +17 -12
  118. package/lib/reply.js +6 -3
  119. package/lib/run.js +72 -54
  120. package/lib/say.d.ts +2 -0
  121. package/lib/say.js +7 -0
  122. package/lib/scheduled.js +54 -43
  123. package/lib/section.d.ts +1 -0
  124. package/lib/section.js +8 -2
  125. package/lib/settings/account.d.ts +1 -0
  126. package/lib/settings/account.js +31 -21
  127. package/lib/settings/billing.js +55 -28
  128. package/lib/settings/capabilities.js +26 -15
  129. package/lib/settings/card.js +19 -12
  130. package/lib/settings/code.js +22 -11
  131. package/lib/settings/credits.d.ts +4 -4
  132. package/lib/settings/credits.js +34 -21
  133. package/lib/settings/environments.js +21 -14
  134. package/lib/settings/general.js +26 -14
  135. package/lib/settings/index.js +6 -3
  136. package/lib/settings/integrations.js +42 -28
  137. package/lib/settings/keys.js +37 -19
  138. package/lib/settings/list.d.ts +0 -5
  139. package/lib/settings/list.js +24 -15
  140. package/lib/settings/machines.d.ts +3 -0
  141. package/lib/settings/machines.js +63 -38
  142. package/lib/settings/members.js +23 -14
  143. package/lib/settings/memory.js +12 -6
  144. package/lib/settings/notifications.js +32 -19
  145. package/lib/settings/plan.d.ts +1 -1
  146. package/lib/settings/plan.js +40 -17
  147. package/lib/settings/privacy.js +26 -13
  148. package/lib/settings/ui.d.ts +3 -2
  149. package/lib/settings/ui.js +9 -3
  150. package/lib/settings/usage.js +7 -3
  151. package/lib/shelf.js +18 -15
  152. package/lib/side.d.ts +1 -26
  153. package/lib/side.js +11 -5
  154. package/lib/standing.d.ts +2 -10
  155. package/lib/standing.js +5 -3
  156. package/lib/suggest.d.ts +8 -28
  157. package/lib/suggest.js +39 -4
  158. package/lib/suggestions.js +14 -5
  159. package/lib/summary.d.ts +1 -1
  160. package/lib/summary.js +4 -1
  161. package/lib/sync.js +22 -12
  162. package/lib/tools.d.ts +4 -3
  163. package/lib/tools.js +10 -7
  164. package/lib/transcript.js +10 -8
  165. package/package.json +4 -2
package/lib/api/coding.js CHANGED
@@ -28,11 +28,17 @@
28
28
  * A sandbox run keeps its sandbox after it ends, so `after` continues the earlier
29
29
  * run in that same sandbox and working tree, resumed if it parked.
30
30
  */
31
+ import { phrase } from '@hanzo/i18n';
32
+ import { english } from '../say.js';
31
33
  import { call, headers, reason, Refusal, seg } from './call.js';
32
- /** Why this mode cannot run where it is sent, or '' when it can. */
33
- export const unhonoured = (mode, target) => (mode === 'plan' || mode === 'setup') && target?.trim()
34
- ? `A ${mode} runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Cloud, or switch to Build.`
35
- : '';
34
+ /** Why this mode cannot run where it is sent, in the words of the `t` handed (English without one), or '' when it can. */
35
+ export const unhonoured = (mode, target, t = english) => !target?.trim()
36
+ ? ''
37
+ : mode === 'plan'
38
+ ? t('A plan runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Cloud, or switch to Build.')
39
+ : mode === 'setup'
40
+ ? t('A setup runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Cloud, or switch to Build.')
41
+ : '';
36
42
  /**
37
43
  * The request body: only what was asked, with no empty strings sent as values.
38
44
  * A run in the sandbox asks for one with a desktop, so its Desktop tab has a
@@ -53,14 +59,14 @@ export function body(ask) {
53
59
  }
54
60
  export async function start(t, ask) {
55
61
  if (!ask.prompt.trim())
56
- throw new Refusal(400, 'Say what the run should do');
62
+ throw new Refusal(400, phrase('Say what the run should do'));
57
63
  const why = unhonoured(ask.mode, ask.targetId);
58
64
  if (why)
59
65
  throw new Refusal(400, why);
60
66
  const r = await call(t, 'POST', '/v1/agent/coding', body(ask));
61
67
  const s = (k) => (typeof r?.[k] === 'string' ? r[k] : '');
62
68
  if (!s('sessionId'))
63
- throw new Refusal(502, 'The run was admitted but named no session');
69
+ throw new Refusal(502, phrase('The run was admitted but named no session'));
64
70
  return {
65
71
  session: s('sessionId'),
66
72
  repo: s('repo'),
@@ -154,7 +160,7 @@ export async function artifacts(t, session) {
154
160
  export async function artifact(t, session, name) {
155
161
  const parts = name.split('/');
156
162
  if (parts.some((p) => p === '' || p === '.' || p === '..'))
157
- throw new Refusal(400, 'That artifact has no name to fetch it by');
163
+ throw new Refusal(400, phrase('That artifact has no name to fetch it by'));
158
164
  const res = await fetch(`${t.api}/v1/agent/coding/${seg(session)}/artifacts/${parts.map(seg).join('/')}`, { headers: headers(t), cache: 'no-store' });
159
165
  if (!res.ok)
160
166
  throw new Refusal(res.status, (await reason(res)) || `The artifact answered ${res.status}`);
@@ -1,19 +1,3 @@
1
- /**
2
- * Connectors: the MCP servers an org adds, and the shelf it picks them from.
3
- *
4
- * GET /v1/tool/mcp/servers {servers: [server]}
5
- * POST /v1/tool/mcp/servers {name, url} or {listing, name?}, with {authHeader, secret} → 201 server
6
- * DELETE /v1/tool/mcp/servers/{id} 204
7
- * GET /v1/tool/catalog?q=&limit=&offset= {catalog: [listing], total, limit, offset}
8
- * GET /v1/tool/catalog/{id} one listing in full
9
- *
10
- * A server's secret is sealed in KMS by the platform and never answered again;
11
- * a record says only whether it has one. Its tools are GET /v1/tool?source=mcp
12
- * (tools.ts), named `<server id>_<tool>`, and each is called only once it is on.
13
- * A listing can be added here only when it serves streamable HTTP; one that
14
- * ships only a package needs somewhere to run first. The fleet's own servers,
15
- * on for every run, are mcp.ts.
16
- */
17
1
  import { type Target } from './call.ts';
18
2
  export interface Server {
19
3
  /** The server's id in the org, and the prefix of every tool it brings. */
@@ -79,7 +63,7 @@ export interface Adding {
79
63
  header?: string;
80
64
  secret?: string;
81
65
  }
82
- /** Why a server cannot be added as it stands, or '' when it can. */
66
+ /** Why a server cannot be added as it stands, or '' when it can. English, marked (`phrase`): a screen says it through `t`. */
83
67
  export declare function refuse(a: Adding): string;
84
68
  export declare function add(t: Target, a: Adding): Promise<Server>;
85
69
  export declare function remove(t: Target, id: string): Promise<void>;
@@ -14,6 +14,7 @@
14
14
  * ships only a package needs somewhere to run first. The fleet's own servers,
15
15
  * on for every run, are mcp.ts.
16
16
  */
17
+ import { phrase } from '@hanzo/i18n';
17
18
  import { call, query, seg } from './call.js';
18
19
  const str = (v) => (typeof v === 'string' ? v : '');
19
20
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
@@ -67,16 +68,16 @@ export const owns = (s, toolName) => toolName.startsWith(`${s.id}_`);
67
68
  export async function servers(t) {
68
69
  return rows(obj(await call(t, 'GET', '/v1/tool/mcp/servers')).servers).map(server).filter((s) => s.id);
69
70
  }
70
- /** Why a server cannot be added as it stands, or '' when it can. */
71
+ /** Why a server cannot be added as it stands, or '' when it can. English, marked (`phrase`): a screen says it through `t`. */
71
72
  export function refuse(a) {
72
73
  if (!a.listing) {
73
74
  if (!a.name?.trim())
74
- return 'A connector needs a name';
75
+ return phrase('A connector needs a name');
75
76
  if (!/^https?:\/\/[^\s/]+/.test(a.url?.trim() ?? ''))
76
- return 'The URL is an http(s) address';
77
+ return phrase('The URL is an http(s) address');
77
78
  }
78
79
  if (a.secret && !a.header?.trim())
79
- return 'Name the header the secret is sent in';
80
+ return phrase('Name the header the secret is sent in');
80
81
  return '';
81
82
  }
82
83
  export async function add(t, a) {
@@ -1,19 +1,4 @@
1
- /**
2
- * A codebase's environment: what a run does to its checkout before the agent
3
- * starts, and which secrets it exports.
4
- *
5
- * GET /v1/environment {data: [environment]}
6
- * GET /v1/environment/{repo} one; a codebase with none answers state "none"
7
- * PUT /v1/environment/{repo} save {install, start} (org admin)
8
- * DELETE /v1/environment/{repo} forget it and its secrets (org admin)
9
- * PUT /v1/environment/{repo}/secrets/{name} set one value (org admin)
10
- * DELETE /v1/environment/{repo}/secrets/{name} remove one (org admin)
11
- *
12
- * A setup run is a coding run in mode `setup` (coding.ts): its agent explores the
13
- * checkout, installs and checks what it finds, and the platform keeps its answer
14
- * here as `proposal` until someone saves it. Secrets are names only; their values
15
- * are sealed in KMS and reach the run's environment, never this record.
16
- */
1
+ import type { Say } from '@hanzo/i18n/react';
17
2
  import { type Target } from './call.ts';
18
3
  export type State = 'none' | 'proposed' | 'ready';
19
4
  export interface Proposal {
@@ -46,9 +31,10 @@ export declare function environment(raw: unknown, repo?: string): Environment;
46
31
  export declare const unset: (e: Environment) => boolean;
47
32
  /**
48
33
  * What a refused save or setup run says to the person who pressed it: what is
49
- * wrong and what to do, in the reader's words rather than the platform's.
34
+ * wrong and what to do, in the reader's words rather than the platform's, and
35
+ * in the language of the `t` handed (English without one).
50
36
  */
51
- export declare function plain(e: unknown, repo: string, act: 'save' | 'start' | 'run'): string;
37
+ export declare function plain(e: unknown, repo: string, act: 'save' | 'start' | 'run', t?: Say): string;
52
38
  export declare function environments(t: Target): Promise<Environment[]>;
53
39
  export declare function read(t: Target, repo: string): Promise<Environment>;
54
40
  export declare function save(t: Target, repo: string, scripts: {
@@ -58,8 +44,8 @@ export declare function save(t: Target, repo: string, scripts: {
58
44
  export declare function forget(t: Target, repo: string): Promise<void>;
59
45
  /** An environment variable name the run can export, and not one of its own. */
60
46
  export declare const NAME: RegExp;
61
- /** Why a secret name cannot be set, or '' when it can. */
62
- export declare function refuse(name: string): string;
47
+ /** Why a secret name cannot be set, in the words of the `t` handed, or '' when it can. */
48
+ export declare function refuse(name: string, t?: Say): string;
63
49
  export declare function setSecret(t: Target, repo: string, name: string, value: string): Promise<Environment>;
64
50
  export declare function removeSecret(t: Target, repo: string, name: string): Promise<Environment>;
65
51
  /**
@@ -14,7 +14,9 @@
14
14
  * here as `proposal` until someone saves it. Secrets are names only; their values
15
15
  * are sealed in KMS and reach the run's environment, never this record.
16
16
  */
17
- import { call, Refusal, seg } from './call.js';
17
+ import { phrase } from '@hanzo/i18n';
18
+ import { english } from '../say.js';
19
+ import { call, Refusal, seg, told } from './call.js';
18
20
  const str = (v) => (typeof v === 'string' ? v : '');
19
21
  const obj = (v) => (v && typeof v === 'object' ? v : {});
20
22
  const names = (v) => (Array.isArray(v) ? v.filter((n) => typeof n === 'string' && n !== '') : []);
@@ -43,34 +45,35 @@ export function environment(raw, repo = '') {
43
45
  export const unset = (e) => e.state === 'none' && !e.updated;
44
46
  /**
45
47
  * What a refused save or setup run says to the person who pressed it: what is
46
- * wrong and what to do, in the reader's words rather than the platform's.
48
+ * wrong and what to do, in the reader's words rather than the platform's, and
49
+ * in the language of the `t` handed (English without one).
47
50
  */
48
- export function plain(e, repo, act) {
51
+ export function plain(e, repo, act, t = english) {
49
52
  // This page's own refusals (a secret's name, an empty value) are already a reader's words.
50
53
  if (e instanceof Error && !(e instanceof Refusal) && e.name === 'Error' && e.message)
51
- return e.message;
54
+ return told(e, t);
52
55
  if (e instanceof Refusal) {
53
56
  const why = e.message;
54
57
  if (e.status === 401)
55
- return 'Your session ended. Sign in again, then try again.';
58
+ return t('Your session ended. Sign in again, then try again.');
56
59
  if (e.status === 403 && act === 'save')
57
- return 'Only an organization admin saves an environment. Start the agent instead; an admin reviews what it proposes.';
60
+ return t('Only an organization admin saves an environment. Start the agent instead; an admin reviews what it proposes.');
58
61
  if (e.status === 402)
59
- return 'This run needs a plan with runs left. See Plans to change it.';
62
+ return t('This run needs a plan with runs left. See Plans to change it.');
60
63
  if (e.status === 403)
61
- return `This account cannot start a run on ${repo}. Ask an organization admin for access.`;
64
+ return t('This account cannot start a run on {repo}. Ask an organization admin for access.', { repo });
62
65
  if (e.status === 429)
63
- return 'Too many runs are going right now. Try again in a minute.';
66
+ return t('Too many runs are going right now. Try again in a minute.');
64
67
  if (e.status >= 500)
65
- return act === 'save' ? 'The environment store is not answering right now. Try again shortly.' : 'The code workspace is not answering right now. Try again shortly.';
68
+ return act === 'save' ? t('The environment store is not answering right now. Try again shortly.') : t('The code workspace is not answering right now. Try again shortly.');
66
69
  if (/no repository named|not one of this organization|is not a repository/.test(why))
67
- return `${repo} is not a repository this organization has on the forge. Choose one of its repositories.`;
70
+ return t('{repo} is not a repository this organization has on the forge. Choose one of its repositories.', { repo });
68
71
  if (/no account on the forge|verify .* first|no forge identity/.test(why))
69
- return 'Your forge account is not ready yet: sign in to the forge once, then try again.';
72
+ return t('Your forge account is not ready yet: sign in to the forge once, then try again.');
70
73
  }
71
74
  if (act === 'save')
72
- return `The environment for ${repo} could not be saved. Try again.`;
73
- return act === 'start' ? `The setup run on ${repo} could not start. Try again.` : `The run on ${repo} could not start. Try again.`;
75
+ return t('The environment for {repo} could not be saved. Try again.', { repo });
76
+ return act === 'start' ? t('The setup run on {repo} could not start. Try again.', { repo }) : t('The run on {repo} could not start. Try again.', { repo });
74
77
  }
75
78
  export async function environments(t) {
76
79
  const r = await call(t, 'GET', '/v1/environment');
@@ -88,13 +91,13 @@ export async function forget(t, repo) {
88
91
  /** An environment variable name the run can export, and not one of its own. */
89
92
  export const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
90
93
  const RESERVED = ['PATH', 'HOME', 'USER', 'SHELL', 'PWD', 'HOSTNAME', 'IFS', 'ENV', 'BASH_ENV'];
91
- /** Why a secret name cannot be set, or '' when it can. */
92
- export function refuse(name) {
94
+ /** Why a secret name cannot be set, in the words of the `t` handed, or '' when it can. */
95
+ export function refuse(name, t = english) {
93
96
  if (!NAME.test(name))
94
- return `${name || 'That'} is not an environment variable name`;
97
+ return name ? t('{name} is not an environment variable name', { name }) : t('That is not an environment variable name');
95
98
  const up = name.toUpperCase();
96
99
  if (up.startsWith('HANZO_') || RESERVED.includes(up))
97
- return `${name} is reserved for the run itself`;
100
+ return t('{name} is reserved for the run itself', { name });
98
101
  return '';
99
102
  }
100
103
  export async function setSecret(t, repo, name, value) {
@@ -102,7 +105,7 @@ export async function setSecret(t, repo, name, value) {
102
105
  if (why)
103
106
  throw new Error(why);
104
107
  if (!value)
105
- throw new Error('A secret needs a value');
108
+ throw new Error(phrase('A secret needs a value'));
106
109
  return environment(await call(t, 'PUT', `/v1/environment/${seg(repo)}/secrets/${seg(name)}`, { value }), repo);
107
110
  }
108
111
  export async function removeSecret(t, repo, name) {
@@ -1,25 +1,3 @@
1
- /**
2
- * GitHub, as the platform's GitHub App sees it for the signed-in person.
3
- *
4
- * GET /v1/provider/github/repos?q=&owner=&limit=&after= most recently pushed first
5
- * GET /v1/provider/github/repos/{owner}/{repo}/branches?q=&limit=&after= default first
6
- * GET /v1/provider/github/user the person's own connection
7
- * POST /v1/provider/github/user/connect {return} → {authorizeUrl}
8
- * POST /v1/provider/github/user/complete {grant} → the connection
9
- * POST /v1/provider/github/user/disconnect
10
- * GET /v1/provider/github/installations the accounts this org has bound the App on
11
- * POST /v1/provider/github/issues/backfill {state, repo?} → mirror GitHub issues onto the board
12
- *
13
- * Paged by an opaque `after` cursor; `next` is the cursor for the page after
14
- * this one, or empty on the last. Rows are read defensively: a field the
15
- * platform left out is an empty value here, never `undefined` in a label.
16
- *
17
- * `connected` false with repositories is an org admin seeing the org's
18
- * installations; false with none is the cue to connect. GitHub returns a
19
- * connecting person to the page the connect named as its return (a Hanzo app
20
- * page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
21
- * and that page completes it (`complete`, and `landed` in ../back.ts).
22
- */
23
1
  import { type Target } from './call.ts';
24
2
  export interface Repo {
25
3
  owner: string;
package/lib/api/github.js CHANGED
@@ -20,6 +20,7 @@
20
20
  * page; the console's /connectors otherwise) with `?complete=github&grant=<id>`,
21
21
  * and that page completes it (`complete`, and `landed` in ../back.ts).
22
22
  */
23
+ import { phrase } from '@hanzo/i18n';
23
24
  import { call, query, seg } from './call.js';
24
25
  import { codebases } from './codebases.js';
25
26
  import { links } from './links.js';
@@ -79,7 +80,7 @@ export async function connect(t, back) {
79
80
  const raw = obj(await call(t, 'POST', '/v1/provider/github/user/connect', back ? { return: back } : undefined));
80
81
  const url = str(raw.authorizeUrl);
81
82
  if (!/^https:\/\/github\.com\//.test(url))
82
- throw new Error('The platform did not name a GitHub address to connect at');
83
+ throw new Error(phrase('The platform did not name a GitHub address to connect at'));
83
84
  return url;
84
85
  }
85
86
  /** Finish the connect GitHub's return handed this page as `grant`. */
@@ -177,7 +178,7 @@ export async function grants(t, signal) {
177
178
  export async function bring(t, repos) {
178
179
  const names = repos.map((n) => n.trim()).filter(Boolean);
179
180
  if (names.length === 0)
180
- throw new Error('Choose a repository first');
181
+ throw new Error(phrase('Choose a repository first'));
181
182
  const raw = obj(await call(t, 'POST', '/v1/provider/github/repos/import', { repos: names }));
182
183
  const said = Array.isArray(raw.repos) ? raw.repos : [];
183
184
  const made = Array.isArray(raw.codebases) ? raw.codebases : [];
package/lib/api/keys.d.ts CHANGED
@@ -1,17 +1,3 @@
1
- /**
2
- * A person's API keys, each its own credential: as many as they like, of either
3
- * type — secret (sk-, it belongs on a server) or publishable (pk-, safe in a
4
- * page's source).
5
- *
6
- * GET /v1/account/keys {keys: [{id, name, type, prefix, key?, status, limit?, created, …}]}
7
- * POST /v1/account/keys {name, type, limit?} → the key, its secret answered once
8
- * DELETE /v1/account/keys/{id} revoke exactly that one; it stays listed as revoked
9
- *
10
- * Creating a key never touches another. A secret key is answered once and by its
11
- * prefix after; a publishable key is public by construction and always comes back
12
- * whole. A limit only narrows what a key may reach — `model:zen5`,
13
- * `project:acme`, `product:train`, `read:*`.
14
- */
15
1
  import { type Target } from './call.ts';
16
2
  export declare const KINDS: readonly ['secret', 'publishable'];
17
3
  export type Kind = (typeof KINDS)[number];
package/lib/api/keys.js CHANGED
@@ -12,7 +12,8 @@
12
12
  * whole. A limit only narrows what a key may reach — `model:zen5`,
13
13
  * `project:acme`, `product:train`, `read:*`.
14
14
  */
15
- import { call } from './call.js';
15
+ import { phrase } from '@hanzo/i18n';
16
+ import { call, Sentence } from './call.js';
16
17
  export const KINDS = ['secret', 'publishable'];
17
18
  const str = (v) => (typeof v === 'string' ? v : '');
18
19
  const list = (v) => (Array.isArray(v) ? v.filter((s) => typeof s === 'string' && s !== '') : []);
@@ -49,7 +50,7 @@ export function limits(raw) {
49
50
  .filter(Boolean);
50
51
  const bad = out.find((s) => !LIMIT.test(s));
51
52
  if (bad)
52
- throw new Error(`${bad} is not a limit — write it as kind:name, like model:zen5`);
53
+ throw new Sentence(phrase('{limit} is not a limit — write it as kind:name, like model:zen5'), { limit: bad });
53
54
  return out;
54
55
  }
55
56
  /** Create a new key beside the ones held. The answer is the only time a secret key is shown. */
@@ -62,7 +63,7 @@ export async function mint(t, type, name = '', limit = []) {
62
63
  const r = await call(t, 'POST', '/v1/account/keys', body);
63
64
  const key = str(r?.key);
64
65
  if (!key)
65
- throw new Error('The platform answered no key');
66
+ throw new Error(phrase('The platform answered no key'));
66
67
  return { id: str(r?.id), name: str(r?.name), type: kind(r?.type) ?? type, key, limit: list(r?.limit) };
67
68
  }
68
69
  /** Revoke exactly one key, by id. Every other key keeps working. */
@@ -1,21 +1,7 @@
1
- /**
2
- * The Library: everything a person made, in Chat, Dev, Studio and workflows, in
3
- * one index.
4
- *
5
- * GET /v1/library {data: [item], next} newest first, filtered
6
- * GET /v1/library/{id} one item, with fresh signed URLs
7
- * POST /v1/library keep one: its bytes as base64, at most 10 MiB
8
- * PATCH /v1/library/{id} rename, share with the org, move to a project
9
- * DELETE /v1/library/{id} its row and its bytes
10
- *
11
- * An item is its owner's; another member of the org reads it only once its owner
12
- * shares it, and changes it never. Its `url`, `download` and `preview` are
13
- * signed and stop working at `expires`: read the item again for fresh ones.
14
- */
15
1
  import { type Target } from './call.ts';
16
2
  export declare const KIND_IDS: readonly ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
3
  export type Kind = (typeof KIND_IDS)[number];
18
- /** The kinds a reader narrows to, in the order the Library lists them, '' being all. */
4
+ /** The kinds a reader narrows to, in the order the Library lists them, '' being all; each label English and marked (`phrase`), drawn through `t`. */
19
5
  export declare const KINDS: readonly {
20
6
  id: Kind | '';
21
7
  label: string;
@@ -23,7 +9,7 @@ export declare const KINDS: readonly {
23
9
  export declare const SOURCES: readonly ['chat', 'dev', 'studio', 'workflow'];
24
10
  export type Source = (typeof SOURCES)[number];
25
11
  export type Scope = 'all' | 'mine' | 'shared';
26
- /** A product, as a person names it. */
12
+ /** A product, as a person names it: Chat, Dev and Studio are names, and Workflow a word, marked (`phrase`). */
27
13
  export declare const SOURCE: Record<Source, string>;
28
14
  export interface Item {
29
15
  id: string;
@@ -117,7 +103,7 @@ export declare function change(t: Target, id: string, c: Change): Promise<Item>;
117
103
  export declare function remove(t: Target, id: string): Promise<void>;
118
104
  /** A size the way a person reads it. */
119
105
  export declare function size(bytes: number): string;
120
- /** The day an instant falls on, as a section of the Library: Today, Yesterday, then the month. */
106
+ /** The day an instant falls on, as a section of the Library: Today, Yesterday (English, marked), then the month. */
121
107
  export declare function day(iso: string, now?: Date): string;
122
108
  /** Items under the day each was made, in the order given (newest first). */
123
109
  export declare function days(list: Item[], now?: Date): {
@@ -12,23 +12,24 @@
12
12
  * shares it, and changes it never. Its `url`, `download` and `preview` are
13
13
  * signed and stop working at `expires`: read the item again for fresh ones.
14
14
  */
15
- import { call, query, seg } from './call.js';
15
+ import { phrase } from '@hanzo/i18n';
16
+ import { call, query, seg, Sentence } from './call.js';
16
17
  export const KIND_IDS = ['image', 'video', 'audio', 'document', 'slides', 'type', 'model', 'code', 'file'];
17
- /** The kinds a reader narrows to, in the order the Library lists them, '' being all. */
18
+ /** The kinds a reader narrows to, in the order the Library lists them, '' being all; each label English and marked (`phrase`), drawn through `t`. */
18
19
  export const KINDS = [
19
- { id: '', label: 'All' },
20
- { id: 'image', label: 'Images' },
21
- { id: 'video', label: 'Videos' },
22
- { id: 'audio', label: 'Audio' },
23
- { id: 'document', label: 'Documents' },
24
- { id: 'slides', label: 'Slides' },
25
- { id: 'model', label: '3D' },
26
- { id: 'code', label: 'Code' },
27
- { id: 'file', label: 'Files' },
20
+ { id: '', label: phrase('All') },
21
+ { id: 'image', label: phrase('Images') },
22
+ { id: 'video', label: phrase('Videos') },
23
+ { id: 'audio', label: phrase('Audio') },
24
+ { id: 'document', label: phrase('Documents') },
25
+ { id: 'slides', label: phrase('Slides') },
26
+ { id: 'model', label: phrase('3D') },
27
+ { id: 'code', label: phrase('Code') },
28
+ { id: 'file', label: phrase('Files') },
28
29
  ];
29
30
  export const SOURCES = ['chat', 'dev', 'studio', 'workflow'];
30
- /** A product, as a person names it. */
31
- export const SOURCE = { chat: 'Chat', dev: 'Dev', studio: 'Studio', workflow: 'Workflow' };
31
+ /** A product, as a person names it: Chat, Dev and Studio are names, and Workflow a word, marked (`phrase`). */
32
+ export const SOURCE = { chat: 'Chat', dev: 'Dev', studio: 'Studio', workflow: phrase('Workflow') };
32
33
  const str = (v) => (typeof v === 'string' ? v : '');
33
34
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0);
34
35
  const obj = (v) => (v && typeof v === 'object' ? v : {});
@@ -91,7 +92,7 @@ export async function items(t, f = {}, init) {
91
92
  export async function get(t, id) {
92
93
  const x = item(await call(t, 'GET', `/v1/library/${seg(id)}`));
93
94
  if (!x)
94
- throw new Error('The item came back without an id');
95
+ throw new Error(phrase('The item came back without an id'));
95
96
  return x;
96
97
  }
97
98
  /** The largest thing one upload carries. */
@@ -106,9 +107,9 @@ export function base64(bytes) {
106
107
  /** Keep a file in the Library. */
107
108
  export async function upload(t, file, made) {
108
109
  if (!file.bytes.length)
109
- throw new Error(`${file.name} is empty`);
110
+ throw new Sentence(phrase('{name} is empty'), { name: file.name });
110
111
  if (file.bytes.length > INLINE)
111
- throw new Error(`${file.name} is larger than 10 MB`);
112
+ throw new Sentence(phrase('{name} is larger than 10 MB'), { name: file.name });
112
113
  const x = item(await call(t, 'POST', '/v1/library', {
113
114
  name: file.name,
114
115
  mime: file.type || 'application/octet-stream',
@@ -116,13 +117,13 @@ export async function upload(t, file, made) {
116
117
  ...made,
117
118
  }));
118
119
  if (!x)
119
- throw new Error('The item was kept and came back without an id');
120
+ throw new Error(phrase('The item was kept and came back without an id'));
120
121
  return x;
121
122
  }
122
123
  export async function change(t, id, c) {
123
124
  const x = item(await call(t, 'PATCH', `/v1/library/${seg(id)}`, c));
124
125
  if (!x)
125
- throw new Error('The item was changed and came back without an id');
126
+ throw new Error(phrase('The item was changed and came back without an id'));
126
127
  return x;
127
128
  }
128
129
  export async function remove(t, id) {
@@ -141,17 +142,17 @@ export function size(bytes) {
141
142
  }
142
143
  return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[u]}`;
143
144
  }
144
- /** The day an instant falls on, as a section of the Library: Today, Yesterday, then the month. */
145
+ /** The day an instant falls on, as a section of the Library: Today, Yesterday (English, marked), then the month. */
145
146
  export function day(iso, now = new Date()) {
146
147
  const d = new Date(iso);
147
148
  if (Number.isNaN(d.getTime()))
148
- return 'Earlier';
149
+ return phrase('Earlier');
149
150
  const start = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
150
151
  const days = Math.round((start(now) - start(d)) / 86_400_000);
151
152
  if (days <= 0)
152
- return 'Today';
153
+ return phrase('Today');
153
154
  if (days === 1)
154
- return 'Yesterday';
155
+ return phrase('Yesterday');
155
156
  return d.toLocaleDateString(undefined, d.getFullYear() === now.getFullYear() ? { month: 'long' } : { month: 'long', year: 'numeric' });
156
157
  }
157
158
  /** Items under the day each was made, in the order given (newest first). */
@@ -1,18 +1,3 @@
1
- /**
2
- * The org's machines: its own computers a run can be sent to, beside the sandbox.
3
- *
4
- * GET /v1/agent/targets {targets: [...]}, newest first
5
- * POST /v1/agent/targets register one by hand {label, kind, host}
6
- * PATCH /v1/agent/targets/{id} rename it, drain it, bring it back
7
- * DELETE /v1/agent/targets/{id} deregister it
8
- * POST /v1/agent/targets/{id}/key mint or rotate its claim key, answered once
9
- *
10
- * A machine belongs to the member who registered it. Only that member or an org
11
- * admin changes, removes or keys it, and the platform answers anyone else the
12
- * same 404 an unknown id gets — the record names no owner — so a 404 here is
13
- * said as that rule. `hanzo link`, run on the machine, registers it under its
14
- * hostname and keeps it online while it runs.
15
- */
16
1
  import { type Target } from './call.ts';
17
2
  export declare const KINDS: readonly ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
18
3
  export type Kind = (typeof KINDS)[number];
@@ -13,6 +13,7 @@
13
13
  * said as that rule. `hanzo link`, run on the machine, registers it under its
14
14
  * hostname and keeps it online while it runs.
15
15
  */
16
+ import { phrase } from '@hanzo/i18n';
16
17
  import { call, Refusal, seg } from './call.js';
17
18
  export const KINDS = ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
18
19
  const str = (v) => (typeof v === 'string' ? v : '');
@@ -142,7 +143,7 @@ async function owned(p) {
142
143
  }
143
144
  catch (e) {
144
145
  if (e instanceof Refusal && e.status === 404)
145
- throw new Refusal(404, 'This machine is gone, or not yours to change: only the member who linked it, or an org admin, can.');
146
+ throw new Refusal(404, phrase('This machine is gone, or not yours to change: only the member who linked it, or an org admin, can.'));
146
147
  throw e;
147
148
  }
148
149
  }
@@ -154,7 +155,7 @@ export async function add(t, what) {
154
155
  const host = (what.host ?? '').trim();
155
156
  const label = what.label.trim() || host;
156
157
  if (!label)
157
- throw new Error('A machine needs a name or a hostname');
158
+ throw new Error(phrase('A machine needs a name or a hostname'));
158
159
  const body = { label, kind: what.kind };
159
160
  if (host)
160
161
  body.host = host;
@@ -168,7 +169,7 @@ export async function add(t, what) {
168
169
  * once one has; before that it is `not seen yet`, and nothing is there to take a run.
169
170
  */
170
171
  export function state(m) {
171
- return m.status === 'online' && !m.seen ? 'not seen yet' : m.status;
172
+ return m.status === 'online' && !m.seen ? phrase('not seen yet') : m.status;
172
173
  }
173
174
  /** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
174
175
  export async function change(t, id, what) {
@@ -176,7 +177,7 @@ export async function change(t, id, what) {
176
177
  if (what.label !== undefined) {
177
178
  const label = what.label.trim();
178
179
  if (!label)
179
- throw new Error('A machine needs a name');
180
+ throw new Error(phrase('A machine needs a name'));
180
181
  body.label = label;
181
182
  }
182
183
  if (what.status !== undefined)
@@ -201,6 +202,6 @@ export async function key(t, id) {
201
202
  const r = await owned(call(t, 'POST', `/v1/agent/targets/${seg(id)}/key`));
202
203
  const k = str(r?.claimKey);
203
204
  if (!k)
204
- throw new Error('The platform answered no key');
205
+ throw new Error(phrase('The platform answered no key'));
205
206
  return k;
206
207
  }
package/lib/api/mcp.d.ts CHANGED
@@ -1,12 +1,3 @@
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
1
  import { type Target } from './call.ts';
11
2
  export interface Native {
12
3
  name: string;
package/lib/api/mcp.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * starts a server the first time it calls it; the server stays up afterwards.
8
8
  * `describe` is how a caller reads one operation's schema, so it is not a server.
9
9
  */
10
+ import { phrase } from '@hanzo/i18n';
10
11
  import { call, Refusal } from './call.js';
11
12
  const words = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string' && x !== '') : []);
12
13
  /** The servers in one tools/list answer. A missing tool array is a refusal. */
@@ -15,12 +16,12 @@ export function nativesOf(body) {
15
16
  const err = root.error;
16
17
  if (err && typeof err === 'object') {
17
18
  const message = err.message;
18
- throw new Refusal(200, typeof message === 'string' && message ? message : 'The MCP server refused the list');
19
+ throw new Refusal(200, typeof message === 'string' && message ? message : phrase('The MCP server refused the list'));
19
20
  }
20
21
  const result = root.result && typeof root.result === 'object' ? root.result : null;
21
22
  const tools = result && Array.isArray(result.tools) ? result.tools : null;
22
23
  if (!tools)
23
- throw new Refusal(502, 'The MCP server did not list its tools');
24
+ throw new Refusal(502, phrase('The MCP server did not list its tools'));
24
25
  const out = [];
25
26
  for (const raw of tools) {
26
27
  if (!raw || typeof raw !== 'object')
@@ -1,18 +1,3 @@
1
- /**
2
- * Who is in the organization, and who has been asked to join it, as Hanzo IAM
3
- * holds them.
4
- *
5
- * GET /v1/iam/memberships?org= the roster: {status, data: [membership], data2: count}
6
- * GET /v1/iam/invitations?owner= {invitations, total} (org admin)
7
- * POST /v1/iam/invitations issue one (org admin)
8
- * DELETE /v1/iam/invitations/{owner}/{name} withdraw one (org admin)
9
- * GET /v1/iam/organizations/admin/{org} the org's record, whose `logo` is its mark (member)
10
- *
11
- * IAM decides the scope from the credential: a member reads the roster of an org
12
- * they belong to, and only an admin of the org reads or writes its invitations.
13
- * An invitation is a code IAM redeems when someone signs up with it; IAM sends
14
- * no email of its own.
15
- */
16
1
  import { type Target } from './call.ts';
17
2
  export type Role = 'owner' | 'admin' | 'member';
18
3
  export interface Member {
@@ -13,7 +13,8 @@
13
13
  * An invitation is a code IAM redeems when someone signs up with it; IAM sends
14
14
  * no email of its own.
15
15
  */
16
- import { call, query, seg } from './call.js';
16
+ import { phrase } from '@hanzo/i18n';
17
+ import { call, query, seg, Sentence } from './call.js';
17
18
  const str = (v) => (typeof v === 'string' ? v : '');
18
19
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
19
20
  const obj = (v) => (v && typeof v === 'object' ? v : {});
@@ -34,7 +35,7 @@ export function member(raw) {
34
35
  export async function roster(t, org) {
35
36
  const raw = obj(await call(t, 'GET', `/v1/iam/memberships${query({ org })}`));
36
37
  if (raw.status === 'error')
37
- throw new Error(str(raw.msg) || 'IAM refused the roster');
38
+ throw new Error(str(raw.msg) || phrase('IAM refused the roster'));
38
39
  const rank = (r) => ROLES.indexOf(r);
39
40
  return arr(raw.data)
40
41
  .map(obj)
@@ -71,7 +72,7 @@ function hex(bytes) {
71
72
  export async function invite(t, org, email) {
72
73
  const address = email.trim().toLowerCase();
73
74
  if (!EMAIL.test(address))
74
- throw new Error(`${email.trim() || 'That'} is not an email address`);
75
+ throw email.trim() ? new Sentence(phrase('{email} is not an email address'), { email: email.trim() }) : new Error(phrase('That is not an email address'));
75
76
  const local = address.slice(0, address.indexOf('@')).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24);
76
77
  return invitation(await call(t, 'POST', '/v1/iam/invitations', {
77
78
  owner: org,