@mgsoftwarebv/mg-dashboard-mcp 7.4.13 → 7.4.14

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.
Files changed (2) hide show
  1. package/dist/index.js +779 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import crypto, { randomUUID, createHash, randomBytes, createCipheriv, createDeci
13
13
  import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
14
14
  import { createServer } from 'http';
15
15
  import { tmpdir } from 'os';
16
+ import { AsyncLocalStorage } from 'async_hooks';
16
17
  import { drizzle } from 'drizzle-orm/postgres-js';
17
18
  import postgres from 'postgres';
18
19
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -454,6 +455,12 @@ var init_db_ssh_tunnel = __esm({
454
455
  "src/db-ssh-tunnel.ts"() {
455
456
  }
456
457
  });
458
+ new AsyncLocalStorage();
459
+ var reportingContext = new AsyncLocalStorage();
460
+ function maybeReportSlowQuery(durationMs, query) {
461
+ if (reportingContext.getStore()) return;
462
+ return;
463
+ }
457
464
  function getConnectionConfig() {
458
465
  return {
459
466
  prepare: false,
@@ -490,7 +497,20 @@ function getPool() {
490
497
  }
491
498
  function getDb() {
492
499
  if (!_db) {
493
- _db = drizzle(getPool(), { casing: "snake_case" });
500
+ const raw = drizzle(getPool(), { casing: "snake_case" });
501
+ const originalExecute = raw.execute.bind(raw);
502
+ raw.execute = ((query) => {
503
+ const start = performance.now();
504
+ const result = originalExecute(query);
505
+ if (result && typeof result.then === "function") {
506
+ return result.finally(() => {
507
+ maybeReportSlowQuery(performance.now() - start);
508
+ });
509
+ }
510
+ maybeReportSlowQuery(performance.now() - start);
511
+ return result;
512
+ });
513
+ _db = raw;
494
514
  }
495
515
  return _db;
496
516
  }
@@ -703,10 +723,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
703
723
  "trigger-env": "settings"
704
724
  };
705
725
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
706
- const sql29 = `SELECT re.\\"apiKey\\" || '~~' || p.\\"externalRef\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
726
+ const sql31 = `SELECT re.\\"apiKey\\" || '~~' || p.\\"externalRef\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
707
727
  const cmd = [
708
728
  `PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
709
- `ROW=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null | tr -d '[:space:]')`,
729
+ `ROW=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql31}" 2>/dev/null | tr -d '[:space:]')`,
710
730
  'echo "$PORT|$ROW"'
711
731
  ].join(" && ");
712
732
  const result = await sshExec2(conn, cmd, proxy);
@@ -728,8 +748,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
728
748
  return { port, apiKey: apiKey2, projectRef: projectRef || "" };
729
749
  }
730
750
  async function fetchRunLogs(runId, conn, proxy, sshExec2) {
731
- const sql29 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
732
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
751
+ const sql31 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
752
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql31}" 2>/dev/null`;
733
753
  const result = await sshExec2(conn, cmd, proxy);
734
754
  const output = result.stdout.trim();
735
755
  if (!output) return "";
@@ -811,8 +831,8 @@ async function handleTriggerTool(name, args2, deps) {
811
831
  switch (name) {
812
832
  // -----------------------------------------------------------------
813
833
  case "trigger-list": {
814
- const sql29 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
815
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
834
+ const sql31 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
835
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql31}" 2>/dev/null`;
816
836
  const result = await sshExec2(conn, cmd, proxy);
817
837
  const output = result.stdout.trim();
818
838
  if (!output) {
@@ -990,9 +1010,9 @@ ${raw2.substring(0, 500)}` }] };
990
1010
  return { content: [{ type: "text", text: `No environment variables for ${project}/${env}.` }] };
991
1011
  }
992
1012
  const lines = vars.map((v) => `${v.isSecret ? "[secret]" : " "} ${v.name}`).sort();
993
- const text19 = `Env vars for ${project}/${env} (${vars.length}) \u2014 values hidden, use action="get" with a key:
1013
+ const text22 = `Env vars for ${project}/${env} (${vars.length}) \u2014 values hidden, use action="get" with a key:
994
1014
  ` + "-".repeat(50) + "\n" + lines.join("\n");
995
- return { content: [{ type: "text", text: text19 }] };
1015
+ return { content: [{ type: "text", text: text22 }] };
996
1016
  }
997
1017
  if (action === "get") {
998
1018
  const key2 = String(args2.key ?? "");
@@ -1079,9 +1099,9 @@ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
1079
1099
  return { content: [{ type: "text", text: `Invalid API response:
1080
1100
  ${rawJson.substring(0, 500)}` }] };
1081
1101
  }
1082
- let text19 = formatRunDetail(run);
1083
- if (logs) text19 += "\n\n--- Logs ---\n" + logs;
1084
- return { content: [{ type: "text", text: text19 }] };
1102
+ let text22 = formatRunDetail(run);
1103
+ if (logs) text22 += "\n\n--- Logs ---\n" + logs;
1104
+ return { content: [{ type: "text", text: text22 }] };
1085
1105
  }
1086
1106
  async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
1087
1107
  const pollInterval = 3e3;
@@ -1103,10 +1123,10 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
1103
1123
  continue;
1104
1124
  }
1105
1125
  if (TERMINAL_STATUSES.has(run.status)) {
1106
- let text19 = formatRunDetail(run);
1126
+ let text22 = formatRunDetail(run);
1107
1127
  const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
1108
- if (logs) text19 += "\n\n--- Logs ---\n" + logs;
1109
- return { content: [{ type: "text", text: text19 }] };
1128
+ if (logs) text22 += "\n\n--- Logs ---\n" + logs;
1129
+ return { content: [{ type: "text", text: text22 }] };
1110
1130
  }
1111
1131
  }
1112
1132
  return {
@@ -3793,10 +3813,10 @@ var ZodObject = class _ZodObject extends ZodType {
3793
3813
  // }) as any;
3794
3814
  // return merged;
3795
3815
  // }
3796
- catchall(index16) {
3816
+ catchall(index19) {
3797
3817
  return new _ZodObject({
3798
3818
  ...this._def,
3799
- catchall: index16
3819
+ catchall: index19
3800
3820
  });
3801
3821
  }
3802
3822
  pick(mask) {
@@ -4114,9 +4134,9 @@ function mergeValues(a, b) {
4114
4134
  return { valid: false };
4115
4135
  }
4116
4136
  const newArray = [];
4117
- for (let index16 = 0; index16 < a.length; index16++) {
4118
- const itemA = a[index16];
4119
- const itemB = b[index16];
4137
+ for (let index19 = 0; index19 < a.length; index19++) {
4138
+ const itemA = a[index19];
4139
+ const itemB = b[index19];
4120
4140
  const sharedValue = mergeValues(itemA, itemB);
4121
4141
  if (!sharedValue.valid) {
4122
4142
  return { valid: false };
@@ -4322,10 +4342,10 @@ var ZodMap = class extends ZodType {
4322
4342
  }
4323
4343
  const keyType = this._def.keyType;
4324
4344
  const valueType = this._def.valueType;
4325
- const pairs = [...ctx.data.entries()].map(([key, value], index16) => {
4345
+ const pairs = [...ctx.data.entries()].map(([key, value], index19) => {
4326
4346
  return {
4327
- key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index16, "key"])),
4328
- value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index16, "value"]))
4347
+ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index19, "key"])),
4348
+ value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index19, "value"]))
4329
4349
  };
4330
4350
  });
4331
4351
  if (ctx.common.async) {
@@ -5340,13 +5360,19 @@ pgTable(
5340
5360
  verifyNote: text("verify_note"),
5341
5361
  dirty: boolean("dirty").notNull().default(false),
5342
5362
  vaultSyncedAt: timestamp("vault_synced_at", { withTimezone: true }),
5363
+ /** Reviewer / itemized-phase lease holder. */
5364
+ claimedBy: text("claimed_by"),
5365
+ claimedAt: timestamp("claimed_at", { withTimezone: true }),
5366
+ /** Expired leases are reclaimable by any runner. */
5367
+ leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
5343
5368
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5344
5369
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5345
5370
  },
5346
5371
  (table) => [
5347
5372
  index("idx_wiki_page_status").on(table.status),
5348
5373
  index("idx_wiki_page_last_verified").on(table.lastVerifiedOn),
5349
- index("idx_wiki_page_dirty").on(table.dirty)
5374
+ index("idx_wiki_page_dirty").on(table.dirty),
5375
+ index("idx_wiki_page_lease_expires").on(table.leaseExpiresAt).where(sql`${table.leaseExpiresAt} IS NOT NULL`)
5350
5376
  ]
5351
5377
  );
