@hanzo/build 0.2.52 → 0.2.53
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 +103 -49
- package/lib/api/billing.d.ts +39 -0
- package/lib/api/billing.js +41 -0
- package/lib/api/coding.d.ts +4 -1
- package/lib/api/coding.js +2 -0
- package/lib/api/environment.d.ts +12 -0
- package/lib/api/environment.js +39 -1
- package/lib/api/github.d.ts +24 -4
- package/lib/api/github.js +60 -16
- package/lib/api/limits.d.ts +69 -0
- package/lib/api/limits.js +86 -0
- package/lib/api/links.d.ts +42 -0
- package/lib/api/links.js +65 -0
- package/lib/api/pref.d.ts +5 -3
- package/lib/api/pref.js +0 -3
- package/lib/api/turn.d.ts +10 -1
- package/lib/api/turn.js +16 -4
- package/lib/api/work.d.ts +75 -38
- package/lib/api/work.js +132 -44
- package/lib/builder.d.ts +0 -10
- package/lib/builder.js +12 -52
- package/lib/choice.d.ts +21 -0
- package/lib/choice.js +40 -1
- package/lib/customize/agents.d.ts +1 -1
- package/lib/customize/agents.js +21 -19
- package/lib/customize/connectors.d.ts +1 -1
- package/lib/customize/connectors.js +80 -31
- package/lib/customize/index.d.ts +2 -0
- package/lib/customize/index.js +23 -19
- package/lib/customize/load.d.ts +9 -0
- package/lib/customize/load.js +22 -0
- package/lib/customize/plugins.d.ts +1 -1
- package/lib/customize/plugins.js +21 -19
- package/lib/customize/say.d.ts +1 -0
- package/lib/customize/say.js +38 -0
- package/lib/customize/skills.d.ts +1 -1
- package/lib/customize/skills.js +23 -17
- package/lib/customize/ui.d.ts +28 -11
- package/lib/customize/ui.js +29 -21
- package/lib/data.d.ts +13 -0
- package/lib/data.js +70 -0
- package/lib/desk.d.ts +6 -1
- package/lib/desk.js +43 -19
- package/lib/environment.d.ts +11 -3
- package/lib/environment.js +53 -13
- package/lib/foot.js +10 -6
- package/lib/forge.js +139 -71
- package/lib/index.d.ts +28 -2
- package/lib/index.js +26 -2
- package/lib/landing.d.ts +0 -10
- package/lib/landing.js +70 -38
- package/lib/merge.d.ts +223 -0
- package/lib/merge.js +380 -0
- package/lib/meter.d.ts +31 -0
- package/lib/meter.js +45 -0
- package/lib/mind.d.ts +60 -0
- package/lib/mind.js +187 -0
- package/lib/nav.d.ts +11 -0
- package/lib/nav.js +75 -0
- package/lib/panel.d.ts +45 -0
- package/lib/panel.js +115 -0
- package/lib/pick.d.ts +8 -0
- package/lib/pick.js +18 -1
- package/lib/plan.d.ts +78 -0
- package/lib/plan.js +133 -0
- package/lib/project.js +16 -6
- package/lib/prompt.d.ts +75 -0
- package/lib/prompt.js +125 -0
- package/lib/prose.js +14 -10
- package/lib/reply.d.ts +36 -0
- package/lib/reply.js +124 -0
- package/lib/route.d.ts +16 -0
- package/lib/route.js +40 -0
- package/lib/run.js +87 -52
- package/lib/section.d.ts +1 -1
- package/lib/section.js +7 -11
- package/lib/settings/billing.js +16 -9
- package/lib/settings/code.js +15 -12
- package/lib/settings/credits.d.ts +9 -0
- package/lib/settings/credits.js +110 -0
- package/lib/settings/plan.d.ts +17 -0
- package/lib/settings/plan.js +108 -0
- package/lib/settings/usage.js +11 -119
- package/lib/shelf.js +3 -3
- package/lib/standing.d.ts +44 -0
- package/lib/standing.js +64 -0
- package/lib/sync.js +22 -8
- package/lib/tabs.d.ts +53 -0
- package/lib/tabs.js +197 -0
- package/lib/tools.d.ts +2 -9
- package/lib/tools.js +20 -25
- package/lib/transcript.d.ts +10 -1
- package/lib/transcript.js +25 -10
- package/lib/voice.d.ts +2 -13
- package/lib/voice.js +7 -73
- package/package.json +20 -3
package/lib/api/github.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* and that page completes it (`complete`, and `landed` in ../back.ts).
|
|
22
22
|
*/
|
|
23
23
|
import { call, query, seg } from './call.js';
|
|
24
|
+
import { codebases } from './codebases.js';
|
|
25
|
+
import { links } from './links.js';
|
|
24
26
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
25
27
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
26
28
|
const obj = (v) => v && typeof v === 'object' ? v : {};
|
|
@@ -36,6 +38,7 @@ export function repo(raw) {
|
|
|
36
38
|
default_branch: str(r.default_branch),
|
|
37
39
|
pushed_at: str(r.pushed_at),
|
|
38
40
|
installation_id: num(r.installation_id),
|
|
41
|
+
codebase: str(r.codebase),
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
export function branch(raw) {
|
|
@@ -94,21 +97,28 @@ export async function installations(t) {
|
|
|
94
97
|
.map((i) => ({ login: str(i.login), type: str(i.type), grant: str(i.grant), connected: i.connected === true }))
|
|
95
98
|
.filter((i) => i.login);
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* One row of GET /v1/provider/github/repos (apps/provider github_index.go
|
|
102
|
+
* repoView): `owner`, `name`, `full_name`, `private`, `default_branch`,
|
|
103
|
+
* `pushed_at`, `installation_id`, `codebase`. Whether it is on the forge is not
|
|
104
|
+
* in the row — `codebase` is the name it would take, not a sign it has — so
|
|
105
|
+
* `imported` is decided by `grants` from the links and the forge.
|
|
106
|
+
*/
|
|
97
107
|
export function grant(raw) {
|
|
98
|
-
const r =
|
|
99
|
-
|
|
100
|
-
if (!name)
|
|
108
|
+
const r = repo(raw);
|
|
109
|
+
if (!r.name)
|
|
101
110
|
return null;
|
|
102
|
-
const full =
|
|
103
|
-
const owner =
|
|
111
|
+
const full = r.full_name;
|
|
112
|
+
const owner = r.owner || (full.includes('/') ? full.slice(0, full.indexOf('/')) : '');
|
|
104
113
|
return {
|
|
105
114
|
owner,
|
|
106
|
-
name,
|
|
107
|
-
fullName: full || (owner ? `${owner}/${name}` : name),
|
|
108
|
-
private: r.private
|
|
109
|
-
branch:
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
name: r.name,
|
|
116
|
+
fullName: full || (owner ? `${owner}/${r.name}` : r.name),
|
|
117
|
+
private: r.private,
|
|
118
|
+
branch: r.default_branch || 'main',
|
|
119
|
+
codebase: r.codebase,
|
|
120
|
+
imported: false,
|
|
121
|
+
status: '',
|
|
112
122
|
};
|
|
113
123
|
}
|
|
114
124
|
/** Accounts the grant names, plus any account that could not be read. */
|
|
@@ -128,12 +138,39 @@ export function accounts(g) {
|
|
|
128
138
|
return { name, count, blocked: unread.has(name) && count === 0 };
|
|
129
139
|
});
|
|
130
140
|
}
|
|
131
|
-
/**
|
|
141
|
+
/** How many of GitHub's pages a full read takes at most: a hundred rows each. */
|
|
142
|
+
const PAGES = 20;
|
|
143
|
+
/**
|
|
144
|
+
* Every repository the GitHub connection grants, every page, with whether the
|
|
145
|
+
* forge already has it: a link that has landed (GET /v1/sync, its `native`
|
|
146
|
+
* state), or a forge repository under its codebase name (GET /v1/git/repos).
|
|
147
|
+
*/
|
|
132
148
|
export async function grants(t, signal) {
|
|
133
|
-
const
|
|
149
|
+
const rows = [];
|
|
150
|
+
let unread = [];
|
|
151
|
+
let after = '';
|
|
152
|
+
for (let n = 0; n < PAGES; n++) {
|
|
153
|
+
const page = await repos(t, { limit: 100, after }, signal);
|
|
154
|
+
rows.push(...page.repos);
|
|
155
|
+
unread = page.unread;
|
|
156
|
+
if (!page.next)
|
|
157
|
+
break;
|
|
158
|
+
after = page.next;
|
|
159
|
+
}
|
|
160
|
+
const [linked, held] = await Promise.all([links(t, signal).catch(() => []), codebases(t).catch(() => [])]);
|
|
161
|
+
const state = new Map(linked.map((l) => [`${l.owner}/${l.name}`.toLowerCase(), l.status]));
|
|
162
|
+
const names = new Set(held.map((c) => c.name.toLowerCase()));
|
|
134
163
|
return {
|
|
135
|
-
repos:
|
|
136
|
-
|
|
164
|
+
repos: rows
|
|
165
|
+
.map(grant)
|
|
166
|
+
.filter((r) => r !== null)
|
|
167
|
+
.map((r) => {
|
|
168
|
+
// A link's state, '' when the forge could not be read for it; absent with no link.
|
|
169
|
+
const said = state.get(r.fullName.toLowerCase());
|
|
170
|
+
const there = names.has(r.codebase.toLowerCase());
|
|
171
|
+
return { ...r, imported: there || said === 'synced' || said === 'conflict' || said === 'paused', status: said || (there ? 'synced' : '') };
|
|
172
|
+
}),
|
|
173
|
+
unread: unread.filter(Boolean),
|
|
137
174
|
};
|
|
138
175
|
}
|
|
139
176
|
/** Queue a mirror of the named repositories onto the forge. The names are `owner/name`. */
|
|
@@ -142,7 +179,14 @@ export async function bring(t, repos) {
|
|
|
142
179
|
if (names.length === 0)
|
|
143
180
|
throw new Error('Choose a repository first');
|
|
144
181
|
const raw = obj(await call(t, 'POST', '/v1/provider/github/repos/import', { repos: names }));
|
|
145
|
-
|
|
182
|
+
const said = Array.isArray(raw.repos) ? raw.repos : [];
|
|
183
|
+
const made = Array.isArray(raw.codebases) ? raw.codebases : [];
|
|
184
|
+
const codebases = {};
|
|
185
|
+
said.forEach((r, i) => {
|
|
186
|
+
if (typeof r === 'string' && typeof made[i] === 'string' && made[i])
|
|
187
|
+
codebases[r.toLowerCase()] = made[i];
|
|
188
|
+
});
|
|
189
|
+
return { queued: num(raw.queued), codebases };
|
|
146
190
|
}
|
|
147
191
|
/**
|
|
148
192
|
* Mirror GitHub issues onto the board — open and closed, so a closed one reads as
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan's usage, as the gate counts it.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/ai/limits the plan, its request windows and each class it includes, as shares
|
|
5
|
+
* PUT /v1/ai/limits {creditsAfterAllowance}: keep using a model on credits once the
|
|
6
|
+
* plan's included usage of it is spent; answers the limits after it
|
|
7
|
+
*
|
|
8
|
+
* The shape is cloud's `aiLimits` (apps/ai/limits_ops.go): `plan` is the rung the
|
|
9
|
+
* caller is served as, `free` with none; `session` and `day` bound every request
|
|
10
|
+
* the plan covers; `classes.premium` (third-party frontier models) and
|
|
11
|
+
* `classes.ours` (Hanzo's) are each the billing period's share used, with a
|
|
12
|
+
* shorter `window` where the plan sets one. Never a dollar, a count or a cap.
|
|
13
|
+
*
|
|
14
|
+
* Only an org admin may change how a pooled org wallet pays (403 otherwise).
|
|
15
|
+
*/
|
|
16
|
+
import { type Share, type State } from '../plan.ts';
|
|
17
|
+
import { type Target } from './call.ts';
|
|
18
|
+
export type Class = 'premium' | 'ours';
|
|
19
|
+
export declare const CLASSES: readonly Class[];
|
|
20
|
+
/** A class's billing period, who pays for its next request, and its short window. */
|
|
21
|
+
export interface Allowance extends Share {
|
|
22
|
+
/** plan, prepaid, credits, or none. */
|
|
23
|
+
paying: string;
|
|
24
|
+
window: Share | null;
|
|
25
|
+
}
|
|
26
|
+
/** A way past a limit, in the order the server sends them. */
|
|
27
|
+
export interface Action {
|
|
28
|
+
/** upgrade and topup are pages (`url`); credits is the choice above; switch names a model. */
|
|
29
|
+
kind: 'upgrade' | 'topup' | 'credits' | 'switch';
|
|
30
|
+
label: string;
|
|
31
|
+
url: string;
|
|
32
|
+
plan: string;
|
|
33
|
+
model: string;
|
|
34
|
+
}
|
|
35
|
+
/** One model whose share of the plan is used for now, answered by its fallback in chat. */
|
|
36
|
+
export interface Paused {
|
|
37
|
+
model: string;
|
|
38
|
+
fallback: string;
|
|
39
|
+
resets: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Limits {
|
|
42
|
+
/** The rung served (`max-20x`); `free` where no plan counts. */
|
|
43
|
+
plan: string;
|
|
44
|
+
/** The billing period, RFC 3339; '' with no plan. */
|
|
45
|
+
start: string;
|
|
46
|
+
end: string;
|
|
47
|
+
/** The worst class's state. */
|
|
48
|
+
state: State;
|
|
49
|
+
session: Share | null;
|
|
50
|
+
day: Share | null;
|
|
51
|
+
classes: Partial<Record<Class, Allowance>>;
|
|
52
|
+
/** Present in limited mode: why, and in which classes. */
|
|
53
|
+
limited: {
|
|
54
|
+
reason: string;
|
|
55
|
+
classes: Class[];
|
|
56
|
+
message: string;
|
|
57
|
+
} | null;
|
|
58
|
+
actions: Action[];
|
|
59
|
+
/** The plan that raises these limits, '' at the top. */
|
|
60
|
+
upgrade: string;
|
|
61
|
+
/** Whether the payer keeps using a model on credits once the plan's usage of it is spent. */
|
|
62
|
+
credits: boolean;
|
|
63
|
+
paused: Paused[];
|
|
64
|
+
}
|
|
65
|
+
/** The body of `GET /v1/ai/limits`, read field by field: what is not there is not drawn. */
|
|
66
|
+
export declare function parse(raw: unknown): Limits;
|
|
67
|
+
export declare function limits(t: Target): Promise<Limits>;
|
|
68
|
+
/** Turn on or off paying from credits once the plan's included usage runs out; answers the limits after. */
|
|
69
|
+
export declare function allow(t: Target, on: boolean): Promise<Limits>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan's usage, as the gate counts it.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/ai/limits the plan, its request windows and each class it includes, as shares
|
|
5
|
+
* PUT /v1/ai/limits {creditsAfterAllowance}: keep using a model on credits once the
|
|
6
|
+
* plan's included usage of it is spent; answers the limits after it
|
|
7
|
+
*
|
|
8
|
+
* The shape is cloud's `aiLimits` (apps/ai/limits_ops.go): `plan` is the rung the
|
|
9
|
+
* caller is served as, `free` with none; `session` and `day` bound every request
|
|
10
|
+
* the plan covers; `classes.premium` (third-party frontier models) and
|
|
11
|
+
* `classes.ours` (Hanzo's) are each the billing period's share used, with a
|
|
12
|
+
* shorter `window` where the plan sets one. Never a dollar, a count or a cap.
|
|
13
|
+
*
|
|
14
|
+
* Only an org admin may change how a pooled org wallet pays (403 otherwise).
|
|
15
|
+
*/
|
|
16
|
+
import { share } from '../plan.js';
|
|
17
|
+
import { call } from './call.js';
|
|
18
|
+
export const CLASSES = ['premium', 'ours'];
|
|
19
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
20
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
21
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
22
|
+
const KINDS = ['upgrade', 'topup', 'credits', 'switch'];
|
|
23
|
+
const STATES = ['ok', 'near', 'limited'];
|
|
24
|
+
/** A class off the wire: its period always carries a state, so one without is not a class. */
|
|
25
|
+
function allowance(v) {
|
|
26
|
+
const w = share(v);
|
|
27
|
+
if (!w)
|
|
28
|
+
return null;
|
|
29
|
+
const o = obj(v);
|
|
30
|
+
return { ...w, paying: str(o.paying) || 'none', window: share(o.window) };
|
|
31
|
+
}
|
|
32
|
+
/** The body of `GET /v1/ai/limits`, read field by field: what is not there is not drawn. */
|
|
33
|
+
export function parse(raw) {
|
|
34
|
+
const r = obj(raw);
|
|
35
|
+
const classes = {};
|
|
36
|
+
for (const c of CLASSES) {
|
|
37
|
+
const got = allowance(obj(r.classes)[c]);
|
|
38
|
+
if (got)
|
|
39
|
+
classes[c] = got;
|
|
40
|
+
}
|
|
41
|
+
const lim = r.limited ? obj(r.limited) : null;
|
|
42
|
+
return {
|
|
43
|
+
plan: str(r.plan) || 'free',
|
|
44
|
+
start: str(r.period_start),
|
|
45
|
+
end: str(r.period_end),
|
|
46
|
+
state: STATES.find((s) => s === r.state) ?? 'ok',
|
|
47
|
+
session: share(r.session),
|
|
48
|
+
day: share(r.day),
|
|
49
|
+
classes,
|
|
50
|
+
limited: lim
|
|
51
|
+
? {
|
|
52
|
+
reason: str(lim.reason),
|
|
53
|
+
classes: arr(lim.classes).filter((c) => CLASSES.includes(c)),
|
|
54
|
+
message: str(lim.message),
|
|
55
|
+
}
|
|
56
|
+
: null,
|
|
57
|
+
actions: arr(r.actions)
|
|
58
|
+
.map(obj)
|
|
59
|
+
.flatMap((a) => {
|
|
60
|
+
const kind = KINDS.find((k) => k === a.kind);
|
|
61
|
+
if (!kind)
|
|
62
|
+
return [];
|
|
63
|
+
const url = str(a.url);
|
|
64
|
+
const model = str(a.model);
|
|
65
|
+
// A page with no address, or a switch to no model, is not a way past anything.
|
|
66
|
+
if ((kind === 'upgrade' || kind === 'topup') && !url)
|
|
67
|
+
return [];
|
|
68
|
+
if (kind === 'switch' && !model)
|
|
69
|
+
return [];
|
|
70
|
+
return [{ kind, label: str(a.label), url, plan: str(a.plan), model }];
|
|
71
|
+
}),
|
|
72
|
+
upgrade: str(r.upgrade),
|
|
73
|
+
credits: r.credits_after_allowance === true,
|
|
74
|
+
paused: arr(r.paused)
|
|
75
|
+
.map(obj)
|
|
76
|
+
.filter((p) => str(p.model))
|
|
77
|
+
.map((p) => ({ model: str(p.model), fallback: str(p.fallback), resets: str(p.resets_at) })),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function limits(t) {
|
|
81
|
+
return parse(await call(t, 'GET', '/v1/ai/limits'));
|
|
82
|
+
}
|
|
83
|
+
/** Turn on or off paying from credits once the plan's included usage runs out; answers the limits after. */
|
|
84
|
+
export async function allow(t, on) {
|
|
85
|
+
return parse(await call(t, 'PUT', '/v1/ai/limits', { creditsAfterAllowance: on }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repositories an organization has linked from GitHub, as the sync app
|
|
3
|
+
* holds them: one link per repository (an account link declares one for every
|
|
4
|
+
* repository its account holds), each with its copy on the forge.
|
|
5
|
+
*
|
|
6
|
+
* GET /v1/sync {data: [syncView]} apps/sync sync_api.go
|
|
7
|
+
*
|
|
8
|
+
* A repository link's source is GitHub's https clone address; its target is the
|
|
9
|
+
* forge, by the bare name the copy has there. `updatedAt` is bumped by every
|
|
10
|
+
* reconcile, so it reads as the last time the link synced. `native` is the
|
|
11
|
+
* forge copy: its page, its default branch and how it stands — `synced`,
|
|
12
|
+
* `conflict`, `pending` (not on the forge yet) or `paused` — and is absent when
|
|
13
|
+
* the forge could not be read, which is not reported as healthy.
|
|
14
|
+
*
|
|
15
|
+
* Every link the sync app holds is also a row of GET /v1/projects (apps/sync
|
|
16
|
+
* project.go), so this list is the org's linked projects and not a copy of them.
|
|
17
|
+
*/
|
|
18
|
+
import { type Target } from './call.ts';
|
|
19
|
+
export interface Link {
|
|
20
|
+
/** The GitHub account. */
|
|
21
|
+
owner: string;
|
|
22
|
+
/** The repository; '' for an account link. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** The forge copy's name, which a run and an environment address; ''. */
|
|
25
|
+
forge: string;
|
|
26
|
+
/** RFC 3339: when it last synced, or ''. */
|
|
27
|
+
synced: string;
|
|
28
|
+
/** `synced`, `conflict`, `pending`, `paused`, or '' when the forge could not be read. */
|
|
29
|
+
status: string;
|
|
30
|
+
/** The forge copy's default branch, or ''. */
|
|
31
|
+
branch: string;
|
|
32
|
+
/** `both`, `pull`, `push` or `off`. */
|
|
33
|
+
direction: string;
|
|
34
|
+
}
|
|
35
|
+
/** `owner` and `name` of a GitHub https address, `name` '' for an account; null for any other host. */
|
|
36
|
+
export declare function github(locator: string): {
|
|
37
|
+
owner: string;
|
|
38
|
+
name: string;
|
|
39
|
+
} | null;
|
|
40
|
+
export declare function link(raw: unknown): Link | null;
|
|
41
|
+
/** Every repository link the org has; account links are left out, as each declares its repositories' own. */
|
|
42
|
+
export declare function links(t: Target, signal?: AbortSignal): Promise<Link[]>;
|
package/lib/api/links.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repositories an organization has linked from GitHub, as the sync app
|
|
3
|
+
* holds them: one link per repository (an account link declares one for every
|
|
4
|
+
* repository its account holds), each with its copy on the forge.
|
|
5
|
+
*
|
|
6
|
+
* GET /v1/sync {data: [syncView]} apps/sync sync_api.go
|
|
7
|
+
*
|
|
8
|
+
* A repository link's source is GitHub's https clone address; its target is the
|
|
9
|
+
* forge, by the bare name the copy has there. `updatedAt` is bumped by every
|
|
10
|
+
* reconcile, so it reads as the last time the link synced. `native` is the
|
|
11
|
+
* forge copy: its page, its default branch and how it stands — `synced`,
|
|
12
|
+
* `conflict`, `pending` (not on the forge yet) or `paused` — and is absent when
|
|
13
|
+
* the forge could not be read, which is not reported as healthy.
|
|
14
|
+
*
|
|
15
|
+
* Every link the sync app holds is also a row of GET /v1/projects (apps/sync
|
|
16
|
+
* project.go), so this list is the org's linked projects and not a copy of them.
|
|
17
|
+
*/
|
|
18
|
+
import { call } from './call.js';
|
|
19
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
20
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
21
|
+
/** `owner` and `name` of a GitHub https address, `name` '' for an account; null for any other host. */
|
|
22
|
+
export function github(locator) {
|
|
23
|
+
try {
|
|
24
|
+
const u = new URL(locator.trim());
|
|
25
|
+
if (u.protocol !== 'https:' || u.hostname.toLowerCase() !== 'github.com')
|
|
26
|
+
return null;
|
|
27
|
+
const [owner = '', name = '', ...rest] = u.pathname
|
|
28
|
+
.replace(/\.git$/, '')
|
|
29
|
+
.split('/')
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (!owner || rest.length)
|
|
32
|
+
return null;
|
|
33
|
+
return { owner, name };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function link(raw) {
|
|
40
|
+
const o = obj(raw);
|
|
41
|
+
if (str(o.kind) && str(o.kind) !== 'git')
|
|
42
|
+
return null;
|
|
43
|
+
const source = obj(o.source);
|
|
44
|
+
const target = obj(o.target);
|
|
45
|
+
const at = str(source.provider) === 'github' ? github(str(source.locator)) : null;
|
|
46
|
+
if (!at)
|
|
47
|
+
return null;
|
|
48
|
+
const native = o.native ? obj(o.native) : null;
|
|
49
|
+
return {
|
|
50
|
+
owner: at.owner,
|
|
51
|
+
name: str(o.scope) === 'account' ? '' : at.name,
|
|
52
|
+
forge: str(target.provider) === 'hanzo-git' ? str(target.locator) : '',
|
|
53
|
+
synced: str(o.updatedAt),
|
|
54
|
+
status: native ? str(native.status) : '',
|
|
55
|
+
branch: native ? str(native.branch) : '',
|
|
56
|
+
direction: str(o.direction),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Every repository link the org has; account links are left out, as each declares its repositories' own. */
|
|
60
|
+
export async function links(t, signal) {
|
|
61
|
+
const raw = obj(await call(t, 'GET', '/v1/sync', undefined, { signal }));
|
|
62
|
+
return (Array.isArray(raw.data) ? raw.data : [])
|
|
63
|
+
.map(link)
|
|
64
|
+
.filter((l) => l !== null && l.name !== '');
|
|
65
|
+
}
|
package/lib/api/pref.d.ts
CHANGED
|
@@ -14,10 +14,12 @@ import { type Target } from './call.ts';
|
|
|
14
14
|
export type Theme = 'system' | 'light' | 'dark';
|
|
15
15
|
export type Text = 'small' | 'medium' | 'large';
|
|
16
16
|
export type Motion = 'system' | 'reduced';
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Where and how a new run starts when New has no choice of its own kept. The
|
|
19
|
+
* model and effort are not here: they are the one choice Chat and Dev share,
|
|
20
|
+
* kept in the browser (mind.tsx).
|
|
21
|
+
*/
|
|
18
22
|
export interface Code {
|
|
19
|
-
model?: string;
|
|
20
|
-
effort?: 'low' | 'medium' | 'high';
|
|
21
23
|
mode?: 'build' | 'plan';
|
|
22
24
|
/** A machine's id, or '' for the sandbox. */
|
|
23
25
|
place?: string;
|
package/lib/api/pref.js
CHANGED
|
@@ -14,7 +14,6 @@ import { call } from './call.js';
|
|
|
14
14
|
const THEMES = ['system', 'light', 'dark'];
|
|
15
15
|
const TEXTS = ['small', 'medium', 'large'];
|
|
16
16
|
const MOTIONS = ['system', 'reduced'];
|
|
17
|
-
const EFFORTS = ['low', 'medium', 'high'];
|
|
18
17
|
const MODES = ['build', 'plan'];
|
|
19
18
|
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
20
19
|
const text = (v) => (typeof v === 'string' && v.trim() ? v : undefined);
|
|
@@ -26,8 +25,6 @@ function defined(o) {
|
|
|
26
25
|
export function code(raw) {
|
|
27
26
|
const c = obj(raw);
|
|
28
27
|
const out = defined({
|
|
29
|
-
model: text(c.model),
|
|
30
|
-
effort: one(c.effort, EFFORTS),
|
|
31
28
|
mode: one(c.mode, MODES),
|
|
32
29
|
place: typeof c.place === 'string' ? c.place : undefined,
|
|
33
30
|
});
|
package/lib/api/turn.d.ts
CHANGED
|
@@ -24,9 +24,18 @@
|
|
|
24
24
|
import type { Event } from './sessions.ts';
|
|
25
25
|
/** The payload as an object. A live frame may carry it as a JSON string. */
|
|
26
26
|
export declare function decode(payload: unknown): Record<string, unknown> | string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Whether an error is a model vendor's own words: a provider's name, its rate
|
|
29
|
+
* headers, a key, or the JSON body it answers with. Those never reach the run
|
|
30
|
+
* page — they say whose model it was and nothing a person can act on — so the
|
|
31
|
+
* run says its plain line alone.
|
|
32
|
+
*/
|
|
33
|
+
export declare function vendor(error: string): boolean;
|
|
34
|
+
/** An error as the run page may show it: its own words, or the plain line when they are a vendor's. */
|
|
35
|
+
export declare const scrub: (error: string) => string;
|
|
27
36
|
/**
|
|
28
37
|
* A run's error as one plain line with what to do, never the log it came from:
|
|
29
|
-
* that goes in the Error step beside it (cards).
|
|
38
|
+
* that goes in the Error step beside it (cards), unless it is a vendor's.
|
|
30
39
|
*/
|
|
31
40
|
export declare function plain(error: string): string;
|
|
32
41
|
/**
|
package/lib/api/turn.js
CHANGED
|
@@ -59,9 +59,20 @@ function status(b, mode) {
|
|
|
59
59
|
}
|
|
60
60
|
return str(b.status);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether an error is a model vendor's own words: a provider's name, its rate
|
|
64
|
+
* headers, a key, or the JSON body it answers with. Those never reach the run
|
|
65
|
+
* page — they say whose model it was and nothing a person can act on — so the
|
|
66
|
+
* run says its plain line alone.
|
|
67
|
+
*/
|
|
68
|
+
export function vendor(error) {
|
|
69
|
+
return /open\s?router|anthropic|openai|azure|bedrock|vertex|gemini|mistral|cohere|deepinfra|fireworks|together\.ai|groq|x-ratelimit|upstream|\bprovider\b|\bsk-[A-Za-z0-9_-]{6,}|"(error|code|message)"\s*:/i.test(error);
|
|
70
|
+
}
|
|
71
|
+
/** An error as the run page may show it: its own words, or the plain line when they are a vendor's. */
|
|
72
|
+
export const scrub = (error) => (vendor(error) ? plain(error) : error);
|
|
62
73
|
/**
|
|
63
74
|
* A run's error as one plain line with what to do, never the log it came from:
|
|
64
|
-
* that goes in the Error step beside it (cards).
|
|
75
|
+
* that goes in the Error step beside it (cards), unless it is a vendor's.
|
|
65
76
|
*/
|
|
66
77
|
export function plain(error) {
|
|
67
78
|
if (!error)
|
|
@@ -101,7 +112,7 @@ export function said(e, mode = '') {
|
|
|
101
112
|
: `Done — changed ${names.length} files`;
|
|
102
113
|
}
|
|
103
114
|
if (str(body.type) === 'error')
|
|
104
|
-
return str(body.error) || 'The run hit an error';
|
|
115
|
+
return scrub(str(body.error)) || 'The run hit an error';
|
|
105
116
|
return str(body.step) || str(body.name);
|
|
106
117
|
}
|
|
107
118
|
/**
|
|
@@ -197,7 +208,7 @@ export function failure(events) {
|
|
|
197
208
|
const b = decode(e.payload);
|
|
198
209
|
if (!b || typeof b === 'string' || !str(b.status))
|
|
199
210
|
continue;
|
|
200
|
-
out = str(b.status) === 'error' ? str(b.error) || 'The run hit an error' : '';
|
|
211
|
+
out = str(b.status) === 'error' ? scrub(str(b.error)) || 'The run hit an error' : '';
|
|
201
212
|
}
|
|
202
213
|
return out;
|
|
203
214
|
}
|
|
@@ -324,7 +335,8 @@ export function cards(events, mode = '') {
|
|
|
324
335
|
return;
|
|
325
336
|
failures.add(message);
|
|
326
337
|
out.push({ kind: 'note', key: mint(), text: plain(message) });
|
|
327
|
-
|
|
338
|
+
// The reason, read whole, when it is the run's own; a vendor's words stay off the page.
|
|
339
|
+
if (message && !vendor(message))
|
|
328
340
|
out.push({ kind: 'step', key: mint(), name: 'Error', detail: '', output: message, ran: 'error' });
|
|
329
341
|
};
|
|
330
342
|
for (const e of [...events].sort((a, b) => a.seq - b.seq)) {
|
package/lib/api/work.d.ts
CHANGED
|
@@ -1,53 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The work an organization does: its projects, their issues, and its boards.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* GET /v1/provider/github/repos?q=&owner=&limit=&after= what the GitHub connection grants, most recently pushed first
|
|
5
|
+
* GET /v1/provider/github/installations the GitHub accounts it grants them from
|
|
6
|
+
* GET /v1/git/repos the forge's repositories
|
|
7
|
+
* GET /v1/task/board every issue and pull request: the forge's, and the task index's
|
|
8
|
+
* GET /v1/task/projects the boards
|
|
9
|
+
* GET /v1/task/projects/{key}/issues one board's
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* A project is a repository, so the list is those sources merged (merge.ts) and
|
|
12
|
+
* not a second copy of them. An issue row from the board is read for what it
|
|
13
|
+
* is: a forge issue names its forge repository and links to its page; a GitHub
|
|
14
|
+
* issue the index mirrors names its address only in `extRef`
|
|
15
|
+
* (`github:<owner>/<name>#<n>`), and its `number` there is the board's, not
|
|
16
|
+
* GitHub's; any other index row's `repo` is what its writer said — a run's
|
|
17
|
+
* repository, or a Linear team's key — and is resolved, never used raw.
|
|
10
18
|
*
|
|
11
19
|
* Creating, renaming and deleting a board are forge operations. This surface
|
|
12
20
|
* reads the work and opens a run against it.
|
|
13
21
|
*/
|
|
14
22
|
import { type Target } from './call.ts';
|
|
23
|
+
import { type Repo } from './github.ts';
|
|
24
|
+
import { type Held, type Issue, type Linked, type Site } from '../merge.ts';
|
|
15
25
|
export interface Board {
|
|
16
26
|
id: string;
|
|
17
27
|
key: string;
|
|
18
28
|
name: string;
|
|
19
29
|
description: string;
|
|
20
30
|
}
|
|
21
|
-
export interface Work {
|
|
22
|
-
id: string;
|
|
23
|
-
/** `<key>#<number>`, the handle a person reads. */
|
|
24
|
-
identifier: string;
|
|
25
|
-
project: string;
|
|
26
|
-
number: number;
|
|
27
|
-
kind: string;
|
|
28
|
-
title: string;
|
|
29
|
-
status: string;
|
|
30
|
-
priority: string;
|
|
31
|
-
assignee: string;
|
|
32
|
-
/** The repository the item is bound to, or ''. */
|
|
33
|
-
repo: string;
|
|
34
|
-
/** Where it was opened: `git` (the forge, or mirrored from GitHub), `team`, `agent`, … */
|
|
35
|
-
source: string;
|
|
36
|
-
labels: string[];
|
|
37
|
-
/** Its page upstream (a GitHub issue for a mirrored one), https only, or ''. */
|
|
38
|
-
url: string;
|
|
39
|
-
/** Unix milliseconds, or 0 when the platform did not say. */
|
|
40
|
-
created: number;
|
|
41
|
-
updated: number;
|
|
42
|
-
}
|
|
43
|
-
/** The project an item is filed under: its repository, else its board. */
|
|
44
|
-
export declare const projectOf: (w: Work) => string;
|
|
45
|
-
/** Done and canceled are closed; every other column is open work. */
|
|
46
|
-
export declare const closed: (w: Work) => boolean;
|
|
47
|
-
/** Whether `w` belongs to the project `key` — by repository or by board, either case. */
|
|
48
|
-
export declare function inProject(w: Work, key: string): boolean;
|
|
49
31
|
export declare function board(raw: unknown): Board | null;
|
|
50
|
-
|
|
51
|
-
export declare function
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
/** One board row as an issue: what it is, where it lives, and which repository it says it is for. */
|
|
33
|
+
export declare function work(raw: unknown): Issue | null;
|
|
34
|
+
export declare function boards(t: Target, signal?: AbortSignal): Promise<Board[]>;
|
|
35
|
+
/** Every issue and pull request the org has, from the forge and the task index, each once. */
|
|
36
|
+
export declare function everything(t: Target, signal?: AbortSignal): Promise<Issue[]>;
|
|
37
|
+
/** The rows a list of projects is assembled from (merge.ts `assemble`), and which sources did not answer. */
|
|
38
|
+
export interface Sources {
|
|
39
|
+
/** GitHub's grants to the org's connection, every page read so far: empty for anyone it does not answer. */
|
|
40
|
+
hub: Repo[];
|
|
41
|
+
/** The repositories linked from GitHub. */
|
|
42
|
+
linked: Linked[];
|
|
43
|
+
/** The forge's repositories. */
|
|
44
|
+
held: Held[];
|
|
45
|
+
/** The org's projects that build from a repository. */
|
|
46
|
+
sites: Site[];
|
|
47
|
+
/** The GitHub accounts the org's connection grants from, for the owner filter. */
|
|
48
|
+
owners: string[];
|
|
49
|
+
/** GitHub's cursor for its next page, or ''. */
|
|
50
|
+
next: string;
|
|
51
|
+
/** GitHub accounts the connection could not read. */
|
|
52
|
+
unread: string[];
|
|
53
|
+
/** Sources that did not answer, as a reader names them. */
|
|
54
|
+
missing: string[];
|
|
55
|
+
}
|
|
56
|
+
export interface SourceQuery {
|
|
57
|
+
/** Searched by GitHub; the rest are searched where they are drawn. */
|
|
58
|
+
q?: string;
|
|
59
|
+
/** One GitHub account. */
|
|
60
|
+
owner?: string;
|
|
61
|
+
/** GitHub's cursor: read only GitHub's page after it. */
|
|
62
|
+
after?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What the org's projects are made from, each read on its own: the links (GET
|
|
66
|
+
* /v1/sync), the forge's repositories, the org's projects, and GitHub's grants
|
|
67
|
+
* — searched and paged by GitHub, and empty for an operator with no GitHub of
|
|
68
|
+
* their own, which is why it is never the only source. One that fails leaves the
|
|
69
|
+
* others standing and is named in `missing`. A next page reads GitHub alone.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sources(t: Target, p?: SourceQuery, signal?: AbortSignal): Promise<Sources>;
|
|
72
|
+
/** A repository a run and an environment can address on the forge, by the name its copy has there. */
|
|
73
|
+
export interface Home {
|
|
74
|
+
/** The forge name: what a run's `repo` and `/v1/environment/{repo}` take. */
|
|
75
|
+
name: string;
|
|
76
|
+
/** `owner/name` as people know it: GitHub's address for a linked one, the org's for one made on the forge. */
|
|
77
|
+
label: string;
|
|
78
|
+
branch: string;
|
|
79
|
+
}
|
|
80
|
+
export interface Homes {
|
|
81
|
+
homes: Home[];
|
|
82
|
+
/** Both sources answered, so a name missing from `homes` is not the org's. */
|
|
83
|
+
whole: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every repository on the forge a run can work on: the forge's own and every
|
|
87
|
+
* linked repository's copy (GET /v1/git/repos does not list an estate's copies;
|
|
88
|
+
* GET /v1/sync does). Throws only when neither answered.
|
|
89
|
+
*/
|
|
90
|
+
export declare function homes(t: Target, signal?: AbortSignal): Promise<Homes>;
|