@s-hirano-ist/s-scripts 4.2.0 → 4.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.
@@ -0,0 +1,447 @@
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
+ "tool",
82
+ "client",
83
+ "method",
84
+ "route",
85
+ "domain",
86
+ "status",
87
+ "job",
88
+ "sentry.environment",
89
+ "sentry.release",
90
+ "sentry.sdk.name",
91
+ "sentry.sdk.version",
92
+ "sentry.trace.parent_span_id",
93
+ "sentry.replay_id"
94
+ ].includes(key) && (typeof value === "string" || typeof value === "number");
95
+ }));
96
+ }
97
+ function createOperationalTelemetry(sdk, enabled) {
98
+ return {
99
+ log(level, message, attributes = {}) {
100
+ if (enabled())
101
+ observe(() => sdk.logger[level](message, operationalAttributes(attributes)));
102
+ },
103
+ count(name, value, attributes = {}) {
104
+ if (enabled())
105
+ observe(() => sdk.metrics.count(name, value, {
106
+ attributes: operationalAttributes(attributes)
107
+ }));
108
+ },
109
+ duration(name, milliseconds, attributes = {}) {
110
+ if (enabled())
111
+ observe(() => sdk.metrics.distribution(name, milliseconds, {
112
+ attributes: operationalAttributes(attributes),
113
+ unit: "millisecond"
114
+ }));
115
+ }
116
+ };
117
+ }
118
+
119
+ // ../infrastructure/dist/observability/log-level.js
120
+ function mapStatusToLogLevel(status) {
121
+ if (status >= 500)
122
+ return "error";
123
+ if (status >= 400)
124
+ return "warn";
125
+ return "info";
126
+ }
127
+
128
+ // ../infrastructure/dist/observability/server-logger.js
129
+ import pino from "pino";
130
+ var pinoConfig = {
131
+ browser: { asObject: true }
132
+ };
133
+ var ServerLogger = class {
134
+ telemetry;
135
+ logger;
136
+ notificationService;
137
+ reporter;
138
+ constructor(notificationService, reporter, telemetry2) {
139
+ this.telemetry = telemetry2;
140
+ this.reporter = reporter;
141
+ this.logger = pino(pinoConfig);
142
+ this.notificationService = notificationService;
143
+ }
144
+ async info(message, context, options) {
145
+ await this.logWithLevel("info", message, context, void 0, options);
146
+ }
147
+ async warn(message, context, options) {
148
+ await this.logWithLevel("warn", message, context, void 0, options);
149
+ }
150
+ async error(message, context, error, options) {
151
+ await this.logWithLevel("error", message, context, error, options);
152
+ }
153
+ async logWithLevel(level, message, context, error, options) {
154
+ const expectedLevel = mapStatusToLogLevel(context.status);
155
+ const finalLevel = level === expectedLevel ? level : expectedLevel;
156
+ this.telemetry?.log(finalLevel, "Application operation", {
157
+ status: context.status
158
+ });
159
+ const logData = {
160
+ caller: context.caller,
161
+ status: context.status,
162
+ userId: context.userId,
163
+ ...context.additionalContext
164
+ };
165
+ if (level === "error") {
166
+ try {
167
+ this.captureError(message, context, error);
168
+ } catch (reportError) {
169
+ this.logger.warn({ error: reportError }, "Failed to report error");
170
+ }
171
+ }
172
+ if (error !== void 0) {
173
+ console.error("Additional error details:", error);
174
+ }
175
+ switch (finalLevel) {
176
+ case "info":
177
+ this.logger.info(logData, message);
178
+ break;
179
+ case "warn":
180
+ this.logger.warn(logData, message);
181
+ break;
182
+ case "error":
183
+ this.logger.error(logData, message);
184
+ break;
185
+ }
186
+ if (level === "error" && options?.severity === "critical" || level === "info" && options?.notifyCompletion) {
187
+ const notificationContext = {
188
+ caller: context.caller,
189
+ userId: context.userId
190
+ };
191
+ try {
192
+ switch (level) {
193
+ case "info":
194
+ await this.notificationService.notifyInfo(message, notificationContext);
195
+ break;
196
+ case "error":
197
+ await this.notificationService.notifyError(message, notificationContext);
198
+ break;
199
+ }
200
+ } catch (notificationError) {
201
+ this.logger.warn({ error: notificationError }, "Failed to send notification");
202
+ try {
203
+ this.reporter.captureException(notificationError, {
204
+ tags: {
205
+ caller: context.caller,
206
+ error_type: "notification_delivery",
207
+ status: String(context.status)
208
+ },
209
+ user: context.userId ? { id: context.userId } : void 0
210
+ });
211
+ } catch (reportError) {
212
+ this.logger.warn({ error: reportError }, "Failed to report notification failure");
213
+ }
214
+ }
215
+ }
216
+ }
217
+ captureError(message, context, error) {
218
+ const capturedError = this.findError(error);
219
+ const options = {
220
+ extra: {
221
+ additionalContext: context.additionalContext,
222
+ originalError: capturedError ? void 0 : error
223
+ },
224
+ tags: {
225
+ caller: context.caller,
226
+ error_type: capturedError?.name ?? "message",
227
+ status: String(context.status)
228
+ },
229
+ user: context.userId ? { id: context.userId } : void 0
230
+ };
231
+ if (capturedError) {
232
+ this.reporter.captureException(capturedError, options);
233
+ return;
234
+ }
235
+ this.reporter.captureMessage(message, { ...options, level: "error" });
236
+ }
237
+ findError(value) {
238
+ if (value instanceof Error)
239
+ return value;
240
+ if (typeof value === "object" && value !== null && "cause" in value && value.cause instanceof Error) {
241
+ return value.cause;
242
+ }
243
+ return void 0;
244
+ }
245
+ };
246
+
247
+ // ../notification/dist/errors.js
248
+ var NotificationError = class extends Error {
249
+ constructor(message = "notificationSend") {
250
+ super(message);
251
+ this.name = "NotificationError";
252
+ }
253
+ };
254
+
255
+ // ../notification/dist/pushover.js
256
+ var PushoverService = class {
257
+ config;
258
+ constructor(config) {
259
+ this.config = config;
260
+ }
261
+ /**
262
+ * Sends a message to the Pushover API.
263
+ *
264
+ * @param message - The message to send
265
+ * @param priority - The priority level (-1 = low, 0 = normal, 1 = high)
266
+ *
267
+ * @internal
268
+ */
269
+ async sendMessage(message, priority = 0) {
270
+ const body = new URLSearchParams({
271
+ token: this.config.appToken,
272
+ user: this.config.userKey,
273
+ message,
274
+ priority: String(priority)
275
+ });
276
+ const result = await fetch(this.config.url, {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
279
+ body,
280
+ signal: AbortSignal.timeout(5e3)
281
+ });
282
+ if (!result.ok)
283
+ throw new NotificationError();
284
+ }
285
+ async notifyError(message, context) {
286
+ const enrichedMessage = `[ERROR] ${context.caller}: ${message}`;
287
+ await this.sendMessage(enrichedMessage, 1);
288
+ }
289
+ async notifyWarning(message, context) {
290
+ const enrichedMessage = `[WARN] ${context.caller}: ${message}`;
291
+ await this.sendMessage(enrichedMessage, 0);
292
+ }
293
+ async notifyInfo(message, context) {
294
+ const enrichedMessage = `[INFO] ${context.caller}: ${message}`;
295
+ await this.sendMessage(enrichedMessage, -1);
296
+ }
297
+ };
298
+ function createPushoverService(config) {
299
+ return new PushoverService(config);
300
+ }
301
+
302
+ // ../notification/dist/web-push.js
303
+ import { sendNotification, setVapidDetails } from "web-push";
304
+
305
+ // src/lib/cli-observability.ts
306
+ import * as Sentry from "@sentry/node";
307
+ var dryRun = () => process.argv.includes("--dry-run");
308
+ var initialized = false;
309
+ var telemetry = createOperationalTelemetry(
310
+ Sentry,
311
+ () => !dryRun() && process.env.NODE_ENV === "production" && Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN)
312
+ );
313
+ function initialize() {
314
+ if (initialized || dryRun()) return;
315
+ initialized = true;
316
+ if (!process.env.NEXT_PUBLIC_SENTRY_DSN)
317
+ console.warn(
318
+ "NEXT_PUBLIC_SENTRY_DSN is not set; errors will only be logged locally and critical alerts sent via Pushover."
319
+ );
320
+ try {
321
+ Sentry.init({
322
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
323
+ enabled: process.env.NODE_ENV === "production" && Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN),
324
+ environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
325
+ release: process.env.SENTRY_RELEASE ?? process.env.VERCEL_GIT_COMMIT_SHA,
326
+ beforeSendLog(log) {
327
+ log.attributes = operationalAttributes(log.attributes);
328
+ return log;
329
+ },
330
+ beforeSendMetric(metric) {
331
+ metric.attributes = operationalAttributes(metric.attributes);
332
+ return metric;
333
+ },
334
+ // The runner owns reporting and flushing, including handled item failures.
335
+ defaultIntegrations: false,
336
+ dataCollection: {
337
+ userInfo: false,
338
+ cookies: false,
339
+ httpHeaders: false,
340
+ httpBodies: [],
341
+ urlQueryParams: false,
342
+ databaseQueryData: false,
343
+ stackFrameVariables: false
344
+ },
345
+ beforeSend(event) {
346
+ delete event.user;
347
+ delete event.request;
348
+ delete event.breadcrumbs;
349
+ return event;
350
+ }
351
+ });
352
+ } catch (error) {
353
+ console.error(
354
+ "Failed to initialize Sentry",
355
+ createDiagnosticError(process.env)(error)
356
+ );
357
+ }
358
+ }
359
+ function logger() {
360
+ initialize();
361
+ const diagnose = createDiagnosticError(process.env);
362
+ return new ServerLogger(
363
+ createPushoverService({
364
+ url: process.env.PUSHOVER_URL ?? "https://api.pushover.net/1/messages.json",
365
+ userKey: process.env.PUSHOVER_USER_KEY ?? "",
366
+ appToken: process.env.PUSHOVER_APP_TOKEN ?? ""
367
+ }),
368
+ {
369
+ captureException: (error, context) => Sentry.captureException(diagnose(error), context),
370
+ captureMessage: (message, context) => Sentry.captureMessage(message, context)
371
+ }
372
+ );
373
+ }
374
+ function reportCliError(error, caller) {
375
+ const diagnostic = createDiagnosticError(process.env)(error);
376
+ console.error(diagnostic);
377
+ if (dryRun()) return;
378
+ initialize();
379
+ telemetry.log("error", "CLI item failed", { job: telemetryJob(caller) });
380
+ try {
381
+ Sentry.captureException(diagnostic, {
382
+ tags: { caller, severity: "normal" }
383
+ });
384
+ } catch (reportError) {
385
+ console.error(
386
+ "Failed to report error",
387
+ createDiagnosticError(process.env)(reportError)
388
+ );
389
+ }
390
+ }
391
+ var cliNotifications = {
392
+ async notifyInfo(message, context) {
393
+ if (dryRun()) return;
394
+ await logger().info(
395
+ message,
396
+ { ...context, caller: context.caller ?? "cli", status: 200 },
397
+ { notifyCompletion: true }
398
+ );
399
+ }
400
+ };
401
+ async function runCli(caller, execute) {
402
+ const started = performance.now();
403
+ const job = telemetryJob(caller);
404
+ let status = "ok";
405
+ try {
406
+ initialize();
407
+ telemetry.log("info", "CLI started", { job });
408
+ await execute();
409
+ telemetry.log("info", "CLI completed", { job, status: "ok" });
410
+ } catch (error) {
411
+ status = "error";
412
+ telemetry.log("error", "CLI failed", { job, status: "error" });
413
+ process.exitCode = 1;
414
+ const diagnostic = createDiagnosticError(process.env)(error);
415
+ console.error(diagnostic);
416
+ if (!dryRun())
417
+ await logger().error(
418
+ `${caller} failed`,
419
+ { caller, status: 500 },
420
+ diagnostic,
421
+ { severity: "critical" }
422
+ );
423
+ } finally {
424
+ telemetry.count("cli.runs", 1, { job, status });
425
+ telemetry.duration("cli.duration", performance.now() - started, {
426
+ job,
427
+ status
428
+ });
429
+ if (initialized && !dryRun()) {
430
+ try {
431
+ if (!await Sentry.flush(2e3))
432
+ console.error("Sentry flush timed out");
433
+ } catch (error) {
434
+ console.error(
435
+ "Sentry flush failed",
436
+ createDiagnosticError(process.env)(error)
437
+ );
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ export {
444
+ reportCliError,
445
+ cliNotifications,
446
+ runCli
447
+ };
@@ -57,13 +57,13 @@ function createObjectStorageClient(config = loadStorageConfig()) {
57
57
  async getObject(bucket, key) {
58
58
  const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
59
59
  if (!(result.Body instanceof Readable))
60
- throw new Error("Object storage returned no Node readable body");
60
+ throw new TypeError("Object storage returned no Node readable body");
61
61
  return result.Body;
62
62
  },
63
63
  async downloadObject(bucket, key, destination) {
64
64
  const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
65
65
  if (!(result.Body instanceof Readable))
66
- throw new Error("Object storage returned no Node readable body");
66
+ throw new TypeError("Object storage returned no Node readable body");
67
67
  await pipeline(result.Body, createWriteStream(destination));
68
68
  },
69
69
  async statObject(bucket, key) {
@@ -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-KKWRXAAY.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
  }
@@ -0,0 +1,87 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+ import {
3
+ ensureCollection,
4
+ search
5
+ } from "./chunk-J4VLH4VA.js";
6
+ import "./chunk-JZOR5GA2.js";
7
+
8
+ // src/rag/search-command.ts
9
+ function parseArgs() {
10
+ const args = process.argv.slice(2);
11
+ const options = {};
12
+ const positional = [];
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === "--top-k" && i + 1 < args.length) {
16
+ options.topK = Number(args[++i]);
17
+ } else if (arg === "--type" && i + 1 < args.length) {
18
+ options.type = args[++i];
19
+ } else if (arg === "--heading" && i + 1 < args.length) {
20
+ options.heading = args[++i];
21
+ } else if (arg === "--content-type" && i + 1 < args.length) {
22
+ const value = args[++i];
23
+ const types = value.split(",");
24
+ options.contentType = types.length === 1 ? types[0] : types;
25
+ } else {
26
+ positional.push(arg);
27
+ }
28
+ }
29
+ const query = positional.join(" ");
30
+ if (!query) {
31
+ console.error(
32
+ "Usage: rag-search <query> [--top-k N] [--type markdown_note|bookmark_json] [--heading HEADING] [--content-type articles|books|notes]"
33
+ );
34
+ process.exitCode = 1;
35
+ return void 0;
36
+ }
37
+ return { query, options };
38
+ }
39
+ async function searchContent(query, options = {}) {
40
+ const results = await search(query, {
41
+ topK: options.topK,
42
+ filter: {
43
+ type: options.type,
44
+ top_heading: options.heading,
45
+ content_type: options.contentType
46
+ }
47
+ });
48
+ return { results, query, totalResults: results.length };
49
+ }
50
+ async function search2() {
51
+ const args = parseArgs();
52
+ if (!args) return;
53
+ const { query, options } = args;
54
+ console.log(`Searching for: "${query}"`);
55
+ if (options.topK) console.log(` top-k: ${options.topK}`);
56
+ if (options.type) console.log(` type: ${options.type}`);
57
+ if (options.heading) console.log(` heading: ${options.heading}`);
58
+ if (options.contentType)
59
+ console.log(
60
+ ` content-type: ${Array.isArray(options.contentType) ? options.contentType.join(",") : options.contentType}`
61
+ );
62
+ console.log();
63
+ await ensureCollection();
64
+ const response = await searchContent(query, options);
65
+ console.log(`Found ${response.totalResults} results:
66
+ `);
67
+ for (const [i, result] of response.results.entries()) {
68
+ console.log(`--- Result ${i + 1} (score: ${result.score.toFixed(4)}) ---`);
69
+ console.log(`Title: ${result.title}`);
70
+ if (result.url) console.log(`URL: ${result.url}`);
71
+ console.log(`Type: ${result.type}`);
72
+ console.log(`Content-Type: ${result.content_type}`);
73
+ console.log(`Heading: ${result.heading_path.join(" > ")}`);
74
+ console.log(`
75
+ ${result.text}
76
+ `);
77
+ }
78
+ }
79
+ async function main() {
80
+ if (!process.env.QDRANT_URL) {
81
+ throw new Error("QDRANT_URL environment variable is required.");
82
+ }
83
+ await search2();
84
+ }
85
+ export {
86
+ main
87
+ };