5352
5378
  pgTable(
@@ -5391,11 +5417,9 @@ pgTable(
5391
5417
  resolvedAt: timestamp("resolved_at", { withTimezone: true })
5392
5418
  },
5393
5419
  (table) => [
5394
- uniqueIndex("wiki_page_evidence_page_slug_evidence_type_evidence_ref_key").on(
5395
- table.pageSlug,
5396
- table.evidenceType,
5397
- table.evidenceRef
5398
- ),
5420
+ uniqueIndex(
5421
+ "wiki_page_evidence_page_slug_evidence_type_evidence_ref_key"
5422
+ ).on(table.pageSlug, table.evidenceType, table.evidenceRef),
5399
5423
  index("idx_wiki_page_evidence_created").on(table.createdAt)
5400
5424
  ]
5401
5425
  );
@@ -5450,6 +5474,65 @@ pgTable(
5450
5474
  )
5451
5475
  ]
5452
5476
  );
5477
+ pgTable(
5478
+ "wiki_invariant_violation",
5479
+ {
5480
+ id: uuid("id").primaryKey().defaultRandom(),
5481
+ slug: text("slug").notNull(),
5482
+ code: text("code").notNull(),
5483
+ severity: text("severity").notNull().default("warning"),
5484
+ detail: text("detail").notNull().default(""),
5485
+ suggestedFix: text("suggested_fix"),
5486
+ suggestedSlug: text("suggested_slug"),
5487
+ occurrenceCount: integer("occurrence_count").notNull().default(1),
5488
+ reopenCount: integer("reopen_count").notNull().default(0),
5489
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
5490
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5491
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
5492
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5493
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5494
+ },
5495
+ (table) => [
5496
+ uniqueIndex("wiki_invariant_violation_slug_code_key").on(
5497
+ table.slug,
5498
+ table.code
5499
+ ),
5500
+ index("idx_wiki_invariant_violation_severity").on(
5501
+ table.severity,
5502
+ table.resolvedAt
5503
+ )
5504
+ ]
5505
+ );
5506
+ pgTable(
5507
+ "wiki_pipeline_rule",
5508
+ {
5509
+ id: uuid("id").primaryKey().defaultRandom(),
5510
+ scope: text("scope").notNull(),
5511
+ ruleKey: text("rule_key").notNull(),
5512
+ text: text("text").notNull(),
5513
+ /** A = deterministic auto-applied, B = judgement, gated by insight accept. */
5514
+ tier: text("tier").notNull().default("B"),
5515
+ status: text("status").notNull().default("proposed"),
5516
+ sourceCode: text("source_code"),
5517
+ evidenceRefs: text("evidence_refs").array().notNull().default([]),
5518
+ violationCount: integer("violation_count").notNull().default(0),
5519
+ lastViolationAt: timestamp("last_violation_at", { withTimezone: true }),
5520
+ activatedAt: timestamp("activated_at", { withTimezone: true }),
5521
+ retiredAt: timestamp("retired_at", { withTimezone: true }),
5522
+ retiredReason: text("retired_reason"),
5523
+ rejectedAt: timestamp("rejected_at", { withTimezone: true }),
5524
+ rejectedReason: text("rejected_reason"),
5525
+ approvedBy: text("approved_by"),
5526
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5527
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5528
+ },
5529
+ (table) => [
5530
+ uniqueIndex("wiki_pipeline_rule_scope_key_key").on(
5531
+ table.scope,
5532
+ table.ruleKey
5533
+ )
5534
+ ]
5535
+ );
5453
5536
  pgTable(
5454
5537
  "wiki_search_log",
5455
5538
  {
@@ -5477,6 +5560,69 @@ pgTable(
5477
5560
  },
5478
5561
  (table) => [index("idx_team_memory_search_log_created").on(table.createdAt)]
5479
5562
  );
5563
+ pgTable(
5564
+ "wiki_agent_run",
5565
+ {
5566
+ id: uuid("id").primaryKey().defaultRandom(),
5567
+ phase: text("phase").notNull(),
5568
+ model: text("model"),
5569
+ claimed: jsonb("claimed").$type(),
5570
+ proven: jsonb("proven").$type(),
5571
+ violationCount: integer("violation_count").notNull().default(0),
5572
+ violations: jsonb("violations").$type(),
5573
+ outcome: text("outcome").notNull().default("ok"),
5574
+ agentId: text("agent_id"),
5575
+ runId: text("run_id"),
5576
+ passId: text("pass_id"),
5577
+ changedPaths: jsonb("changed_paths").$type(),
5578
+ summary: text("summary"),
5579
+ userVerdict: text("user_verdict"),
5580
+ userVerdictNote: text("user_verdict_note"),
5581
+ verdictAt: timestamp("verdict_at", { withTimezone: true }),
5582
+ reviewed: boolean("reviewed").notNull().default(false),
5583
+ reviewNotes: text("review_notes"),
5584
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
5585
+ startedAt: timestamp("started_at", { withTimezone: true }),
5586
+ finishedAt: timestamp("finished_at", { withTimezone: true }).notNull().defaultNow(),
5587
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5588
+ },
5589
+ (table) => [
5590
+ index("idx_wiki_agent_run_phase_created").on(table.phase, table.createdAt),
5591
+ index("idx_wiki_agent_run_created").on(table.createdAt),
5592
+ index("idx_wiki_agent_run_reviewed").on(table.reviewed),
5593
+ index("idx_wiki_agent_run_user_verdict").on(table.userVerdict),
5594
+ index("idx_wiki_agent_run_pass_id").on(table.passId)
5595
+ ]
5596
+ );
5597
+ pgTable(
5598
+ "wiki_agent_review_suggestion",
5599
+ {
5600
+ id: uuid("id").primaryKey().defaultRandom(),
5601
+ status: text("status").notNull().default("proposed"),
5602
+ implementationClass: text("implementation_class").notNull(),
5603
+ phase: text("phase"),
5604
+ scope: text("scope"),
5605
+ title: text("title").notNull(),
5606
+ summary: text("summary").notNull(),
5607
+ targetFile: text("target_file"),
5608
+ targetKey: text("target_key"),
5609
+ beforeText: text("before_text"),
5610
+ afterText: text("after_text"),
5611
+ evidenceRunIds: uuid("evidence_run_ids").array().notNull().default([]),
5612
+ evidenceRefs: text("evidence_refs").array().notNull().default([]),
5613
+ dedupeKey: text("dedupe_key").notNull(),
5614
+ source: text("source").notNull().default("auto"),
5615
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5616
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5617
+ },
5618
+ (table) => [
5619
+ uniqueIndex("wiki_agent_review_suggestion_dedupe_key").on(table.dedupeKey),
5620
+ index("idx_wiki_agent_review_suggestion_status_created").on(
5621
+ table.status,
5622
+ table.createdAt
5623
+ )
5624
+ ]
5625
+ );
5480
5626
 
5481
5627
  // ../db/src/schema/agent-memory.ts
5482
5628
  pgTable(
@@ -5521,6 +5667,83 @@ pgTable(
5521
5667
  index("idx_agent_memory_category").on(table.category)
5522
5668
  ]
5523
5669
  );
5670
+ var agentWorldAgent = pgTable(
5671
+ "agent_world_agent",
5672
+ {
5673
+ id: uuid("id").primaryKey().defaultRandom(),
5674
+ name: text("name").notNull(),
5675
+ color: text("color").notNull(),
5676
+ /** Personality sliders 0–100: curiosity, sociability, diligence, calm. */
5677
+ personality: jsonb("personality").$type().notNull().default({
5678
+ curiosity: 50,
5679
+ sociability: 50,
5680
+ diligence: 50,
5681
+ calm: 50
5682
+ }),
5683
+ /** Needs 0–100: higher = more urgent. */
5684
+ needs: jsonb("needs").$type().notNull().default({ energy: 40, coffee: 30, focus: 35, social: 25 }),
5685
+ position: jsonb("position").$type().notNull().default({ x: 0, y: 0, z: 0 }),
5686
+ target: jsonb("target").$type(),
5687
+ activity: text("activity").notNull().default("idle"),
5688
+ waypoint: text("waypoint"),
5689
+ memories: jsonb("memories").$type().notNull().default([]),
5690
+ diaryCount: integer("diary_count").notNull().default(0),
5691
+ lastSay: text("last_say"),
5692
+ lastSayAt: timestamp("last_say_at", { withTimezone: true }),
5693
+ sortOrder: integer("sort_order").notNull().default(0),
5694
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5695
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5696
+ },
5697
+ (table) => [index("idx_agent_world_agent_sort").on(table.sortOrder)]
5698
+ );
5699
+ pgTable(
5700
+ "agent_world_event",
5701
+ {
5702
+ id: uuid("id").primaryKey().defaultRandom(),
5703
+ /** say | diary | think | god | activity | memo | system */
5704
+ type: text("type").notNull(),
5705
+ agentId: uuid("agent_id").references(() => agentWorldAgent.id, {
5706
+ onDelete: "set null"
5707
+ }),
5708
+ message: text("message").notNull(),
5709
+ meta: jsonb("meta").$type().notNull().default({}),
5710
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5711
+ },
5712
+ (table) => [
5713
+ index("idx_agent_world_event_created").on(table.createdAt),
5714
+ index("idx_agent_world_event_type").on(table.type),
5715
+ index("idx_agent_world_event_agent").on(table.agentId)
5716
+ ]
5717
+ );
5718
+ pgTable(
5719
+ "agent_world_memo",
5720
+ {
5721
+ id: uuid("id").primaryKey().defaultRandom(),
5722
+ agentId: uuid("agent_id").references(() => agentWorldAgent.id, {
5723
+ onDelete: "set null"
5724
+ }),
5725
+ title: text("title").notNull(),
5726
+ content: text("content").notNull(),
5727
+ location: text("location").notNull().default("whiteboard"),
5728
+ reads: integer("reads").notNull().default(0),
5729
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5730
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5731
+ },
5732
+ (table) => [
5733
+ index("idx_agent_world_memo_created").on(table.createdAt),
5734
+ index("idx_agent_world_memo_location").on(table.location)
5735
+ ]
5736
+ );
5737
+ pgTable("agent_world_state", {
5738
+ id: text("id").primaryKey().default("world"),
5739
+ tick: integer("tick").notNull().default(0),
5740
+ /** Simulated minutes since day start (0–1439 wraps). */
5741
+ simMinutes: integer("sim_minutes").notNull().default(540),
5742
+ paused: boolean("paused").notNull().default(false),
5743
+ lastThinkAt: timestamp("last_think_at", { withTimezone: true }),
5744
+ lastThinkAgentId: uuid("last_think_agent_id"),
5745
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5746
+ });
5524
5747
  var contentSourceType = pgEnum("content_source_type", [
5525
5748
  "own_site",
5526
5749
  "client_site",
@@ -6086,7 +6309,11 @@ pgTable(
6086
6309
  table.resourceId,
6087
6310
  table.metric,
6088
6311
  table.sampledAt
6089
- )
6312
+ ),
6313
+ // Retention cleanup (monitoring-sample-retention-cleanup) filters only on
6314
+ // sampled_at. Without this index Postgres seq-scans the full firehose.
6315
+ // Live DB name uses idx_ prefix (see migration 20260731070000).
6316
+ index("idx_monitoring_sample_sampled_at").on(table.sampledAt)
6090
6317
  ]
6091
6318
  );
