@monadeo.com/grimoire-cli 0.3.7 → 0.4.1
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 +430 -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);
|
|
162
|
-
});
|
|
163
|
-
server.listen(0, "127.0.0.1", () => {
|
|
164
|
-
const port = server.address().port;
|
|
165
|
-
const redirect = `http://127.0.0.1:${port}/callback`;
|
|
166
|
-
openBrowser2(`${broker}/auth/cli/start?code_challenge=${challenge}&code_challenge_method=S256&state=${state}&redirect_uri=${encodeURIComponent(redirect)}`);
|
|
246
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("Logged in to Grimoire. You can close this tab.");
|
|
247
|
+
finish2({ code: received });
|
|
167
248
|
});
|
|
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,58 @@ 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
|
+
mintToken(name, quotaPerDay) {
|
|
407
|
+
return this.request("/v1/staff/tokens", {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: JSON.stringify({ name, quota_per_day: quotaPerDay })
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
recrawlSource(sourceId) {
|
|
413
|
+
return this.request(`/v1/staff/sources/${encodeURIComponent(sourceId)}/recrawl`, { method: "POST" });
|
|
414
|
+
}
|
|
415
|
+
rejectJob(jobId, reason) {
|
|
416
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/reject`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
body: JSON.stringify({ reason })
|
|
419
|
+
});
|
|
420
|
+
}
|
|
301
421
|
};
|
|
302
422
|
|
|
303
423
|
// src/args.ts
|
|
304
424
|
var UsageError = class extends Error {
|
|
305
425
|
};
|
|
306
|
-
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
426
|
+
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
427
|
+
"json",
|
|
428
|
+
"compact",
|
|
429
|
+
"watch",
|
|
430
|
+
"names",
|
|
431
|
+
"http",
|
|
432
|
+
"unset",
|
|
433
|
+
"rolling",
|
|
434
|
+
"debug",
|
|
435
|
+
"no-launch-browser"
|
|
436
|
+
]);
|
|
307
437
|
function parseArgs(argv, aliases = {}, allowed) {
|
|
308
438
|
const positionals = [];
|
|
309
439
|
const flags = {};
|
|
@@ -354,39 +484,39 @@ function requireFlagOneOf(parsed, name, allowed, usage) {
|
|
|
354
484
|
|
|
355
485
|
// src/output.ts
|
|
356
486
|
var EXIT = { ok: 0, apiError: 1, authRequired: 2, quota: 3, notFound: 4 };
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
process.stderr.write(`${label}
|
|
487
|
+
function preamble(res) {
|
|
488
|
+
const versions = Object.entries(res.resolved_versions).map(([product, version]) => `${product}@${version}`).join(", ");
|
|
489
|
+
process.stderr.write(`sources: ${versions} \xB7 retrievals remaining: ${res.retrievals_remaining}
|
|
361
490
|
`);
|
|
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
|
-
);
|
|
491
|
+
process.stderr.write(`note: ${res.untrusted_content_notice}
|
|
492
|
+
`);
|
|
493
|
+
if (res.results.length === 0) {
|
|
494
|
+
process.stderr.write("note: no result passed the relevance threshold \u2014 the docs may not cover this.\n");
|
|
368
495
|
}
|
|
369
496
|
}
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
497
|
+
function heading(r) {
|
|
498
|
+
return r.heading_path.join(" \u203A ");
|
|
499
|
+
}
|
|
500
|
+
function printResults(res) {
|
|
501
|
+
preamble(res);
|
|
502
|
+
for (const r of res.results) {
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
`
|
|
505
|
+
${r.score.toFixed(3)} ${r.product}@${r.version} ${heading(r)}
|
|
506
|
+
${r.source_url}
|
|
507
|
+
point_id: ${r.point_id}
|
|
508
|
+
`
|
|
509
|
+
);
|
|
379
510
|
const preview = r.text.length > 500 ? `${r.text.slice(0, 500)}\u2026` : r.text;
|
|
380
511
|
process.stdout.write(`${preview}
|
|
381
512
|
`);
|
|
382
513
|
}
|
|
383
514
|
}
|
|
384
|
-
function printCompact(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (const r of results) {
|
|
515
|
+
function printCompact(res) {
|
|
516
|
+
preamble(res);
|
|
517
|
+
for (const r of res.results) {
|
|
388
518
|
process.stdout.write(
|
|
389
|
-
`${r.score.toFixed(3)} | ${r.
|
|
519
|
+
`${r.score.toFixed(3)} | ${r.product}@${r.version} | ${heading(r)} | ${r.source_url} | ${r.point_id}
|
|
390
520
|
`
|
|
391
521
|
);
|
|
392
522
|
}
|
|
@@ -438,7 +568,7 @@ function setupCodex() {
|
|
|
438
568
|
function hasSession() {
|
|
439
569
|
if (process.env.GRIMOIRE_AUTH_TOKEN) return true;
|
|
440
570
|
try {
|
|
441
|
-
return readMachineToken() !== void 0 ||
|
|
571
|
+
return readMachineToken() !== void 0 || readSession() !== void 0;
|
|
442
572
|
} catch {
|
|
443
573
|
return false;
|
|
444
574
|
}
|
|
@@ -538,7 +668,7 @@ var CONFIG_KEYS = {
|
|
|
538
668
|
describe: `API origin without a path (default ${DEFAULT_API_BASE}; env GRIMOIRE_API_URL overrides)`,
|
|
539
669
|
parse: (raw) => {
|
|
540
670
|
if (!/^https?:\/\/[^/]+$/.test(raw)) {
|
|
541
|
-
throw new UsageError("api-url must be a bare origin, e.g. https://
|
|
671
|
+
throw new UsageError("api-url must be a bare origin, e.g. https://api.example.com");
|
|
542
672
|
}
|
|
543
673
|
return raw;
|
|
544
674
|
}
|
|
@@ -547,23 +677,10 @@ var CONFIG_KEYS = {
|
|
|
547
677
|
prop: "updateCheckHours",
|
|
548
678
|
describe: "hours between CLI update checks (default 24, 0 disables)",
|
|
549
679
|
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
680
|
}
|
|
564
681
|
};
|
|
565
682
|
var AUTH_TOKEN_KEY = "auth-token";
|
|
566
|
-
var MACHINE_TOKEN_RE = /^mt_[
|
|
683
|
+
var MACHINE_TOKEN_RE = /^mt_[A-Za-z0-9_-]{32,}$/;
|
|
567
684
|
var USAGE = "Usage: grimoire config [<key>] [<value>] [--unset] (keys: " + [...Object.keys(CONFIG_KEYS), AUTH_TOKEN_KEY].join(", ") + ")";
|
|
568
685
|
function runConfig(args) {
|
|
569
686
|
const [key, value] = args.positionals;
|
|
@@ -595,7 +712,7 @@ function runConfig(args) {
|
|
|
595
712
|
return EXIT.ok;
|
|
596
713
|
}
|
|
597
714
|
if (!MACHINE_TOKEN_RE.test(value)) {
|
|
598
|
-
throw new UsageError("auth-token must be a machine token
|
|
715
|
+
throw new UsageError("auth-token must be a machine token starting with mt_");
|
|
599
716
|
}
|
|
600
717
|
storeMachineToken(value);
|
|
601
718
|
process.stdout.write(`${AUTH_TOKEN_KEY} set
|
|
@@ -668,6 +785,33 @@ function runUpdate() {
|
|
|
668
785
|
return result.status === 0 ? EXIT.ok : EXIT.apiError;
|
|
669
786
|
}
|
|
670
787
|
|
|
788
|
+
// src/commands/ingest.ts
|
|
789
|
+
function submissionFromArgs(url, args) {
|
|
790
|
+
const usage = "Usage: grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)";
|
|
791
|
+
const product = args.flags.product?.[0];
|
|
792
|
+
if (!product) throw new UsageError(usage);
|
|
793
|
+
const fixed = args.flags.fixed?.[0];
|
|
794
|
+
const npm = args.flags.npm?.[0];
|
|
795
|
+
const pypi = args.flags.pypi?.[0];
|
|
796
|
+
const github = args.flags.github?.[0];
|
|
797
|
+
const probes = [npm, pypi, github].filter(Boolean).length;
|
|
798
|
+
if (probes > 1) throw new UsageError("Pass only one of --npm, --pypi, --github");
|
|
799
|
+
const body = {
|
|
800
|
+
url,
|
|
801
|
+
product,
|
|
802
|
+
version_rule: { kind: "rolling" },
|
|
803
|
+
include_patterns: args.flags.include ?? [],
|
|
804
|
+
exclude_patterns: args.flags.exclude ?? []
|
|
805
|
+
};
|
|
806
|
+
if (args.bools.has("rolling")) return body;
|
|
807
|
+
if (fixed) return { ...body, version_rule: { kind: "fixed", value: fixed } };
|
|
808
|
+
if (npm) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "npm", package: npm } };
|
|
809
|
+
if (pypi) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "pypi", package: pypi } };
|
|
810
|
+
if (github) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "github", repo: github } };
|
|
811
|
+
throw new UsageError(`${usage}
|
|
812
|
+
A version rule is mandatory: --rolling for unversioned docs, --fixed, or a release probe.`);
|
|
813
|
+
}
|
|
814
|
+
|
|
671
815
|
// src/updatecheck.ts
|
|
672
816
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
673
817
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -724,15 +868,21 @@ async function refreshUpdateState(current, configuredHours) {
|
|
|
724
868
|
}
|
|
725
869
|
|
|
726
870
|
// 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}
|
|
871
|
+
function loginOpener(launchBrowser) {
|
|
872
|
+
return (url) => {
|
|
873
|
+
process.stdout.write(`${url}
|
|
734
874
|
`);
|
|
735
|
-
|
|
875
|
+
if (!launchBrowser) {
|
|
876
|
+
process.stderr.write("Open the URL above in a browser to log in; waiting for the callback\u2026\n");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
880
|
+
try {
|
|
881
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
882
|
+
} catch {
|
|
883
|
+
process.stderr.write("Could not open a browser; open the URL above manually.\n");
|
|
884
|
+
}
|
|
885
|
+
};
|
|
736
886
|
}
|
|
737
887
|
function parseSourceFlags(values) {
|
|
738
888
|
return (values ?? []).map((v) => {
|
|
@@ -740,33 +890,40 @@ function parseSourceFlags(values) {
|
|
|
740
890
|
return version ? { source, version } : { source };
|
|
741
891
|
});
|
|
742
892
|
}
|
|
743
|
-
|
|
893
|
+
function toSelectors(pins) {
|
|
894
|
+
return pins.map((p) => ({ product: p.source, ...p.version ? { version: p.version } : {} }));
|
|
895
|
+
}
|
|
896
|
+
var VERSION = true ? "0.4.1" : "dev";
|
|
744
897
|
var HELP = `grimoire ${VERSION} \u2014 documentation retrieval for AI agents
|
|
745
898
|
|
|
746
|
-
grimoire login | logout | whoami
|
|
899
|
+
grimoire login [--no-launch-browser] | logout | whoami
|
|
747
900
|
grimoire setup <claude-code|cursor|windsurf|codex>
|
|
748
901
|
grimoire init
|
|
749
|
-
grimoire search "<query>" [-s nextjs@15 -s react] [--
|
|
902
|
+
grimoire search "<query>" [-s nextjs@15 -s react] [--json|--compact] [--debug]
|
|
750
903
|
grimoire sources [--q <kw>] [--names|--json]
|
|
751
|
-
grimoire versions <
|
|
904
|
+
grimoire versions <product> [--json]
|
|
752
905
|
grimoire config [<key>] [<value>] [--unset]
|
|
753
906
|
grimoire update
|
|
754
|
-
grimoire doc <
|
|
755
|
-
grimoire report <
|
|
756
|
-
grimoire ingest <url>
|
|
907
|
+
grimoire doc <point_id> [--window 2] [--json]
|
|
908
|
+
grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]
|
|
909
|
+
grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)
|
|
910
|
+
[--include <pattern>]... [--exclude <pattern>]... [--watch]
|
|
757
911
|
grimoire jobs <job_id> [--watch]
|
|
912
|
+
grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..."
|
|
913
|
+
grimoire staff users [--json] | grant <subject> --name "..." | revoke <subject>
|
|
914
|
+
grimoire staff token <name> --quota <per-day> | recrawl <source_id>
|
|
758
915
|
grimoire mcp [--http]
|
|
759
916
|
grimoire help | --help | -h
|
|
760
917
|
grimoire version | --version | -v
|
|
761
918
|
|
|
762
919
|
env: GRIMOIRE_AUTH_TOKEN \u2014 machine token (CI, instead of login)
|
|
763
|
-
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
920
|
+
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
764
921
|
`;
|
|
765
922
|
var COMMAND_FLAGS = {
|
|
766
923
|
version: [],
|
|
767
924
|
"--version": [],
|
|
768
925
|
"-v": [],
|
|
769
|
-
login: [],
|
|
926
|
+
login: ["no-launch-browser"],
|
|
770
927
|
logout: [],
|
|
771
928
|
setup: [],
|
|
772
929
|
init: [],
|
|
@@ -777,14 +934,23 @@ var COMMAND_FLAGS = {
|
|
|
777
934
|
mcp: ["http"],
|
|
778
935
|
config: ["unset"],
|
|
779
936
|
update: [],
|
|
780
|
-
search: ["source", "
|
|
937
|
+
search: ["source", "json", "compact", "debug"],
|
|
781
938
|
sources: ["q", "names", "json"],
|
|
782
939
|
versions: ["json"],
|
|
783
|
-
doc: ["window"],
|
|
940
|
+
doc: ["window", "json"],
|
|
784
941
|
report: ["verdict", "note"],
|
|
785
|
-
ingest: ["
|
|
786
|
-
jobs: ["watch"]
|
|
942
|
+
ingest: ["product", "rolling", "fixed", "npm", "pypi", "github", "include", "exclude", "watch"],
|
|
943
|
+
jobs: ["watch"],
|
|
944
|
+
staff: ["reason", "name", "quota", "json"]
|
|
787
945
|
};
|
|
946
|
+
function describeError(err) {
|
|
947
|
+
const detail = typeof err.body === "string" ? err.body : err.body !== void 0 && err.body !== null ? JSON.stringify(err.body) : "";
|
|
948
|
+
return `error: ${err.code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
949
|
+
}
|
|
950
|
+
function describeJob(job) {
|
|
951
|
+
const extra = [job.reason ? `reason: ${job.reason}` : "", job.source_id ? `source: ${job.source_id}` : ""].filter(Boolean).join(" ");
|
|
952
|
+
return `${job.state}${extra ? ` ${extra}` : ""}`;
|
|
953
|
+
}
|
|
788
954
|
async function main(argv) {
|
|
789
955
|
const [command, ...rest] = argv;
|
|
790
956
|
const args = parseArgs(rest, { "-s": "source", "-q": "q" }, command !== void 0 ? COMMAND_FLAGS[command] : []);
|
|
@@ -797,12 +963,14 @@ async function main(argv) {
|
|
|
797
963
|
`);
|
|
798
964
|
return EXIT.ok;
|
|
799
965
|
case "login": {
|
|
800
|
-
|
|
966
|
+
const launch = !args.bools.has("no-launch-browser");
|
|
967
|
+
process.stderr.write(launch ? "Opening your browser to log in\u2026\n" : "Login URL:\n");
|
|
968
|
+
await browserLogin(loadGlobalConfig().apiBaseUrl, loginOpener(launch));
|
|
801
969
|
process.stdout.write("Logged in.\n");
|
|
802
970
|
return EXIT.ok;
|
|
803
971
|
}
|
|
804
972
|
case "logout":
|
|
805
|
-
|
|
973
|
+
clearSession();
|
|
806
974
|
process.stdout.write("Logged out.\n");
|
|
807
975
|
return EXIT.ok;
|
|
808
976
|
case "setup":
|
|
@@ -827,33 +995,22 @@ async function main(argv) {
|
|
|
827
995
|
try {
|
|
828
996
|
switch (command) {
|
|
829
997
|
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()) {
|
|
998
|
+
const via = process.env.GRIMOIRE_AUTH_TOKEN ? "GRIMOIRE_AUTH_TOKEN" : readMachineToken() ? "grimoire config auth-token" : readSession() ? "browser login" : void 0;
|
|
999
|
+
if (!via) {
|
|
839
1000
|
process.stderr.write("not logged in \u2014 run `grimoire login`\n");
|
|
840
1001
|
return EXIT.authRequired;
|
|
841
1002
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
process.stderr.write(`not logged in (${reason}) \u2014 run \`grimoire login\`
|
|
849
|
-
`);
|
|
850
|
-
return EXIT.authRequired;
|
|
851
|
-
}
|
|
1003
|
+
const me = await client.me();
|
|
1004
|
+
process.stdout.write(
|
|
1005
|
+
`${me.kind} ${me.subject}${me.is_staff ? " staff" : ""} quota ${me.quota_per_day}/day via ${via}
|
|
1006
|
+
`
|
|
1007
|
+
);
|
|
1008
|
+
return EXIT.ok;
|
|
852
1009
|
}
|
|
853
1010
|
case "search": {
|
|
854
1011
|
const query = args.positionals[0];
|
|
855
1012
|
if (!query) {
|
|
856
|
-
process.stderr.write('Usage: grimoire search "<query>" -s <
|
|
1013
|
+
process.stderr.write('Usage: grimoire search "<query>" -s <product>[@version]\n');
|
|
857
1014
|
return EXIT.apiError;
|
|
858
1015
|
}
|
|
859
1016
|
const explicit = parseSourceFlags(args.flags.source);
|
|
@@ -861,99 +1018,73 @@ async function main(argv) {
|
|
|
861
1018
|
if (sources.length === 0) {
|
|
862
1019
|
const initHelps = existsSync6(join6(process.cwd(), "package.json")) || existsSync6(join6(process.cwd(), "requirements.txt"));
|
|
863
1020
|
process.stderr.write(
|
|
864
|
-
'No sources selected. List what is indexed:\n grimoire sources\nthen scope the search:\n grimoire search "<query>" -s <
|
|
1021
|
+
'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
1022
|
);
|
|
866
1023
|
return EXIT.apiError;
|
|
867
1024
|
}
|
|
868
|
-
const res = await client.search({
|
|
869
|
-
query,
|
|
870
|
-
sources,
|
|
871
|
-
language: args.flags.lang?.[0],
|
|
872
|
-
top_k: intFlag(args, "top-k")
|
|
873
|
-
});
|
|
1025
|
+
const res = await client.search({ query, sources: toSelectors(sources), debug: args.bools.has("debug") });
|
|
874
1026
|
if (json) process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
875
|
-
else if (args.bools.has("compact")) printCompact(res
|
|
876
|
-
else printResults(res
|
|
1027
|
+
else if (args.bools.has("compact")) printCompact(res);
|
|
1028
|
+
else printResults(res);
|
|
877
1029
|
return EXIT.ok;
|
|
878
1030
|
}
|
|
879
1031
|
case "sources": {
|
|
880
|
-
const
|
|
881
|
-
const sources =
|
|
1032
|
+
const needle = args.flags.q?.[0]?.toLowerCase();
|
|
1033
|
+
const sources = (await client.listSources()).filter(
|
|
1034
|
+
(s) => !needle || s.product.toLowerCase().includes(needle) || s.base_url.toLowerCase().includes(needle)
|
|
1035
|
+
);
|
|
882
1036
|
if (json) {
|
|
883
1037
|
process.stdout.write(JSON.stringify(sources, null, 2) + "\n");
|
|
884
1038
|
} else if (args.bools.has("names")) {
|
|
885
|
-
for (const s of sources) process.stdout.write(`${s.
|
|
1039
|
+
for (const s of sources) process.stdout.write(`${s.product}
|
|
886
1040
|
`);
|
|
887
1041
|
} else {
|
|
888
1042
|
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
|
-
);
|
|
1043
|
+
const versions = s.versions.length > 0 ? s.versions.join(", ") : "(not indexed yet)";
|
|
1044
|
+
process.stdout.write(`${s.product} ${versions} ${s.base_url}
|
|
1045
|
+
`);
|
|
899
1046
|
}
|
|
900
1047
|
}
|
|
901
1048
|
return EXIT.ok;
|
|
902
1049
|
}
|
|
903
1050
|
case "versions": {
|
|
904
|
-
const
|
|
905
|
-
const res = await client.listVersions(
|
|
906
|
-
const versions = res.versions ?? [];
|
|
1051
|
+
const product = requirePositional(args, 0, "Usage: grimoire versions <product> [--json]");
|
|
1052
|
+
const res = await client.listVersions(product);
|
|
907
1053
|
if (json) {
|
|
908
|
-
process.stdout.write(JSON.stringify(
|
|
1054
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
909
1055
|
return EXIT.ok;
|
|
910
1056
|
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
process.stdout.write(`no active versions for ${source}
|
|
1057
|
+
for (const v of res.versions) {
|
|
1058
|
+
process.stdout.write(`${v.version}${v.version === res.latest ? " latest" : ""} ${v.chunk_count} chunks
|
|
914
1059
|
`);
|
|
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
1060
|
}
|
|
928
1061
|
return EXIT.ok;
|
|
929
1062
|
}
|
|
930
1063
|
case "doc": {
|
|
931
|
-
const
|
|
1064
|
+
const pointId = requirePositional(args, 0, "Usage: grimoire doc <point_id> [--window 2] [--json]");
|
|
932
1065
|
const window = intFlag(args, "window", { min: 0, max: 5 }) ?? 2;
|
|
933
|
-
const res = await client.
|
|
934
|
-
|
|
1066
|
+
const res = await client.getDoc(pointId, window);
|
|
1067
|
+
if (json) {
|
|
1068
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1069
|
+
return EXIT.ok;
|
|
1070
|
+
}
|
|
1071
|
+
process.stderr.write(`${res.product}@${res.version} ${res.heading_path.join(" \u203A ")}
|
|
1072
|
+
${res.source_url}
|
|
1073
|
+
`);
|
|
1074
|
+
process.stdout.write(res.text + "\n");
|
|
935
1075
|
return EXIT.ok;
|
|
936
1076
|
}
|
|
937
1077
|
case "report": {
|
|
938
|
-
const usage = 'Usage: grimoire report <
|
|
939
|
-
const
|
|
1078
|
+
const usage = 'Usage: grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]';
|
|
1079
|
+
const pointId = requirePositional(args, 0, usage);
|
|
940
1080
|
const verdict = requireFlagOneOf(args, "verdict", ["helpful", "incorrect", "outdated"], usage);
|
|
941
|
-
await client.reportResult(
|
|
1081
|
+
await client.reportResult(pointId, verdict, args.flags.note?.[0]);
|
|
942
1082
|
process.stdout.write("Reported.\n");
|
|
943
1083
|
return EXIT.ok;
|
|
944
1084
|
}
|
|
945
1085
|
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
|
-
});
|
|
1086
|
+
const url = requirePositional(args, 0, "Usage: grimoire ingest <url> --product <name> --rolling|--fixed|--npm|--pypi|--github");
|
|
1087
|
+
const res = await client.submitSource(submissionFromArgs(url, args));
|
|
957
1088
|
process.stdout.write(`Job: ${res.job_id}
|
|
958
1089
|
`);
|
|
959
1090
|
if (args.bools.has("watch")) return watchJob(client, res.job_id);
|
|
@@ -963,6 +1094,70 @@ async function main(argv) {
|
|
|
963
1094
|
const jobId = requirePositional(args, 0, "Usage: grimoire jobs <job_id> [--watch]");
|
|
964
1095
|
return args.bools.has("watch") ? watchJob(client, jobId) : printJob(client, jobId);
|
|
965
1096
|
}
|
|
1097
|
+
case "staff": {
|
|
1098
|
+
const usage = 'Usage: grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..." | users [--json] | grant <subject> --name "..." | revoke <subject> | token <name> --quota <per-day> | recrawl <source_id>';
|
|
1099
|
+
const [action, jobId] = args.positionals;
|
|
1100
|
+
if (action === "token") {
|
|
1101
|
+
const quota = intFlag(args, "quota", { min: 1, max: 1e6 });
|
|
1102
|
+
if (!jobId || quota === void 0) throw new UsageError(usage);
|
|
1103
|
+
const minted = await client.mintToken(jobId, quota);
|
|
1104
|
+
process.stderr.write(`token ${minted.id} ${minted.name} quota ${minted.quota_per_day}/day \u2014 shown once, store it now
|
|
1105
|
+
`);
|
|
1106
|
+
process.stdout.write(`${minted.token}
|
|
1107
|
+
`);
|
|
1108
|
+
return EXIT.ok;
|
|
1109
|
+
}
|
|
1110
|
+
if (action === "recrawl") {
|
|
1111
|
+
if (!jobId) throw new UsageError(usage);
|
|
1112
|
+
process.stdout.write(`${describeJob(await client.recrawlSource(jobId))}
|
|
1113
|
+
`);
|
|
1114
|
+
return EXIT.ok;
|
|
1115
|
+
}
|
|
1116
|
+
if (action === "users") {
|
|
1117
|
+
const users = await client.listUsers();
|
|
1118
|
+
if (json) process.stdout.write(JSON.stringify(users, null, 2) + "\n");
|
|
1119
|
+
else for (const u of users) process.stdout.write(`${u.subject} ${u.status} ${u.name} by ${u.granted_by}
|
|
1120
|
+
`);
|
|
1121
|
+
return EXIT.ok;
|
|
1122
|
+
}
|
|
1123
|
+
if (action === "grant") {
|
|
1124
|
+
const name = args.flags.name?.[0];
|
|
1125
|
+
if (!jobId || !name) throw new UsageError(usage);
|
|
1126
|
+
const granted = await client.grantUser(jobId, name);
|
|
1127
|
+
process.stdout.write(`${granted.subject} ${granted.status} ${granted.name}
|
|
1128
|
+
`);
|
|
1129
|
+
return EXIT.ok;
|
|
1130
|
+
}
|
|
1131
|
+
if (action === "revoke") {
|
|
1132
|
+
if (!jobId) throw new UsageError(usage);
|
|
1133
|
+
const revoked = await client.revokeUser(jobId);
|
|
1134
|
+
process.stdout.write(`${revoked.subject} ${revoked.status}
|
|
1135
|
+
`);
|
|
1136
|
+
return EXIT.ok;
|
|
1137
|
+
}
|
|
1138
|
+
if (action === "queue") {
|
|
1139
|
+
const queue = await client.reviewQueue();
|
|
1140
|
+
if (json) process.stdout.write(JSON.stringify(queue, null, 2) + "\n");
|
|
1141
|
+
else if (queue.length === 0) process.stdout.write("review queue is empty\n");
|
|
1142
|
+
else for (const job of queue) process.stdout.write(`${job.id} ${describeJob(job)}
|
|
1143
|
+
`);
|
|
1144
|
+
return EXIT.ok;
|
|
1145
|
+
}
|
|
1146
|
+
if (!jobId) throw new UsageError(usage);
|
|
1147
|
+
if (action === "approve") {
|
|
1148
|
+
process.stdout.write(`${describeJob(await client.approveJob(jobId))}
|
|
1149
|
+
`);
|
|
1150
|
+
return EXIT.ok;
|
|
1151
|
+
}
|
|
1152
|
+
if (action === "reject") {
|
|
1153
|
+
const reason = args.flags.reason?.[0];
|
|
1154
|
+
if (!reason) throw new UsageError(usage);
|
|
1155
|
+
process.stdout.write(`${describeJob(await client.rejectJob(jobId, reason))}
|
|
1156
|
+
`);
|
|
1157
|
+
return EXIT.ok;
|
|
1158
|
+
}
|
|
1159
|
+
throw new UsageError(usage);
|
|
1160
|
+
}
|
|
966
1161
|
default:
|
|
967
1162
|
process.stderr.write(`Unknown command: ${command}
|
|
968
1163
|
${HELP}`);
|
|
@@ -970,14 +1165,10 @@ ${HELP}`);
|
|
|
970
1165
|
}
|
|
971
1166
|
} catch (err) {
|
|
972
1167
|
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(", ")}
|
|
1168
|
+
process.stderr.write(`${describeError(err)}
|
|
979
1169
|
`);
|
|
980
1170
|
if (err.status === 401) return EXIT.authRequired;
|
|
1171
|
+
if (err.status === 403) return EXIT.authRequired;
|
|
981
1172
|
if (err.status === 429) return EXIT.quota;
|
|
982
1173
|
if (err.status === 404) return EXIT.notFound;
|
|
983
1174
|
return EXIT.apiError;
|
|
@@ -985,14 +1176,13 @@ ${HELP}`);
|
|
|
985
1176
|
throw err;
|
|
986
1177
|
}
|
|
987
1178
|
}
|
|
988
|
-
var
|
|
989
|
-
var
|
|
1179
|
+
var TERMINAL_STATES = ["accepted", "done", "rejected", "failed", "pending_review"];
|
|
1180
|
+
var FAILED_STATES = ["rejected", "failed"];
|
|
990
1181
|
async function printJob(client, jobId) {
|
|
991
1182
|
const job = await client.getJob(jobId);
|
|
992
|
-
|
|
993
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1183
|
+
process.stdout.write(`${describeJob(job)}
|
|
994
1184
|
`);
|
|
995
|
-
return
|
|
1185
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
996
1186
|
}
|
|
997
1187
|
var WATCH_POLL_MS = 5e3;
|
|
998
1188
|
var WATCH_BACKOFF_CAP_MS = 3e4;
|
|
@@ -1006,11 +1196,13 @@ async function watchJob(client, jobId) {
|
|
|
1006
1196
|
while (Date.now() < deadline) {
|
|
1007
1197
|
try {
|
|
1008
1198
|
const job = await client.getJob(jobId);
|
|
1009
|
-
|
|
1010
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1199
|
+
process.stdout.write(`${describeJob(job)}
|
|
1011
1200
|
`);
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1201
|
+
if (TERMINAL_STATES.includes(job.state)) {
|
|
1202
|
+
if (job.state === "pending_review") {
|
|
1203
|
+
process.stderr.write("Parked for staff review: grimoire staff queue\n");
|
|
1204
|
+
}
|
|
1205
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
1014
1206
|
}
|
|
1015
1207
|
backoff = WATCH_POLL_MS;
|
|
1016
1208
|
await sleep(WATCH_POLL_MS);
|