harborloop 0.3.0 → 0.4.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/checksums.json +12 -0
- package/bin/harbor.mjs +188 -0
- package/package.json +14 -30
- package/dist/api.d.ts +0 -109
- package/dist/api.js +0 -409
- package/dist/auth/login.d.ts +0 -14
- package/dist/auth/login.js +0 -158
- package/dist/auth/loopback.d.ts +0 -11
- package/dist/auth/loopback.js +0 -87
- package/dist/auth/pkce.d.ts +0 -7
- package/dist/auth/pkce.js +0 -14
- package/dist/auth/store.d.ts +0 -26
- package/dist/auth/store.js +0 -107
- package/dist/cli-release.d.ts +0 -31
- package/dist/cli-release.js +0 -102
- package/dist/commands/bind.d.ts +0 -23
- package/dist/commands/bind.js +0 -86
- package/dist/commands/context.d.ts +0 -12
- package/dist/commands/context.js +0 -80
- package/dist/commands/daemon.d.ts +0 -39
- package/dist/commands/daemon.js +0 -140
- package/dist/commands/doctor.d.ts +0 -16
- package/dist/commands/doctor.js +0 -937
- package/dist/commands/init.d.ts +0 -36
- package/dist/commands/init.js +0 -192
- package/dist/commands/status.d.ts +0 -5
- package/dist/commands/status.js +0 -164
- package/dist/commands/sync.d.ts +0 -40
- package/dist/commands/sync.js +0 -242
- package/dist/commands/toggle.d.ts +0 -16
- package/dist/commands/toggle.js +0 -69
- package/dist/commands/unbind.d.ts +0 -14
- package/dist/commands/unbind.js +0 -60
- package/dist/commands/workspaces.d.ts +0 -18
- package/dist/commands/workspaces.js +0 -41
- package/dist/context-budget.d.ts +0 -40
- package/dist/context-budget.js +0 -259
- package/dist/daemon/install.d.ts +0 -50
- package/dist/daemon/install.js +0 -192
- package/dist/daemon/loop.d.ts +0 -15
- package/dist/daemon/loop.js +0 -84
- package/dist/daemon/state.d.ts +0 -79
- package/dist/daemon/state.js +0 -213
- package/dist/daemon/tick.d.ts +0 -37
- package/dist/daemon/tick.js +0 -231
- package/dist/documents/apply.d.ts +0 -21
- package/dist/documents/apply.js +0 -58
- package/dist/documents/bundle.d.ts +0 -41
- package/dist/documents/bundle.js +0 -43
- package/dist/documents/inspect.d.ts +0 -12
- package/dist/documents/inspect.js +0 -33
- package/dist/documents/manifest.d.ts +0 -14
- package/dist/documents/manifest.js +0 -52
- package/dist/documents/pointer.d.ts +0 -32
- package/dist/documents/pointer.js +0 -135
- package/dist/documents/write.d.ts +0 -57
- package/dist/documents/write.js +0 -211
- package/dist/endpoints.d.ts +0 -7
- package/dist/endpoints.js +0 -15
- package/dist/env-bundle.d.ts +0 -25
- package/dist/env-bundle.js +0 -33
- package/dist/env-scan.d.ts +0 -27
- package/dist/env-scan.js +0 -41
- package/dist/env-sync.d.ts +0 -31
- package/dist/env-sync.js +0 -37
- package/dist/index.d.ts +0 -11
- package/dist/index.js +0 -282
- package/dist/install-hooks.d.ts +0 -11
- package/dist/install-hooks.js +0 -64
- package/dist/lock.d.ts +0 -31
- package/dist/lock.js +0 -192
- package/dist/machine.d.ts +0 -13
- package/dist/machine.js +0 -53
- package/dist/memories.d.ts +0 -31
- package/dist/memories.js +0 -74
- package/dist/origin.d.ts +0 -4
- package/dist/origin.js +0 -59
- package/dist/paths.d.ts +0 -12
- package/dist/paths.js +0 -45
- package/dist/providers/claude.d.ts +0 -65
- package/dist/providers/claude.js +0 -412
- package/dist/providers/codex-trust.d.ts +0 -36
- package/dist/providers/codex-trust.js +0 -141
- package/dist/providers/codex.d.ts +0 -46
- package/dist/providers/codex.js +0 -520
- package/dist/providers/cursor.d.ts +0 -45
- package/dist/providers/cursor.js +0 -344
- package/dist/providers/detect.d.ts +0 -16
- package/dist/providers/detect.js +0 -70
- package/dist/providers/hook-coverage.d.ts +0 -5
- package/dist/providers/hook-coverage.js +0 -47
- package/dist/providers/hook-entry.d.ts +0 -1
- package/dist/providers/hook-entry.js +0 -8
- package/dist/providers/surfaces.d.ts +0 -19
- package/dist/providers/surfaces.js +0 -91
- package/dist/reclaim.d.ts +0 -17
- package/dist/reclaim.js +0 -33
- package/dist/render.d.ts +0 -17
- package/dist/render.js +0 -61
- package/dist/repo-sync.d.ts +0 -27
- package/dist/repo-sync.js +0 -34
- package/dist/repos.d.ts +0 -11
- package/dist/repos.js +0 -82
- package/dist/skills.d.ts +0 -84
- package/dist/skills.js +0 -383
- package/dist/slugify.d.ts +0 -1
- package/dist/slugify.js +0 -8
- package/dist/state.d.ts +0 -23
- package/dist/state.js +0 -161
- package/dist/sync-report.d.ts +0 -59
- package/dist/sync-report.js +0 -87
- package/hooks/_recall.mjs +0 -288
- package/hooks/_receipt.mjs +0 -47
- package/hooks/_shared.mjs +0 -1610
- package/hooks/_upgrade.mjs +0 -51
- package/hooks/session-capture.mjs +0 -60
- package/hooks/session-compact.mjs +0 -19
- package/hooks/session-context.mjs +0 -258
- package/hooks/session-report.mjs +0 -42
- package/hooks/session-toolcontext.mjs +0 -183
package/dist/api.js
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
export class ApiError extends Error {
|
|
2
|
-
kind;
|
|
3
|
-
status;
|
|
4
|
-
body;
|
|
5
|
-
constructor(kind, message, status = null, body = null) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.kind = kind;
|
|
8
|
-
this.status = status;
|
|
9
|
-
this.body = body;
|
|
10
|
-
this.name = 'ApiError';
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function classify(status) {
|
|
14
|
-
if (status === 401)
|
|
15
|
-
return 'unauthorized';
|
|
16
|
-
if (status === 403)
|
|
17
|
-
return 'forbidden';
|
|
18
|
-
if (status === 402)
|
|
19
|
-
return 'payment_required';
|
|
20
|
-
if (status === 404)
|
|
21
|
-
return 'not_found';
|
|
22
|
-
if (status === 429)
|
|
23
|
-
return 'rate_limited';
|
|
24
|
-
if (status >= 500)
|
|
25
|
-
return 'server';
|
|
26
|
-
return 'shape';
|
|
27
|
-
}
|
|
28
|
-
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
29
|
-
export async function apiRequest(url, opts = {}) {
|
|
30
|
-
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
31
|
-
const controller = new AbortController();
|
|
32
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
33
|
-
let response;
|
|
34
|
-
try {
|
|
35
|
-
response = await fetchImpl(url, {
|
|
36
|
-
method: opts.method ?? 'GET',
|
|
37
|
-
headers: {
|
|
38
|
-
...(opts.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
39
|
-
...(opts.bearer ? { Authorization: `Bearer ${opts.bearer}` } : {}),
|
|
40
|
-
},
|
|
41
|
-
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
42
|
-
signal: controller.signal,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
throw new ApiError('network', `Could not reach ${url}: ${err.message}`);
|
|
47
|
-
}
|
|
48
|
-
finally {
|
|
49
|
-
clearTimeout(timer);
|
|
50
|
-
}
|
|
51
|
-
const text = await response.text().catch(() => '');
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const detail = extractError(text) ?? `${response.status} from ${url}`;
|
|
54
|
-
throw new ApiError(classify(response.status), response.status === 429 ? `${detail}${describeRetryAfter(response.headers)}` : detail, response.status, parseJsonOrNull(text));
|
|
55
|
-
}
|
|
56
|
-
if (text.trim() === '')
|
|
57
|
-
return undefined;
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(text);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
throw new ApiError('shape', `Non-JSON response from ${url}`, response.status);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export function describeRetryAfter(headers, now = Date.now()) {
|
|
66
|
-
const seconds = retryAfterSeconds(headers, now);
|
|
67
|
-
if (seconds === null || seconds <= 0)
|
|
68
|
-
return '';
|
|
69
|
-
const minutes = Math.ceil(seconds / 60);
|
|
70
|
-
const wait = minutes <= 1 ? 'about a minute' : `about ${minutes} minutes`;
|
|
71
|
-
return ` — try again in ${wait}.`;
|
|
72
|
-
}
|
|
73
|
-
function retryAfterSeconds(headers, now) {
|
|
74
|
-
const retryAfter = headers.get('retry-after');
|
|
75
|
-
if (retryAfter !== null && retryAfter.trim() !== '') {
|
|
76
|
-
const asSeconds = Number(retryAfter);
|
|
77
|
-
if (Number.isFinite(asSeconds))
|
|
78
|
-
return asSeconds;
|
|
79
|
-
const asDate = Date.parse(retryAfter);
|
|
80
|
-
if (!Number.isNaN(asDate))
|
|
81
|
-
return (asDate - now) / 1000;
|
|
82
|
-
}
|
|
83
|
-
const reset = headers.get('ratelimit-reset');
|
|
84
|
-
if (reset !== null && reset.trim() !== '') {
|
|
85
|
-
const asSeconds = Number(reset);
|
|
86
|
-
if (Number.isFinite(asSeconds))
|
|
87
|
-
return asSeconds;
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
function parseJsonOrNull(text) {
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(text);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function extractError(text) {
|
|
100
|
-
try {
|
|
101
|
-
const body = JSON.parse(text);
|
|
102
|
-
const headline = typeof body.error === 'string'
|
|
103
|
-
? body.error
|
|
104
|
-
: typeof body.message === 'string'
|
|
105
|
-
? body.message
|
|
106
|
-
: null;
|
|
107
|
-
const fields = fieldErrors(body.details?.errors);
|
|
108
|
-
if (headline !== null && fields !== null)
|
|
109
|
-
return `${headline}: ${fields}`;
|
|
110
|
-
return headline ?? fields;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
function fieldErrors(errors) {
|
|
117
|
-
if (!Array.isArray(errors) || errors.length === 0)
|
|
118
|
-
return null;
|
|
119
|
-
const parts = errors.flatMap((entry) => {
|
|
120
|
-
if (entry === null || typeof entry !== 'object')
|
|
121
|
-
return [];
|
|
122
|
-
const { field, message } = entry;
|
|
123
|
-
if (typeof message !== 'string' || message === '')
|
|
124
|
-
return [];
|
|
125
|
-
return [typeof field === 'string' && field !== '' ? `${field} — ${message}` : message];
|
|
126
|
-
});
|
|
127
|
-
return parts.length === 0 ? null : parts.join('; ');
|
|
128
|
-
}
|
|
129
|
-
function unwrap(body, what) {
|
|
130
|
-
if (body !== null && typeof body === 'object' && !Array.isArray(body) && 'data' in body) {
|
|
131
|
-
return body.data;
|
|
132
|
-
}
|
|
133
|
-
if (Array.isArray(body) || (body !== null && typeof body === 'object')) {
|
|
134
|
-
return body;
|
|
135
|
-
}
|
|
136
|
-
throw new ApiError('shape', `Unexpected ${what} response shape`);
|
|
137
|
-
}
|
|
138
|
-
export async function exchangeAuthCode(apiUrl, code, codeVerifier, fetchImpl) {
|
|
139
|
-
const body = await apiRequest(`${apiUrl}/auth/desktop-token-exchange`, { method: 'POST', body: { code, codeVerifier }, fetchImpl });
|
|
140
|
-
if (typeof body?.token !== 'string' || body.token === '') {
|
|
141
|
-
throw new ApiError('shape', 'Token exchange returned no token');
|
|
142
|
-
}
|
|
143
|
-
return body.token;
|
|
144
|
-
}
|
|
145
|
-
export async function refreshRunnerToken(apiUrl, runnerToken, fetchImpl) {
|
|
146
|
-
const body = await apiRequest(`${apiUrl}/auth/runner-token/refresh`, { method: 'POST', bearer: runnerToken, fetchImpl });
|
|
147
|
-
if (typeof body?.token !== 'string' || body.token === '') {
|
|
148
|
-
throw new ApiError('shape', 'Refresh returned no token');
|
|
149
|
-
}
|
|
150
|
-
return body.token;
|
|
151
|
-
}
|
|
152
|
-
export async function fetchCurrentUser(apiUrl, runnerToken, fetchImpl) {
|
|
153
|
-
const body = await apiRequest(`${apiUrl}/users/me`, {
|
|
154
|
-
bearer: runnerToken,
|
|
155
|
-
fetchImpl,
|
|
156
|
-
});
|
|
157
|
-
const user = unwrap(body, 'user');
|
|
158
|
-
const preferences = user.preferences !== null && typeof user.preferences === 'object'
|
|
159
|
-
? user.preferences
|
|
160
|
-
: {};
|
|
161
|
-
const last = preferences.lastWorkspaceId;
|
|
162
|
-
return {
|
|
163
|
-
id: typeof user.id === 'string' ? user.id : '',
|
|
164
|
-
email: typeof user.email === 'string' ? user.email : '',
|
|
165
|
-
name: typeof user.name === 'string' ? user.name : '',
|
|
166
|
-
lastWorkspaceId: typeof last === 'string' && last !== '' ? last : null,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
export async function listWorkspaces(apiUrl, runnerToken, fetchImpl) {
|
|
170
|
-
const body = await apiRequest(`${apiUrl}/workspaces`, {
|
|
171
|
-
bearer: runnerToken,
|
|
172
|
-
fetchImpl,
|
|
173
|
-
});
|
|
174
|
-
const rows = unwrap(body, 'workspaces');
|
|
175
|
-
if (!Array.isArray(rows))
|
|
176
|
-
throw new ApiError('shape', 'Workspaces is not a list');
|
|
177
|
-
return rows.flatMap((row) => {
|
|
178
|
-
if (row === null || typeof row !== 'object')
|
|
179
|
-
return [];
|
|
180
|
-
const obj = row;
|
|
181
|
-
const ws = (obj.workspace ?? obj);
|
|
182
|
-
if (typeof ws.id !== 'string')
|
|
183
|
-
return [];
|
|
184
|
-
return [{ id: ws.id, name: typeof ws.name === 'string' ? ws.name : ws.id }];
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
export async function mintMcpToken(apiUrl, runnerToken, workspaceId, provider, fetchImpl) {
|
|
188
|
-
const body = await apiRequest(`${apiUrl}/workspaces/${workspaceId}/context/setup/token`, { method: 'POST', body: { provider }, bearer: runnerToken, fetchImpl });
|
|
189
|
-
const data = unwrap(body, 'mint');
|
|
190
|
-
if (typeof data.token !== 'string')
|
|
191
|
-
throw new ApiError('shape', 'Mint returned no token');
|
|
192
|
-
return {
|
|
193
|
-
token: data.token,
|
|
194
|
-
expiresIn: typeof data.expiresIn === 'string' ? data.expiresIn : 'unknown',
|
|
195
|
-
connectionId: typeof data.connectionId === 'string' ? data.connectionId : '',
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function toBinding(row) {
|
|
199
|
-
if (row === null || typeof row !== 'object')
|
|
200
|
-
return [];
|
|
201
|
-
const obj = row;
|
|
202
|
-
if (typeof obj.originUrl !== 'string')
|
|
203
|
-
return [];
|
|
204
|
-
const project = obj.project;
|
|
205
|
-
return [
|
|
206
|
-
{
|
|
207
|
-
id: typeof obj.id === 'string' ? obj.id : '',
|
|
208
|
-
originUrl: obj.originUrl,
|
|
209
|
-
displayName: typeof obj.displayName === 'string' ? obj.displayName : null,
|
|
210
|
-
projectId: typeof obj.projectId === 'string' ? obj.projectId : null,
|
|
211
|
-
projectName: project && typeof project.name === 'string' ? project.name : null,
|
|
212
|
-
},
|
|
213
|
-
];
|
|
214
|
-
}
|
|
215
|
-
export async function listRepoBindings(apiUrl, runnerToken, workspaceId, fetchImpl) {
|
|
216
|
-
const body = await apiRequest(`${apiUrl}/workspaces/${workspaceId}/context/repo-bindings`, { bearer: runnerToken, fetchImpl });
|
|
217
|
-
const rows = unwrap(body, 'repo-bindings');
|
|
218
|
-
if (!Array.isArray(rows))
|
|
219
|
-
return [];
|
|
220
|
-
return rows.flatMap(toBinding);
|
|
221
|
-
}
|
|
222
|
-
export async function createRepoBinding(apiUrl, runnerToken, workspaceId, input, fetchImpl) {
|
|
223
|
-
const body = await apiRequest(`${apiUrl}/workspaces/${workspaceId}/context/repo-bindings`, { method: 'POST', body: input, bearer: runnerToken, fetchImpl });
|
|
224
|
-
const data = unwrap(body, 'repo-binding');
|
|
225
|
-
const parsed = toBinding(data);
|
|
226
|
-
if (parsed.length === 0)
|
|
227
|
-
throw new ApiError('shape', 'Binding response had no originUrl');
|
|
228
|
-
return parsed[0];
|
|
229
|
-
}
|
|
230
|
-
export async function listProjects(apiUrl, runnerToken, workspaceId, fetchImpl) {
|
|
231
|
-
const body = await apiRequest(`${apiUrl}/workspaces/${workspaceId}/projects`, { bearer: runnerToken, fetchImpl });
|
|
232
|
-
const rows = unwrap(body, 'projects');
|
|
233
|
-
if (!Array.isArray(rows))
|
|
234
|
-
return [];
|
|
235
|
-
return rows.flatMap((row) => {
|
|
236
|
-
if (row === null || typeof row !== 'object')
|
|
237
|
-
return [];
|
|
238
|
-
const obj = row;
|
|
239
|
-
if (typeof obj.id !== 'string')
|
|
240
|
-
return [];
|
|
241
|
-
return [{ id: obj.id, name: typeof obj.name === 'string' ? obj.name : obj.id }];
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
function requireNumber(source, block, field) {
|
|
245
|
-
const value = source[field];
|
|
246
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
247
|
-
throw new ApiError('shape', `loop-metrics: ${block}.${field} is ${describeType(value)}`);
|
|
248
|
-
}
|
|
249
|
-
return value;
|
|
250
|
-
}
|
|
251
|
-
function requireBlock(data, name) {
|
|
252
|
-
const block = data[name];
|
|
253
|
-
if (block === null || typeof block !== 'object' || Array.isArray(block)) {
|
|
254
|
-
throw new ApiError('shape', `loop-metrics: ${name} is ${describeType(block)}`);
|
|
255
|
-
}
|
|
256
|
-
return block;
|
|
257
|
-
}
|
|
258
|
-
function describeType(value) {
|
|
259
|
-
if (value === undefined)
|
|
260
|
-
return 'missing';
|
|
261
|
-
if (value === null)
|
|
262
|
-
return 'null';
|
|
263
|
-
return Array.isArray(value) ? 'a list' : `a ${typeof value}`;
|
|
264
|
-
}
|
|
265
|
-
function optionalNumber(source, field) {
|
|
266
|
-
const value = source[field];
|
|
267
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
268
|
-
}
|
|
269
|
-
export function parseLoopMetrics(data) {
|
|
270
|
-
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
271
|
-
throw new ApiError('shape', `loop-metrics: response is ${describeType(data)}`);
|
|
272
|
-
}
|
|
273
|
-
const root = data;
|
|
274
|
-
const recall = requireBlock(root, 'recall');
|
|
275
|
-
const fit = requireBlock(root, 'fit');
|
|
276
|
-
const attribution = requireBlock(root, 'attribution');
|
|
277
|
-
const contributed = requireBlock(root, 'contributed');
|
|
278
|
-
const projectId = root.projectId;
|
|
279
|
-
const corpus = readCorpus(root.corpus);
|
|
280
|
-
const spend = readSpend(root.spend);
|
|
281
|
-
const byTransport = readTransports(root.byTransport);
|
|
282
|
-
return {
|
|
283
|
-
windowDays: requireNumber(root, 'loop-metrics', 'windowDays'),
|
|
284
|
-
projectId: typeof projectId === 'string' ? projectId : null,
|
|
285
|
-
recall: {
|
|
286
|
-
sessions: requireNumber(recall, 'recall', 'sessions'),
|
|
287
|
-
served: requireNumber(recall, 'recall', 'served'),
|
|
288
|
-
},
|
|
289
|
-
fit: {
|
|
290
|
-
servedAtoms: requireNumber(fit, 'fit', 'servedAtoms'),
|
|
291
|
-
citedAtoms: requireNumber(fit, 'fit', 'citedAtoms'),
|
|
292
|
-
avgBundle: optionalNumber(fit, 'avgBundle'),
|
|
293
|
-
},
|
|
294
|
-
attribution: {
|
|
295
|
-
recalls: requireNumber(attribution, 'attribution', 'recalls'),
|
|
296
|
-
keyed: requireNumber(attribution, 'attribution', 'keyed'),
|
|
297
|
-
},
|
|
298
|
-
contributed: {
|
|
299
|
-
total: requireNumber(contributed, 'contributed', 'total'),
|
|
300
|
-
approved: requireNumber(contributed, 'contributed', 'approved'),
|
|
301
|
-
pending: requireNumber(contributed, 'contributed', 'pending'),
|
|
302
|
-
},
|
|
303
|
-
...(corpus ? { corpus } : {}),
|
|
304
|
-
...(spend ? { spend } : {}),
|
|
305
|
-
...(byTransport ? { byTransport } : {}),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
function readTransports(value) {
|
|
309
|
-
if (!Array.isArray(value))
|
|
310
|
-
return undefined;
|
|
311
|
-
const rows = [];
|
|
312
|
-
for (const entry of value) {
|
|
313
|
-
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
314
|
-
return undefined;
|
|
315
|
-
}
|
|
316
|
-
const block = entry;
|
|
317
|
-
const { transport, label } = block;
|
|
318
|
-
const attribution = block.attribution;
|
|
319
|
-
if (typeof transport !== 'string' ||
|
|
320
|
-
typeof label !== 'string' ||
|
|
321
|
-
attribution === null ||
|
|
322
|
-
typeof attribution !== 'object' ||
|
|
323
|
-
Array.isArray(attribution)) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
const pair = attribution;
|
|
327
|
-
const recalls = optionalNumber(pair, 'recalls');
|
|
328
|
-
const keyed = optionalNumber(pair, 'keyed');
|
|
329
|
-
if (recalls === null || keyed === null)
|
|
330
|
-
return undefined;
|
|
331
|
-
rows.push({
|
|
332
|
-
transport,
|
|
333
|
-
label,
|
|
334
|
-
recalls,
|
|
335
|
-
keyed,
|
|
336
|
-
clients: Array.isArray(block.clients)
|
|
337
|
-
? block.clients
|
|
338
|
-
.map((client) => client !== null && typeof client === 'object'
|
|
339
|
-
? client.surface
|
|
340
|
-
: undefined)
|
|
341
|
-
.filter((surface) => typeof surface === 'string')
|
|
342
|
-
: [],
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
return rows;
|
|
346
|
-
}
|
|
347
|
-
function readCorpus(value) {
|
|
348
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
349
|
-
return undefined;
|
|
350
|
-
const block = value;
|
|
351
|
-
const admitted = optionalNumber(block, 'admitted');
|
|
352
|
-
const retired = optionalNumber(block, 'retired');
|
|
353
|
-
const liveAtoms = optionalNumber(block, 'liveAtoms');
|
|
354
|
-
if (admitted === null || retired === null || liveAtoms === null)
|
|
355
|
-
return undefined;
|
|
356
|
-
const causes = Array.isArray(block.retiredByCause)
|
|
357
|
-
? block.retiredByCause.flatMap((row) => {
|
|
358
|
-
if (row === null || typeof row !== 'object')
|
|
359
|
-
return [];
|
|
360
|
-
const { cause, count } = row;
|
|
361
|
-
if (typeof cause !== 'string' || typeof count !== 'number')
|
|
362
|
-
return [];
|
|
363
|
-
return [{ cause, count }];
|
|
364
|
-
})
|
|
365
|
-
: undefined;
|
|
366
|
-
return {
|
|
367
|
-
admitted,
|
|
368
|
-
retired,
|
|
369
|
-
liveAtoms,
|
|
370
|
-
...(causes ? { retiredByCause: causes } : {}),
|
|
371
|
-
ledgerEmpty: block.ledgerEmpty === true,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
function readSpend(value) {
|
|
375
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
376
|
-
return undefined;
|
|
377
|
-
const block = value;
|
|
378
|
-
const writeSpendCents = optionalNumber(block, 'writeSpendCents');
|
|
379
|
-
const writeSpendUsd = optionalNumber(block, 'writeSpendUsd');
|
|
380
|
-
const spendRows = optionalNumber(block, 'spendRows');
|
|
381
|
-
const citedAtoms = optionalNumber(block, 'citedAtoms');
|
|
382
|
-
if (writeSpendCents === null ||
|
|
383
|
-
writeSpendUsd === null ||
|
|
384
|
-
spendRows === null ||
|
|
385
|
-
citedAtoms === null) {
|
|
386
|
-
return undefined;
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
writeSpendCents,
|
|
390
|
-
writeSpendUsd,
|
|
391
|
-
spendRows,
|
|
392
|
-
unpricedRows: optionalNumber(block, 'unpricedRows') ?? 0,
|
|
393
|
-
citedAtoms,
|
|
394
|
-
costPerCitedAtomUsd: optionalNumber(block, 'costPerCitedAtomUsd'),
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
export async function fetchLoopMetrics(apiUrl, mcpToken, params = {}, fetchImpl) {
|
|
398
|
-
const query = new URLSearchParams();
|
|
399
|
-
if (params.originUrl)
|
|
400
|
-
query.set('originUrl', params.originUrl);
|
|
401
|
-
if (params.windowDays)
|
|
402
|
-
query.set('windowDays', String(params.windowDays));
|
|
403
|
-
const suffix = query.toString() === '' ? '' : `?${query.toString()}`;
|
|
404
|
-
const body = await apiRequest(`${apiUrl}/context/loop-metrics${suffix}`, {
|
|
405
|
-
bearer: mcpToken,
|
|
406
|
-
fetchImpl,
|
|
407
|
-
});
|
|
408
|
-
return parseLoopMetrics(unwrap(body, 'loop-metrics'));
|
|
409
|
-
}
|
package/dist/auth/login.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Endpoints } from '../endpoints.js';
|
|
2
|
-
import { type StoredAuth } from './store.js';
|
|
3
|
-
export interface LoginOptions {
|
|
4
|
-
readonly noBrowser?: boolean;
|
|
5
|
-
readonly workspaceId?: string;
|
|
6
|
-
readonly fetchImpl?: typeof fetch;
|
|
7
|
-
}
|
|
8
|
-
export declare function openBrowser(url: string): void;
|
|
9
|
-
export declare function buildSignInUrl(endpoints: Endpoints, challenge: string, state: string, redirectUri: string | null): string;
|
|
10
|
-
export interface LoginResult {
|
|
11
|
-
readonly auth: StoredAuth;
|
|
12
|
-
readonly reused: boolean;
|
|
13
|
-
}
|
|
14
|
-
export declare function login(endpoints: Endpoints, opts?: LoginOptions): Promise<LoginResult>;
|
package/dist/auth/login.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { ApiError, exchangeAuthCode, listWorkspaces } from '../api.js';
|
|
4
|
-
import { startLoopbackListener } from './loopback.js';
|
|
5
|
-
import { createPkcePair, createState } from './pkce.js';
|
|
6
|
-
import { isExpired, readAuth, writeAuth } from './store.js';
|
|
7
|
-
export function openBrowser(url) {
|
|
8
|
-
const [cmd, args] = process.platform === 'darwin'
|
|
9
|
-
? ['open', [url]]
|
|
10
|
-
: process.platform === 'win32'
|
|
11
|
-
? ['cmd', ['/c', 'start', '', url]]
|
|
12
|
-
: ['xdg-open', [url]];
|
|
13
|
-
const child = execFile(cmd, args, () => undefined);
|
|
14
|
-
child.unref();
|
|
15
|
-
}
|
|
16
|
-
export function buildSignInUrl(endpoints, challenge, state, redirectUri) {
|
|
17
|
-
const url = new URL('/desktop-auth', endpoints.webUrl);
|
|
18
|
-
url.searchParams.set('code_challenge', challenge);
|
|
19
|
-
url.searchParams.set('state', state);
|
|
20
|
-
url.searchParams.set('client', 'cli');
|
|
21
|
-
if (redirectUri)
|
|
22
|
-
url.searchParams.set('redirect_uri', redirectUri);
|
|
23
|
-
return url.toString();
|
|
24
|
-
}
|
|
25
|
-
async function ask(question) {
|
|
26
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
27
|
-
try {
|
|
28
|
-
const eof = new Promise((resolve) => rl.once('close', () => resolve(null)));
|
|
29
|
-
const answer = await Promise.race([rl.question(question), eof]);
|
|
30
|
-
return answer === null ? null : answer.trim();
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
rl.close();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function pickWorkspace(workspaces, preferred) {
|
|
37
|
-
if (workspaces.length === 0) {
|
|
38
|
-
throw new Error('This account is not a member of any workspace. Create one in the webapp first.');
|
|
39
|
-
}
|
|
40
|
-
if (preferred) {
|
|
41
|
-
const match = workspaces.find((w) => w.id === preferred);
|
|
42
|
-
if (!match) {
|
|
43
|
-
throw new Error(`Workspace ${preferred} not found. Available: ${workspaces
|
|
44
|
-
.map((w) => `${w.name} (${w.id})`)
|
|
45
|
-
.join(', ')}`);
|
|
46
|
-
}
|
|
47
|
-
return match;
|
|
48
|
-
}
|
|
49
|
-
if (workspaces.length === 1)
|
|
50
|
-
return workspaces[0];
|
|
51
|
-
if (!process.stdin.isTTY) {
|
|
52
|
-
throw new Error(`Several workspaces are available and there is no terminal to ask on. ` +
|
|
53
|
-
`Re-run with --workspace <id>: ${workspaces
|
|
54
|
-
.map((w) => `${w.name} (${w.id})`)
|
|
55
|
-
.join(', ')}`);
|
|
56
|
-
}
|
|
57
|
-
workspaces.forEach((w, i) => process.stderr.write(` ${i + 1}. ${w.name}\n`));
|
|
58
|
-
const answer = await ask(`Workspace [1-${workspaces.length}]: `);
|
|
59
|
-
const chosen = workspaces[Number.parseInt(answer ?? '', 10) - 1];
|
|
60
|
-
if (!chosen) {
|
|
61
|
-
throw new Error(`No workspace chosen. Re-run with --workspace <id>: ${workspaces
|
|
62
|
-
.map((w) => `${w.name} (${w.id})`)
|
|
63
|
-
.join(', ')}`);
|
|
64
|
-
}
|
|
65
|
-
return chosen;
|
|
66
|
-
}
|
|
67
|
-
export async function login(endpoints, opts = {}) {
|
|
68
|
-
const existing = await readAuth();
|
|
69
|
-
if (existing &&
|
|
70
|
-
existing.apiUrl === endpoints.apiUrl &&
|
|
71
|
-
!isExpired(existing.runnerToken)) {
|
|
72
|
-
if (existing.workspaceId === '') {
|
|
73
|
-
const resumed = await resolveWorkspace(existing, endpoints, opts);
|
|
74
|
-
if (resumed)
|
|
75
|
-
return { auth: resumed, reused: true };
|
|
76
|
-
}
|
|
77
|
-
else if (!opts.workspaceId || existing.workspaceId === opts.workspaceId) {
|
|
78
|
-
return { auth: existing, reused: true };
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
const switched = await resolveWorkspace(existing, endpoints, opts);
|
|
82
|
-
if (switched)
|
|
83
|
-
return { auth: switched, reused: true };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const { verifier, challenge } = createPkcePair();
|
|
87
|
-
const state = createState();
|
|
88
|
-
const code = opts.noBrowser
|
|
89
|
-
? await runPasteFlow(endpoints, challenge, state)
|
|
90
|
-
: await runLoopbackFlow(endpoints, challenge, state);
|
|
91
|
-
const runnerToken = await exchangeAuthCode(endpoints.apiUrl, code, verifier, opts.fetchImpl);
|
|
92
|
-
const pending = {
|
|
93
|
-
runnerToken,
|
|
94
|
-
workspaceId: '',
|
|
95
|
-
workspaceName: '',
|
|
96
|
-
apiUrl: endpoints.apiUrl,
|
|
97
|
-
signedInAt: new Date().toISOString(),
|
|
98
|
-
};
|
|
99
|
-
await writeAuth(pending);
|
|
100
|
-
const auth = await resolveWorkspace(pending, endpoints, opts);
|
|
101
|
-
if (!auth) {
|
|
102
|
-
throw new Error('Signed in, but the new credential was rejected when reading your workspaces. ' +
|
|
103
|
-
'Run `harbor doctor` to see which endpoint it was minted against.');
|
|
104
|
-
}
|
|
105
|
-
return { auth, reused: false };
|
|
106
|
-
}
|
|
107
|
-
async function resolveWorkspace(existing, endpoints, opts) {
|
|
108
|
-
let workspaces;
|
|
109
|
-
try {
|
|
110
|
-
workspaces = await listWorkspaces(endpoints.apiUrl, existing.runnerToken, opts.fetchImpl);
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
if (err instanceof ApiError && (err.kind === 'unauthorized' || err.kind === 'forbidden')) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
if (opts.workspaceId && !workspaces.some((w) => w.id === opts.workspaceId)) {
|
|
119
|
-
throw new Error(`Workspace ${opts.workspaceId} not found. Available: ${workspaces
|
|
120
|
-
.map((w) => `${w.name} (${w.id})`)
|
|
121
|
-
.join(', ')}. ` +
|
|
122
|
-
`If it belongs to another account, run \`harbor logout\` first.`);
|
|
123
|
-
}
|
|
124
|
-
const workspace = await pickWorkspace(workspaces, opts.workspaceId);
|
|
125
|
-
const auth = {
|
|
126
|
-
...existing,
|
|
127
|
-
workspaceId: workspace.id,
|
|
128
|
-
workspaceName: workspace.name,
|
|
129
|
-
};
|
|
130
|
-
await writeAuth(auth);
|
|
131
|
-
return auth;
|
|
132
|
-
}
|
|
133
|
-
async function runLoopbackFlow(endpoints, challenge, state) {
|
|
134
|
-
const listener = await startLoopbackListener(state);
|
|
135
|
-
try {
|
|
136
|
-
const url = buildSignInUrl(endpoints, challenge, state, listener.redirectUri);
|
|
137
|
-
process.stderr.write(`Opening ${url}\n`);
|
|
138
|
-
process.stderr.write('Waiting for the browser…\n');
|
|
139
|
-
openBrowser(url);
|
|
140
|
-
const { code } = await listener.result;
|
|
141
|
-
return code;
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
listener.close();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async function runPasteFlow(endpoints, challenge, state) {
|
|
148
|
-
if (!process.stdin.isTTY) {
|
|
149
|
-
throw new Error('harbor login --no-browser needs a terminal to paste the code into. ' +
|
|
150
|
-
'Run it interactively, or drop --no-browser to use the browser redirect.');
|
|
151
|
-
}
|
|
152
|
-
const url = buildSignInUrl(endpoints, challenge, state, null);
|
|
153
|
-
process.stderr.write(`Open this URL and sign in:\n\n ${url}\n\n`);
|
|
154
|
-
const code = await ask('Paste the code from your browser: ');
|
|
155
|
-
if (code === null || code === '')
|
|
156
|
-
throw new Error('No code entered.');
|
|
157
|
-
return code;
|
|
158
|
-
}
|
package/dist/auth/loopback.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface LoopbackResult {
|
|
2
|
-
readonly code: string;
|
|
3
|
-
}
|
|
4
|
-
export interface LoopbackListener {
|
|
5
|
-
readonly redirectUri: string;
|
|
6
|
-
readonly result: Promise<LoopbackResult>;
|
|
7
|
-
close(): void;
|
|
8
|
-
}
|
|
9
|
-
export declare function startLoopbackListener(expectedState: string, opts?: {
|
|
10
|
-
readonly timeoutMs?: number;
|
|
11
|
-
}): Promise<LoopbackListener>;
|
package/dist/auth/loopback.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { statesMatch } from './pkce.js';
|
|
3
|
-
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4
|
-
function respond(res, status, body) {
|
|
5
|
-
res.writeHead(status, {
|
|
6
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
7
|
-
'Cache-Control': 'no-store',
|
|
8
|
-
'Referrer-Policy': 'no-referrer',
|
|
9
|
-
});
|
|
10
|
-
res.end(body);
|
|
11
|
-
}
|
|
12
|
-
function page(title, detail) {
|
|
13
|
-
return `<!doctype html><meta charset="utf-8"><title>${title}</title>
|
|
14
|
-
<style>
|
|
15
|
-
:root { color-scheme: light dark }
|
|
16
|
-
body { font: 15px/1.6 ui-sans-serif, system-ui, sans-serif; display: grid;
|
|
17
|
-
place-items: center; min-height: 100vh; margin: 0; padding: 2rem }
|
|
18
|
-
main { max-width: 30rem; text-align: center }
|
|
19
|
-
h1 { font-size: 1.25rem; margin: 0 0 .5rem }
|
|
20
|
-
p { margin: 0; opacity: .75 }
|
|
21
|
-
</style>
|
|
22
|
-
<main><h1>${title}</h1><p>${detail}</p></main>`;
|
|
23
|
-
}
|
|
24
|
-
export async function startLoopbackListener(expectedState, opts = {}) {
|
|
25
|
-
let settle = () => undefined;
|
|
26
|
-
let fail = () => undefined;
|
|
27
|
-
const result = new Promise((resolve, reject) => {
|
|
28
|
-
settle = resolve;
|
|
29
|
-
fail = reject;
|
|
30
|
-
});
|
|
31
|
-
const server = createServer((req, res) => {
|
|
32
|
-
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
33
|
-
if (url.pathname !== '/callback') {
|
|
34
|
-
respond(res, 404, page('Not found', 'This page is not part of sign-in.'));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const code = url.searchParams.get('code') ?? '';
|
|
38
|
-
const state = url.searchParams.get('state') ?? '';
|
|
39
|
-
const error = url.searchParams.get('error');
|
|
40
|
-
if (error) {
|
|
41
|
-
respond(res, 400, page('Sign-in failed', escapeHtml(error)));
|
|
42
|
-
fail(new Error(`Sign-in failed: ${error}`));
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (!statesMatch(expectedState, state)) {
|
|
46
|
-
respond(res, 400, page('Sign-in failed', 'This request did not match the pending sign-in.'));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (code === '') {
|
|
50
|
-
respond(res, 400, page('Sign-in failed', 'No authorization code was returned.'));
|
|
51
|
-
fail(new Error('No authorization code was returned'));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
respond(res, 200, page('You are signed in', 'You can close this tab and return to your terminal.'));
|
|
55
|
-
settle({ code });
|
|
56
|
-
});
|
|
57
|
-
await new Promise((resolve, reject) => {
|
|
58
|
-
server.once('error', reject);
|
|
59
|
-
server.listen(0, '127.0.0.1', resolve);
|
|
60
|
-
});
|
|
61
|
-
const timer = setTimeout(() => {
|
|
62
|
-
fail(new Error('Timed out waiting for the browser to complete sign-in.'));
|
|
63
|
-
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
64
|
-
timer.unref?.();
|
|
65
|
-
const { port } = server.address();
|
|
66
|
-
let closed = false;
|
|
67
|
-
const close = () => {
|
|
68
|
-
if (closed)
|
|
69
|
-
return;
|
|
70
|
-
closed = true;
|
|
71
|
-
clearTimeout(timer);
|
|
72
|
-
server.close();
|
|
73
|
-
server.closeAllConnections?.();
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
77
|
-
result: result.finally(close),
|
|
78
|
-
close,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function escapeHtml(value) {
|
|
82
|
-
return value
|
|
83
|
-
.replace(/&/g, '&')
|
|
84
|
-
.replace(/</g, '<')
|
|
85
|
-
.replace(/>/g, '>')
|
|
86
|
-
.replace(/"/g, '"');
|
|
87
|
-
}
|
package/dist/auth/pkce.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export interface PkcePair {
|
|
2
|
-
readonly verifier: string;
|
|
3
|
-
readonly challenge: string;
|
|
4
|
-
}
|
|
5
|
-
export declare function createPkcePair(): PkcePair;
|
|
6
|
-
export declare function createState(): string;
|
|
7
|
-
export declare function statesMatch(expected: string, received: string): boolean;
|