@s-hirano-ist/s-scripts 4.2.0 → 4.3.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 CHANGED
@@ -73,3 +73,11 @@ validate-json-articles
73
73
  ## License
74
74
 
75
75
  AGPL-3.0
76
+
77
+ ## Operational notifications
78
+
79
+ Set `NEXT_PUBLIC_SENTRY_DSN` to record CLI errors in Sentry. If it is missing, the command warns and continues with local diagnostics. Set `PUSHOVER_USER_KEY` and `PUSHOVER_APP_TOKEN` for completion and critical alerts; `PUSHOVER_URL` defaults to `https://api.pushover.net/1/messages.json`.
80
+
81
+ Unexpected item failures go to Sentry only. A failure that aborts the command, including initialization or cleanup, also sends a critical Pushover alert and sets a nonzero exit code. Existing completion notifications use Pushover only; auxiliary commands do not add success notifications. Expected usage and content validation failures keep their nonzero exit codes without critical alerts.
82
+
83
+ Notification delivery errors go to Sentry without changing the command's result or sending another Pushover alert. Commands wait up to two seconds for Sentry to flush before exiting; Pushover requests have a five-second timeout. Commands running with `--dry-run` do not initialize Sentry or send external notifications.
@@ -0,0 +1,445 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+
3
+ // ../infrastructure/dist/observability/diagnostic-error.js
4
+ function createDiagnosticError(env) {
5
+ const secrets = [
6
+ env.DATABASE_URL,
7
+ env.AUTH_SECRET,
8
+ env.AUTH0_CLIENT_SECRET,
9
+ env.LOCAL_AUTH_PASSWORD,
10
+ env.QDRANT_API_KEY,
11
+ env.S3_ACCESS_KEY_ID,
12
+ env.S3_SECRET_ACCESS_KEY,
13
+ env.WEB_PUSH_PRIVATE_KEY,
14
+ env.PUSHOVER_APP_TOKEN,
15
+ env.PUSHOVER_USER_KEY,
16
+ env.CRON_SECRET,
17
+ env.GOOGLE_BOOKS_API_KEY,
18
+ env.NEXT_PUBLIC_SENTRY_DSN
19
+ ].filter((value) => typeof value === "string" && value.length > 0);
20
+ function redact(input) {
21
+ let value = input;
22
+ for (const secret of secrets)
23
+ value = value.replaceAll(secret, "[REDACTED]");
24
+ return value.replaceAll(/\bBearer\s+[^\s,;]+/giu, "Bearer [REDACTED]").replaceAll(/\b(?:cookie|authorization|set-cookie)\s*[:=][^\r\n]*/giu, "[REDACTED HEADER]").replaceAll(/\b(?:https?|postgres(?:ql)?):\/\/\S+/giu, (match) => {
25
+ try {
26
+ const url = new URL(match);
27
+ if (!url.username && !url.password && !url.search && !url.hash)
28
+ return match;
29
+ if (url.username || url.password) {
30
+ url.username = "REDACTED";
31
+ url.password = "";
32
+ }
33
+ if (url.search || url.hash)
34
+ url.search = "?[REDACTED]";
35
+ url.hash = "";
36
+ return url.href;
37
+ } catch {
38
+ return "[REDACTED URL]";
39
+ }
40
+ });
41
+ }
42
+ return (error) => {
43
+ const diagnostic = new Error(error instanceof Error ? redact(error.message) : "Unknown operational error");
44
+ if (error instanceof Error) {
45
+ diagnostic.name = redact(error.name);
46
+ diagnostic.stack = error.stack ? redact(error.stack) : void 0;
47
+ }
48
+ return diagnostic;
49
+ };
50
+ }
51
+
52
+ // ../infrastructure/dist/observability/operational-telemetry.js
53
+ function observe(action) {
54
+ try {
55
+ action();
56
+ } catch {
57
+ }
58
+ }
59
+ var jobs = /* @__PURE__ */ new Set([
60
+ "cleanup-minio-images",
61
+ "enrich-books",
62
+ "find-duplicate-json-articles",
63
+ "fix-book-frontmatter",
64
+ "fix-image-formats",
65
+ "validate-json-articles",
66
+ "update-json-articles",
67
+ "update-raw-articles",
68
+ "s-content-format",
69
+ "rag-delete-collection",
70
+ "rag-search",
71
+ "rag-ingest",
72
+ ...["fetch", "ingest", "reset", "revert"].flatMap((operation) => ["articles", "books", "images", "notes"].map((domain) => `${operation}-${domain}`))
73
+ ]);
74
+ function telemetryJob(value) {
75
+ return jobs.has(value) ? value : "other";
76
+ }
77
+ function operationalAttributes(attributes = {}) {
78
+ return Object.fromEntries(Object.entries(attributes).filter((entry) => {
79
+ const [key, value] = entry;
80
+ return [
81
+ "method",
82
+ "route",
83
+ "domain",
84
+ "status",
85
+ "job",
86
+ "sentry.environment",
87
+ "sentry.release",
88
+ "sentry.sdk.name",
89
+ "sentry.sdk.version",
90
+ "sentry.trace.parent_span_id",
91
+ "sentry.replay_id"
92
+ ].includes(key) && (typeof value === "string" || typeof value === "number");
93
+ }));
94
+ }
95
+ function createOperationalTelemetry(sdk, enabled) {
96
+ return {
97
+ log(level, message, attributes = {}) {
98
+ if (enabled())
99
+ observe(() => sdk.logger[level](message, operationalAttributes(attributes)));
100
+ },
101
+ count(name, value, attributes = {}) {
102
+ if (enabled())
103
+ observe(() => sdk.metrics.count(name, value, {
104
+ attributes: operationalAttributes(attributes)
105
+ }));
106
+ },
107
+ duration(name, milliseconds, attributes = {}) {
108
+ if (enabled())
109
+ observe(() => sdk.metrics.distribution(name, milliseconds, {
110
+ attributes: operationalAttributes(attributes),
111
+ unit: "millisecond"
112
+ }));
113
+ }
114
+ };
115
+ }
116
+
117
+ // ../infrastructure/dist/observability/log-level.js
118
+ function mapStatusToLogLevel(status) {
119
+ if (status >= 500)
120
+ return "error";
121
+ if (status >= 400)
122
+ return "warn";
123
+ return "info";
124
+ }
125
+
126
+ // ../infrastructure/dist/observability/server-logger.js
127
+ import pino from "pino";
128
+ var pinoConfig = {
129
+ browser: { asObject: true }
130
+ };
131
+ var ServerLogger = class {
132
+ telemetry;
133
+ logger;
134
+ notificationService;
135
+ reporter;
136
+ constructor(notificationService, reporter, telemetry2) {
137
+ this.telemetry = telemetry2;
138
+ this.reporter = reporter;
139
+ this.logger = pino(pinoConfig);
140
+ this.notificationService = notificationService;
141
+ }
142
+ async info(message, context, options) {
143
+ await this.logWithLevel("info", message, context, void 0, options);
144
+ }
145
+ async warn(message, context, options) {
146
+ await this.logWithLevel("warn", message, context, void 0, options);
147
+ }
148
+ async error(message, context, error, options) {
149
+ await this.logWithLevel("error", message, context, error, options);
150
+ }
151
+ async logWithLevel(level, message, context, error, options) {
152
+ const expectedLevel = mapStatusToLogLevel(context.status);
153
+ const finalLevel = level === expectedLevel ? level : expectedLevel;
154
+ this.telemetry?.log(finalLevel, "Application operation", {
155
+ status: context.status
156
+ });
157
+ const logData = {
158
+ caller: context.caller,
159
+ status: context.status,
160
+ userId: context.userId,
161
+ ...context.additionalContext
162
+ };
163
+ if (level === "error") {
164
+ try {
165
+ this.captureError(message, context, error);
166
+ } catch (reportError) {
167
+ this.logger.warn({ error: reportError }, "Failed to report error");
168
+ }
169
+ }
170
+ if (error !== void 0) {
171
+ console.error("Additional error details:", error);
172
+ }
173
+ switch (finalLevel) {
174
+ case "info":
175
+ this.logger.info(logData, message);
176
+ break;
177
+ case "warn":
178
+ this.logger.warn(logData, message);
179
+ break;
180
+ case "error":
181
+ this.logger.error(logData, message);
182
+ break;
183
+ }
184
+ if (level === "error" && options?.severity === "critical" || level === "info" && options?.notifyCompletion) {
185
+ const notificationContext = {
186
+ caller: context.caller,
187
+ userId: context.userId
188
+ };
189
+ try {
190
+ switch (level) {
191
+ case "info":
192
+ await this.notificationService.notifyInfo(message, notificationContext);
193
+ break;
194
+ case "error":
195
+ await this.notificationService.notifyError(message, notificationContext);
196
+ break;
197
+ }
198
+ } catch (notificationError) {
199
+ this.logger.warn({ error: notificationError }, "Failed to send notification");
200
+ try {
201
+ this.reporter.captureException(notificationError, {
202
+ tags: {
203
+ caller: context.caller,
204
+ error_type: "notification_delivery",
205
+ status: String(context.status)
206
+ },
207
+ user: context.userId ? { id: context.userId } : void 0
208
+ });
209
+ } catch (reportError) {
210
+ this.logger.warn({ error: reportError }, "Failed to report notification failure");
211
+ }
212
+ }
213
+ }
214
+ }
215
+ captureError(message, context, error) {
216
+ const capturedError = this.findError(error);
217
+ const options = {
218
+ extra: {
219
+ additionalContext: context.additionalContext,
220
+ originalError: capturedError ? void 0 : error
221
+ },
222
+ tags: {
223
+ caller: context.caller,
224
+ error_type: capturedError?.name ?? "message",
225
+ status: String(context.status)
226
+ },
227
+ user: context.userId ? { id: context.userId } : void 0
228
+ };
229
+ if (capturedError) {
230
+ this.reporter.captureException(capturedError, options);
231
+ return;
232
+ }
233
+ this.reporter.captureMessage(message, { ...options, level: "error" });
234
+ }
235
+ findError(value) {
236
+ if (value instanceof Error)
237
+ return value;
238
+ if (typeof value === "object" && value !== null && "cause" in value && value.cause instanceof Error) {
239
+ return value.cause;
240
+ }
241
+ return void 0;
242
+ }
243
+ };
244
+
245
+ // ../notification/dist/errors.js
246
+ var NotificationError = class extends Error {
247
+ constructor(message = "notificationSend") {
248
+ super(message);
249
+ this.name = "NotificationError";
250
+ }
251
+ };
252
+
253
+ // ../notification/dist/pushover.js
254
+ var PushoverService = class {
255
+ config;
256
+ constructor(config) {
257
+ this.config = config;
258
+ }
259
+ /**
260
+ * Sends a message to the Pushover API.
261
+ *
262
+ * @param message - The message to send
263
+ * @param priority - The priority level (-1 = low, 0 = normal, 1 = high)
264
+ *
265
+ * @internal
266
+ */
267
+ async sendMessage(message, priority = 0) {
268
+ const body = new URLSearchParams({
269
+ token: this.config.appToken,
270
+ user: this.config.userKey,
271
+ message,
272
+ priority: String(priority)
273
+ });
274
+ const result = await fetch(this.config.url, {
275
+ method: "POST",
276
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
277
+ body,
278
+ signal: AbortSignal.timeout(5e3)
279
+ });
280
+ if (!result.ok)
281
+ throw new NotificationError();
282
+ }
283
+ async notifyError(message, context) {
284
+ const enrichedMessage = `[ERROR] ${context.caller}: ${message}`;
285
+ await this.sendMessage(enrichedMessage, 1);
286
+ }
287
+ async notifyWarning(message, context) {
288
+ const enrichedMessage = `[WARN] ${context.caller}: ${message}`;
289
+ await this.sendMessage(enrichedMessage, 0);
290
+ }
291
+ async notifyInfo(message, context) {
292
+ const enrichedMessage = `[INFO] ${context.caller}: ${message}`;
293
+ await this.sendMessage(enrichedMessage, -1);
294
+ }
295
+ };
296
+ function createPushoverService(config) {
297
+ return new PushoverService(config);
298
+ }
299
+
300
+ // ../notification/dist/web-push.js
301
+ import { sendNotification, setVapidDetails } from "web-push";
302
+
303
+ // src/lib/cli-observability.ts
304
+ import * as Sentry from "@sentry/node";
305
+ var dryRun = () => process.argv.includes("--dry-run");
306
+ var initialized = false;
307
+ var telemetry = createOperationalTelemetry(
308
+ Sentry,
309
+ () => !dryRun() && process.env.NODE_ENV === "production" && Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN)
310
+ );
311
+ function initialize() {
312
+ if (initialized || dryRun()) return;
313
+ initialized = true;
314
+ if (!process.env.NEXT_PUBLIC_SENTRY_DSN)
315
+ console.warn(
316
+ "NEXT_PUBLIC_SENTRY_DSN is not set; errors will only be logged locally and critical alerts sent via Pushover."
317
+ );
318
+ try {
319
+ Sentry.init({
320
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
321
+ enabled: process.env.NODE_ENV === "production" && Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN),
322
+ environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
323
+ release: process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA,
324
+ beforeSendLog(log) {
325
+ log.attributes = operationalAttributes(log.attributes);
326
+ return log;
327
+ },
328
+ beforeSendMetric(metric) {
329
+ metric.attributes = operationalAttributes(metric.attributes);
330
+ return metric;
331
+ },
332
+ // The runner owns reporting and flushing, including handled item failures.
333
+ defaultIntegrations: false,
334
+ dataCollection: {
335
+ userInfo: false,
336
+ cookies: false,
337
+ httpHeaders: false,
338
+ httpBodies: [],
339
+ urlQueryParams: false,
340
+ databaseQueryData: false,
341
+ stackFrameVariables: false
342
+ },
343
+ beforeSend(event) {
344
+ delete event.user;
345
+ delete event.request;
346
+ delete event.breadcrumbs;
347
+ return event;
348
+ }
349
+ });
350
+ } catch (error) {
351
+ console.error(
352
+ "Failed to initialize Sentry",
353
+ createDiagnosticError(process.env)(error)
354
+ );
355
+ }
356
+ }
357
+ function logger() {
358
+ initialize();
359
+ const diagnose = createDiagnosticError(process.env);
360
+ return new ServerLogger(
361
+ createPushoverService({
362
+ url: process.env.PUSHOVER_URL ?? "https://api.pushover.net/1/messages.json",
363
+ userKey: process.env.PUSHOVER_USER_KEY ?? "",
364
+ appToken: process.env.PUSHOVER_APP_TOKEN ?? ""
365
+ }),
366
+ {
367
+ captureException: (error, context) => Sentry.captureException(diagnose(error), context),
368
+ captureMessage: (message, context) => Sentry.captureMessage(message, context)
369
+ }
370
+ );
371
+ }
372
+ function reportCliError(error, caller) {
373
+ const diagnostic = createDiagnosticError(process.env)(error);
374
+ console.error(diagnostic);
375
+ if (dryRun()) return;
376
+ initialize();
377
+ telemetry.log("error", "CLI item failed", { job: telemetryJob(caller) });
378
+ try {
379
+ Sentry.captureException(diagnostic, {
380
+ tags: { caller, severity: "normal" }
381
+ });
382
+ } catch (reportError) {
383
+ console.error(
384
+ "Failed to report error",
385
+ createDiagnosticError(process.env)(reportError)
386
+ );
387
+ }
388
+ }
389
+ var cliNotifications = {
390
+ async notifyInfo(message, context) {
391
+ if (dryRun()) return;
392
+ await logger().info(
393
+ message,
394
+ { ...context, caller: context.caller ?? "cli", status: 200 },
395
+ { notifyCompletion: true }
396
+ );
397
+ }
398
+ };
399
+ async function runCli(caller, execute) {
400
+ const started = performance.now();
401
+ const job = telemetryJob(caller);
402
+ let status = "ok";
403
+ try {
404
+ initialize();
405
+ telemetry.log("info", "CLI started", { job });
406
+ await execute();
407
+ telemetry.log("info", "CLI completed", { job, status: "ok" });
408
+ } catch (error) {
409
+ status = "error";
410
+ telemetry.log("error", "CLI failed", { job, status: "error" });
411
+ process.exitCode = 1;
412
+ const diagnostic = createDiagnosticError(process.env)(error);
413
+ console.error(diagnostic);
414
+ if (!dryRun())
415
+ await logger().error(
416
+ `${caller} failed`,
417
+ { caller, status: 500 },
418
+ diagnostic,
419
+ { severity: "critical" }
420
+ );
421
+ } finally {
422
+ telemetry.count("cli.runs", 1, { job, status });
423
+ telemetry.duration("cli.duration", performance.now() - started, {
424
+ job,
425
+ status
426
+ });
427
+ if (initialized && !dryRun()) {
428
+ try {
429
+ if (!await Sentry.flush(2e3))
430
+ console.error("Sentry flush timed out");
431
+ } catch (error) {
432
+ console.error(
433
+ "Sentry flush failed",
434
+ createDiagnosticError(process.env)(error)
435
+ );
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ export {
442
+ reportCliError,
443
+ cliNotifications,
444
+ runCli
445
+ };
@@ -0,0 +1,36 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+ import {
3
+ deleteCollection,
4
+ getQdrantClient
5
+ } from "./chunk-J4VLH4VA.js";
6
+ import "./chunk-JZOR5GA2.js";
7
+
8
+ // src/rag/delete-collection-command.ts
9
+ async function main() {
10
+ const collectionName = process.argv[2];
11
+ if (!collectionName) {
12
+ console.error(
13
+ "Usage: rag-delete-collection <collection-name>\nExample: rag-delete-collection knowledge_v1"
14
+ );
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+ if (!process.env.QDRANT_URL) {
19
+ throw new Error("QDRANT_URL environment variable is required.");
20
+ }
21
+ const qdrant = getQdrantClient();
22
+ const collections = await qdrant.getCollections();
23
+ const exists = collections.collections.some((c) => c.name === collectionName);
24
+ if (!exists) {
25
+ console.log(
26
+ `Collection "${collectionName}" does not exist. Nothing to delete.`
27
+ );
28
+ return;
29
+ }
30
+ console.log(`Deleting collection "${collectionName}"...`);
31
+ await deleteCollection(collectionName);
32
+ console.log(`Collection "${collectionName}" deleted successfully.`);
33
+ }
34
+ export {
35
+ main
36
+ };
@@ -8,7 +8,7 @@ import {
8
8
  createObjectStorageClient,
9
9
  createStorageService,
10
10
  isObjectNotFound
11
- } from "./chunk-M6LFKOUH.js";
11
+ } from "./chunk-MVYAZJB2.js";
12
12
  import {
13
13
  CreatedAt,
14
14
  ExportedStatus,
@@ -22,8 +22,8 @@ import {
22
22
  withTransactionRetry
23
23
  } from "./chunk-RLAYINCA.js";
24
24
  import {
25
- createPushoverService
26
- } from "./chunk-7PVU2Z32.js";
25
+ cliNotifications
26
+ } from "./chunk-4CXH2QIX.js";
27
27
 
28
28
  // ../core/shared-kernel/entities/file-entity.ts
29
29
  import { z } from "zod";
@@ -542,11 +542,7 @@ async function runContentImport(domain, source) {
542
542
  throw new Error("DATABASE_URL and USERNAME_TO_EXPORT are required.");
543
543
  const dryRun = process.argv.includes("--dry-run");
544
544
  const prisma = createPrismaClient(databaseUrl);
545
- const notification = createPushoverService({
546
- url: process.env.PUSHOVER_URL ?? "",
547
- userKey: process.env.PUSHOVER_USER_KEY ?? "",
548
- appToken: process.env.PUSHOVER_APP_TOKEN ?? ""
549
- });
545
+ const notification = cliNotifications;
550
546
  try {
551
547
  const mediaDomain = domain === "images" || domain === "books";
552
548
  const bucket = process.env.S3_BUCKET_NAME;
@@ -580,14 +576,7 @@ async function runContentImport(domain, source) {
580
576
  if (!dryRun)
581
577
  await notification.notifyInfo(`ingest-${domain} completed`, {
582
578
  caller: `ingest-${domain}`
583
- }).catch((error) => console.error("\u901A\u77E5\u5931\u6557:", error));
584
- } catch (error) {
585
- console.error(error);
586
- process.exitCode = 1;
587
- if (!dryRun)
588
- await notification.notifyError(`ingest-${domain} failed`, { caller: `ingest-${domain}` }).catch(
589
- (notifyError) => console.error("\u901A\u77E5\u5931\u6557:", notifyError)
590
- );
579
+ });
591
580
  } finally {
592
581
  await prisma.$disconnect();
593
582
  }