6092
6319
  var appLogSource = pgTable(
@@ -6420,7 +6647,8 @@ var directorySubmissionStatus = pgEnum(
6420
6647
  "rejected",
6421
6648
  "failed",
6422
6649
  "manual",
6423
- "captcha"
6650
+ "captcha",
6651
+ "skipped"
6424
6652
  ]
6425
6653
  );
6426
6654
  var directoryAccountStatus = pgEnum("directory_account_status", [
@@ -6444,6 +6672,13 @@ var linkDirectory = pgTable(
6444
6672
  automationLevel: directoryAutomationLevel("automation_level").notNull().default("auto"),
6445
6673
  enabled: boolean("enabled").notNull().default(false),
6446
6674
  formMapping: jsonb("form_mapping").$type(),
6675
+ /** Set when a submission for this directory reached live/submitted end-to-end. */
6676
+ provenAt: timestamp("proven_at", { withTimezone: true }),
6677
+ successCount: integer("success_count").notNull().default(0),
6678
+ failCount: integer("fail_count").notNull().default(0),
6679
+ /** Hard captcha (even after residential) — disable after CAPTCHA_STRIKE_LIMIT. */
6680
+ captchaStrike: integer("captcha_strike").notNull().default(0),
6681
+ lastOutcome: text("last_outcome"),
6447
6682
  lastCheckedAt: timestamp("last_checked_at", { withTimezone: true }),
6448
6683
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6449
6684
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
@@ -6451,7 +6686,8 @@ var linkDirectory = pgTable(
6451
6686
  (table) => [
6452
6687
  uniqueIndex("link_directory_submit_url_uidx").on(table.submitUrl),
6453
6688
  index("link_directory_enabled_dr_idx").on(table.enabled, table.dr),
6454
- index("link_directory_region_idx").on(table.region)
6689
+ index("link_directory_region_idx").on(table.region),
6690
+ index("link_directory_proven_at_idx").on(table.provenAt)
6455
6691
  ]
6456
6692
  );
6457
6693
  var linkBuildingClient = pgTable(
@@ -6463,19 +6699,26 @@ var linkBuildingClient = pgTable(
6463
6699
  mailboxAddress: text("mailbox_address"),
6464
6700
  refrontMailAccountId: text("refront_mail_account_id"),
6465
6701
  profile: jsonb("profile").$type().notNull().default({}),
6466
- weeklyPace: integer("weekly_pace").notNull().default(12),
6702
+ weeklyPace: integer("weekly_pace").notNull().default(35),
6467
6703
  /** Target package size (number of directory backlinks sold). */
6468
6704
  packageSize: integer("package_size").notNull().default(100),
6469
- autoSubmit: boolean("auto_submit").notNull().default(false),
6705
+ autoSubmit: boolean("auto_submit").notNull().default(true),
6470
6706
  startingDr: doublePrecision("starting_dr"),
6471
6707
  currentDr: doublePrecision("current_dr"),
6472
6708
  isActive: boolean("is_active").notNull().default(true),
6709
+ /** Opaque token for public customer progress reports (backlinking.eu). */
6710
+ reportToken: text("report_token"),
6711
+ /** External shop order id from backlinking.eu checkout. */
6712
+ shopOrderId: text("shop_order_id"),
6713
+ shopCustomerEmail: text("shop_customer_email"),
6473
6714
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6474
6715
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6475
6716
  },
6476
6717
  (table) => [
6477
6718
  uniqueIndex("link_building_client_website_uidx").on(table.websiteUrl),
6478
- index("link_building_client_active_idx").on(table.isActive)
6719
+ index("link_building_client_active_idx").on(table.isActive),
6720
+ uniqueIndex("link_building_client_report_token_uidx").on(table.reportToken),
6721
+ index("link_building_client_shop_order_idx").on(table.shopOrderId)
6479
6722
  ]
6480
6723
  );
6481
6724
  var directorySubmission = pgTable(
@@ -6583,6 +6826,78 @@ pgTable(
6583
6826
  )
6584
6827
  ]
6585
6828
  );
6829
+ var networkListingStatus = pgEnum("network_listing_status", [
6830
+ "pending",
6831
+ "queued",
6832
+ "placing",
6833
+ "live",
6834
+ "failed",
6835
+ "expired",
6836
+ "cancelled"
6837
+ ]);
6838
+ var networkSite = pgTable(
6839
+ "network_site",
6840
+ {
6841
+ id: uuid("id").primaryKey().defaultRandom(),
6842
+ name: text("name").notNull(),
6843
+ domain: text("domain").notNull(),
6844
+ websiteUrl: text("website_url").notNull(),
6845
+ dr: integer("dr").notNull().default(0),
6846
+ theme: text("theme").notNull().default("general"),
6847
+ /** Yearly listing price in euro cents. */
6848
+ priceYearCents: integer("price_year_cents").notNull().default(9900),
6849
+ categories: jsonb("categories").$type().notNull().default([]),
6850
+ /** GitHub repo for placement commits, e.g. MGSoftwareBV/catalogcompany.nl */
6851
+ repoFullName: text("repo_full_name"),
6852
+ /** Path in repo for partner/listing data (relative). */
6853
+ listingDataPath: text("listing_data_path"),
6854
+ dofollow: boolean("dofollow").notNull().default(true),
6855
+ enabled: boolean("enabled").notNull().default(false),
6856
+ maxListings: integer("max_listings").notNull().default(50),
6857
+ notes: text("notes"),
6858
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6859
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6860
+ },
6861
+ (table) => [
6862
+ uniqueIndex("network_site_domain_uidx").on(table.domain),
6863
+ index("network_site_enabled_dr_idx").on(table.enabled, table.dr),
6864
+ index("network_site_theme_idx").on(table.theme)
6865
+ ]
6866
+ );
6867
+ pgTable(
6868
+ "network_listing",
6869
+ {
6870
+ id: uuid("id").primaryKey().defaultRandom(),
6871
+ siteId: uuid("site_id").notNull().references(() => networkSite.id, { onDelete: "cascade" }),
6872
+ clientId: uuid("client_id").references(() => linkBuildingClient.id, {
6873
+ onDelete: "set null"
6874
+ }),
6875
+ customerName: text("customer_name").notNull(),
6876
+ customerEmail: text("customer_email").notNull(),
6877
+ targetUrl: text("target_url").notNull(),
6878
+ anchorText: text("anchor_text").notNull(),
6879
+ categorySlug: text("category_slug").notNull().default("general"),
6880
+ status: networkListingStatus("status").notNull().default("pending"),
6881
+ listingUrl: text("listing_url"),
6882
+ /** External shop order reference from backlinking.eu */
6883
+ shopOrderId: text("shop_order_id"),
6884
+ shopOrderItemId: text("shop_order_item_id"),
6885
+ priceCents: integer("price_cents").notNull().default(0),
6886
+ startsAt: timestamp("starts_at", { withTimezone: true }),
6887
+ renewsAt: timestamp("renews_at", { withTimezone: true }),
6888
+ placedAt: timestamp("placed_at", { withTimezone: true }),
6889
+ lastError: text("last_error"),
6890
+ log: jsonb("log").$type().notNull().default({}),
6891
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6892
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6893
+ },
6894
+ (table) => [
6895
+ index("network_listing_site_status_idx").on(table.siteId, table.status),
6896
+ index("network_listing_client_idx").on(table.clientId),
6897
+ index("network_listing_renews_at_idx").on(table.renewsAt),
6898
+ index("network_listing_shop_order_idx").on(table.shopOrderId)
6899
+ ]
6900
+ );
6586
6901
  pgTable("github_api_budget", {
6587
6902
  tokenKey: text("token_key").primaryKey(),
6588
6903
  windowStartedAt: timestamp("window_started_at", { withTimezone: true }).notNull().defaultNow(),
@@ -6611,6 +6926,7 @@ pgTable(
6611
6926
  requestCount: integer("request_count").notNull().default(0),
6612
6927
  errorCount: integer("error_count").notNull().default(0),
6613
6928
  cacheHitCount: integer("cache_hit_count").notNull().default(0),
6929
+ breakerHitCount: integer("breaker_hit_count").notNull().default(0),
6614
6930
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6615
6931
  },
6616
6932
  (table) => [
@@ -6655,11 +6971,66 @@ pgTable(
6655
6971
  index("github_webhook_secret_owner_idx").on(table.owner)
6656
6972
  ]
6657
6973
  );
6974
+ var gscOpportunityPass = pgTable(
6975
+ "gsc_opportunity_pass",
6976
+ {
6977
+ id: uuid("id").primaryKey().defaultRandom(),
6978
+ siteKey: text("site_key").notNull(),
6979
+ gscProperty: text("gsc_property").notNull(),
6980
+ passedAt: timestamp("passed_at", { withTimezone: true }).notNull().defaultNow(),
6981
+ kind: text("kind").notNull(),
6982
+ headline: text("headline"),
6983
+ windowFrom: date("window_from"),
6984
+ windowTo: date("window_to"),
6985
+ canvasPath: text("canvas_path"),
6986
+ createdBy: text("created_by"),
6987
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6988
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6989
+ },
6990
+ (table) => [
6991
+ index("idx_gsc_opportunity_pass_site_passed").on(
6992
+ table.siteKey,
6993
+ table.passedAt
6994
+ ),
6995
+ index("idx_gsc_opportunity_pass_kind").on(table.siteKey, table.kind)
6996
+ ]
6997
+ );
6998
+ pgTable(
6999
+ "gsc_opportunity_item",
7000
+ {
7001
+ id: uuid("id").primaryKey().defaultRandom(),
7002
+ passId: uuid("pass_id").notNull().references(() => gscOpportunityPass.id, { onDelete: "cascade" }),
7003
+ siteKey: text("site_key").notNull(),
7004
+ url: text("url").notNull(),
7005
+ queryCluster: text("query_cluster"),
7006
+ status: text("status").notNull().default("awaiting"),
7007
+ changeSummary: text("change_summary"),
7008
+ shippedAt: date("shipped_at"),
7009
+ expectEffectAfter: date("expect_effect_after"),
7010
+ sourceUrls: jsonb("source_urls").$type().notNull().default([]),
7011
+ notes: text("notes"),
7012
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
7013
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
7014
+ },
7015
+ (table) => [
7016
+ index("idx_gsc_opportunity_item_site_status").on(
7017
+ table.siteKey,
7018
+ table.status
7019
+ ),
7020
+ index("idx_gsc_opportunity_item_pass").on(table.passId),
7021
+ index("idx_gsc_opportunity_item_url").on(table.siteKey, table.url)
7022
+ ]
7023
+ );
6658
7024
  pgTable(
6659
7025
  "handoff_loop_run",
6660
7026
  {
6661
7027
  id: uuid("id").primaryKey().defaultRandom(),
6662
7028
  runSlug: text("run_slug").notNull(),
7029
+ /**
7030
+ * Skill pack that produced the run. Default is the plan-handoff loop;
7031
+ * quality leaves push mg-strict-truth / mg-seo-strict.
7032
+ */
7033
+ skill: text("skill").notNull().default("mg-plan-handoff-loop"),
6663
7034
  userId: text("user_id"),
6664
7035
  gitRepository: text("git_repository"),
6665
7036
  machine: text("machine"),
@@ -6676,12 +7047,23 @@ pgTable(
6676
7047
  checklistProposed: integer("checklist_proposed").notNull().default(0),
6677
7048
  checklistAccepted: integer("checklist_accepted").notNull().default(0),
6678
7049
  checklistVerified: integer("checklist_verified").notNull().default(0),
7050
+ /** Items resolved with evidence instead of a check (e.g. no browser session). */
7051
+ checklistNa: integer("checklist_na").notNull().default(0),
6679
7052
  inputTokens: bigint("input_tokens", { mode: "number" }).notNull().default(0),
6680
7053
  outputTokens: bigint("output_tokens", { mode: "number" }).notNull().default(0),
6681
7054
  cacheReadTokens: bigint("cache_read_tokens", { mode: "number" }).notNull().default(0),
6682
7055
  totalDurationMs: bigint("total_duration_ms", { mode: "number" }).notNull().default(0),
6683
7056
  retro: text("retro"),
6684
7057
  metrics: jsonb("metrics"),
7058
+ /**
7059
+ * Human end-verdict: good | mixed | bad. The orchestrator asks once at
7060
+ * Phase 4 and the AI Agents table offers a second capture point, so the
7061
+ * review skill can score runs against the user instead of the loop's own
7062
+ * self-report.
7063
+ */
7064
+ userVerdict: text("user_verdict"),
7065
+ userVerdictNote: text("user_verdict_note"),
7066
+ verdictAt: timestamp("verdict_at", { withTimezone: true }),
6685
7067
  reviewed: boolean("reviewed").notNull().default(false),
6686
7068
  reviewNotes: text("review_notes"),
6687
7069
  reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
@@ -6694,8 +7076,10 @@ pgTable(
6694
7076
  uniqueIndex("uq_handoff_loop_run_slug").on(table.runSlug),
6695
7077
  index("idx_handoff_loop_run_user").on(table.userId),
6696
7078
  index("idx_handoff_loop_run_outcome").on(table.outcome),
7079
+ index("idx_handoff_loop_run_skill").on(table.skill),
6697
7080
  index("idx_handoff_loop_run_reviewed").on(table.reviewed),
6698
- index("idx_handoff_loop_run_created").on(table.createdAt)
7081
+ index("idx_handoff_loop_run_created").on(table.createdAt),
7082
+ index("idx_handoff_loop_run_verdict").on(table.userVerdict)
6699
7083
  ]
6700
7084
  );
6701
7085
  var managedSiteSyncMode = pgEnum("managed_site_sync_mode", [
@@ -7052,6 +7436,8 @@ var TRANSIENT_GITHUB_STATUSES = /* @__PURE__ */ new Set([
7052
7436
  524
7053
7437
  ]);
7054
7438
  var DEFAULT_BUDGET_PER_MINUTE = 75;
7439
+ var DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS = 5 * 6e4;
7440
+ var DEFAULT_INTERACTIVE_STALE_MAX_AGE_MS = 30 * 6e4;
7055
7441
  var BUDGET_STATE_KEY = "github_api_budget_per_minute";
7056
7442
  var DEFAULT_BLOCK_MS = 15 * 60 * 1e3;
7057
7443
  var MAX_ATTEMPTS = 4;
@@ -7059,6 +7445,7 @@ var BASE_DELAY_MS = 750;
7059
7445
  var RATE_LIMIT_FLOOR = 50;
7060
7446
  var BUDGET_WAIT_ATTEMPTS = 4;
7061
7447
  var BUDGET_WAIT_CAP_MS = 15e3;
7448
+ var INTERACTIVE_BUDGET_WAIT_CAP_MS = 1e3;
7062
7449
  var GitHubGatewayBlockedError = class extends Error {
7063
7450
  blockedUntil;
7064
7451
  constructor(blockedUntil, message) {
@@ -7095,6 +7482,31 @@ function summarizeGitHubErrorBody(body, maxLen = 240) {
7095
7482
  function isTransientGitHubStatus(status) {
7096
7483
  return TRANSIENT_GITHUB_STATUSES.has(status);
7097
7484
  }
7485
+ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
7486
+ "ECONNRESET",
7487
+ "ECONNREFUSED",
7488
+ "ETIMEDOUT",
7489
+ "ENOTFOUND",
7490
+ "EAI_AGAIN",
7491
+ "ENETUNREACH",
7492
+ "EHOSTUNREACH",
7493
+ "UND_ERR_CONNECT_TIMEOUT",
7494
+ "UND_ERR_HEADERS_TIMEOUT",
7495
+ "UND_ERR_BODY_TIMEOUT",
7496
+ "UND_ERR_SOCKET"
7497
+ ]);
7498
+ function isTransientNetworkError(err) {
7499
+ if (!(err instanceof Error)) return false;
7500
+ const message = err.message.toLowerCase();
7501
+ if (message.includes("fetch failed") || message.includes("socket hang up") || message.includes("network") || message.includes("other side closed")) {
7502
+ return true;
7503
+ }
7504
+ const code = err.code;
7505
+ if (code && TRANSIENT_NETWORK_CODES.has(code)) return true;
7506
+ const cause = err.cause;
7507
+ if (cause && cause !== err) return isTransientNetworkError(cause);
7508
+ return false;
7509
+ }
7098
7510
  function hourBucket(now = /* @__PURE__ */ new Date()) {
7099
7511
  return new Date(
7100
7512
  Date.UTC(
@@ -7111,6 +7523,55 @@ function hourBucket(now = /* @__PURE__ */ new Date()) {
7111
7523
  function cacheKeyFor(url, tokenKey) {
7112
7524
  return createHash("sha256").update(`${tokenKey}:${url}`).digest("hex");
7113
7525
  }
7526
+ function toDate(value) {
7527
+ if (value == null) return null;
7528
+ const date3 = typeof value === "string" ? new Date(value) : value;
7529
+ return Number.isNaN(date3.getTime()) ? null : date3;
7530
+ }
7531
+ function isSoftCacheFresh(input) {
7532
+ const nowMs = input.nowMs ?? Date.now();
7533
+ const expiresAt = toDate(input.expiresAt ?? null);
7534
+ if (expiresAt && expiresAt.getTime() > nowMs) return true;
7535
+ if (input.maxAgeMs == null || input.maxAgeMs <= 0) return false;
7536
+ const updatedAt = toDate(input.updatedAt ?? null);
7537
+ if (!updatedAt) return false;
7538
+ return nowMs - updatedAt.getTime() <= input.maxAgeMs;
7539
+ }
7540
+ function canInteractiveSoftServe(input) {
7541
+ if (isSoftCacheFresh({
7542
+ expiresAt: input.expiresAt,
7543
+ updatedAt: input.updatedAt,
7544
+ maxAgeMs: input.maxAgeMs,
7545
+ nowMs: input.nowMs
7546
+ })) {
7547
+ return true;
7548
+ }
7549
+ const staleMaxAgeMs = input.staleMaxAgeMs ?? DEFAULT_INTERACTIVE_STALE_MAX_AGE_MS;
7550
+ if (staleMaxAgeMs <= 0) return false;
7551
+ return isSoftCacheFresh({
7552
+ expiresAt: null,
7553
+ updatedAt: input.updatedAt,
7554
+ maxAgeMs: staleMaxAgeMs,
7555
+ nowMs: input.nowMs
7556
+ });
7557
+ }
7558
+ function resolveMaxAgeMs(options, priority, useCache) {
7559
+ if (!useCache) return void 0;
7560
+ if (options.maxAgeMs !== void 0) {
7561
+ return options.maxAgeMs > 0 ? options.maxAgeMs : void 0;
7562
+ }
7563
+ if (priority === "interactive") return DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS;
7564
+ return void 0;
7565
+ }
7566
+ function cachedJsonResponse(body, status, cacheHeader) {
7567
+ return new Response(body, {
7568
+ status,
7569
+ headers: {
7570
+ "x-github-gateway-cache": cacheHeader,
7571
+ "content-type": "application/json"
7572
+ }
7573
+ });
7574
+ }
7114
7575
  async function isGithubSyncPaused() {
7115
7576
  try {
7116
7577
  const rows = await getDb().execute(sql`
@@ -7133,9 +7594,9 @@ async function getBlockedUntil(tokenKey) {
7133
7594
  );
7134
7595
  const raw = rows[0]?.blocked_until;
7135
7596
  if (!raw) return null;
7136
- const date2 = typeof raw === "string" ? new Date(raw) : raw;
7137
- if (Number.isNaN(date2.getTime()) || date2.getTime() <= Date.now()) return null;
7138
- return date2;
7597
+ const date3 = typeof raw === "string" ? new Date(raw) : raw;
7598
+ if (Number.isNaN(date3.getTime()) || date3.getTime() <= Date.now()) return null;
7599
+ return date3;
7139
7600
  }
7140
7601
  async function setGithubCircuitBreaker(tokenKey, blockedUntil) {
7141
7602
  await getDb().execute(sql`
@@ -7215,7 +7676,15 @@ async function consumeBudgetOrWait(input) {
7215
7676
  return;
7216
7677
  }
7217
7678
  if (input.priority === "interactive") {
7218
- await sleep(BASE_DELAY_MS);
7679
+ if (attempt >= 2) {
7680
+ await forceConsumeBudget(input.tokenKey);
7681
+ return;
7682
+ }
7683
+ const waitMs2 = Math.min(
7684
+ await msUntilBudgetWindowReset(input.tokenKey),
7685
+ INTERACTIVE_BUDGET_WAIT_CAP_MS
7686
+ );
7687
+ await sleep(waitMs2);
7219
7688
  continue;
7220
7689
  }
7221
7690
  if (attempt === BUDGET_WAIT_ATTEMPTS) {
@@ -7229,40 +7698,76 @@ async function consumeBudgetOrWait(input) {
7229
7698
  }
7230
7699
  throw new GitHubGatewayBudgetError(input.tokenKey);
7231
7700
  }
7701
+ async function forceConsumeBudget(tokenKey) {
7702
+ const updated = await getDb().execute(sql`
7703
+ UPDATE github_api_budget
7704
+ SET request_count = request_count + 1, updated_at = now()
7705
+ WHERE token_key = ${tokenKey}
7706
+ RETURNING request_count
7707
+ `);
7708
+ if (updated[0]) return;
7709
+ await getDb().execute(sql`
7710
+ INSERT INTO github_api_budget (token_key, window_started_at, request_count, updated_at)
7711
+ VALUES (${tokenKey}, now(), 1, now())
7712
+ ON CONFLICT (token_key) DO UPDATE SET
7713
+ request_count = github_api_budget.request_count + 1,
7714
+ updated_at = now()
7715
+ `);
7716
+ }
7232
7717
  async function recordStat(input) {
7233
7718
  const bucket = hourBucket().toISOString();
7234
7719
  await getDb().execute(sql`
7235
7720
  INSERT INTO github_api_stat (
7236
- token_key, job_id, hour_bucket, request_count, error_count, cache_hit_count, updated_at
7721
+ token_key, job_id, hour_bucket, request_count, error_count,
7722
+ cache_hit_count, breaker_hit_count, updated_at
7237
7723
  )
7238
7724
  VALUES (
7239
7725
  ${input.tokenKey},
7240
7726
  ${input.jobId},
7241
7727
  ${bucket}::timestamptz,
7242
- 1,
7728
+ ${input.breakerHit ? 0 : 1},
7243
7729
  ${input.error ? 1 : 0},
7244
7730
  ${input.cacheHit ? 1 : 0},
7731
+ ${input.breakerHit ? 1 : 0},
7245
7732
  now()
7246
7733
  )
7247
7734
  ON CONFLICT (token_key, job_id, hour_bucket) DO UPDATE SET
7248
- request_count = github_api_stat.request_count + 1,
7735
+ request_count = github_api_stat.request_count + ${input.breakerHit ? 0 : 1},
7249
7736
  error_count = github_api_stat.error_count + ${input.error ? 1 : 0},
7250
7737
  cache_hit_count = github_api_stat.cache_hit_count + ${input.cacheHit ? 1 : 0},
7738
+ breaker_hit_count = github_api_stat.breaker_hit_count + ${input.breakerHit ? 1 : 0},
7251
7739
  updated_at = now()
7252
7740
  `);
7253
7741
  }
7742
+ async function recordBreakerHit(input) {
7743
+ await recordStat({
7744
+ tokenKey: input.tokenKey,
7745
+ jobId: input.jobId,
7746
+ breakerHit: true
7747
+ });
7748
+ }
7254
7749
  async function loadEtagCache(key) {
7255
7750
  const rows = await getDb().execute(sql`
7256
- SELECT etag, body, status FROM github_api_cache
7751
+ SELECT etag, body, status, expires_at, updated_at
7752
+ FROM github_api_cache
7257
7753
  WHERE cache_key = ${key}
7258
7754
  LIMIT 1
7259
7755
  `);
7260
- return rows[0] ?? null;
7756
+ const row = rows[0];
7757
+ if (!row) return null;
7758
+ return {
7759
+ etag: row.etag,
7760
+ body: row.body,
7761
+ status: row.status,
7762
+ expiresAt: row.expires_at,
7763
+ updatedAt: row.updated_at
7764
+ };
7261
7765
  }
7262
7766
  async function saveEtagCache(input) {
7767
+ const expiresAt = input.maxAgeMs != null && input.maxAgeMs > 0 ? new Date(Date.now() + input.maxAgeMs).toISOString() : null;
7263
7768
  await getDb().execute(sql`
7264
7769
  INSERT INTO github_api_cache (
7265
- cache_key, etag, body, status, content_type, updated_at
7770
+ cache_key, etag, body, status, content_type, expires_at, updated_at
7266
7771
  )
7267
7772
  VALUES (
7268
7773
  ${input.key},
@@ -7270,6 +7775,7 @@ async function saveEtagCache(input) {
7270
7775
  ${input.body},
7271
7776
  ${input.status},
7272
7777
  ${input.contentType},
7778
+ ${expiresAt}::timestamptz,
7273
7779
  now()
7274
7780
  )
7275
7781
  ON CONFLICT (cache_key) DO UPDATE SET
@@ -7277,6 +7783,7 @@ async function saveEtagCache(input) {
7277
7783
  body = EXCLUDED.body,
7278
7784
  status = EXCLUDED.status,
7279
7785
  content_type = EXCLUDED.content_type,
7786
+ expires_at = EXCLUDED.expires_at,
7280
7787
  updated_at = now()
7281
7788
  `);
7282
7789
  }
@@ -7319,21 +7826,65 @@ function tripBreakerFromResponse(response) {
7319
7826
  }
7320
7827
  async function githubGatewayFetch(url, init, options) {
7321
7828
  const priority = options.priority ?? "background";
7322
- const budgetPerMinute = await resolveGithubBudgetPerMinute(
7323
- options.budgetPerMinute
7324
- );
7325
7829
  const method = (init.method ?? "GET").toUpperCase();
7326
7830
  const useCache = options.useEtagCache !== false && method === "GET";
7831
+ const maxAgeMs = resolveMaxAgeMs(options, priority, useCache);
7327
7832
  if (priority !== "interactive" && await isGithubSyncPaused()) {
7328
7833
  throw new GitHubGatewayPausedError();
7329
7834
  }
7835
+ const key = useCache ? cacheKeyFor(url, options.tokenKey) : null;
7836
+ if (key && !options.bypassSoftCache) {
7837
+ const soft = await loadEtagCache(key);
7838
+ const softOk = soft?.body != null && (priority === "interactive" ? canInteractiveSoftServe({
7839
+ expiresAt: soft.expiresAt,
7840
+ updatedAt: soft.updatedAt,
7841
+ maxAgeMs
7842
+ }) : isSoftCacheFresh({
7843
+ expiresAt: soft.expiresAt,
7844
+ updatedAt: soft.updatedAt,
7845
+ maxAgeMs
7846
+ }));
7847
+ if (softOk && soft?.body != null) {
7848
+ const isFresh = isSoftCacheFresh({
7849
+ expiresAt: soft.expiresAt,
7850
+ updatedAt: soft.updatedAt,
7851
+ maxAgeMs
7852
+ });
7853
+ await recordStat({
7854
+ tokenKey: options.tokenKey,
7855
+ jobId: options.jobId,
7856
+ cacheHit: true
7857
+ });
7858
+ if (priority === "interactive" && !isFresh) {
7859
+ void githubGatewayFetch(url, init, {
7860
+ ...options,
7861
+ bypassSoftCache: true,
7862
+ priority: "background",
7863
+ maxAgeMs: maxAgeMs ?? DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS
7864
+ }).catch(() => {
7865
+ });
7866
+ }
7867
+ return cachedJsonResponse(soft.body, soft.status ?? 200, "soft");
7868
+ }
7869
+ }
7870
+ const budgetPerMinute = await resolveGithubBudgetPerMinute(
7871
+ options.budgetPerMinute
7872
+ );
7330
7873
  const blocked = await getBlockedUntil(options.tokenKey);
7331
7874
  if (blocked) {
7332
7875
  if (priority === "interactive") {
7333
7876
  if (blocked.getTime() - Date.now() > 6e4) {
7877
+ await recordBreakerHit({
7878
+ tokenKey: options.tokenKey,
7879
+ jobId: options.jobId
7880
+ });
7334
7881
  throw new GitHubGatewayBlockedError(blocked);
7335
7882
  }
7336
7883
  } else {
7884
+ await recordBreakerHit({
7885
+ tokenKey: options.tokenKey,
7886
+ jobId: options.jobId
7887
+ });
7337
7888
  throw new GitHubGatewayBlockedError(blocked);
7338
7889
  }
7339
7890
  }
@@ -7352,14 +7903,32 @@ async function githubGatewayFetch(url, init, options) {
7352
7903
  if (!headers.has("X-GitHub-Api-Version")) {
7353
7904
  headers.set("X-GitHub-Api-Version", "2022-11-28");
7354
7905
  }
7355
- const key = useCache ? cacheKeyFor(url, options.tokenKey) : null;
7356
7906
  if (key) {
7357
7907
  const cached = await loadEtagCache(key);
7358
7908
  if (cached?.etag) headers.set("If-None-Match", cached.etag);
7359
7909
  }
7360
7910
  let lastError = null;
7361
7911
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
7362
- const response = await fetch(url, { ...init, headers });
7912
+ let response;
7913
+ try {
7914
+ response = await fetch(url, {
7915
+ ...init,
7916
+ headers,
7917
+ signal: init.signal ?? AbortSignal.timeout(2e4)
7918
+ });
7919
+ } catch (err) {
7920
+ lastError = err instanceof Error ? err : new Error(String(err));
7921
+ await recordStat({
7922
+ tokenKey: options.tokenKey,
7923
+ jobId: options.jobId,
7924
+ error: true
7925
+ });
7926
+ if (!isTransientNetworkError(err) || attempt === MAX_ATTEMPTS) {
7927
+ throw lastError;
7928
+ }
7929
+ await sleep(BASE_DELAY_MS * 2 ** (attempt - 1));
7930
+ continue;
7931
+ }
7363
7932
  await updateRateLimitHeaders(options.tokenKey, response);
7364
7933
  if (response.status === 304 && key) {
7365
7934
  const cached = await loadEtagCache(key);
@@ -7368,13 +7937,11 @@ async function githubGatewayFetch(url, init, options) {
7368
7937
  jobId: options.jobId,
7369
7938
  cacheHit: true
7370
7939
  });
7371
- return new Response(cached?.body ?? "", {
7372
- status: cached?.status ?? 200,
7373
- headers: {
7374
- "x-github-gateway-cache": "hit",
7375
- "content-type": "application/json"
7376
- }
7377
- });
7940
+ return cachedJsonResponse(
7941
+ cached?.body ?? "",
7942
+ cached?.status ?? 200,
7943
+ "hit"
7944
+ );
7378
7945
  }
7379
7946
  if (response.status === 403 || response.status === 429 || isTransientGitHubStatus(response.status)) {
7380
7947
  const bodyText = summarizeGitHubErrorBody(
@@ -7393,6 +7960,10 @@ async function githubGatewayFetch(url, init, options) {
7393
7960
  error: true
7394
7961
  });
7395
7962
  if (attempt === MAX_ATTEMPTS || secondary) {
7963
+ await recordBreakerHit({
7964
+ tokenKey: options.tokenKey,
7965
+ jobId: options.jobId
7966
+ });
7396
7967
  throw new GitHubGatewayBlockedError(until, lastError.message);
7397
7968
  }
7398
7969
  const retryAfter = Number(response.headers.get("retry-after"));
@@ -7409,7 +7980,8 @@ async function githubGatewayFetch(url, init, options) {
7409
7980
  etag,
7410
7981
  body,
7411
7982
  status: response.status,
7412
- contentType: response.headers.get("content-type")
7983
+ contentType: response.headers.get("content-type"),
7984
+ maxAgeMs
7413
7985
  });
7414
7986
  }
7415
7987
  await recordStat({
@@ -8840,7 +9412,7 @@ function getEncryptionKey() {
8840
9412
  throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
8841
9413
  return buf;
8842
9414
  }
8843
- function encrypt(text19) {
9415
+ function encrypt(text22) {
8844
9416
  const key = getEncryptionKey();
8845
9417
  const iv = randomBytes(ENC_IV_LENGTH);
8846
9418
  const cipher = createCipheriv(
@@ -8848,7 +9420,7 @@ function encrypt(text19) {
8848
9420
  new Uint8Array(key),
8849
9421
  new Uint8Array(iv)
8850
9422
  );
8851
- let encrypted = cipher.update(text19, "utf8", "hex");
9423
+ let encrypted = cipher.update(text22, "utf8", "hex");
8852
9424
  encrypted += cipher.final("hex");
8853
9425
  const authTag = cipher.getAuthTag();
8854
9426
  return Buffer.concat([
@@ -9568,10 +10140,10 @@ async function r2GetObjectRange(bucket, key, range) {
9568
10140
  const body = result.Body;
9569
10141
  if (!body?.transformToString)
9570
10142
  throw new Error("R2 returned no readable body");
9571
- const text19 = await body.transformToString();
10143
+ const text22 = await body.transformToString();
9572
10144
  const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
9573
10145
  return `${header}
9574
- ${text19}`;
10146
+ ${text22}`;
9575
10147
  } catch (e) {
9576
10148
  throw r2WrapError(bucket, key, e);
9577
10149
  }
@@ -9934,15 +10506,15 @@ async function sftpRead(opts, filePath, proxy, options) {
9934
10506
  clearTimeout(timer);
9935
10507
  cleanup?.();
9936
10508
  cleanup = void 0;
9937
- const text19 = Buffer.concat(
10509
+ const text22 = Buffer.concat(
9938
10510
  chunks.map((ch) => new Uint8Array(ch))
9939
10511
  ).toString("utf-8");
9940
10512
  if (!isWholeFileRequest) {
9941
10513
  const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
9942
10514
  resolve(`${header}
9943
- ${text19}`);
10515
+ ${text22}`);
9944
10516
  } else {
9945
- resolve(text19);
10517
+ resolve(text22);
9946
10518
  }
9947
10519
  });
9948
10520
  rs.on("error", (e) => {
@@ -9990,7 +10562,9 @@ var RAW_JSON_TOOLS = /* @__PURE__ */ new Set([
9990
10562
  "managed-site",
9991
10563
  "content-sync",
9992
10564
  // Skill registry metadata only (no file bodies) — must stay valid JSON.
9993
- "cursor-skill"
10565
+ "cursor-skill",
10566
+ // GSC opportunity store (pass + items)
10567
+ "gsc-opportunity"
9994
10568
  ]);
9995
10569
  var TOOL_CACHE_TTL_MS = {
9996
10570
  "list-servers": 6e4,
@@ -10091,11 +10665,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
10091
10665
  if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
10092
10666
  return e.names;
10093
10667
  }
10094
- function truncateForLLM(text19, maxBytes) {
10095
- const totalBytes = Buffer.byteLength(text19, "utf8");
10668
+ function truncateForLLM(text22, maxBytes) {
10669
+ const totalBytes = Buffer.byteLength(text22, "utf8");
10096
10670
  if (totalBytes <= maxBytes)
10097
- return { text: text19, truncated: false, totalBytes, shownBytes: totalBytes };
10098
- const buf = Buffer.from(text19, "utf8");
10671
+ return { text: text22, truncated: false, totalBytes, shownBytes: totalBytes };
10672
+ const buf = Buffer.from(text22, "utf8");
10099
10673
  let cut = maxBytes;
10100
10674
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
10101
10675
  const head = buf.subarray(0, cut).toString("utf8");
@@ -10126,10 +10700,10 @@ function postprocessResult(result, meta) {
10126
10700
  if (!result.content?.length) return result;
10127
10701
  if (RAW_JSON_TOOLS.has(meta.toolName)) return result;
10128
10702
  const block = result.content[0];
10129
- let text19 = String(block.text ?? "");
10130
- const trunc = truncateForLLM(text19, RESPONSE_MAX_BYTES);
10703
+ let text22 = String(block.text ?? "");
10704
+ const trunc = truncateForLLM(text22, RESPONSE_MAX_BYTES);
10131
10705
  if (trunc.truncated) {
10132
- text19 = trunc.text + "\n\n... " + buildTruncationHint(
10706
+ text22 = trunc.text + "\n\n... " + buildTruncationHint(
10133
10707
  meta.toolName,
10134
10708
  meta.args,
10135
10709
  trunc.totalBytes,
@@ -10143,11 +10717,11 @@ function postprocessResult(result, meta) {
10143
10717
  const parts = [`took ${tookStr}`, sizeStr];
10144
10718
  if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
10145
10719
  if (meta.cached) parts.push("cached");
10146
- text19 = `${text19}
10720
+ text22 = `${text22}
10147
10721
 
10148
10722
  [${parts.join(", ")}]`;
10149
10723
  }
10150
- return { ...result, content: [{ ...block, text: text19 }] };
10724
+ return { ...result, content: [{ ...block, text: text22 }] };
10151
10725
  }
10152
10726
  function buildPipelineScript(commands, shell, marker, stopOnError) {
10153
10727
  if (shell === "powershell") {
@@ -10933,11 +11507,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
10933
11507
  applied_by TEXT
10934
11508
  );
10935
11509
  `.trim();
10936
- function normaliseMigrationSql(sql29) {
10937
- return sql29.replace(/\r\n/g, "\n").trim() + "\n";
11510
+ function normaliseMigrationSql(sql31) {
11511
+ return sql31.replace(/\r\n/g, "\n").trim() + "\n";
10938
11512
  }
10939
- function migrationSha256(sql29) {
10940
- return createHash("sha256").update(sql29.replace(/\r\n/g, "\n"), "utf8").digest("hex");
11513
+ function migrationSha256(sql31) {
11514
+ return createHash("sha256").update(sql31.replace(/\r\n/g, "\n"), "utf8").digest("hex");
10941
11515
  }
10942
11516
  function dollarQuoteTag(value) {
10943
11517
  let tag = "_mcp";
@@ -12803,6 +13377,87 @@ var TOOLS = [
12803
13377
  }
12804
13378
  },
12805
13379
  // ----- Shared Cursor skills (metadata only; install via downloads ZIP) -----
13380
+ {
13381
+ name: "gsc-opportunity",
13382
+ description: 'GSC opportunity store (shared SoT across MG-managed sites). Pick `action`:\n- "list": open items/passes for a siteKey (filter by status/statuses/kind). Default excludes closed.\n- "upsert": create/update a pass + optional items (url, queryCluster, status, expectEffectAfter, sourceUrls).\n- "close": mark an item closed by id or siteKey+url(+queryCluster).\nUse before/after gsc-opportunities skill passes instead of growing markdown logs. On CTR/policy polish upserts, include sourceUrls (official claim URLs).',
13383
+ inputSchema: {
13384
+ type: "object",
13385
+ properties: {
13386
+ action: {
13387
+ type: "string",
13388
+ enum: ["list", "upsert", "close"],
13389
+ description: "list | upsert | close"
13390
+ },
13391
+ siteKey: {
13392
+ type: "string",
13393
+ description: "Stable site id, e.g. solarfast (required for all actions)."
13394
+ },
13395
+ status: {
13396
+ type: "string",
13397
+ enum: ["awaiting", "monitor", "pending_reindex", "skipped", "closed"],
13398
+ description: "action=list: single status filter."
13399
+ },
13400
+ statuses: {
13401
+ type: "array",
13402
+ items: {
13403
+ type: "string",
13404
+ enum: ["awaiting", "monitor", "pending_reindex", "skipped", "closed"]
13405
+ },
13406
+ description: "action=list: multi status filter."
13407
+ },
13408
+ kind: {
13409
+ type: "string",
13410
+ enum: ["analysis", "ctr_polish", "reindex", "cls", "other"],
13411
+ description: "action=list/upsert: pass kind."
13412
+ },
13413
+ includeClosed: {
13414
+ type: "boolean",
13415
+ description: "action=list: include closed items (default false)."
13416
+ },
13417
+ limit: { type: "number", description: "action=list: max items (default 50)." },
13418
+ offset: { type: "number", description: "action=list: pagination offset." },
13419
+ gscProperty: {
13420
+ type: "string",
13421
+ description: "action=upsert: e.g. sc-domain:solarfast.nl"
13422
+ },
13423
+ passId: {
13424
+ type: "string",
13425
+ description: "action=upsert: update existing pass uuid."
13426
+ },
13427
+ headline: { type: "string", description: "action=upsert: pass headline." },
13428
+ windowFrom: { type: "string", description: "action=upsert: YYYY-MM-DD." },
13429
+ windowTo: { type: "string", description: "action=upsert: YYYY-MM-DD." },
13430
+ canvasPath: { type: "string", description: "action=upsert: optional canvas path." },
13431
+ createdBy: { type: "string", description: "action=upsert: agent/user label." },
13432
+ passedAt: {
13433
+ type: "string",
13434
+ description: "action=upsert: ISO datetime for the pass."
13435
+ },
13436
+ items: {
13437
+ type: "array",
13438
+ description: "action=upsert: items [{url, queryCluster?, status?, changeSummary?, shippedAt?, expectEffectAfter?, sourceUrls?, notes?, id?}]",
13439
+ items: { type: "object" }
13440
+ },
13441
+ id: {
13442
+ type: "string",
13443
+ description: "action=close: item uuid (preferred)."
13444
+ },
13445
+ url: {
13446
+ type: "string",
13447
+ description: "action=close: item url when id omitted."
13448
+ },
13449
+ queryCluster: {
13450
+ type: "string",
13451
+ description: "action=close: optional cluster disambiguator."
13452
+ },
13453
+ notes: {
13454
+ type: "string",
13455
+ description: "action=close/upsert item notes."
13456
+ }
13457
+ },
13458
+ required: ["action", "siteKey"]
13459
+ }
13460
+ },
12806
13461
  {
12807
13462
  name: "cursor-skill",
12808
13463
  description: 'Shared Cursor skill registry (metadata only \u2014 never returns file bodies; MCP truncates large text). Actions: list (slugs+versions), status (localVersion vs tip), pull (install pointer only). To install bytes: GET https://dashboard.mgsoftware.nl/api/downloads/versions.txt (skills/<slug>.zip=N) then GET \u2026/api/downloads/skills/<slug>.zip and expand into .cursor/skills/<slug>/. Publish from mg-dashboard: bun scripts/skills/pack-and-publish.ts --slug <slug> --message "\u2026". No push via this tool.',
@@ -12833,7 +13488,7 @@ var TOOLS = [
12833
13488
  // ----- Trigger.dev -----
12834
13489
  ...TRIGGER_TOOLS
12835
13490
  ];
12836
- var MCP_VERSION = "7.4.13";
13491
+ var MCP_VERSION = "7.4.14";
12837
13492
  async function handleListTools() {
12838
13493
  if (!authContext) return { tools: TOOLS };
12839
13494
  const allowedTools = authContext.allowedTools;
@@ -13050,6 +13705,39 @@ async function executeToolCall(name, a, _serverId) {
13050
13705
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
13051
13706
  };
13052
13707
  }
13708
+ case "gsc-opportunity": {
13709
+ const action = typeof a.action === "string" ? a.action.trim() : "";
13710
+ if (!action) {
13711
+ return {
13712
+ content: [{ type: "text", text: "Error: action is required" }]
13713
+ };
13714
+ }
13715
+ const { action: _action, ...params } = a;
13716
+ void _action;
13717
+ const res = await fetch(`${dashboardBaseUrl}/api/tools/gsc-opportunity`, {
13718
+ method: "POST",
13719
+ headers: {
13720
+ "content-type": "application/json",
13721
+ authorization: `Bearer ${apiKey}`
13722
+ },
13723
+ body: JSON.stringify({ action, params })
13724
+ });
13725
+ if (!res.ok) {
13726
+ const detail = await res.text().catch(() => "");
13727
+ return {
13728
+ content: [
13729
+ {
13730
+ type: "text",
13731
+ text: `Error: gsc-opportunity failed (${res.status}). ${detail.slice(0, 400)}`
13732
+ }
13733
+ ]
13734
+ };
13735
+ }
13736
+ const data = await res.json();
13737
+ return {
13738
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
13739
+ };
13740
+ }
13053
13741
  // ----- Team memory -----
13054
13742
  case "search-team-memory": {
13055
13743
  const query = typeof a.query === "string" ? a.query.trim() : "";
@@ -13091,7 +13779,7 @@ async function executeToolCall(name, a, _serverId) {
13091
13779
  ]
13092
13780
  };
13093
13781
  }
13094
- const lines = data.hits.map((hit, index16) => {
13782
+ const lines = data.hits.map((hit, index19) => {
13095
13783
  const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
13096
13784
  const repo = hit.repo ?? "unknown repo";
13097
13785
  const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
@@ -13101,7 +13789,7 @@ async function executeToolCall(name, a, _serverId) {
13101
13789
  ...Array.isArray(hit.tech) ? hit.tech.slice(0, 4) : []
13102
13790
  ].filter(Boolean);
13103
13791
  const tags = facets.length > 0 ? ` \xB7 ${facets.join(", ")}` : "";
13104
- return `${index16 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
13792
+ return `${index19 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
13105
13793
  id: ${hit.id}
13106
13794
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
13107
13795
  });
@@ -13308,12 +13996,12 @@ Searchable via search-team-memory once indexing completes (usually immediate).`
13308
13996
  ]
13309
13997
  };
13310
13998
  }
13311
- const lines = data.hits.map((hit, index16) => {
13999
+ const lines = data.hits.map((hit, index19) => {
13312
14000
  const sim = hit.similarity !== null ? ` \xB7 ${Math.round(hit.similarity * 100)}% match` : "";
13313
14001
  const tags = hit.tags.length ? ` \xB7 ${hit.tags.slice(0, 5).join(", ")}` : "";
13314
14002
  const sources = hit.sources.length ? `
13315
14003
  sources: ${hit.sources.slice(0, 6).map((s) => `${s.type}:${s.ref}`).join(", ")}` : "";
13316
- return `${index16 + 1}. [${hit.status} \xB7 updated ${hit.pageUpdatedOn ?? "unknown"}${sim}${tags}] ${hit.title}
14004
+ return `${index19 + 1}. [${hit.status} \xB7 updated ${hit.pageUpdatedOn ?? "unknown"}${sim}${tags}] ${hit.title}
13317
14005
  slug: ${hit.slug}${sources}
13318
14006
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 400)}`;
13319
14007
  });
@@ -13370,8 +14058,8 @@ Call get-wiki-page with a slug for the full page; follow its sources (team-memor
13370
14058
  const formatSource = (s) => {
13371
14059
  const base = `${s.type}:${s.ref}`;
13372
14060
  if (s.title) {
13373
- const date2 = s.date ? ` (${s.date})` : "";
13374
- return `${base} \u2014 "${s.title}"${date2}`;
14061
+ const date3 = s.date ? ` (${s.date})` : "";
14062
+ return `${base} \u2014 "${s.title}"${date3}`;
13375
14063
  }
13376
14064
  return base;
13377
14065
  };
@@ -14570,8 +15258,8 @@ ${sample.join("\n")}${files.length > 5 ? `
14570
15258
  };
14571
15259
  const filtered = sortRows(applyFilter(only.rows));
14572
15260
  if (format === "json") {
14573
- const text20 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
14574
- return { content: [{ type: "text", text: text20 }] };
15261
+ const text23 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
15262
+ return { content: [{ type: "text", text: text23 }] };
14575
15263
  }
14576
15264
  if (groupByProject) {
14577
15265
  const groups = /* @__PURE__ */ new Map();
@@ -14598,8 +15286,8 @@ ${sample.join("\n")}${files.length > 5 ? `
14598
15286
  }
14599
15287
  const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
14600
15288
  const body = filtered.map((r) => `${fmtRow(r)} ${r.Project || ""}`);
14601
- const text19 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
14602
- return { content: [{ type: "text", text: text19 }] };
15289
+ const text22 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
15290
+ return { content: [{ type: "text", text: text22 }] };
14603
15291
  }
14604
15292
  if (format === "json") {
14605
15293
  const lines = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgsoftwarebv/mg-dashboard-mcp",
3
- "version": "7.4.13",
3
+ "version": "7.4.14",
4
4
  "description": "MCP Server for MG Dashboard - SSH, SFTP, Docker, domains, DNS, and environment config tools for Cursor",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",