@monadeo.com/grimoire-cli 0.3.7 → 0.4.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/README.md +5 -2
- package/dist/index.js +404 -238
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join as join6 } from "node:path";
|
|
|
10
10
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
var DEFAULT_API_BASE = "https://grimoire
|
|
13
|
+
var DEFAULT_API_BASE = "https://cli.grimoire.monadeo.com";
|
|
14
14
|
function globalConfigPath() {
|
|
15
15
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire", "config.json");
|
|
16
16
|
}
|
|
@@ -99,28 +99,38 @@ async function fetchWithTimeout(url, init = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ../core/dist/auth.js
|
|
102
|
+
function configDir() {
|
|
103
|
+
return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "grimoire");
|
|
104
|
+
}
|
|
102
105
|
function credentialsPath() {
|
|
103
|
-
return join2(
|
|
106
|
+
return join2(configDir(), "credentials.json");
|
|
104
107
|
}
|
|
105
|
-
function
|
|
108
|
+
function storeSession(session) {
|
|
106
109
|
const path = credentialsPath();
|
|
107
110
|
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
108
|
-
writeFileSync2(path, JSON.stringify(
|
|
111
|
+
writeFileSync2(path, JSON.stringify(session) + "\n", { mode: 384 });
|
|
109
112
|
}
|
|
110
|
-
function
|
|
113
|
+
function readSession() {
|
|
111
114
|
try {
|
|
112
115
|
const parsed = JSON.parse(readFileSync2(credentialsPath(), "utf8"));
|
|
113
|
-
|
|
116
|
+
if (typeof parsed.refresh_token === "string" && parsed.refresh_token !== "" && typeof parsed.supabase_url === "string" && typeof parsed.supabase_anon_key === "string") {
|
|
117
|
+
return {
|
|
118
|
+
refresh_token: parsed.refresh_token,
|
|
119
|
+
supabase_url: parsed.supabase_url,
|
|
120
|
+
supabase_anon_key: parsed.supabase_anon_key
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return void 0;
|
|
114
124
|
} catch {
|
|
115
125
|
return void 0;
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
|
-
function
|
|
128
|
+
function clearSession() {
|
|
119
129
|
if (existsSync2(credentialsPath()))
|
|
120
130
|
rmSync(credentialsPath());
|
|
121
131
|
}
|
|
122
132
|
function machineTokenPath() {
|
|
123
|
-
return join2(
|
|
133
|
+
return join2(configDir(), "machine-token");
|
|
124
134
|
}
|
|
125
135
|
function storeMachineToken(token) {
|
|
126
136
|
const path = machineTokenPath();
|
|
@@ -139,56 +149,136 @@ function clearMachineToken() {
|
|
|
139
149
|
if (existsSync2(machineTokenPath()))
|
|
140
150
|
rmSync(machineTokenPath());
|
|
141
151
|
}
|
|
152
|
+
async function fetchAuthConfig(apiBase) {
|
|
153
|
+
const res = await fetchWithTimeout(`${apiBase.replace(/\/+$/, "")}/v1/auth/config`);
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
throw new ApiError(res.status, "auth_config_unavailable", await res.text());
|
|
156
|
+
const body = await res.json();
|
|
157
|
+
if (typeof body.supabase_url !== "string" || typeof body.supabase_anon_key !== "string" || typeof body.oauth_provider !== "string" || !Array.isArray(body.redirect_urls) || body.redirect_urls.length === 0) {
|
|
158
|
+
throw new ApiError(res.status, "auth_config_malformed", body);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
supabase_url: body.supabase_url,
|
|
162
|
+
supabase_anon_key: body.supabase_anon_key,
|
|
163
|
+
oauth_provider: body.oauth_provider,
|
|
164
|
+
redirect_urls: body.redirect_urls.map(String)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseDetail(text) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(text);
|
|
170
|
+
} catch {
|
|
171
|
+
return text;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function gotrueToken(supabaseUrl, anonKey, grantType, body) {
|
|
175
|
+
const res = await fetchWithTimeout(`${supabaseUrl.replace(/\/+$/, "")}/auth/v1/token?grant_type=${grantType}`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json", apikey: anonKey },
|
|
178
|
+
body: JSON.stringify(body)
|
|
179
|
+
});
|
|
180
|
+
const text = await res.text();
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
throw new ApiError(res.status, grantType === "pkce" ? "login_failed" : "refresh_failed", parseDetail(text));
|
|
183
|
+
}
|
|
184
|
+
const parsed = JSON.parse(text);
|
|
185
|
+
if (typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string" || typeof parsed.expires_in !== "number") {
|
|
186
|
+
throw new ApiError(res.status, "token_malformed", parsed);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
access_token: parsed.access_token,
|
|
190
|
+
refresh_token: parsed.refresh_token,
|
|
191
|
+
expires_in: parsed.expires_in
|
|
192
|
+
};
|
|
193
|
+
}
|
|
142
194
|
function base64url(buf) {
|
|
143
195
|
return buf.toString("base64url");
|
|
144
196
|
}
|
|
145
|
-
async function
|
|
146
|
-
const
|
|
197
|
+
async function bindCallback(server, redirectUrls) {
|
|
198
|
+
for (const candidate of redirectUrls) {
|
|
199
|
+
const port = Number(new URL(candidate).port);
|
|
200
|
+
const bound = await new Promise((resolve) => {
|
|
201
|
+
const onError = () => {
|
|
202
|
+
server.off("error", onError);
|
|
203
|
+
resolve(false);
|
|
204
|
+
};
|
|
205
|
+
server.once("error", onError);
|
|
206
|
+
server.listen(port, "127.0.0.1", () => {
|
|
207
|
+
server.off("error", onError);
|
|
208
|
+
resolve(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
if (bound)
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`No free login callback port among: ${redirectUrls.join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
async function browserLogin(apiBase, openBrowser, timeoutMs = 3e5) {
|
|
217
|
+
const config = await fetchAuthConfig(apiBase);
|
|
147
218
|
const verifier = base64url(randomBytes(32));
|
|
148
219
|
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
149
220
|
const state = base64url(randomBytes(16));
|
|
150
221
|
const code = await new Promise((resolve, reject) => {
|
|
151
|
-
|
|
222
|
+
let timer;
|
|
223
|
+
const finish2 = (outcome) => {
|
|
224
|
+
if (timer)
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
server.closeAllConnections();
|
|
227
|
+
server.close();
|
|
228
|
+
if ("code" in outcome)
|
|
229
|
+
resolve(outcome.code);
|
|
230
|
+
else
|
|
231
|
+
reject(outcome.error);
|
|
232
|
+
};
|
|
233
|
+
const server = createServer((req, res) => {
|
|
152
234
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
235
|
+
if (req.method !== "GET" || url.pathname !== `/callback/${state}`) {
|
|
236
|
+
res.writeHead(404).end();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const failure = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
|
153
240
|
const received = url.searchParams.get("code");
|
|
154
|
-
if (
|
|
155
|
-
|
|
241
|
+
if (failure || !received) {
|
|
242
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end(`Login failed: ${failure ?? "no code"}`);
|
|
243
|
+
finish2({ error: new Error(`Login failed: ${failure ?? "no code returned"}`) });
|
|
156
244
|
return;
|
|
157
245
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
server.close();
|
|
161
|
-
resolve(received);
|
|
246
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("Logged in to Grimoire. You can close this tab.");
|
|
247
|
+
finish2({ code: received });
|
|
162
248
|
});
|
|
163
|
-
server
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
249
|
+
bindCallback(server, config.redirect_urls).then((redirectBase) => {
|
|
250
|
+
timer = setTimeout(() => finish2({ error: new Error("Login timed out") }), timeoutMs);
|
|
251
|
+
const authorize = new URL(`${config.supabase_url.replace(/\/+$/, "")}/auth/v1/authorize`);
|
|
252
|
+
authorize.searchParams.set("provider", config.oauth_provider);
|
|
253
|
+
authorize.searchParams.set("redirect_to", `${redirectBase.replace(/\/+$/, "")}/${state}`);
|
|
254
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
255
|
+
authorize.searchParams.set("code_challenge_method", "s256");
|
|
256
|
+
openBrowser(authorize.toString());
|
|
257
|
+
}).catch((err) => finish2({ error: err }));
|
|
172
258
|
});
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
259
|
+
const tokens = await gotrueToken(config.supabase_url, config.supabase_anon_key, "pkce", {
|
|
260
|
+
auth_code: code,
|
|
261
|
+
code_verifier: verifier
|
|
262
|
+
});
|
|
263
|
+
storeSession({
|
|
264
|
+
refresh_token: tokens.refresh_token,
|
|
265
|
+
supabase_url: config.supabase_url,
|
|
266
|
+
supabase_anon_key: config.supabase_anon_key
|
|
177
267
|
});
|
|
178
|
-
if (!res.ok)
|
|
179
|
-
throw new Error(`Token exchange failed: ${res.status}`);
|
|
180
|
-
const { refresh_token } = await res.json();
|
|
181
|
-
if (typeof refresh_token !== "string" || refresh_token.length === 0) {
|
|
182
|
-
throw new Error("Token exchange succeeded but returned no refresh token");
|
|
183
|
-
}
|
|
184
|
-
storeRefreshToken(refresh_token);
|
|
185
268
|
}
|
|
186
269
|
|
|
187
270
|
// ../core/dist/client.js
|
|
271
|
+
var CODES = {
|
|
272
|
+
401: "unauthorized",
|
|
273
|
+
403: "forbidden",
|
|
274
|
+
404: "not_found",
|
|
275
|
+
422: "invalid_request",
|
|
276
|
+
429: "quota_exceeded"
|
|
277
|
+
};
|
|
188
278
|
var GrimoireClient = class {
|
|
189
279
|
baseUrl;
|
|
190
280
|
machineToken;
|
|
191
|
-
|
|
281
|
+
cachedAccessToken;
|
|
192
282
|
refreshInFlight;
|
|
193
283
|
constructor(opts = {}) {
|
|
194
284
|
this.baseUrl = (opts.baseUrl ?? loadGlobalConfig().apiBaseUrl).replace(/\/+$/, "");
|
|
@@ -197,38 +287,36 @@ var GrimoireClient = class {
|
|
|
197
287
|
async bearer() {
|
|
198
288
|
if (this.machineToken)
|
|
199
289
|
return this.machineToken;
|
|
200
|
-
if (this.
|
|
201
|
-
return this.
|
|
290
|
+
if (this.cachedAccessToken && this.cachedAccessToken.expiresAt > Date.now() + 6e4) {
|
|
291
|
+
return this.cachedAccessToken.token;
|
|
202
292
|
}
|
|
203
|
-
this.refreshInFlight ??= this.
|
|
293
|
+
this.refreshInFlight ??= this.refreshAccessToken().finally(() => {
|
|
204
294
|
this.refreshInFlight = void 0;
|
|
205
295
|
});
|
|
206
296
|
return this.refreshInFlight;
|
|
207
297
|
}
|
|
208
|
-
// Exchange the
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
298
|
+
// Exchange the stored refresh token for a fresh access token. GoTrue rotates
|
|
299
|
+
// refresh tokens, so the new one replaces the stored one every time.
|
|
300
|
+
async refreshAccessToken() {
|
|
301
|
+
const session = readSession();
|
|
302
|
+
if (!session)
|
|
212
303
|
throw new ApiError(401, "not_logged_in", "Run `grimoire login`");
|
|
213
|
-
|
|
214
|
-
method: "POST",
|
|
215
|
-
headers: { "Content-Type": "application/json" },
|
|
216
|
-
body: JSON.stringify({ refresh_token: refresh })
|
|
217
|
-
});
|
|
218
|
-
if (!res.ok)
|
|
219
|
-
throw new ApiError(res.status, "refresh_failed", "Run `grimoire login`");
|
|
220
|
-
let payload;
|
|
304
|
+
let tokens;
|
|
221
305
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
throw
|
|
306
|
+
tokens = await gotrueToken(session.supabase_url, session.supabase_anon_key, "refresh_token", {
|
|
307
|
+
refresh_token: session.refresh_token
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof ApiError)
|
|
311
|
+
throw new ApiError(err.status, "refresh_failed", "Run `grimoire login`");
|
|
312
|
+
throw err;
|
|
229
313
|
}
|
|
230
|
-
|
|
231
|
-
|
|
314
|
+
storeSession({ ...session, refresh_token: tokens.refresh_token });
|
|
315
|
+
this.cachedAccessToken = {
|
|
316
|
+
token: tokens.access_token,
|
|
317
|
+
expiresAt: Date.now() + tokens.expires_in * 1e3
|
|
318
|
+
};
|
|
319
|
+
return tokens.access_token;
|
|
232
320
|
}
|
|
233
321
|
async refreshSession() {
|
|
234
322
|
await this.bearer();
|
|
@@ -245,11 +333,12 @@ var GrimoireClient = class {
|
|
|
245
333
|
async request(path, init = {}, auth = true) {
|
|
246
334
|
let res = await this.send(path, init, auth);
|
|
247
335
|
if (res.status === 401 && auth && !this.machineToken) {
|
|
248
|
-
this.
|
|
336
|
+
this.cachedAccessToken = void 0;
|
|
249
337
|
res = await this.send(path, init, auth);
|
|
250
338
|
}
|
|
251
339
|
return this.parseResponse(res);
|
|
252
340
|
}
|
|
341
|
+
// FastAPI errors carry {"detail": "..."} (string) or {"detail": [...]} (422).
|
|
253
342
|
async parseResponse(res) {
|
|
254
343
|
const isJson = res.headers.get("content-type")?.includes("json") ?? false;
|
|
255
344
|
const text = await res.text();
|
|
@@ -264,33 +353,28 @@ var GrimoireClient = class {
|
|
|
264
353
|
}
|
|
265
354
|
}
|
|
266
355
|
if (!res.ok) {
|
|
267
|
-
const
|
|
268
|
-
throw new ApiError(res.status,
|
|
356
|
+
const detail = parsed && typeof body === "object" && body !== null ? body.detail : void 0;
|
|
357
|
+
throw new ApiError(res.status, CODES[res.status] ?? "error", detail ?? (parsed ? body : text));
|
|
269
358
|
}
|
|
270
359
|
if (isJson)
|
|
271
360
|
return parsed ? body : {};
|
|
272
361
|
return text;
|
|
273
362
|
}
|
|
274
363
|
search(input) {
|
|
275
|
-
return this.request("/v1/search", {
|
|
276
|
-
method: "POST",
|
|
277
|
-
body: JSON.stringify(input)
|
|
278
|
-
});
|
|
364
|
+
return this.request("/v1/search", { method: "POST", body: JSON.stringify(input) });
|
|
279
365
|
}
|
|
280
|
-
listSources(
|
|
281
|
-
return this.request(
|
|
366
|
+
listSources() {
|
|
367
|
+
return this.request("/v1/sources");
|
|
282
368
|
}
|
|
283
|
-
listVersions(
|
|
284
|
-
return this.request(`/v1/sources/${encodeURIComponent(
|
|
369
|
+
listVersions(product) {
|
|
370
|
+
return this.request(`/v1/sources/${encodeURIComponent(product)}/versions`);
|
|
285
371
|
}
|
|
286
|
-
|
|
287
|
-
return this.request(`/v1/
|
|
372
|
+
getDoc(pointId, window = 2) {
|
|
373
|
+
return this.request(`/v1/doc/${encodeURIComponent(pointId)}?window=${window}`);
|
|
288
374
|
}
|
|
289
|
-
reportResult(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
body: JSON.stringify({ chunk_id: chunkId, verdict, note })
|
|
293
|
-
});
|
|
375
|
+
reportResult(pointId, verdict, note) {
|
|
376
|
+
const body = { point_id: pointId, verdict, ...note ? { note } : {} };
|
|
377
|
+
return this.request("/v1/report", { method: "POST", body: JSON.stringify(body) });
|
|
294
378
|
}
|
|
295
379
|
submitSource(body) {
|
|
296
380
|
return this.request("/v1/sources", { method: "POST", body: JSON.stringify(body) });
|
|
@@ -298,12 +382,49 @@ var GrimoireClient = class {
|
|
|
298
382
|
getJob(jobId) {
|
|
299
383
|
return this.request(`/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
300
384
|
}
|
|
385
|
+
me() {
|
|
386
|
+
return this.request("/v1/me");
|
|
387
|
+
}
|
|
388
|
+
reviewQueue() {
|
|
389
|
+
return this.request("/v1/staff/review-queue");
|
|
390
|
+
}
|
|
391
|
+
approveJob(jobId) {
|
|
392
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/approve`, { method: "POST" });
|
|
393
|
+
}
|
|
394
|
+
listUsers() {
|
|
395
|
+
return this.request("/v1/staff/users");
|
|
396
|
+
}
|
|
397
|
+
grantUser(subject, name) {
|
|
398
|
+
return this.request("/v1/staff/users", {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body: JSON.stringify({ subject, name })
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
revokeUser(subject) {
|
|
404
|
+
return this.request(`/v1/staff/users/${encodeURIComponent(subject)}`, { method: "DELETE" });
|
|
405
|
+
}
|
|
406
|
+
rejectJob(jobId, reason) {
|
|
407
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/reject`, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: JSON.stringify({ reason })
|
|
410
|
+
});
|
|
411
|
+
}
|
|
301
412
|
};
|
|
302
413
|
|
|
303
414
|
// src/args.ts
|
|
304
415
|
var UsageError = class extends Error {
|
|
305
416
|
};
|
|
306
|
-
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
417
|
+
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
418
|
+
"json",
|
|
419
|
+
"compact",
|
|
420
|
+
"watch",
|
|
421
|
+
"names",
|
|
422
|
+
"http",
|
|
423
|
+
"unset",
|
|
424
|
+
"rolling",
|
|
425
|
+
"debug",
|
|
426
|
+
"no-launch-browser"
|
|
427
|
+
]);
|
|
307
428
|
function parseArgs(argv, aliases = {}, allowed) {
|
|
308
429
|
const positionals = [];
|
|
309
430
|
const flags = {};
|
|
@@ -354,39 +475,39 @@ function requireFlagOneOf(parsed, name, allowed, usage) {
|
|
|
354
475
|
|
|
355
476
|
// src/output.ts
|
|
356
477
|
var EXIT = { ok: 0, apiError: 1, authRequired: 2, quota: 3, notFound: 4 };
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
process.stderr.write(`${label}
|
|
478
|
+
function preamble(res) {
|
|
479
|
+
const versions = Object.entries(res.resolved_versions).map(([product, version]) => `${product}@${version}`).join(", ");
|
|
480
|
+
process.stderr.write(`sources: ${versions} \xB7 retrievals remaining: ${res.retrievals_remaining}
|
|
361
481
|
`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
process.stderr.write(
|
|
366
|
-
"note: low-confidence results \u2014 the docs may not cover this; tell the user rather than guessing.\n"
|
|
367
|
-
);
|
|
482
|
+
process.stderr.write(`note: ${res.untrusted_content_notice}
|
|
483
|
+
`);
|
|
484
|
+
if (res.results.length === 0) {
|
|
485
|
+
process.stderr.write("note: no result passed the relevance threshold \u2014 the docs may not cover this.\n");
|
|
368
486
|
}
|
|
369
487
|
}
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
488
|
+
function heading(r) {
|
|
489
|
+
return r.heading_path.join(" \u203A ");
|
|
490
|
+
}
|
|
491
|
+
function printResults(res) {
|
|
492
|
+
preamble(res);
|
|
493
|
+
for (const r of res.results) {
|
|
494
|
+
process.stdout.write(
|
|
495
|
+
`
|
|
496
|
+
${r.score.toFixed(3)} ${r.product}@${r.version} ${heading(r)}
|
|
497
|
+
${r.source_url}
|
|
498
|
+
point_id: ${r.point_id}
|
|
499
|
+
`
|
|
500
|
+
);
|
|
379
501
|
const preview = r.text.length > 500 ? `${r.text.slice(0, 500)}\u2026` : r.text;
|
|
380
502
|
process.stdout.write(`${preview}
|
|
381
503
|
`);
|
|
382
504
|
}
|
|
383
505
|
}
|
|
384
|
-
function printCompact(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const r of results) {
|
|
506
|
+
function printCompact(res) {
|
|
507
|
+
preamble(res);
|
|
508
|
+
for (const r of res.results) {
|
|
388
509
|
process.stdout.write(
|
|
389
|
-
`${r.score.toFixed(3)} | ${r.
|
|
510
|
+
`${r.score.toFixed(3)} | ${r.product}@${r.version} | ${heading(r)} | ${r.source_url} | ${r.point_id}
|
|
390
511
|
`
|
|
391
512
|
);
|
|
392
513
|
}
|
|
@@ -438,7 +559,7 @@ function setupCodex() {
|
|
|
438
559
|
function hasSession() {
|
|
439
560
|
if (process.env.GRIMOIRE_AUTH_TOKEN) return true;
|
|
440
561
|
try {
|
|
441
|
-
return readMachineToken() !== void 0 ||
|
|
562
|
+
return readMachineToken() !== void 0 || readSession() !== void 0;
|
|
442
563
|
} catch {
|
|
443
564
|
return false;
|
|
444
565
|
}
|
|
@@ -538,7 +659,7 @@ var CONFIG_KEYS = {
|
|
|
538
659
|
describe: `API origin without a path (default ${DEFAULT_API_BASE}; env GRIMOIRE_API_URL overrides)`,
|
|
539
660
|
parse: (raw) => {
|
|
540
661
|
if (!/^https?:\/\/[^/]+$/.test(raw)) {
|
|
541
|
-
throw new UsageError("api-url must be a bare origin, e.g. https://
|
|
662
|
+
throw new UsageError("api-url must be a bare origin, e.g. https://api.example.com");
|
|
542
663
|
}
|
|
543
664
|
return raw;
|
|
544
665
|
}
|
|
@@ -547,23 +668,10 @@ var CONFIG_KEYS = {
|
|
|
547
668
|
prop: "updateCheckHours",
|
|
548
669
|
describe: "hours between CLI update checks (default 24, 0 disables)",
|
|
549
670
|
parse: (raw) => intInRange("update-check-hours", raw, 0)
|
|
550
|
-
},
|
|
551
|
-
language: {
|
|
552
|
-
prop: "defaultLanguage",
|
|
553
|
-
describe: "default search language filter",
|
|
554
|
-
parse: (raw) => {
|
|
555
|
-
if (!raw) throw new UsageError("language must be non-empty");
|
|
556
|
-
return raw;
|
|
557
|
-
}
|
|
558
|
-
},
|
|
559
|
-
"max-response-tokens": {
|
|
560
|
-
prop: "maxResponseTokens",
|
|
561
|
-
describe: "search response token budget",
|
|
562
|
-
parse: (raw) => intInRange("max-response-tokens", raw, 1)
|
|
563
671
|
}
|
|
564
672
|
};
|
|
565
673
|
var AUTH_TOKEN_KEY = "auth-token";
|
|
566
|
-
var MACHINE_TOKEN_RE = /^mt_[
|
|
674
|
+
var MACHINE_TOKEN_RE = /^mt_[A-Za-z0-9_-]{32,}$/;
|
|
567
675
|
var USAGE = "Usage: grimoire config [<key>] [<value>] [--unset] (keys: " + [...Object.keys(CONFIG_KEYS), AUTH_TOKEN_KEY].join(", ") + ")";
|
|
568
676
|
function runConfig(args) {
|
|
569
677
|
const [key, value] = args.positionals;
|
|
@@ -595,7 +703,7 @@ function runConfig(args) {
|
|
|
595
703
|
return EXIT.ok;
|
|
596
704
|
}
|
|
597
705
|
if (!MACHINE_TOKEN_RE.test(value)) {
|
|
598
|
-
throw new UsageError("auth-token must be a machine token
|
|
706
|
+
throw new UsageError("auth-token must be a machine token starting with mt_");
|
|
599
707
|
}
|
|
600
708
|
storeMachineToken(value);
|
|
601
709
|
process.stdout.write(`${AUTH_TOKEN_KEY} set
|
|
@@ -668,6 +776,33 @@ function runUpdate() {
|
|
|
668
776
|
return result.status === 0 ? EXIT.ok : EXIT.apiError;
|
|
669
777
|
}
|
|
670
778
|
|
|
779
|
+
// src/commands/ingest.ts
|
|
780
|
+
function submissionFromArgs(url, args) {
|
|
781
|
+
const usage = "Usage: grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)";
|
|
782
|
+
const product = args.flags.product?.[0];
|
|
783
|
+
if (!product) throw new UsageError(usage);
|
|
784
|
+
const fixed = args.flags.fixed?.[0];
|
|
785
|
+
const npm = args.flags.npm?.[0];
|
|
786
|
+
const pypi = args.flags.pypi?.[0];
|
|
787
|
+
const github = args.flags.github?.[0];
|
|
788
|
+
const probes = [npm, pypi, github].filter(Boolean).length;
|
|
789
|
+
if (probes > 1) throw new UsageError("Pass only one of --npm, --pypi, --github");
|
|
790
|
+
const body = {
|
|
791
|
+
url,
|
|
792
|
+
product,
|
|
793
|
+
version_rule: { kind: "rolling" },
|
|
794
|
+
include_patterns: args.flags.include ?? [],
|
|
795
|
+
exclude_patterns: args.flags.exclude ?? []
|
|
796
|
+
};
|
|
797
|
+
if (args.bools.has("rolling")) return body;
|
|
798
|
+
if (fixed) return { ...body, version_rule: { kind: "fixed", value: fixed } };
|
|
799
|
+
if (npm) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "npm", package: npm } };
|
|
800
|
+
if (pypi) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "pypi", package: pypi } };
|
|
801
|
+
if (github) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "github", repo: github } };
|
|
802
|
+
throw new UsageError(`${usage}
|
|
803
|
+
A version rule is mandatory: --rolling for unversioned docs, --fixed, or a release probe.`);
|
|
804
|
+
}
|
|
805
|
+
|
|
671
806
|
// src/updatecheck.ts
|
|
672
807
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
673
808
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -724,15 +859,21 @@ async function refreshUpdateState(current, configuredHours) {
|
|
|
724
859
|
}
|
|
725
860
|
|
|
726
861
|
// src/index.ts
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
731
|
-
} catch {
|
|
732
|
-
process.stdout.write(`Open this URL to log in:
|
|
733
|
-
${url}
|
|
862
|
+
function loginOpener(launchBrowser) {
|
|
863
|
+
return (url) => {
|
|
864
|
+
process.stdout.write(`${url}
|
|
734
865
|
`);
|
|
735
|
-
|
|
866
|
+
if (!launchBrowser) {
|
|
867
|
+
process.stderr.write("Open the URL above in a browser to log in; waiting for the callback\u2026\n");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
871
|
+
try {
|
|
872
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
873
|
+
} catch {
|
|
874
|
+
process.stderr.write("Could not open a browser; open the URL above manually.\n");
|
|
875
|
+
}
|
|
876
|
+
};
|
|
736
877
|
}
|
|
737
878
|
function parseSourceFlags(values) {
|
|
738
879
|
return (values ?? []).map((v) => {
|
|
@@ -740,33 +881,39 @@ function parseSourceFlags(values) {
|
|
|
740
881
|
return version ? { source, version } : { source };
|
|
741
882
|
});
|
|
742
883
|
}
|
|
743
|
-
|
|
884
|
+
function toSelectors(pins) {
|
|
885
|
+
return pins.map((p) => ({ product: p.source, ...p.version ? { version: p.version } : {} }));
|
|
886
|
+
}
|
|
887
|
+
var VERSION = true ? "0.4.0" : "dev";
|
|
744
888
|
var HELP = `grimoire ${VERSION} \u2014 documentation retrieval for AI agents
|
|
745
889
|
|
|
746
|
-
grimoire login | logout | whoami
|
|
890
|
+
grimoire login [--no-launch-browser] | logout | whoami
|
|
747
891
|
grimoire setup <claude-code|cursor|windsurf|codex>
|
|
748
892
|
grimoire init
|
|
749
|
-
grimoire search "<query>" [-s nextjs@15 -s react] [--
|
|
893
|
+
grimoire search "<query>" [-s nextjs@15 -s react] [--json|--compact] [--debug]
|
|
750
894
|
grimoire sources [--q <kw>] [--names|--json]
|
|
751
|
-
grimoire versions <
|
|
895
|
+
grimoire versions <product> [--json]
|
|
752
896
|
grimoire config [<key>] [<value>] [--unset]
|
|
753
897
|
grimoire update
|
|
754
|
-
grimoire doc <
|
|
755
|
-
grimoire report <
|
|
756
|
-
grimoire ingest <url>
|
|
898
|
+
grimoire doc <point_id> [--window 2] [--json]
|
|
899
|
+
grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]
|
|
900
|
+
grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)
|
|
901
|
+
[--include <pattern>]... [--exclude <pattern>]... [--watch]
|
|
757
902
|
grimoire jobs <job_id> [--watch]
|
|
903
|
+
grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..."
|
|
904
|
+
grimoire staff users [--json] | grant <subject> --name "..." | revoke <subject>
|
|
758
905
|
grimoire mcp [--http]
|
|
759
906
|
grimoire help | --help | -h
|
|
760
907
|
grimoire version | --version | -v
|
|
761
908
|
|
|
762
909
|
env: GRIMOIRE_AUTH_TOKEN \u2014 machine token (CI, instead of login)
|
|
763
|
-
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
910
|
+
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
764
911
|
`;
|
|
765
912
|
var COMMAND_FLAGS = {
|
|
766
913
|
version: [],
|
|
767
914
|
"--version": [],
|
|
768
915
|
"-v": [],
|
|
769
|
-
login: [],
|
|
916
|
+
login: ["no-launch-browser"],
|
|
770
917
|
logout: [],
|
|
771
918
|
setup: [],
|
|
772
919
|
init: [],
|
|
@@ -777,14 +924,23 @@ var COMMAND_FLAGS = {
|
|
|
777
924
|
mcp: ["http"],
|
|
778
925
|
config: ["unset"],
|
|
779
926
|
update: [],
|
|
780
|
-
search: ["source", "
|
|
927
|
+
search: ["source", "json", "compact", "debug"],
|
|
781
928
|
sources: ["q", "names", "json"],
|
|
782
929
|
versions: ["json"],
|
|
783
|
-
doc: ["window"],
|
|
930
|
+
doc: ["window", "json"],
|
|
784
931
|
report: ["verdict", "note"],
|
|
785
|
-
ingest: ["
|
|
786
|
-
jobs: ["watch"]
|
|
932
|
+
ingest: ["product", "rolling", "fixed", "npm", "pypi", "github", "include", "exclude", "watch"],
|
|
933
|
+
jobs: ["watch"],
|
|
934
|
+
staff: ["reason", "name", "json"]
|
|
787
935
|
};
|
|
936
|
+
function describeError(err) {
|
|
937
|
+
const detail = typeof err.body === "string" ? err.body : err.body !== void 0 && err.body !== null ? JSON.stringify(err.body) : "";
|
|
938
|
+
return `error: ${err.code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
939
|
+
}
|
|
940
|
+
function describeJob(job) {
|
|
941
|
+
const extra = [job.reason ? `reason: ${job.reason}` : "", job.source_id ? `source: ${job.source_id}` : ""].filter(Boolean).join(" ");
|
|
942
|
+
return `${job.state}${extra ? ` ${extra}` : ""}`;
|
|
943
|
+
}
|
|
788
944
|
async function main(argv) {
|
|
789
945
|
const [command, ...rest] = argv;
|
|
790
946
|
const args = parseArgs(rest, { "-s": "source", "-q": "q" }, command !== void 0 ? COMMAND_FLAGS[command] : []);
|
|
@@ -797,12 +953,14 @@ async function main(argv) {
|
|
|
797
953
|
`);
|
|
798
954
|
return EXIT.ok;
|
|
799
955
|
case "login": {
|
|
800
|
-
|
|
956
|
+
const launch = !args.bools.has("no-launch-browser");
|
|
957
|
+
process.stderr.write(launch ? "Opening your browser to log in\u2026\n" : "Login URL:\n");
|
|
958
|
+
await browserLogin(loadGlobalConfig().apiBaseUrl, loginOpener(launch));
|
|
801
959
|
process.stdout.write("Logged in.\n");
|
|
802
960
|
return EXIT.ok;
|
|
803
961
|
}
|
|
804
962
|
case "logout":
|
|
805
|
-
|
|
963
|
+
clearSession();
|
|
806
964
|
process.stdout.write("Logged out.\n");
|
|
807
965
|
return EXIT.ok;
|
|
808
966
|
case "setup":
|
|
@@ -827,33 +985,22 @@ async function main(argv) {
|
|
|
827
985
|
try {
|
|
828
986
|
switch (command) {
|
|
829
987
|
case "whoami": {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return EXIT.ok;
|
|
833
|
-
}
|
|
834
|
-
if (readMachineToken()) {
|
|
835
|
-
process.stdout.write("machine token configured (grimoire config auth-token)\n");
|
|
836
|
-
return EXIT.ok;
|
|
837
|
-
}
|
|
838
|
-
if (!readRefreshToken()) {
|
|
988
|
+
const via = process.env.GRIMOIRE_AUTH_TOKEN ? "GRIMOIRE_AUTH_TOKEN" : readMachineToken() ? "grimoire config auth-token" : readSession() ? "browser login" : void 0;
|
|
989
|
+
if (!via) {
|
|
839
990
|
process.stderr.write("not logged in \u2014 run `grimoire login`\n");
|
|
840
991
|
return EXIT.authRequired;
|
|
841
992
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
process.stderr.write(`not logged in (${reason}) \u2014 run \`grimoire login\`
|
|
849
|
-
`);
|
|
850
|
-
return EXIT.authRequired;
|
|
851
|
-
}
|
|
993
|
+
const me = await client.me();
|
|
994
|
+
process.stdout.write(
|
|
995
|
+
`${me.kind} ${me.subject}${me.is_staff ? " staff" : ""} quota ${me.quota_per_day}/day via ${via}
|
|
996
|
+
`
|
|
997
|
+
);
|
|
998
|
+
return EXIT.ok;
|
|
852
999
|
}
|
|
853
1000
|
case "search": {
|
|
854
1001
|
const query = args.positionals[0];
|
|
855
1002
|
if (!query) {
|
|
856
|
-
process.stderr.write('Usage: grimoire search "<query>" -s <
|
|
1003
|
+
process.stderr.write('Usage: grimoire search "<query>" -s <product>[@version]\n');
|
|
857
1004
|
return EXIT.apiError;
|
|
858
1005
|
}
|
|
859
1006
|
const explicit = parseSourceFlags(args.flags.source);
|
|
@@ -861,99 +1008,73 @@ async function main(argv) {
|
|
|
861
1008
|
if (sources.length === 0) {
|
|
862
1009
|
const initHelps = existsSync6(join6(process.cwd(), "package.json")) || existsSync6(join6(process.cwd(), "requirements.txt"));
|
|
863
1010
|
process.stderr.write(
|
|
864
|
-
'No sources selected. List what is indexed:\n grimoire sources\nthen scope the search:\n grimoire search "<query>" -s <
|
|
1011
|
+
'No sources selected. List what is indexed:\n grimoire sources\nthen scope the search:\n grimoire search "<query>" -s <product>[@version]\n' + (initHelps ? "or pin this project's sources from its dependencies:\n grimoire init\n" : "")
|
|
865
1012
|
);
|
|
866
1013
|
return EXIT.apiError;
|
|
867
1014
|
}
|
|
868
|
-
const res = await client.search({
|
|
869
|
-
query,
|
|
870
|
-
sources,
|
|
871
|
-
language: args.flags.lang?.[0],
|
|
872
|
-
top_k: intFlag(args, "top-k")
|
|
873
|
-
});
|
|
1015
|
+
const res = await client.search({ query, sources: toSelectors(sources), debug: args.bools.has("debug") });
|
|
874
1016
|
if (json) process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
875
|
-
else if (args.bools.has("compact")) printCompact(res
|
|
876
|
-
else printResults(res
|
|
1017
|
+
else if (args.bools.has("compact")) printCompact(res);
|
|
1018
|
+
else printResults(res);
|
|
877
1019
|
return EXIT.ok;
|
|
878
1020
|
}
|
|
879
1021
|
case "sources": {
|
|
880
|
-
const
|
|
881
|
-
const sources =
|
|
1022
|
+
const needle = args.flags.q?.[0]?.toLowerCase();
|
|
1023
|
+
const sources = (await client.listSources()).filter(
|
|
1024
|
+
(s) => !needle || s.product.toLowerCase().includes(needle) || s.base_url.toLowerCase().includes(needle)
|
|
1025
|
+
);
|
|
882
1026
|
if (json) {
|
|
883
1027
|
process.stdout.write(JSON.stringify(sources, null, 2) + "\n");
|
|
884
1028
|
} else if (args.bools.has("names")) {
|
|
885
|
-
for (const s of sources) process.stdout.write(`${s.
|
|
1029
|
+
for (const s of sources) process.stdout.write(`${s.product}
|
|
886
1030
|
`);
|
|
887
1031
|
} else {
|
|
888
1032
|
for (const s of sources) {
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
s.latest_chunks != null ? `${s.latest_chunks} chunks` : "",
|
|
893
|
-
s.latest_pages != null ? `${s.latest_pages} pages` : "",
|
|
894
|
-
s.latest_crawled_at ? `crawled ${s.latest_crawled_at.slice(0, 10)}` : ""
|
|
895
|
-
].filter(Boolean).join(" \xB7 ");
|
|
896
|
-
process.stdout.write(
|
|
897
|
-
[`${s.source_id}@${latest}${vis}`, meta, s.origin_url ?? ""].filter(Boolean).join(" ") + "\n"
|
|
898
|
-
);
|
|
1033
|
+
const versions = s.versions.length > 0 ? s.versions.join(", ") : "(not indexed yet)";
|
|
1034
|
+
process.stdout.write(`${s.product} ${versions} ${s.base_url}
|
|
1035
|
+
`);
|
|
899
1036
|
}
|
|
900
1037
|
}
|
|
901
1038
|
return EXIT.ok;
|
|
902
1039
|
}
|
|
903
1040
|
case "versions": {
|
|
904
|
-
const
|
|
905
|
-
const res = await client.listVersions(
|
|
906
|
-
const versions = res.versions ?? [];
|
|
1041
|
+
const product = requirePositional(args, 0, "Usage: grimoire versions <product> [--json]");
|
|
1042
|
+
const res = await client.listVersions(product);
|
|
907
1043
|
if (json) {
|
|
908
|
-
process.stdout.write(JSON.stringify(
|
|
1044
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
909
1045
|
return EXIT.ok;
|
|
910
1046
|
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
process.stdout.write(`no active versions for ${source}
|
|
1047
|
+
for (const v of res.versions) {
|
|
1048
|
+
process.stdout.write(`${v.version}${v.version === res.latest ? " latest" : ""} ${v.chunk_count} chunks
|
|
914
1049
|
`);
|
|
915
|
-
return EXIT.ok;
|
|
916
|
-
}
|
|
917
|
-
for (const v of available) {
|
|
918
|
-
const label = v.semver ?? v.version_id;
|
|
919
|
-
const parts = [
|
|
920
|
-
label,
|
|
921
|
-
v.is_latest ? "latest" : "",
|
|
922
|
-
`${v.chunk_count ?? 0} chunks`,
|
|
923
|
-
`crawled ${(v.ingested_at ?? "").slice(0, 10)}`,
|
|
924
|
-
v.semver ? `(${v.version_id})` : ""
|
|
925
|
-
].filter(Boolean);
|
|
926
|
-
process.stdout.write(parts.join(" ") + "\n");
|
|
927
1050
|
}
|
|
928
1051
|
return EXIT.ok;
|
|
929
1052
|
}
|
|
930
1053
|
case "doc": {
|
|
931
|
-
const
|
|
1054
|
+
const pointId = requirePositional(args, 0, "Usage: grimoire doc <point_id> [--window 2] [--json]");
|
|
932
1055
|
const window = intFlag(args, "window", { min: 0, max: 5 }) ?? 2;
|
|
933
|
-
const res = await client.
|
|
934
|
-
|
|
1056
|
+
const res = await client.getDoc(pointId, window);
|
|
1057
|
+
if (json) {
|
|
1058
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1059
|
+
return EXIT.ok;
|
|
1060
|
+
}
|
|
1061
|
+
process.stderr.write(`${res.product}@${res.version} ${res.heading_path.join(" \u203A ")}
|
|
1062
|
+
${res.source_url}
|
|
1063
|
+
`);
|
|
1064
|
+
process.stdout.write(res.text + "\n");
|
|
935
1065
|
return EXIT.ok;
|
|
936
1066
|
}
|
|
937
1067
|
case "report": {
|
|
938
|
-
const usage = 'Usage: grimoire report <
|
|
939
|
-
const
|
|
1068
|
+
const usage = 'Usage: grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]';
|
|
1069
|
+
const pointId = requirePositional(args, 0, usage);
|
|
940
1070
|
const verdict = requireFlagOneOf(args, "verdict", ["helpful", "incorrect", "outdated"], usage);
|
|
941
|
-
await client.reportResult(
|
|
1071
|
+
await client.reportResult(pointId, verdict, args.flags.note?.[0]);
|
|
942
1072
|
process.stdout.write("Reported.\n");
|
|
943
1073
|
return EXIT.ok;
|
|
944
1074
|
}
|
|
945
1075
|
case "ingest": {
|
|
946
|
-
const url = requirePositional(
|
|
947
|
-
|
|
948
|
-
0,
|
|
949
|
-
"Usage: grimoire ingest <url> [--version 15.2] [--private] [--webhook URL] [--watch]"
|
|
950
|
-
);
|
|
951
|
-
const res = await client.submitSource({
|
|
952
|
-
url,
|
|
953
|
-
version: args.flags.version?.[0],
|
|
954
|
-
visibility: args.bools.has("private") ? "private" : "public",
|
|
955
|
-
webhook_url: args.flags.webhook?.[0]
|
|
956
|
-
});
|
|
1076
|
+
const url = requirePositional(args, 0, "Usage: grimoire ingest <url> --product <name> --rolling|--fixed|--npm|--pypi|--github");
|
|
1077
|
+
const res = await client.submitSource(submissionFromArgs(url, args));
|
|
957
1078
|
process.stdout.write(`Job: ${res.job_id}
|
|
958
1079
|
`);
|
|
959
1080
|
if (args.bools.has("watch")) return watchJob(client, res.job_id);
|
|
@@ -963,6 +1084,54 @@ async function main(argv) {
|
|
|
963
1084
|
const jobId = requirePositional(args, 0, "Usage: grimoire jobs <job_id> [--watch]");
|
|
964
1085
|
return args.bools.has("watch") ? watchJob(client, jobId) : printJob(client, jobId);
|
|
965
1086
|
}
|
|
1087
|
+
case "staff": {
|
|
1088
|
+
const usage = 'Usage: grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..." | users [--json] | grant <subject> --name "..." | revoke <subject>';
|
|
1089
|
+
const [action, jobId] = args.positionals;
|
|
1090
|
+
if (action === "users") {
|
|
1091
|
+
const users = await client.listUsers();
|
|
1092
|
+
if (json) process.stdout.write(JSON.stringify(users, null, 2) + "\n");
|
|
1093
|
+
else for (const u of users) process.stdout.write(`${u.subject} ${u.status} ${u.name} by ${u.granted_by}
|
|
1094
|
+
`);
|
|
1095
|
+
return EXIT.ok;
|
|
1096
|
+
}
|
|
1097
|
+
if (action === "grant") {
|
|
1098
|
+
const name = args.flags.name?.[0];
|
|
1099
|
+
if (!jobId || !name) throw new UsageError(usage);
|
|
1100
|
+
const granted = await client.grantUser(jobId, name);
|
|
1101
|
+
process.stdout.write(`${granted.subject} ${granted.status} ${granted.name}
|
|
1102
|
+
`);
|
|
1103
|
+
return EXIT.ok;
|
|
1104
|
+
}
|
|
1105
|
+
if (action === "revoke") {
|
|
1106
|
+
if (!jobId) throw new UsageError(usage);
|
|
1107
|
+
const revoked = await client.revokeUser(jobId);
|
|
1108
|
+
process.stdout.write(`${revoked.subject} ${revoked.status}
|
|
1109
|
+
`);
|
|
1110
|
+
return EXIT.ok;
|
|
1111
|
+
}
|
|
1112
|
+
if (action === "queue") {
|
|
1113
|
+
const queue = await client.reviewQueue();
|
|
1114
|
+
if (json) process.stdout.write(JSON.stringify(queue, null, 2) + "\n");
|
|
1115
|
+
else if (queue.length === 0) process.stdout.write("review queue is empty\n");
|
|
1116
|
+
else for (const job of queue) process.stdout.write(`${job.id} ${describeJob(job)}
|
|
1117
|
+
`);
|
|
1118
|
+
return EXIT.ok;
|
|
1119
|
+
}
|
|
1120
|
+
if (!jobId) throw new UsageError(usage);
|
|
1121
|
+
if (action === "approve") {
|
|
1122
|
+
process.stdout.write(`${describeJob(await client.approveJob(jobId))}
|
|
1123
|
+
`);
|
|
1124
|
+
return EXIT.ok;
|
|
1125
|
+
}
|
|
1126
|
+
if (action === "reject") {
|
|
1127
|
+
const reason = args.flags.reason?.[0];
|
|
1128
|
+
if (!reason) throw new UsageError(usage);
|
|
1129
|
+
process.stdout.write(`${describeJob(await client.rejectJob(jobId, reason))}
|
|
1130
|
+
`);
|
|
1131
|
+
return EXIT.ok;
|
|
1132
|
+
}
|
|
1133
|
+
throw new UsageError(usage);
|
|
1134
|
+
}
|
|
966
1135
|
default:
|
|
967
1136
|
process.stderr.write(`Unknown command: ${command}
|
|
968
1137
|
${HELP}`);
|
|
@@ -970,14 +1139,10 @@ ${HELP}`);
|
|
|
970
1139
|
}
|
|
971
1140
|
} catch (err) {
|
|
972
1141
|
if (err instanceof ApiError) {
|
|
973
|
-
|
|
974
|
-
process.stderr.write(`error: ${err.code}${body.message ? ` \u2014 ${body.message}` : ""}
|
|
975
|
-
`);
|
|
976
|
-
if (body.available?.length) process.stderr.write(`available: ${body.available.join(", ")}
|
|
977
|
-
`);
|
|
978
|
-
if (body.did_you_mean?.length) process.stderr.write(`did you mean: ${body.did_you_mean.join(", ")}
|
|
1142
|
+
process.stderr.write(`${describeError(err)}
|
|
979
1143
|
`);
|
|
980
1144
|
if (err.status === 401) return EXIT.authRequired;
|
|
1145
|
+
if (err.status === 403) return EXIT.authRequired;
|
|
981
1146
|
if (err.status === 429) return EXIT.quota;
|
|
982
1147
|
if (err.status === 404) return EXIT.notFound;
|
|
983
1148
|
return EXIT.apiError;
|
|
@@ -985,14 +1150,13 @@ ${HELP}`);
|
|
|
985
1150
|
throw err;
|
|
986
1151
|
}
|
|
987
1152
|
}
|
|
988
|
-
var
|
|
989
|
-
var
|
|
1153
|
+
var TERMINAL_STATES = ["accepted", "done", "rejected", "failed", "pending_review"];
|
|
1154
|
+
var FAILED_STATES = ["rejected", "failed"];
|
|
990
1155
|
async function printJob(client, jobId) {
|
|
991
1156
|
const job = await client.getJob(jobId);
|
|
992
|
-
|
|
993
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1157
|
+
process.stdout.write(`${describeJob(job)}
|
|
994
1158
|
`);
|
|
995
|
-
return
|
|
1159
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
996
1160
|
}
|
|
997
1161
|
var WATCH_POLL_MS = 5e3;
|
|
998
1162
|
var WATCH_BACKOFF_CAP_MS = 3e4;
|
|
@@ -1006,11 +1170,13 @@ async function watchJob(client, jobId) {
|
|
|
1006
1170
|
while (Date.now() < deadline) {
|
|
1007
1171
|
try {
|
|
1008
1172
|
const job = await client.getJob(jobId);
|
|
1009
|
-
|
|
1010
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1173
|
+
process.stdout.write(`${describeJob(job)}
|
|
1011
1174
|
`);
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1175
|
+
if (TERMINAL_STATES.includes(job.state)) {
|
|
1176
|
+
if (job.state === "pending_review") {
|
|
1177
|
+
process.stderr.write("Parked for staff review: grimoire staff queue\n");
|
|
1178
|
+
}
|
|
1179
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
1014
1180
|
}
|
|
1015
1181
|
backoff = WATCH_POLL_MS;
|
|
1016
1182
|
await sleep(WATCH_POLL_MS);
|