@hanzo/build 0.2.10 → 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 +97 -25
- 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/coding.d.ts +31 -1
- package/lib/api/coding.js +50 -1
- 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/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/sessions.d.ts +32 -4
- package/lib/api/sessions.js +42 -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 +221 -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 +4 -1
- package/lib/desk.js +3 -16
- 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 +4 -4
- package/lib/git.d.ts +4 -0
- package/lib/git.js +6 -2
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +30 -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 +184 -32
- 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/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
import { call, query, seg } from './call.js';
|
|
18
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
19
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
20
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
21
|
+
const rows = (v) => (Array.isArray(v) ? v : []);
|
|
22
|
+
export function server(raw) {
|
|
23
|
+
const o = obj(raw);
|
|
24
|
+
return {
|
|
25
|
+
id: str(o.id),
|
|
26
|
+
name: str(o.name),
|
|
27
|
+
url: str(o.url),
|
|
28
|
+
header: str(o.authHeader),
|
|
29
|
+
secret: o.hasSecret === true,
|
|
30
|
+
listing: str(o.listing),
|
|
31
|
+
created: num(o.createdAt),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function listing(raw) {
|
|
35
|
+
const o = obj(raw);
|
|
36
|
+
return {
|
|
37
|
+
id: str(o.id),
|
|
38
|
+
name: str(o.name),
|
|
39
|
+
title: str(o.title),
|
|
40
|
+
description: str(o.description),
|
|
41
|
+
vendor: str(o.vendor),
|
|
42
|
+
version: str(o.version),
|
|
43
|
+
logo: str(o.logo),
|
|
44
|
+
featured: o.featured === true,
|
|
45
|
+
official: o.official === true,
|
|
46
|
+
transports: rows(o.transports).map(str).filter(Boolean),
|
|
47
|
+
remotes: rows(o.remotes)
|
|
48
|
+
.map((r) => ({ transport: str(obj(r).transport), url: str(obj(r).url) }))
|
|
49
|
+
.filter((r) => r.url),
|
|
50
|
+
packages: rows(o.packages)
|
|
51
|
+
.map((p) => {
|
|
52
|
+
const x = obj(p);
|
|
53
|
+
return { registry: str(x.registry), identifier: str(x.identifier), runtime: str(x.runtime), version: str(x.version) };
|
|
54
|
+
})
|
|
55
|
+
.filter((p) => p.identifier),
|
|
56
|
+
repo: str(o.repo),
|
|
57
|
+
site: str(o.site),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** What a listing is called on screen. */
|
|
61
|
+
export const titleOf = (l) => l.title || l.name;
|
|
62
|
+
/** Whether a listing can be added here and now: the platform dials its streamable-HTTP remote. */
|
|
63
|
+
export const ready = (l) => l.remotes.some((r) => r.transport === 'streamable-http');
|
|
64
|
+
/** A server's tools among the plane's, by the prefix its id gives them. */
|
|
65
|
+
export const owns = (s, toolName) => toolName.startsWith(`${s.id}_`);
|
|
66
|
+
export async function servers(t) {
|
|
67
|
+
return rows(obj(await call(t, 'GET', '/v1/tool/mcp/servers')).servers).map(server).filter((s) => s.id);
|
|
68
|
+
}
|
|
69
|
+
/** Why a server cannot be added as it stands, or '' when it can. */
|
|
70
|
+
export function refuse(a) {
|
|
71
|
+
if (!a.listing) {
|
|
72
|
+
if (!a.name?.trim())
|
|
73
|
+
return 'A connector needs a name';
|
|
74
|
+
if (!/^https?:\/\/[^\s/]+/.test(a.url?.trim() ?? ''))
|
|
75
|
+
return 'The URL is an http(s) address';
|
|
76
|
+
}
|
|
77
|
+
if (a.secret && !a.header?.trim())
|
|
78
|
+
return 'Name the header the secret is sent in';
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
export async function add(t, a) {
|
|
82
|
+
const why = refuse(a);
|
|
83
|
+
if (why)
|
|
84
|
+
throw new Error(why);
|
|
85
|
+
const body = {};
|
|
86
|
+
if (a.listing)
|
|
87
|
+
body.listing = a.listing;
|
|
88
|
+
else
|
|
89
|
+
body.url = a.url.trim();
|
|
90
|
+
if (a.name?.trim())
|
|
91
|
+
body.name = a.name.trim();
|
|
92
|
+
// The header names where the secret goes, so it rides with one or not at all.
|
|
93
|
+
if (a.secret) {
|
|
94
|
+
body.authHeader = a.header.trim();
|
|
95
|
+
body.secret = a.secret;
|
|
96
|
+
}
|
|
97
|
+
return server(await call(t, 'POST', '/v1/tool/mcp/servers', body));
|
|
98
|
+
}
|
|
99
|
+
export async function remove(t, id) {
|
|
100
|
+
await call(t, 'DELETE', `/v1/tool/mcp/servers/${seg(id)}`);
|
|
101
|
+
}
|
|
102
|
+
/** A page of the shelf, featured first, then by name. */
|
|
103
|
+
export async function shelf(t, q = {}) {
|
|
104
|
+
const r = obj(await call(t, 'GET', `/v1/tool/catalog${query({ q: q.text?.trim(), limit: q.limit, offset: q.offset || undefined })}`));
|
|
105
|
+
return { listings: rows(r.catalog).map(listing).filter((l) => l.id), total: num(r.total), offset: num(r.offset) };
|
|
106
|
+
}
|
|
107
|
+
export async function one(t, id) {
|
|
108
|
+
return listing(await call(t, 'GET', `/v1/tool/catalog/${seg(id)}`));
|
|
109
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's answers to Hanzo's data-sharing questions, as IAM
|
|
3
|
+
* records them for every Hanzo surface.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/iam/consent → {status, data: {insights, training}}
|
|
6
|
+
* PUT /v1/iam/consent {insights?, training?} only the answers sent change
|
|
7
|
+
*
|
|
8
|
+
* `training` has three states: unanswered (''), granted, refused. Only granted
|
|
9
|
+
* lets Hanzo train on this person's data; unanswered is read as no. `insights`
|
|
10
|
+
* is anonymous product usage, with no prompt or answer text, and is on until
|
|
11
|
+
* turned off. Every change is audited by IAM with the answer before and after.
|
|
12
|
+
*/
|
|
13
|
+
import { type Target } from './call.ts';
|
|
14
|
+
export type Answer = '' | 'granted' | 'refused';
|
|
15
|
+
export interface Consent {
|
|
16
|
+
insights: boolean;
|
|
17
|
+
training: Answer;
|
|
18
|
+
}
|
|
19
|
+
/** IAM's defaults for anything it did not say: insights on, training unanswered. */
|
|
20
|
+
export declare function consentOf(raw: unknown): Consent;
|
|
21
|
+
export declare function consent(t: Target): Promise<Consent>;
|
|
22
|
+
export declare function setConsent(t: Target, change: Partial<Consent>): Promise<Consent>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in person's answers to Hanzo's data-sharing questions, as IAM
|
|
3
|
+
* records them for every Hanzo surface.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/iam/consent → {status, data: {insights, training}}
|
|
6
|
+
* PUT /v1/iam/consent {insights?, training?} only the answers sent change
|
|
7
|
+
*
|
|
8
|
+
* `training` has three states: unanswered (''), granted, refused. Only granted
|
|
9
|
+
* lets Hanzo train on this person's data; unanswered is read as no. `insights`
|
|
10
|
+
* is anonymous product usage, with no prompt or answer text, and is on until
|
|
11
|
+
* turned off. Every change is audited by IAM with the answer before and after.
|
|
12
|
+
*/
|
|
13
|
+
import { call, unwrap } from './call.js';
|
|
14
|
+
const ANSWERS = ['', 'granted', 'refused'];
|
|
15
|
+
/** IAM's defaults for anything it did not say: insights on, training unanswered. */
|
|
16
|
+
export function consentOf(raw) {
|
|
17
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
18
|
+
return {
|
|
19
|
+
insights: typeof o.insights === 'boolean' ? o.insights : true,
|
|
20
|
+
training: ANSWERS.includes(o.training) ? o.training : '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function consent(t) {
|
|
24
|
+
return consentOf(unwrap(await call(t, 'GET', '/v1/iam/consent')));
|
|
25
|
+
}
|
|
26
|
+
export async function setConsent(t, change) {
|
|
27
|
+
return consentOf(unwrap(await call(t, 'PUT', '/v1/iam/consent', change)));
|
|
28
|
+
}
|
package/lib/api/github.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* GET /v1/provider/github/user the person's own connection
|
|
7
7
|
* POST /v1/provider/github/user/connect → {authorizeUrl}
|
|
8
8
|
* POST /v1/provider/github/user/disconnect
|
|
9
|
+
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
9
10
|
*
|
|
10
11
|
* Paged by an opaque `after` cursor; `next` is the cursor for the page after
|
|
11
12
|
* this one, or empty on the last. Rows are read defensively: a field the
|
|
@@ -76,6 +77,18 @@ export declare function connection(t: Target): Promise<Connection>;
|
|
|
76
77
|
/** Where to send the person to authorize the platform's GitHub App — github.com only. */
|
|
77
78
|
export declare function connect(t: Target): Promise<string>;
|
|
78
79
|
export declare function disconnect(t: Target): Promise<void>;
|
|
80
|
+
/** A GitHub account the platform's App is installed on, as this org sees it. */
|
|
81
|
+
export interface Installation {
|
|
82
|
+
login: string;
|
|
83
|
+
/** `Organization` or `User`. */
|
|
84
|
+
type: string;
|
|
85
|
+
/** `all` or `selected` repositories. */
|
|
86
|
+
grant: string;
|
|
87
|
+
/** This org has bound it, and GitHub still has the App installed there. */
|
|
88
|
+
connected: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** The accounts this org has bound the App on. An org admin adds one through `/v1/provider/github/connect`. */
|
|
91
|
+
export declare function installations(t: Target): Promise<Installation[]>;
|
|
79
92
|
/** One repository the GitHub connection grants this organization. */
|
|
80
93
|
export interface Grant {
|
|
81
94
|
owner: string;
|
package/lib/api/github.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* GET /v1/provider/github/user the person's own connection
|
|
7
7
|
* POST /v1/provider/github/user/connect → {authorizeUrl}
|
|
8
8
|
* POST /v1/provider/github/user/disconnect
|
|
9
|
+
* GET /v1/provider/github/installations the accounts this org has bound the App on
|
|
9
10
|
*
|
|
10
11
|
* Paged by an opaque `after` cursor; `next` is the cursor for the page after
|
|
11
12
|
* this one, or empty on the last. Rows are read defensively: a field the
|
|
@@ -71,6 +72,14 @@ export async function connect(t) {
|
|
|
71
72
|
export async function disconnect(t) {
|
|
72
73
|
await call(t, 'POST', '/v1/provider/github/user/disconnect');
|
|
73
74
|
}
|
|
75
|
+
/** The accounts this org has bound the App on. An org admin adds one through `/v1/provider/github/connect`. */
|
|
76
|
+
export async function installations(t) {
|
|
77
|
+
const raw = obj(await call(t, 'GET', '/v1/provider/github/installations'));
|
|
78
|
+
return (Array.isArray(raw.installations) ? raw.installations : [])
|
|
79
|
+
.map(obj)
|
|
80
|
+
.map((i) => ({ login: str(i.login), type: str(i.type), grant: str(i.grant), connected: i.connected === true }))
|
|
81
|
+
.filter((i) => i.login);
|
|
82
|
+
}
|
|
74
83
|
export function grant(raw) {
|
|
75
84
|
const r = obj(raw);
|
|
76
85
|
const name = str(r.name);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's own output, read as cards.
|
|
3
|
+
*
|
|
4
|
+
* A sandbox run's agent is `dev exec`, and everything it prints reaches the
|
|
5
|
+
* run's session as `log` chunks (apps/sandbox work.go). It prints each item of
|
|
6
|
+
* its turn under a header line (hanzo dev, exec/src/event_processor_with_human_output.rs):
|
|
7
|
+
*
|
|
8
|
+
* exec / <command> in <dir> a command started
|
|
9
|
+
* succeeded in 12ms: / <output> …how it ended, then what it printed
|
|
10
|
+
* codex / <text> the agent said something
|
|
11
|
+
* apply patch / patch: completed / <paths>, then the turn's diff
|
|
12
|
+
* mcp: server/tool (completed) a tool call
|
|
13
|
+
* web search: <query>
|
|
14
|
+
* ✓ step / → step / • step its plan
|
|
15
|
+
* tokens used / <n> the end; the final message follows once more, bare
|
|
16
|
+
*
|
|
17
|
+
* The chunks are cut by a clock, not by line, so a line is read only once it
|
|
18
|
+
* is whole. Lines no header owns — reasoning, or the tail of output from before
|
|
19
|
+
* the recorded events begin — are prose. Everything here becomes TEXT on the
|
|
20
|
+
* screen; nothing is markup.
|
|
21
|
+
*/
|
|
22
|
+
import type { Card } from './turn.ts';
|
|
23
|
+
/**
|
|
24
|
+
* A shell command's words, as `sh` splits them: quotes grouped, `'"'"'`
|
|
25
|
+
* joined, a backslash escaping the next character outside single quotes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function words(line: string): string[];
|
|
28
|
+
/** The command a harness ran, out of the `bash -lc '…'` it wraps each one in. */
|
|
29
|
+
export declare function unwrap(command: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The file a command only reads, or ''. `cat f`, `nl -ba f`, `sed -n '1,80p' f`,
|
|
32
|
+
* `head -n 40 f`, `tail f` — one file, and nothing piped, chained or redirected.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reads(command: string): string;
|
|
35
|
+
/** A path to show: a relative one as it is, an absolute one by its name — never the sandbox's layout. */
|
|
36
|
+
export declare const shown: (path: string) => string;
|
|
37
|
+
/** A unified diff's sections, by the path each changes, from its first hunk on. */
|
|
38
|
+
export declare function sections(diff: string): Map<string, string>;
|
|
39
|
+
/** A reader that appends the cards it finds to `out`, keyed by `mint`. */
|
|
40
|
+
export declare function harness(out: Card[], mint: () => string): {
|
|
41
|
+
feed(chunk: string): void;
|
|
42
|
+
/** The last line even without its newline, and what only the whole output can decide. */
|
|
43
|
+
end(): void;
|
|
44
|
+
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/** Colour codes a terminal would have drawn; a sandbox is not a terminal, but a harness may still print them. */
|
|
2
|
+
const ANSI = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
3
|
+
/**
|
|
4
|
+
* A shell command's words, as `sh` splits them: quotes grouped, `'"'"'`
|
|
5
|
+
* joined, a backslash escaping the next character outside single quotes.
|
|
6
|
+
*/
|
|
7
|
+
export function words(line) {
|
|
8
|
+
const out = [];
|
|
9
|
+
let cur = '';
|
|
10
|
+
let open = false;
|
|
11
|
+
let q = '';
|
|
12
|
+
for (let i = 0; i < line.length; i++) {
|
|
13
|
+
const c = line[i];
|
|
14
|
+
if (q === "'") {
|
|
15
|
+
if (c === "'")
|
|
16
|
+
q = '';
|
|
17
|
+
else
|
|
18
|
+
cur += c;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (q === '"') {
|
|
22
|
+
if (c === '"')
|
|
23
|
+
q = '';
|
|
24
|
+
else if (c === '\\' && i + 1 < line.length)
|
|
25
|
+
cur += line[++i];
|
|
26
|
+
else
|
|
27
|
+
cur += c;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === "'" || c === '"') {
|
|
31
|
+
q = c;
|
|
32
|
+
open = true;
|
|
33
|
+
}
|
|
34
|
+
else if (c === '\\' && i + 1 < line.length) {
|
|
35
|
+
cur += line[++i];
|
|
36
|
+
open = true;
|
|
37
|
+
}
|
|
38
|
+
else if (/\s/.test(c)) {
|
|
39
|
+
if (open)
|
|
40
|
+
out.push(cur);
|
|
41
|
+
cur = '';
|
|
42
|
+
open = false;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
cur += c;
|
|
46
|
+
open = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (open)
|
|
50
|
+
out.push(cur);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** The command a harness ran, out of the `bash -lc '…'` it wraps each one in. */
|
|
54
|
+
export function unwrap(command) {
|
|
55
|
+
const w = words(command);
|
|
56
|
+
if (w.length === 3 && /(^|\/)(ba|z)?sh$/.test(w[0]) && (w[1] === '-lc' || w[1] === '-c'))
|
|
57
|
+
return w[2];
|
|
58
|
+
return command;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The file a command only reads, or ''. `cat f`, `nl -ba f`, `sed -n '1,80p' f`,
|
|
62
|
+
* `head -n 40 f`, `tail f` — one file, and nothing piped, chained or redirected.
|
|
63
|
+
*/
|
|
64
|
+
export function reads(command) {
|
|
65
|
+
const inner = unwrap(command);
|
|
66
|
+
if (/[|;&<>`$]/.test(inner))
|
|
67
|
+
return '';
|
|
68
|
+
const [cmd, ...args] = words(inner);
|
|
69
|
+
const one = (xs) => {
|
|
70
|
+
const files = xs.filter((a) => !a.startsWith('-'));
|
|
71
|
+
return files.length === 1 ? files[0] : '';
|
|
72
|
+
};
|
|
73
|
+
switch (cmd) {
|
|
74
|
+
case 'cat':
|
|
75
|
+
case 'nl':
|
|
76
|
+
return one(args);
|
|
77
|
+
case 'sed':
|
|
78
|
+
return args.length === 3 && args[0] === '-n' && /^\d+(,\d+)?p$/.test(args[1]) ? args[2] : '';
|
|
79
|
+
case 'head':
|
|
80
|
+
case 'tail':
|
|
81
|
+
return one(args[0] === '-n' ? args.slice(2) : args);
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
/** A path to show: a relative one as it is, an absolute one by its name — never the sandbox's layout. */
|
|
86
|
+
export const shown = (path) => {
|
|
87
|
+
if (!path.startsWith('/'))
|
|
88
|
+
return path;
|
|
89
|
+
const cut = path.replace(/\/+$/, '');
|
|
90
|
+
return cut.slice(cut.lastIndexOf('/') + 1);
|
|
91
|
+
};
|
|
92
|
+
/** A unified diff's sections, by the path each changes, from its first hunk on. */
|
|
93
|
+
export function sections(diff) {
|
|
94
|
+
const out = new Map();
|
|
95
|
+
let at = '';
|
|
96
|
+
let lines = [];
|
|
97
|
+
const close = () => {
|
|
98
|
+
if (at)
|
|
99
|
+
out.set(at, lines.join('\n'));
|
|
100
|
+
};
|
|
101
|
+
for (const line of diff.split('\n')) {
|
|
102
|
+
const m = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
103
|
+
if (m) {
|
|
104
|
+
close();
|
|
105
|
+
at = m[2];
|
|
106
|
+
lines = [];
|
|
107
|
+
}
|
|
108
|
+
else if (at && (lines.length || line.startsWith('@@')))
|
|
109
|
+
lines.push(line);
|
|
110
|
+
}
|
|
111
|
+
close();
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
const HEAD = /^(exec|codex|thinking|user|apply patch|tokens used|turn interrupted|context compacted)$/;
|
|
115
|
+
const LEAD = /^(patch|mcp|web search|warning|ERROR|deprecated|model rerouted|hook): /;
|
|
116
|
+
const RESULT = /^\s*(succeeded|exited (-?\d+)|declined|in progress)( in [\d.]+m?s)?:$/;
|
|
117
|
+
/** A reader that appends the cards it finds to `out`, keyed by `mint`. */
|
|
118
|
+
export function harness(out, mint) {
|
|
119
|
+
let rest = '';
|
|
120
|
+
let mode = 'prose';
|
|
121
|
+
// The banner is a rule, the settings, and a rule.
|
|
122
|
+
let rules = 0;
|
|
123
|
+
let card = null;
|
|
124
|
+
let final = [];
|
|
125
|
+
const open = [];
|
|
126
|
+
const edits = [];
|
|
127
|
+
let edit = null;
|
|
128
|
+
let diff = null;
|
|
129
|
+
const said = (text) => {
|
|
130
|
+
const c = { kind: 'said', key: mint(), who: 'agent', text };
|
|
131
|
+
out.push(c);
|
|
132
|
+
return c;
|
|
133
|
+
};
|
|
134
|
+
const patch = () => {
|
|
135
|
+
const c = { kind: 'edit', key: mint(), files: [], patch: '', ran: 'running' };
|
|
136
|
+
out.push(c);
|
|
137
|
+
edit = { card: c, paths: [] };
|
|
138
|
+
edits.push(edit);
|
|
139
|
+
return edit;
|
|
140
|
+
};
|
|
141
|
+
// The diff printed after a patch is the turn's so far; the edit keeps the latest.
|
|
142
|
+
const settle = () => {
|
|
143
|
+
if (diff && edit)
|
|
144
|
+
edit.card.patch = diff.join('\n');
|
|
145
|
+
diff = null;
|
|
146
|
+
};
|
|
147
|
+
const step = (name, detail, ran) => {
|
|
148
|
+
const c = { kind: 'step', key: mint(), name, detail, output: '', ran };
|
|
149
|
+
out.push(c);
|
|
150
|
+
return c;
|
|
151
|
+
};
|
|
152
|
+
const line = (raw) => {
|
|
153
|
+
const l = raw.replace(ANSI, '').replace(/\r$/, '');
|
|
154
|
+
if (mode === 'config') {
|
|
155
|
+
if (l === '--------' && ++rules === 2)
|
|
156
|
+
mode = 'prose';
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (mode === 'command') {
|
|
160
|
+
const cut = l.lastIndexOf(' in ');
|
|
161
|
+
const command = cut > 0 ? l.slice(0, cut) : l;
|
|
162
|
+
const file = reads(command);
|
|
163
|
+
const c = file
|
|
164
|
+
? { kind: 'read', key: mint(), file: shown(file), ran: 'running' }
|
|
165
|
+
: { kind: 'shell', key: mint(), command: unwrap(command), output: '', ran: 'running' };
|
|
166
|
+
out.push(c);
|
|
167
|
+
open.push(c);
|
|
168
|
+
card = null;
|
|
169
|
+
mode = 'prose';
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (mode === 'tokens') {
|
|
173
|
+
mode = 'final';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (mode === 'diff' && !HEAD.test(l) && !LEAD.test(l)) {
|
|
177
|
+
diff.push(l);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const result = RESULT.exec(l);
|
|
181
|
+
if (result && open.length) {
|
|
182
|
+
settle();
|
|
183
|
+
const c = open.pop();
|
|
184
|
+
c.ran = result[1] === 'succeeded' ? 'done' : result[1] === 'declined' ? 'cancelled' : result[1] === 'in progress' ? 'running' : 'error';
|
|
185
|
+
card = c;
|
|
186
|
+
mode = 'output';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (HEAD.test(l)) {
|
|
190
|
+
settle();
|
|
191
|
+
card = null;
|
|
192
|
+
switch (l) {
|
|
193
|
+
case 'exec':
|
|
194
|
+
mode = 'command';
|
|
195
|
+
return;
|
|
196
|
+
case 'codex':
|
|
197
|
+
case 'thinking':
|
|
198
|
+
card = said('');
|
|
199
|
+
mode = 'agent';
|
|
200
|
+
return;
|
|
201
|
+
case 'user':
|
|
202
|
+
mode = 'user';
|
|
203
|
+
return;
|
|
204
|
+
case 'apply patch':
|
|
205
|
+
patch();
|
|
206
|
+
mode = 'prose';
|
|
207
|
+
return;
|
|
208
|
+
case 'tokens used':
|
|
209
|
+
mode = 'tokens';
|
|
210
|
+
return;
|
|
211
|
+
default:
|
|
212
|
+
out.push({ kind: 'note', key: mint(), text: l === 'turn interrupted' ? 'The agent was interrupted' : 'The agent compacted its context' });
|
|
213
|
+
mode = 'prose';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (/^Hanzo Dev v\S+$/.test(l)) {
|
|
218
|
+
settle();
|
|
219
|
+
mode = 'config';
|
|
220
|
+
rules = 0;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const patched = /^patch: (completed|failed|declined|in_progress)$/.exec(l);
|
|
224
|
+
if (patched) {
|
|
225
|
+
settle();
|
|
226
|
+
const e = edit && edit.card.ran === 'running' ? edit : patch();
|
|
227
|
+
e.card.ran = patched[1] === 'completed' ? 'done' : patched[1] === 'in_progress' ? 'running' : 'error';
|
|
228
|
+
card = null;
|
|
229
|
+
mode = 'patch';
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (mode === 'patch' && l.startsWith('diff --git ')) {
|
|
233
|
+
diff = [l];
|
|
234
|
+
mode = 'diff';
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const mcp = /^mcp: (\S+) (started|\((completed|failed|in_progress)\))$/.exec(l);
|
|
238
|
+
if (mcp) {
|
|
239
|
+
settle();
|
|
240
|
+
const ran = mcp[3] === 'completed' ? 'done' : mcp[3] === 'failed' ? 'error' : 'running';
|
|
241
|
+
const started = [...out].reverse().find((c) => c.kind === 'step' && c.name === mcp[1] && c.ran === 'running');
|
|
242
|
+
if (started && started.kind === 'step' && mcp[2] !== 'started')
|
|
243
|
+
started.ran = ran;
|
|
244
|
+
else
|
|
245
|
+
step(mcp[1], 'tool', ran);
|
|
246
|
+
card = null;
|
|
247
|
+
mode = 'prose';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const web = /^web search: (.*)$/.exec(l);
|
|
251
|
+
if (web) {
|
|
252
|
+
settle();
|
|
253
|
+
const prior = out[out.length - 1];
|
|
254
|
+
// Printed when the search starts and again when it lands: one card.
|
|
255
|
+
if (!(prior && prior.kind === 'step' && prior.name === 'Web search' && prior.detail === web[1]))
|
|
256
|
+
step('Web search', web[1], 'done');
|
|
257
|
+
card = null;
|
|
258
|
+
mode = 'prose';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const warn = /^(warning|ERROR|deprecated|model rerouted): ?(.*)$/.exec(l);
|
|
262
|
+
if (warn) {
|
|
263
|
+
settle();
|
|
264
|
+
out.push({ kind: 'note', key: mint(), text: warn[1] === 'ERROR' ? warn[2] : `${warn[1]}: ${warn[2]}` });
|
|
265
|
+
card = null;
|
|
266
|
+
mode = 'prose';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (l.startsWith('hook: '))
|
|
270
|
+
return;
|
|
271
|
+
const todo = /^ {2}(✓|→|•) (.*)$/.exec(l);
|
|
272
|
+
if (todo && mode !== 'output' && mode !== 'agent') {
|
|
273
|
+
const row = `${todo[1] === '✓' ? '✓' : todo[1] === '→' ? '●' : '○'} ${todo[2]}`;
|
|
274
|
+
if (card && card.kind === 'step' && card.name === 'Plan')
|
|
275
|
+
card.output += `\n${row}`;
|
|
276
|
+
else {
|
|
277
|
+
card = step('Plan', '', 'done');
|
|
278
|
+
if (card.kind === 'step')
|
|
279
|
+
card.output = row;
|
|
280
|
+
}
|
|
281
|
+
mode = 'prose';
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
switch (mode) {
|
|
285
|
+
case 'user':
|
|
286
|
+
return;
|
|
287
|
+
case 'patch':
|
|
288
|
+
if (l.trim() && edit)
|
|
289
|
+
edit.paths.push(l.trim());
|
|
290
|
+
return;
|
|
291
|
+
case 'final':
|
|
292
|
+
final.push(l);
|
|
293
|
+
return;
|
|
294
|
+
case 'output':
|
|
295
|
+
if (card && card.kind === 'shell')
|
|
296
|
+
card.output += card.output ? `\n${l}` : l;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Prose: what the agent said under its header, or lines no header owns.
|
|
300
|
+
if (card && card.kind === 'said') {
|
|
301
|
+
card.text += card.text ? `\n${l}` : l;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!l.trim())
|
|
305
|
+
return;
|
|
306
|
+
card = said(l);
|
|
307
|
+
mode = 'agent';
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
feed(chunk) {
|
|
311
|
+
const text = rest + chunk;
|
|
312
|
+
const lines = text.split('\n');
|
|
313
|
+
rest = lines.pop() ?? '';
|
|
314
|
+
for (const l of lines)
|
|
315
|
+
line(l);
|
|
316
|
+
},
|
|
317
|
+
/** The last line even without its newline, and what only the whole output can decide. */
|
|
318
|
+
end() {
|
|
319
|
+
if (rest)
|
|
320
|
+
line(rest);
|
|
321
|
+
rest = '';
|
|
322
|
+
settle();
|
|
323
|
+
// The final message is printed twice: under its header, and bare at the end.
|
|
324
|
+
const tail = final.join('\n').trim();
|
|
325
|
+
const spoken = out.filter((c) => c.kind === 'said' && c.who === 'agent').map((c) => c.text.trim());
|
|
326
|
+
if (tail && !spoken.includes(tail))
|
|
327
|
+
said(tail);
|
|
328
|
+
final = [];
|
|
329
|
+
// A patch names the sandbox's paths; the diff names them as the repository
|
|
330
|
+
// does, and is the whole turn's, so an edit keeps the sections of its own files.
|
|
331
|
+
for (const e of edits) {
|
|
332
|
+
const parts = sections(e.card.patch);
|
|
333
|
+
const known = [...parts.keys()];
|
|
334
|
+
e.card.files = e.paths.map((p) => known.find((k) => p === k || p.endsWith(`/${k}`)) ?? shown(p));
|
|
335
|
+
if (!e.card.files.length)
|
|
336
|
+
e.card.files = known;
|
|
337
|
+
const mine = e.card.files.filter((f) => parts.has(f));
|
|
338
|
+
e.card.patch = mine.length === 1 ? parts.get(mine[0]) : mine.map((f) => `${f}\n${parts.get(f)}`).join('\n');
|
|
339
|
+
}
|
|
340
|
+
edits.length = 0;
|
|
341
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
342
|
+
const c = out[i];
|
|
343
|
+
if (c.kind === 'said' && !c.text.trim())
|
|
344
|
+
out.splice(i, 1);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's own API keys: one secret key (sk-, it belongs on a server) and one
|
|
3
|
+
* publishable key (pk-, safe in a page's source), at most one of each.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/account/keys {keys: [{type, prefix, key?, limit?, createdAt}]}
|
|
6
|
+
* POST /v1/account/keys {type, limit?} → {key, type, limit}, answered once
|
|
7
|
+
* DELETE /v1/account/keys?type=… revoke that one
|
|
8
|
+
*
|
|
9
|
+
* Creating is rotating: a new key of a type ends the one before it. A secret key
|
|
10
|
+
* is answered once and by its prefix after; a publishable key is public by
|
|
11
|
+
* construction and always comes back whole. A limit only narrows what a key may
|
|
12
|
+
* reach — `model:zen5`, `project:acme`, `product:commerce`, `model:*`.
|
|
13
|
+
*/
|
|
14
|
+
import { type Target } from './call.ts';
|
|
15
|
+
export declare const KINDS: readonly ['secret', 'publishable'];
|
|
16
|
+
export type Kind = (typeof KINDS)[number];
|
|
17
|
+
export interface Key {
|
|
18
|
+
type: Kind;
|
|
19
|
+
/** The recognizable head, never enough to use the key. */
|
|
20
|
+
prefix: string;
|
|
21
|
+
/** The whole key: a publishable key's only. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** `kind:name` entries; none reaches whatever its holder does. */
|
|
24
|
+
limit: string[];
|
|
25
|
+
/** When it last changed, as IAM records it. */
|
|
26
|
+
created: string;
|
|
27
|
+
}
|
|
28
|
+
export interface Minted {
|
|
29
|
+
type: Kind;
|
|
30
|
+
/** The key itself. A secret one is never readable again. */
|
|
31
|
+
key: string;
|
|
32
|
+
limit: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function keys(t: Target): Promise<Key[]>;
|
|
35
|
+
/** A `kind:name` limit: a lowercase kind, a colon, and a name or `*`. */
|
|
36
|
+
export declare const LIMIT: RegExp;
|
|
37
|
+
/** The limits a person typed, split on commas and spaces; throws on the first that is not one. */
|
|
38
|
+
export declare function limits(raw: string): string[];
|
|
39
|
+
/** Create a key of this type, ending the one before it. The answer is the only time a secret key is shown. */
|
|
40
|
+
export declare function mint(t: Target, type: Kind, limit?: string[]): Promise<Minted>;
|
|
41
|
+
export declare function revoke(t: Target, type: Kind): Promise<void>;
|