@hanzo/build 0.2.9 → 0.2.11
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 +126 -23
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +42 -4
- package/lib/api/coding.js +62 -3
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +39 -4
- package/lib/api/sessions.js +58 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +224 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +197 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +50 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +125 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +8 -1
- package/lib/desk.js +29 -35
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +8 -4
- package/lib/git.d.ts +9 -0
- package/lib/git.js +74 -0
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +47 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +207 -28
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
*
|
|
10
|
+
* IAM decides the scope from the credential: a member reads the roster of an org
|
|
11
|
+
* they belong to, and only an admin of the org reads or writes its invitations.
|
|
12
|
+
* An invitation is a code IAM redeems when someone signs up with it; IAM sends
|
|
13
|
+
* no email of its own.
|
|
14
|
+
*/
|
|
15
|
+
import { call, query, seg } from './call.js';
|
|
16
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
17
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
18
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
19
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
20
|
+
const ROLES = ['owner', 'admin', 'member'];
|
|
21
|
+
export function member(raw) {
|
|
22
|
+
const m = obj(raw);
|
|
23
|
+
const user = str(m.user);
|
|
24
|
+
const role = str(m.role);
|
|
25
|
+
return {
|
|
26
|
+
user,
|
|
27
|
+
name: user.includes('/') ? user.slice(user.indexOf('/') + 1) : user,
|
|
28
|
+
role: ROLES.includes(role) ? role : 'member',
|
|
29
|
+
since: str(m.createdTime),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** The org's people, owners first. Grants to a team or narrowed to one project are not the org's roster. */
|
|
33
|
+
export async function roster(t, org) {
|
|
34
|
+
const raw = obj(await call(t, 'GET', `/v1/iam/memberships${query({ org })}`));
|
|
35
|
+
if (raw.status === 'error')
|
|
36
|
+
throw new Error(str(raw.msg) || 'IAM refused the roster');
|
|
37
|
+
const rank = (r) => ROLES.indexOf(r);
|
|
38
|
+
return arr(raw.data)
|
|
39
|
+
.map(obj)
|
|
40
|
+
.filter((m) => str(m.user) && !str(m.workspace) && !str(m.project))
|
|
41
|
+
.map(member)
|
|
42
|
+
.sort((a, b) => rank(a.role) - rank(b.role) || a.name.localeCompare(b.name));
|
|
43
|
+
}
|
|
44
|
+
export function invitation(raw) {
|
|
45
|
+
const i = obj(raw);
|
|
46
|
+
return {
|
|
47
|
+
owner: str(i.owner),
|
|
48
|
+
name: str(i.name),
|
|
49
|
+
email: str(i.email),
|
|
50
|
+
code: str(i.code),
|
|
51
|
+
seats: num(i.quota),
|
|
52
|
+
used: num(i.usedCount),
|
|
53
|
+
state: str(i.state),
|
|
54
|
+
created: str(i.createdTime) || str(i.createdAt),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function invitations(t, org) {
|
|
58
|
+
const raw = obj(await call(t, 'GET', `/v1/iam/invitations${query({ owner: org })}`));
|
|
59
|
+
return arr(raw.invitations).map(invitation).filter((i) => i.name);
|
|
60
|
+
}
|
|
61
|
+
/** An address that can receive an invitation. */
|
|
62
|
+
export const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
63
|
+
/** Random hex, from the browser's own generator. */
|
|
64
|
+
function hex(bytes) {
|
|
65
|
+
const b = new Uint8Array(bytes);
|
|
66
|
+
crypto.getRandomValues(b);
|
|
67
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
|
68
|
+
}
|
|
69
|
+
/** Invite one address into the org: one seat, redeemable now, pinned to that address. */
|
|
70
|
+
export async function invite(t, org, email) {
|
|
71
|
+
const address = email.trim().toLowerCase();
|
|
72
|
+
if (!EMAIL.test(address))
|
|
73
|
+
throw new Error(`${email.trim() || 'That'} is not an email address`);
|
|
74
|
+
const local = address.slice(0, address.indexOf('@')).replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24);
|
|
75
|
+
return invitation(await call(t, 'POST', '/v1/iam/invitations', {
|
|
76
|
+
owner: org,
|
|
77
|
+
name: `invite-${local || 'member'}-${hex(3)}`,
|
|
78
|
+
displayName: address,
|
|
79
|
+
email: address,
|
|
80
|
+
code: hex(8),
|
|
81
|
+
quota: 1,
|
|
82
|
+
usedCount: 0,
|
|
83
|
+
state: 'Active',
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
export async function revoke(t, i) {
|
|
87
|
+
await call(t, 'DELETE', `/v1/iam/invitations/${seg(i.owner)}/${seg(i.name)}`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Hanzo remembers about the signed-in person, in the org they act in.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/ai/memory/list?limit= {status, data: [memory]}, newest first
|
|
5
|
+
* POST /v1/ai/memory/remember {content} → {status, data: memory}
|
|
6
|
+
* POST /v1/ai/memory/delete {id: "<owner>/<name>"}
|
|
7
|
+
*
|
|
8
|
+
* The platform keys every memory on the validated caller and org, never on the
|
|
9
|
+
* request, so these read and change the caller's own and nobody else's. A
|
|
10
|
+
* refusal arrives in the envelope, usually under a 200.
|
|
11
|
+
*/
|
|
12
|
+
import { type Target } from './call.ts';
|
|
13
|
+
export interface Memory {
|
|
14
|
+
/** `<owner>/<name>`, what a delete names. */
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
/** user, fact, … as the platform files it. */
|
|
18
|
+
kind: string;
|
|
19
|
+
/** When it was last written, as the platform wrote it. */
|
|
20
|
+
updated: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function memoryOf(raw: unknown): Memory | null;
|
|
23
|
+
export declare function memories(t: Target, limit?: number): Promise<Memory[]>;
|
|
24
|
+
export declare function remember(t: Target, content: string): Promise<Memory | null>;
|
|
25
|
+
export declare function forget(t: Target, id: string): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Hanzo remembers about the signed-in person, in the org they act in.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/ai/memory/list?limit= {status, data: [memory]}, newest first
|
|
5
|
+
* POST /v1/ai/memory/remember {content} → {status, data: memory}
|
|
6
|
+
* POST /v1/ai/memory/delete {id: "<owner>/<name>"}
|
|
7
|
+
*
|
|
8
|
+
* The platform keys every memory on the validated caller and org, never on the
|
|
9
|
+
* request, so these read and change the caller's own and nobody else's. A
|
|
10
|
+
* refusal arrives in the envelope, usually under a 200.
|
|
11
|
+
*/
|
|
12
|
+
import { call, query, unwrap } from './call.js';
|
|
13
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
14
|
+
export function memoryOf(raw) {
|
|
15
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
16
|
+
const owner = str(o.owner);
|
|
17
|
+
const name = str(o.name);
|
|
18
|
+
if (!owner || !name)
|
|
19
|
+
return null;
|
|
20
|
+
return { id: `${owner}/${name}`, content: str(o.content), kind: str(o.kind), updated: str(o.updatedTime) || str(o.createdTime) };
|
|
21
|
+
}
|
|
22
|
+
export async function memories(t, limit = 100) {
|
|
23
|
+
const rows = unwrap(await call(t, 'GET', `/v1/ai/memory/list${query({ limit })}`));
|
|
24
|
+
return (Array.isArray(rows) ? rows : []).map(memoryOf).filter((m) => m !== null);
|
|
25
|
+
}
|
|
26
|
+
export async function remember(t, content) {
|
|
27
|
+
const said = content.trim();
|
|
28
|
+
if (!said)
|
|
29
|
+
throw new Error('Say what Hanzo should remember');
|
|
30
|
+
return memoryOf(unwrap(await call(t, 'POST', '/v1/ai/memory/remember', { content: said })));
|
|
31
|
+
}
|
|
32
|
+
export async function forget(t, id) {
|
|
33
|
+
unwrap(await call(t, 'POST', '/v1/ai/memory/delete', { id }));
|
|
34
|
+
}
|
package/lib/api/places.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where a run can run: the platform's sandbox, or one of the org's machines.
|
|
3
3
|
*
|
|
4
|
-
* GET /v1/agent/targets → {targets: [...]}, newest first
|
|
4
|
+
* GET /v1/agent/targets → {targets: [...]}, newest first (machines.ts)
|
|
5
5
|
*
|
|
6
6
|
* The sandbox is not a row the platform lists — it is what a run gets when it
|
|
7
7
|
* names no target — so it is the first place here and carries no id. A machine
|
|
8
8
|
* can take a run only while it is online.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import type { Target } from './call.ts';
|
|
11
11
|
export interface Place {
|
|
12
12
|
/** `tgt_…`, or '' for the sandbox. */
|
|
13
13
|
id: string;
|
package/lib/api/places.js
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* Where a run can run: the platform's sandbox, or one of the org's machines.
|
|
3
|
-
*
|
|
4
|
-
* GET /v1/agent/targets → {targets: [...]}, newest first
|
|
5
|
-
*
|
|
6
|
-
* The sandbox is not a row the platform lists — it is what a run gets when it
|
|
7
|
-
* names no target — so it is the first place here and carries no id. A machine
|
|
8
|
-
* can take a run only while it is online.
|
|
9
|
-
*/
|
|
10
|
-
import { call } from './call.js';
|
|
1
|
+
import { machines } from './machines.js';
|
|
11
2
|
export const SANDBOX = { id: '', label: 'Hanzo sandbox', kind: 'sandbox', status: 'online', capacity: '' };
|
|
12
|
-
const str = (v) => (typeof v === 'string' ? v : '');
|
|
13
3
|
export async function places(t) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const machines = rows
|
|
17
|
-
.map((r) => {
|
|
18
|
-
const o = (r && typeof r === 'object' ? r : {});
|
|
19
|
-
return {
|
|
20
|
-
id: str(o.id),
|
|
21
|
-
label: str(o.label) || str(o.host) || str(o.id),
|
|
22
|
-
kind: str(o.kind),
|
|
23
|
-
status: str(o.status),
|
|
24
|
-
capacity: str(o.capacity),
|
|
25
|
-
};
|
|
26
|
-
})
|
|
27
|
-
.filter((p) => p.id);
|
|
28
|
-
return [SANDBOX, ...machines];
|
|
4
|
+
const list = await machines(t);
|
|
5
|
+
return [SANDBOX, ...list.map(({ id, label, kind, status, capacity }) => ({ id, label, kind, status, capacity }))];
|
|
29
6
|
}
|
|
30
7
|
/** Whether a run sent here now would be taken. */
|
|
31
8
|
export const ready = (p) => p.status === 'online';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins: TypeScript connectors an org builds, and what this deployment mounts.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/tool/plugins/authored {plugins: [plugin]}, newest first, each with its source
|
|
5
|
+
* POST /v1/tool/plugins/build {name, provider?, source | spec} → 201 {bytes, generated, plugin}
|
|
6
|
+
* DELETE /v1/tool/plugins/authored/{id} {deleted}
|
|
7
|
+
* GET /v1/tool/plugins {plugins: [{name, enabled, prefixes}]}
|
|
8
|
+
*
|
|
9
|
+
* Building is the gate: the platform bundles the source and compiles it in the
|
|
10
|
+
* runtime that will run it, and keeps it only if both succeed — a failure is a
|
|
11
|
+
* 422 whose reason is the bundler's. A spec (an OpenAPI document or prose) has a
|
|
12
|
+
* model write the source, which comes back to be read. A plugin never carries a
|
|
13
|
+
* credential: it names the connectors provider whose credential it reads when it
|
|
14
|
+
* runs. The mounted list is the deployment's own subsystems, read-only.
|
|
15
|
+
*/
|
|
16
|
+
import { type Target } from './call.ts';
|
|
17
|
+
export interface Plugin {
|
|
18
|
+
/** What a delete addresses. */
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** The connectors provider whose credential it reads, or ''. */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** The TypeScript as written or generated. */
|
|
24
|
+
source: string;
|
|
25
|
+
/** When it was last built, Unix seconds. */
|
|
26
|
+
built: number;
|
|
27
|
+
}
|
|
28
|
+
export interface Mount {
|
|
29
|
+
name: string;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
prefixes: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface Built {
|
|
34
|
+
plugin: Plugin;
|
|
35
|
+
/** The size of the bundle the runtime executes. */
|
|
36
|
+
bytes: number;
|
|
37
|
+
/** Whether a model wrote the source from a spec. */
|
|
38
|
+
generated: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** A plugin's name: one lowercase path segment, the id it runs by. */
|
|
41
|
+
export declare const NAME: RegExp;
|
|
42
|
+
export declare function plugin(raw: unknown): Plugin;
|
|
43
|
+
export declare function authored(t: Target): Promise<Plugin[]>;
|
|
44
|
+
export declare function mounted(t: Target): Promise<Mount[]>;
|
|
45
|
+
export interface Build {
|
|
46
|
+
name: string;
|
|
47
|
+
provider: string;
|
|
48
|
+
/** Exactly one of the two carries text. */
|
|
49
|
+
source: string;
|
|
50
|
+
spec: string;
|
|
51
|
+
}
|
|
52
|
+
/** Why a plugin cannot be built as it stands, or '' when it can. */
|
|
53
|
+
export declare function refuse(b: Build): string;
|
|
54
|
+
export declare function build(t: Target, b: Build): Promise<Built>;
|
|
55
|
+
export declare function remove(t: Target, id: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins: TypeScript connectors an org builds, and what this deployment mounts.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/tool/plugins/authored {plugins: [plugin]}, newest first, each with its source
|
|
5
|
+
* POST /v1/tool/plugins/build {name, provider?, source | spec} → 201 {bytes, generated, plugin}
|
|
6
|
+
* DELETE /v1/tool/plugins/authored/{id} {deleted}
|
|
7
|
+
* GET /v1/tool/plugins {plugins: [{name, enabled, prefixes}]}
|
|
8
|
+
*
|
|
9
|
+
* Building is the gate: the platform bundles the source and compiles it in the
|
|
10
|
+
* runtime that will run it, and keeps it only if both succeed — a failure is a
|
|
11
|
+
* 422 whose reason is the bundler's. A spec (an OpenAPI document or prose) has a
|
|
12
|
+
* model write the source, which comes back to be read. A plugin never carries a
|
|
13
|
+
* credential: it names the connectors provider whose credential it reads when it
|
|
14
|
+
* runs. The mounted list is the deployment's own subsystems, read-only.
|
|
15
|
+
*/
|
|
16
|
+
import { call, seg } from './call.js';
|
|
17
|
+
/** A plugin's name: one lowercase path segment, the id it runs by. */
|
|
18
|
+
export const NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
19
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
20
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
21
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
22
|
+
const rows = (v) => (Array.isArray(v) ? v : []);
|
|
23
|
+
export function plugin(raw) {
|
|
24
|
+
const o = obj(raw);
|
|
25
|
+
return { id: str(o.id), name: str(o.name), provider: str(o.provider), source: str(o.source), built: num(o.createdAt) };
|
|
26
|
+
}
|
|
27
|
+
export async function authored(t) {
|
|
28
|
+
return rows(obj(await call(t, 'GET', '/v1/tool/plugins/authored')).plugins).map(plugin).filter((p) => p.id);
|
|
29
|
+
}
|
|
30
|
+
export async function mounted(t) {
|
|
31
|
+
return rows(obj(await call(t, 'GET', '/v1/tool/plugins')).plugins)
|
|
32
|
+
.map((r) => {
|
|
33
|
+
const o = obj(r);
|
|
34
|
+
return { name: str(o.name), enabled: o.enabled === true, prefixes: rows(o.prefixes).map(str).filter(Boolean) };
|
|
35
|
+
})
|
|
36
|
+
.filter((m) => m.name);
|
|
37
|
+
}
|
|
38
|
+
/** Why a plugin cannot be built as it stands, or '' when it can. */
|
|
39
|
+
export function refuse(b) {
|
|
40
|
+
if (!NAME.test(b.name))
|
|
41
|
+
return 'A name is one lowercase word: letters, digits, _ or -';
|
|
42
|
+
if (!b.source.trim() === !b.spec.trim())
|
|
43
|
+
return 'Give the TypeScript or the API it calls, one of the two';
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
export async function build(t, b) {
|
|
47
|
+
const why = refuse(b);
|
|
48
|
+
if (why)
|
|
49
|
+
throw new Error(why);
|
|
50
|
+
const body = { name: b.name };
|
|
51
|
+
if (b.provider.trim())
|
|
52
|
+
body.provider = b.provider.trim();
|
|
53
|
+
if (b.source.trim())
|
|
54
|
+
body.source = b.source;
|
|
55
|
+
else
|
|
56
|
+
body.spec = b.spec;
|
|
57
|
+
const r = obj(await call(t, 'POST', '/v1/tool/plugins/build', body));
|
|
58
|
+
return { plugin: plugin(r.plugin), bytes: num(r.bytes), generated: r.generated === true };
|
|
59
|
+
}
|
|
60
|
+
export async function remove(t, id) {
|
|
61
|
+
await call(t, 'DELETE', `/v1/tool/plugins/authored/${seg(id)}`);
|
|
62
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's own settings, as the platform keeps them for every
|
|
3
|
+
* Hanzo surface.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/pref → {prefs: {…}, updatedAt} an empty document for someone who saved nothing
|
|
6
|
+
* PATCH /v1/pref {key: value | null} a shallow merge; null deletes the key
|
|
7
|
+
*
|
|
8
|
+
* The document is one object shared by every surface, keyed per person and per
|
|
9
|
+
* org. This reads only the keys the builder owns and writes only those, so a
|
|
10
|
+
* key another surface saved is left as it was. A value this version does not
|
|
11
|
+
* recognise reads as unset rather than as a choice.
|
|
12
|
+
*/
|
|
13
|
+
import { type Target } from './call.ts';
|
|
14
|
+
export type Theme = 'system' | 'light' | 'dark';
|
|
15
|
+
export type Text = 'small' | 'medium' | 'large';
|
|
16
|
+
export type Motion = 'system' | 'reduced';
|
|
17
|
+
/** Where and how a new run starts when New has no choice of its own kept. */
|
|
18
|
+
export interface Code {
|
|
19
|
+
model?: string;
|
|
20
|
+
effort?: 'low' | 'medium' | 'high';
|
|
21
|
+
mode?: 'build' | 'plan';
|
|
22
|
+
/** A machine's id, or '' for the sandbox. */
|
|
23
|
+
place?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Prefs {
|
|
26
|
+
theme?: Theme;
|
|
27
|
+
text?: Text;
|
|
28
|
+
motion?: Motion;
|
|
29
|
+
/** The dictation language, as voice.ts names it. */
|
|
30
|
+
language?: string;
|
|
31
|
+
/** What Hanzo calls the person. */
|
|
32
|
+
callName?: string;
|
|
33
|
+
/** What best describes their work. */
|
|
34
|
+
work?: string;
|
|
35
|
+
/** What every coding run is told about how they like to work. */
|
|
36
|
+
instructions?: string;
|
|
37
|
+
code?: Code;
|
|
38
|
+
}
|
|
39
|
+
/** A change: a key set to a value, or to null to delete it. */
|
|
40
|
+
export type Patch = {
|
|
41
|
+
[K in keyof Prefs]?: Prefs[K] | null;
|
|
42
|
+
};
|
|
43
|
+
export declare function code(raw: unknown): Code | undefined;
|
|
44
|
+
/** The builder's keys from a whole preference document. */
|
|
45
|
+
export declare function prefs(raw: unknown): Prefs;
|
|
46
|
+
/** What a document reads as once `patch` is merged into it — the platform's own rule. */
|
|
47
|
+
export declare function merge(base: Prefs, patch: Patch): Prefs;
|
|
48
|
+
export declare function read(t: Target): Promise<Prefs>;
|
|
49
|
+
export declare function save(t: Target, patch: Patch): Promise<Prefs>;
|
package/lib/api/pref.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's own settings, as the platform keeps them for every
|
|
3
|
+
* Hanzo surface.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/pref → {prefs: {…}, updatedAt} an empty document for someone who saved nothing
|
|
6
|
+
* PATCH /v1/pref {key: value | null} a shallow merge; null deletes the key
|
|
7
|
+
*
|
|
8
|
+
* The document is one object shared by every surface, keyed per person and per
|
|
9
|
+
* org. This reads only the keys the builder owns and writes only those, so a
|
|
10
|
+
* key another surface saved is left as it was. A value this version does not
|
|
11
|
+
* recognise reads as unset rather than as a choice.
|
|
12
|
+
*/
|
|
13
|
+
import { call } from './call.js';
|
|
14
|
+
const THEMES = ['system', 'light', 'dark'];
|
|
15
|
+
const TEXTS = ['small', 'medium', 'large'];
|
|
16
|
+
const MOTIONS = ['system', 'reduced'];
|
|
17
|
+
const EFFORTS = ['low', 'medium', 'high'];
|
|
18
|
+
const MODES = ['build', 'plan'];
|
|
19
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
20
|
+
const text = (v) => (typeof v === 'string' && v.trim() ? v : undefined);
|
|
21
|
+
const one = (v, of) => (of.includes(v) ? v : undefined);
|
|
22
|
+
/** Drop the keys that say nothing, so a read and a merge compare cleanly. */
|
|
23
|
+
function defined(o) {
|
|
24
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
25
|
+
}
|
|
26
|
+
export function code(raw) {
|
|
27
|
+
const c = obj(raw);
|
|
28
|
+
const out = defined({
|
|
29
|
+
model: text(c.model),
|
|
30
|
+
effort: one(c.effort, EFFORTS),
|
|
31
|
+
mode: one(c.mode, MODES),
|
|
32
|
+
place: typeof c.place === 'string' ? c.place : undefined,
|
|
33
|
+
});
|
|
34
|
+
return Object.keys(out).length ? out : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** The builder's keys from a whole preference document. */
|
|
37
|
+
export function prefs(raw) {
|
|
38
|
+
const p = obj(raw);
|
|
39
|
+
return defined({
|
|
40
|
+
theme: one(p.theme, THEMES),
|
|
41
|
+
text: one(p.text, TEXTS),
|
|
42
|
+
motion: one(p.motion, MOTIONS),
|
|
43
|
+
language: text(p.language),
|
|
44
|
+
callName: text(p.callName),
|
|
45
|
+
work: text(p.work),
|
|
46
|
+
instructions: text(p.instructions),
|
|
47
|
+
code: code(p.code),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** What a document reads as once `patch` is merged into it — the platform's own rule. */
|
|
51
|
+
export function merge(base, patch) {
|
|
52
|
+
const out = { ...base };
|
|
53
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
54
|
+
if (v === null)
|
|
55
|
+
delete out[k];
|
|
56
|
+
else if (v !== undefined)
|
|
57
|
+
out[k] = v;
|
|
58
|
+
}
|
|
59
|
+
return prefs(out);
|
|
60
|
+
}
|
|
61
|
+
export async function read(t) {
|
|
62
|
+
const r = await call(t, 'GET', '/v1/pref');
|
|
63
|
+
return prefs(r?.prefs);
|
|
64
|
+
}
|
|
65
|
+
export async function save(t, patch) {
|
|
66
|
+
const r = await call(t, 'PATCH', '/v1/pref', patch);
|
|
67
|
+
return prefs(r?.prefs);
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's own profile on Hanzo IAM: the name they are shown by
|
|
3
|
+
* and their photo.
|
|
4
|
+
*
|
|
5
|
+
* PUT /v1/iam/account {displayName} → {status, data: {owner, name, displayName, avatar, …}}
|
|
6
|
+
* POST /v1/account/avatar multipart `file` → {avatar} the photo, also written to IAM
|
|
7
|
+
*
|
|
8
|
+
* Both act on the caller alone: neither request names a person. The email is a
|
|
9
|
+
* sign-in identifier and changes only through IAM's verification, never here.
|
|
10
|
+
*/
|
|
11
|
+
import { type Target } from './call.ts';
|
|
12
|
+
export interface Profile {
|
|
13
|
+
displayName: string;
|
|
14
|
+
avatar: string;
|
|
15
|
+
}
|
|
16
|
+
/** Save the name the person is shown by. An empty name is refused here. */
|
|
17
|
+
export declare function rename(t: Target, name: string): Promise<Profile>;
|
|
18
|
+
/** The formats the platform keeps a photo in; it decides by the bytes, this only filters the picker. */
|
|
19
|
+
export declare const PHOTO = "image/png,image/jpeg,image/gif,image/webp";
|
|
20
|
+
/** Store a new photo and answer its address. */
|
|
21
|
+
export declare function photo(t: Target, file: Blob): Promise<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's own profile on Hanzo IAM: the name they are shown by
|
|
3
|
+
* and their photo.
|
|
4
|
+
*
|
|
5
|
+
* PUT /v1/iam/account {displayName} → {status, data: {owner, name, displayName, avatar, …}}
|
|
6
|
+
* POST /v1/account/avatar multipart `file` → {avatar} the photo, also written to IAM
|
|
7
|
+
*
|
|
8
|
+
* Both act on the caller alone: neither request names a person. The email is a
|
|
9
|
+
* sign-in identifier and changes only through IAM's verification, never here.
|
|
10
|
+
*/
|
|
11
|
+
import { call, headers, reason, Refusal, unwrap } from './call.js';
|
|
12
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
13
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
14
|
+
/** Save the name the person is shown by. An empty name is refused here. */
|
|
15
|
+
export async function rename(t, name) {
|
|
16
|
+
const displayName = name.trim();
|
|
17
|
+
if (!displayName)
|
|
18
|
+
throw new Error('A name cannot be empty');
|
|
19
|
+
const p = obj(unwrap(await call(t, 'PUT', '/v1/iam/account', { displayName })));
|
|
20
|
+
return { displayName: str(p.displayName), avatar: str(p.avatar) };
|
|
21
|
+
}
|
|
22
|
+
/** The formats the platform keeps a photo in; it decides by the bytes, this only filters the picker. */
|
|
23
|
+
export const PHOTO = 'image/png,image/jpeg,image/gif,image/webp';
|
|
24
|
+
const LIMIT = 8 << 20;
|
|
25
|
+
/** Store a new photo and answer its address. */
|
|
26
|
+
export async function photo(t, file) {
|
|
27
|
+
if (!file.size)
|
|
28
|
+
throw new Error('That file is empty');
|
|
29
|
+
if (file.size > LIMIT)
|
|
30
|
+
throw new Error('A photo is at most 8 MB');
|
|
31
|
+
const form = new FormData();
|
|
32
|
+
form.append('file', file);
|
|
33
|
+
const res = await fetch(`${t.api}/v1/account/avatar`, { method: 'POST', headers: headers(t), body: form, cache: 'no-store' });
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Refusal(res.status, (await reason(res)) || `POST /v1/account/avatar answered ${res.status}`);
|
|
36
|
+
const url = str(obj(await res.json()).avatar);
|
|
37
|
+
if (!url.startsWith('https://'))
|
|
38
|
+
throw new Refusal(502, 'The photo was stored without an address this page can show');
|
|
39
|
+
return url;
|
|
40
|
+
}
|
package/lib/api/projects.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The org's projects: what it has built, and where each one is served.
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
4
|
+
* GET /v1/projects → Project[]
|
|
5
|
+
* PATCH /v1/projects/{slug} rename it, or make it public or private
|
|
6
|
+
* DELETE /v1/projects/{slug} delete it and take its site down
|
|
5
7
|
*
|
|
6
8
|
* `slug` is the one key every surface shares — the address under /dev, the
|
|
7
|
-
* site's name, the `project` a run is tagged with.
|
|
9
|
+
* site's name, the `project` a run is tagged with. A project belongs to the org,
|
|
10
|
+
* not to a person: the record names no author, so any member lists, changes and
|
|
11
|
+
* deletes it. Private is paid, and an unfunded org is refused rather than left
|
|
12
|
+
* public without being told.
|
|
8
13
|
*/
|
|
9
14
|
import { type Target } from './call.ts';
|
|
10
15
|
export interface Project {
|
|
@@ -17,12 +22,24 @@ export interface Project {
|
|
|
17
22
|
status: string;
|
|
18
23
|
/** The deployed address, or '' when nothing has shipped. */
|
|
19
24
|
live: string;
|
|
25
|
+
/** `public` or `private`, or '' when the platform did not say. */
|
|
26
|
+
visibility: '' | Visibility;
|
|
27
|
+
/** Unix seconds; 0 when unknown. A deploy does not move `updated`. */
|
|
28
|
+
created: number;
|
|
20
29
|
updated: number;
|
|
21
30
|
}
|
|
31
|
+
export type Visibility = 'public' | 'private';
|
|
22
32
|
/** An address the preview may frame: https, or http on loopback when the builder is local too. */
|
|
23
33
|
export declare function safe(url: string, local?: boolean): string;
|
|
24
34
|
export declare function project(raw: unknown): Project;
|
|
25
35
|
export declare function projects(t: Target): Promise<Project[]>;
|
|
36
|
+
/** Change a project's name or who can see it; what is not sent is left as it is. */
|
|
37
|
+
export declare function change(t: Target, slug: string, what: {
|
|
38
|
+
name?: string;
|
|
39
|
+
visibility?: Visibility;
|
|
40
|
+
}): Promise<Project>;
|
|
41
|
+
/** Delete a project. Its site stops answering and its slug is free again. */
|
|
42
|
+
export declare function remove(t: Target, slug: string): Promise<void>;
|
|
26
43
|
/** The repository name a project's clone URL names — the last path segment, without `.git`. */
|
|
27
44
|
export declare function name(clone: string): string;
|
|
28
45
|
/** A starter from the public catalog. */
|
package/lib/api/projects.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The org's projects: what it has built, and where each one is served.
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
4
|
+
* GET /v1/projects → Project[]
|
|
5
|
+
* PATCH /v1/projects/{slug} rename it, or make it public or private
|
|
6
|
+
* DELETE /v1/projects/{slug} delete it and take its site down
|
|
5
7
|
*
|
|
6
8
|
* `slug` is the one key every surface shares — the address under /dev, the
|
|
7
|
-
* site's name, the `project` a run is tagged with.
|
|
9
|
+
* site's name, the `project` a run is tagged with. A project belongs to the org,
|
|
10
|
+
* not to a person: the record names no author, so any member lists, changes and
|
|
11
|
+
* deletes it. Private is paid, and an unfunded org is refused rather than left
|
|
12
|
+
* public without being told.
|
|
8
13
|
*/
|
|
9
|
-
import { call } from './call.js';
|
|
14
|
+
import { call, seg } from './call.js';
|
|
10
15
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
16
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
11
17
|
const loopback = (host) => host === 'localhost' || host === '127.0.0.1';
|
|
12
18
|
/** Whether this page itself is served from loopback — local work. */
|
|
13
19
|
const here = () => typeof window !== 'undefined' && loopback(window.location.hostname);
|
|
@@ -37,7 +43,9 @@ export function project(raw) {
|
|
|
37
43
|
branch: str(repo.branch),
|
|
38
44
|
status: str(p.status),
|
|
39
45
|
live: safe(str(p.liveUrl)),
|
|
40
|
-
|
|
46
|
+
visibility: p.visibility === 'public' || p.visibility === 'private' ? p.visibility : '',
|
|
47
|
+
created: num(p.createdAt),
|
|
48
|
+
updated: num(p.updatedAt),
|
|
41
49
|
};
|
|
42
50
|
}
|
|
43
51
|
export async function projects(t) {
|
|
@@ -48,6 +56,23 @@ export async function projects(t) {
|
|
|
48
56
|
.filter((p) => p.slug)
|
|
49
57
|
.sort((a, b) => b.updated - a.updated);
|
|
50
58
|
}
|
|
59
|
+
/** Change a project's name or who can see it; what is not sent is left as it is. */
|
|
60
|
+
export async function change(t, slug, what) {
|
|
61
|
+
const body = {};
|
|
62
|
+
if (what.name !== undefined) {
|
|
63
|
+
const n = what.name.trim();
|
|
64
|
+
if (!n)
|
|
65
|
+
throw new Error('A project needs a name');
|
|
66
|
+
body.name = n;
|
|
67
|
+
}
|
|
68
|
+
if (what.visibility !== undefined)
|
|
69
|
+
body.visibility = what.visibility;
|
|
70
|
+
return project(await call(t, 'PATCH', `/v1/projects/${seg(slug)}`, body));
|
|
71
|
+
}
|
|
72
|
+
/** Delete a project. Its site stops answering and its slug is free again. */
|
|
73
|
+
export async function remove(t, slug) {
|
|
74
|
+
await call(t, 'DELETE', `/v1/projects/${seg(slug)}`);
|
|
75
|
+
}
|
|
51
76
|
/** The repository name a project's clone URL names — the last path segment, without `.git`. */
|
|
52
77
|
export function name(clone) {
|
|
53
78
|
const cut = clone.replace(/\.git$/, '').replace(/\/+$/, '');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The organization's connectors: a Slack workspace, the GitHub accounts the
|
|
3
|
+
* platform's GitHub App is installed on.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/provider/{id} one connector and this org's connection to it
|
|
6
|
+
* POST /v1/provider/{id}/connect {} → {authorizeUrl}: the provider's own consent page (org admin)
|
|
7
|
+
* POST /v1/provider/{id}/disconnect forget the org's connection (org admin)
|
|
8
|
+
* GET /v1/provider/slack/channels the connected workspace's channels, a page at a time
|
|
9
|
+
*
|
|
10
|
+
* Connecting leaves this page once, for the provider's consent screen, and only
|
|
11
|
+
* for that provider's own host.
|
|
12
|
+
*/
|
|
13
|
+
import { type Target } from './call.ts';
|
|
14
|
+
export interface Connector {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
/** Whether this deployment can connect it at all. */
|
|
18
|
+
available: boolean;
|
|
19
|
+
connected: boolean;
|
|
20
|
+
/** The connected account's own name: the Slack workspace, the GitHub login. */
|
|
21
|
+
account: string;
|
|
22
|
+
since: string;
|
|
23
|
+
/** Why it cannot be connected here, when it cannot. */
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function connector(raw: unknown, id?: string): Connector;
|
|
27
|
+
export declare function read(t: Target, id: string): Promise<Connector>;
|
|
28
|
+
/** The provider's consent page for this org. */
|
|
29
|
+
export declare function authorize(t: Target, id: string): Promise<string>;
|
|
30
|
+
export declare function disconnect(t: Target, id: string): Promise<void>;
|
|
31
|
+
export interface Channel {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
private: boolean;
|
|
35
|
+
/** Whether Hanzo has joined it. */
|
|
36
|
+
member: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface Channels {
|
|
39
|
+
channels: Channel[];
|
|
40
|
+
next: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function channels(t: Target, cursor?: string): Promise<Channels>;
|