@mgsoftwarebv/mg-dashboard-mcp 7.4.13 → 7.4.15

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 +915 -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 {
@@ -1116,6 +1136,128 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
1116
1136
  }]
1117
1137
  };
1118
1138
  }
1139
+
1140
+ // src/github-code-access-tools.ts
1141
+ var GITHUB_CODE_ACCESS_TOOLS = [
1142
+ {
1143
+ name: "get_mg_dashboard_file",
1144
+ description: "Read ONE source file at a git ref for an allowlisted GitHub org/repo (CODE_BATTLE_GITHUB_ORGS, default MGSoftwareBV). Use after get_mg_dashboard_commits when commit subjects are not enough to judge exists / polish / new. Returns metadata + text content (secrets redacted). Credential paths (.env, keys) are denied. Binary files return { binary: true } with no payload. Full diffs are never attached to get_mg_dashboard_commits \u2014 use get_mg_dashboard_diff for a compact patch.",
1145
+ inputSchema: {
1146
+ type: "object",
1147
+ properties: {
1148
+ repository: {
1149
+ type: "string",
1150
+ description: "owner/repo, e.g. MGSoftwareBV/mg-dashboard. Org must be allowlisted."
1151
+ },
1152
+ ref: {
1153
+ type: "string",
1154
+ description: "Commit sha, branch, or tag."
1155
+ },
1156
+ path: {
1157
+ type: "string",
1158
+ description: "File path inside the repo (not a directory)."
1159
+ }
1160
+ },
1161
+ required: ["repository", "ref", "path"]
1162
+ }
1163
+ },
1164
+ {
1165
+ name: "get_mg_dashboard_diff",
1166
+ description: "Fetch a COMPACT unified diff for one commit sha XOR one pull request number, same org allowlist as get_mg_dashboard_file. Returns per-file path/status/+/- and a truncated patch (secrets redacted). Binary files are omitted. Does not change get_mg_dashboard_commits (metadata-only by design).",
1167
+ inputSchema: {
1168
+ type: "object",
1169
+ properties: {
1170
+ repository: {
1171
+ type: "string",
1172
+ description: "owner/repo, e.g. MGSoftwareBV/mg-dashboard. Org must be allowlisted."
1173
+ },
1174
+ sha: {
1175
+ type: "string",
1176
+ description: "Commit sha. Provide exactly one of sha or pullNumber."
1177
+ },
1178
+ pullNumber: {
1179
+ type: "number",
1180
+ description: "Pull request number. Provide exactly one of sha or pullNumber."
1181
+ }
1182
+ },
1183
+ required: ["repository"]
1184
+ }
1185
+ }
1186
+ ];
1187
+ async function handleGithubCodeAccessTool(name, args2, ctx) {
1188
+ if (name === "get_mg_dashboard_file") {
1189
+ const repository = typeof args2.repository === "string" ? args2.repository.trim() : "";
1190
+ const ref = typeof args2.ref === "string" ? args2.ref.trim() : "";
1191
+ const path = typeof args2.path === "string" ? args2.path.trim() : "";
1192
+ if (!repository || !ref || !path) {
1193
+ return {
1194
+ content: [
1195
+ {
1196
+ type: "text",
1197
+ text: "Error: repository, ref and path are required"
1198
+ }
1199
+ ]
1200
+ };
1201
+ }
1202
+ return proxyJson(ctx, "/api/activity/file", { repository, ref, path });
1203
+ }
1204
+ if (name === "get_mg_dashboard_diff") {
1205
+ const repository = typeof args2.repository === "string" ? args2.repository.trim() : "";
1206
+ if (!repository) {
1207
+ return {
1208
+ content: [
1209
+ { type: "text", text: "Error: repository is required" }
1210
+ ]
1211
+ };
1212
+ }
1213
+ const sha = typeof args2.sha === "string" ? args2.sha.trim() : void 0;
1214
+ const pullNumber = typeof args2.pullNumber === "number" ? args2.pullNumber : void 0;
1215
+ const hasSha = Boolean(sha);
1216
+ const hasPr = pullNumber != null;
1217
+ if (hasSha === hasPr) {
1218
+ return {
1219
+ content: [
1220
+ {
1221
+ type: "text",
1222
+ text: "Error: provide exactly one of sha or pullNumber"
1223
+ }
1224
+ ]
1225
+ };
1226
+ }
1227
+ return proxyJson(ctx, "/api/activity/diff", {
1228
+ repository,
1229
+ ...hasSha ? { sha } : { pullNumber }
1230
+ });
1231
+ }
1232
+ return {
1233
+ content: [{ type: "text", text: `Error: unknown tool ${name}` }]
1234
+ };
1235
+ }
1236
+ async function proxyJson(ctx, route, body) {
1237
+ const res = await fetch(`${ctx.dashboardBaseUrl}${route}`, {
1238
+ method: "POST",
1239
+ headers: {
1240
+ "content-type": "application/json",
1241
+ authorization: `Bearer ${ctx.apiKey}`
1242
+ },
1243
+ body: JSON.stringify(body)
1244
+ });
1245
+ if (!res.ok) {
1246
+ const detail = await res.text().catch(() => "");
1247
+ return {
1248
+ content: [
1249
+ {
1250
+ type: "text",
1251
+ text: `Error: mg-dashboard ${route} failed (${res.status}). ${detail.slice(0, 300)}`
1252
+ }
1253
+ ]
1254
+ };
1255
+ }
1256
+ const data = await res.json();
1257
+ return {
1258
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1259
+ };
1260
+ }
1119
1261
  var ALGORITHM = "aes-256-gcm";
1120
1262
  var IV_LENGTH = 16;
1121
1263
  var AUTH_TAG_LENGTH = 16;
@@ -3793,10 +3935,10 @@ var ZodObject = class _ZodObject extends ZodType {
3793
3935
  // }) as any;
3794
3936
  // return merged;
3795
3937
  // }
3796
- catchall(index16) {
3938
+ catchall(index19) {
3797
3939
  return new _ZodObject({
3798
3940
  ...this._def,
3799
- catchall: index16
3941
+ catchall: index19
3800
3942
  });
3801
3943
  }
3802
3944
  pick(mask) {
@@ -4114,9 +4256,9 @@ function mergeValues(a, b) {
4114
4256
  return { valid: false };
4115
4257
  }
4116
4258
  const newArray = [];
4117
- for (let index16 = 0; index16 < a.length; index16++) {
4118
- const itemA = a[index16];
4119
- const itemB = b[index16];
4259
+ for (let index19 = 0; index19 < a.length; index19++) {
4260
+ const itemA = a[index19];
4261
+ const itemB = b[index19];
4120
4262
  const sharedValue = mergeValues(itemA, itemB);
4121
4263
  if (!sharedValue.valid) {
4122
4264
  return { valid: false };
@@ -4322,10 +4464,10 @@ var ZodMap = class extends ZodType {
4322
4464
  }
4323
4465
  const keyType = this._def.keyType;
4324
4466
  const valueType = this._def.valueType;
4325
- const pairs = [...ctx.data.entries()].map(([key, value], index16) => {
4467
+ const pairs = [...ctx.data.entries()].map(([key, value], index19) => {
4326
4468
  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"]))
4469
+ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index19, "key"])),
4470
+ value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index19, "value"]))
4329
4471
  };
4330
4472
  });
4331
4473
  if (ctx.common.async) {
@@ -5340,13 +5482,19 @@ pgTable(
5340
5482
  verifyNote: text("verify_note"),
5341
5483
  dirty: boolean("dirty").notNull().default(false),
5342
5484
  vaultSyncedAt: timestamp("vault_synced_at", { withTimezone: true }),
5485
+ /** Reviewer / itemized-phase lease holder. */
5486
+ claimedBy: text("claimed_by"),
5487
+ claimedAt: timestamp("claimed_at", { withTimezone: true }),
5488
+ /** Expired leases are reclaimable by any runner. */
5489
+ leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
5343
5490
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5344
5491
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5345
5492
  },
5346
5493
  (table) => [
5347
5494
  index("idx_wiki_page_status").on(table.status),
5348
5495
  index("idx_wiki_page_last_verified").on(table.lastVerifiedOn),
5349
- index("idx_wiki_page_dirty").on(table.dirty)
5496
+ index("idx_wiki_page_dirty").on(table.dirty),
5497
+ index("idx_wiki_page_lease_expires").on(table.leaseExpiresAt).where(sql`${table.leaseExpiresAt} IS NOT NULL`)
5350
5498
  ]
5351
5499
  );
5352
5500
  pgTable(
@@ -5391,11 +5539,9 @@ pgTable(
5391
5539
  resolvedAt: timestamp("resolved_at", { withTimezone: true })
5392
5540
  },
5393
5541
  (table) => [
5394
- uniqueIndex("wiki_page_evidence_page_slug_evidence_type_evidence_ref_key").on(
5395
- table.pageSlug,
5396
- table.evidenceType,
5397
- table.evidenceRef
5398
- ),
5542
+ uniqueIndex(
5543
+ "wiki_page_evidence_page_slug_evidence_type_evidence_ref_key"
5544
+ ).on(table.pageSlug, table.evidenceType, table.evidenceRef),
5399
5545
  index("idx_wiki_page_evidence_created").on(table.createdAt)
5400
5546
  ]
5401
5547
  );
@@ -5450,6 +5596,65 @@ pgTable(
5450
5596
  )
5451
5597
  ]
5452
5598
  );
5599
+ pgTable(
5600
+ "wiki_invariant_violation",
5601
+ {
5602
+ id: uuid("id").primaryKey().defaultRandom(),
5603
+ slug: text("slug").notNull(),
5604
+ code: text("code").notNull(),
5605
+ severity: text("severity").notNull().default("warning"),
5606
+ detail: text("detail").notNull().default(""),
5607
+ suggestedFix: text("suggested_fix"),
5608
+ suggestedSlug: text("suggested_slug"),
5609
+ occurrenceCount: integer("occurrence_count").notNull().default(1),
5610
+ reopenCount: integer("reopen_count").notNull().default(0),
5611
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
5612
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5613
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
5614
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5615
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5616
+ },
5617
+ (table) => [
5618
+ uniqueIndex("wiki_invariant_violation_slug_code_key").on(
5619
+ table.slug,
5620
+ table.code
5621
+ ),
5622
+ index("idx_wiki_invariant_violation_severity").on(
5623
+ table.severity,
5624
+ table.resolvedAt
5625
+ )
5626
+ ]
5627
+ );
5628
+ pgTable(
5629
+ "wiki_pipeline_rule",
5630
+ {
5631
+ id: uuid("id").primaryKey().defaultRandom(),
5632
+ scope: text("scope").notNull(),
5633
+ ruleKey: text("rule_key").notNull(),
5634
+ text: text("text").notNull(),
5635
+ /** A = deterministic auto-applied, B = judgement, gated by insight accept. */
5636
+ tier: text("tier").notNull().default("B"),
5637
+ status: text("status").notNull().default("proposed"),
5638
+ sourceCode: text("source_code"),
5639
+ evidenceRefs: text("evidence_refs").array().notNull().default([]),
5640
+ violationCount: integer("violation_count").notNull().default(0),
5641
+ lastViolationAt: timestamp("last_violation_at", { withTimezone: true }),
5642
+ activatedAt: timestamp("activated_at", { withTimezone: true }),
5643
+ retiredAt: timestamp("retired_at", { withTimezone: true }),
5644
+ retiredReason: text("retired_reason"),
5645
+ rejectedAt: timestamp("rejected_at", { withTimezone: true }),
5646
+ rejectedReason: text("rejected_reason"),
5647
+ approvedBy: text("approved_by"),
5648
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5649
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5650
+ },
5651
+ (table) => [
5652
+ uniqueIndex("wiki_pipeline_rule_scope_key_key").on(
5653
+ table.scope,
5654
+ table.ruleKey
5655
+ )
5656
+ ]
5657
+ );
5453
5658
  pgTable(
5454
5659
  "wiki_search_log",
5455
5660
  {
@@ -5477,6 +5682,69 @@ pgTable(
5477
5682
  },
5478
5683
  (table) => [index("idx_team_memory_search_log_created").on(table.createdAt)]
5479
5684
  );
5685
+ pgTable(
5686
+ "wiki_agent_run",
5687
+ {
5688
+ id: uuid("id").primaryKey().defaultRandom(),
5689
+ phase: text("phase").notNull(),
5690
+ model: text("model"),
5691
+ claimed: jsonb("claimed").$type(),
5692
+ proven: jsonb("proven").$type(),
5693
+ violationCount: integer("violation_count").notNull().default(0),
5694
+ violations: jsonb("violations").$type(),
5695
+ outcome: text("outcome").notNull().default("ok"),
5696
+ agentId: text("agent_id"),
5697
+ runId: text("run_id"),
5698
+ passId: text("pass_id"),
5699
+ changedPaths: jsonb("changed_paths").$type(),
5700
+ summary: text("summary"),
5701
+ userVerdict: text("user_verdict"),
5702
+ userVerdictNote: text("user_verdict_note"),
5703
+ verdictAt: timestamp("verdict_at", { withTimezone: true }),
5704
+ reviewed: boolean("reviewed").notNull().default(false),
5705
+ reviewNotes: text("review_notes"),
5706
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
5707
+ startedAt: timestamp("started_at", { withTimezone: true }),
5708
+ finishedAt: timestamp("finished_at", { withTimezone: true }).notNull().defaultNow(),
5709
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5710
+ },
5711
+ (table) => [
5712
+ index("idx_wiki_agent_run_phase_created").on(table.phase, table.createdAt),
5713
+ index("idx_wiki_agent_run_created").on(table.createdAt),
5714
+ index("idx_wiki_agent_run_reviewed").on(table.reviewed),
5715
+ index("idx_wiki_agent_run_user_verdict").on(table.userVerdict),
5716
+ index("idx_wiki_agent_run_pass_id").on(table.passId)
5717
+ ]
5718
+ );
5719
+ pgTable(
5720
+ "wiki_agent_review_suggestion",
5721
+ {
5722
+ id: uuid("id").primaryKey().defaultRandom(),
5723
+ status: text("status").notNull().default("proposed"),
5724
+ implementationClass: text("implementation_class").notNull(),
5725
+ phase: text("phase"),
5726
+ scope: text("scope"),
5727
+ title: text("title").notNull(),
5728
+ summary: text("summary").notNull(),
5729
+ targetFile: text("target_file"),
5730
+ targetKey: text("target_key"),
5731
+ beforeText: text("before_text"),
5732
+ afterText: text("after_text"),
5733
+ evidenceRunIds: uuid("evidence_run_ids").array().notNull().default([]),
5734
+ evidenceRefs: text("evidence_refs").array().notNull().default([]),
5735
+ dedupeKey: text("dedupe_key").notNull(),
5736
+ source: text("source").notNull().default("auto"),
5737
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5738
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5739
+ },
5740
+ (table) => [
5741
+ uniqueIndex("wiki_agent_review_suggestion_dedupe_key").on(table.dedupeKey),
5742
+ index("idx_wiki_agent_review_suggestion_status_created").on(
5743
+ table.status,
5744
+ table.createdAt
5745
+ )
5746
+ ]
5747
+ );
5480
5748
 
5481
5749
  // ../db/src/schema/agent-memory.ts
5482
5750
  pgTable(
@@ -5521,6 +5789,88 @@ pgTable(
5521
5789
  index("idx_agent_memory_category").on(table.category)
5522
5790
  ]
5523
5791
  );
5792
+ var agentWorldAgent = pgTable(
5793
+ "agent_world_agent",
5794
+ {
5795
+ id: uuid("id").primaryKey().defaultRandom(),
5796
+ name: text("name").notNull(),
5797
+ color: text("color").notNull(),
5798
+ /** Personality sliders 0–100: curiosity, sociability, diligence, calm. */
5799
+ personality: jsonb("personality").$type().notNull().default({
5800
+ curiosity: 50,
5801
+ sociability: 50,
5802
+ diligence: 50,
5803
+ calm: 50
5804
+ }),
5805
+ /** Needs 0–100: higher = more urgent. */
5806
+ needs: jsonb("needs").$type().notNull().default({ energy: 40, coffee: 30, focus: 35, social: 25 }),
5807
+ position: jsonb("position").$type().notNull().default({ x: 0, y: 0, z: 0 }),
5808
+ target: jsonb("target").$type(),
5809
+ activity: text("activity").notNull().default("idle"),
5810
+ waypoint: text("waypoint"),
5811
+ memories: jsonb("memories").$type().notNull().default([]),
5812
+ diaryCount: integer("diary_count").notNull().default(0),
5813
+ lastSay: text("last_say"),
5814
+ lastSayAt: timestamp("last_say_at", { withTimezone: true }),
5815
+ sortOrder: integer("sort_order").notNull().default(0),
5816
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5817
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5818
+ },
5819
+ (table) => [index("idx_agent_world_agent_sort").on(table.sortOrder)]
5820
+ );
5821
+ pgTable(
5822
+ "agent_world_event",
5823
+ {
5824
+ id: uuid("id").primaryKey().defaultRandom(),
5825
+ /** say | diary | think | god | activity | memo | system */
5826
+ type: text("type").notNull(),
5827
+ agentId: uuid("agent_id").references(() => agentWorldAgent.id, {
5828
+ onDelete: "set null"
5829
+ }),
5830
+ message: text("message").notNull(),
5831
+ meta: jsonb("meta").$type().notNull().default({}),
5832
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5833
+ },
5834
+ (table) => [
5835
+ index("idx_agent_world_event_created").on(table.createdAt),
5836
+ index("idx_agent_world_event_type").on(table.type),
5837
+ index("idx_agent_world_event_agent").on(table.agentId),
5838
+ // Diary / per-agent feed: WHERE agent_id ORDER BY created_at DESC LIMIT N
5839
+ index("idx_agent_world_event_agent_created").on(
5840
+ table.agentId,
5841
+ table.createdAt
5842
+ )
5843
+ ]
5844
+ );
5845
+ pgTable(
5846
+ "agent_world_memo",
5847
+ {
5848
+ id: uuid("id").primaryKey().defaultRandom(),
5849
+ agentId: uuid("agent_id").references(() => agentWorldAgent.id, {
5850
+ onDelete: "set null"
5851
+ }),
5852
+ title: text("title").notNull(),
5853
+ content: text("content").notNull(),
5854
+ location: text("location").notNull().default("whiteboard"),
5855
+ reads: integer("reads").notNull().default(0),
5856
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5857
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5858
+ },
5859
+ (table) => [
5860
+ index("idx_agent_world_memo_created").on(table.createdAt),
5861
+ index("idx_agent_world_memo_location").on(table.location)
5862
+ ]
5863
+ );
5864
+ pgTable("agent_world_state", {
5865
+ id: text("id").primaryKey().default("world"),
5866
+ tick: integer("tick").notNull().default(0),
5867
+ /** Simulated minutes since day start (0–1439 wraps). */
5868
+ simMinutes: integer("sim_minutes").notNull().default(540),
5869
+ paused: boolean("paused").notNull().default(false),
5870
+ lastThinkAt: timestamp("last_think_at", { withTimezone: true }),
5871
+ lastThinkAgentId: uuid("last_think_agent_id"),
5872
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5873
+ });
5524
5874
  var contentSourceType = pgEnum("content_source_type", [
5525
5875
  "own_site",
5526
5876
  "client_site",
@@ -6086,7 +6436,11 @@ pgTable(
6086
6436
  table.resourceId,
6087
6437
  table.metric,
6088
6438
  table.sampledAt
6089
- )
6439
+ ),
6440
+ // Retention cleanup (monitoring-sample-retention-cleanup) filters only on
6441
+ // sampled_at. Without this index Postgres seq-scans the full firehose.
6442
+ // Live DB name uses idx_ prefix (see migration 20260731070000).
6443
+ index("idx_monitoring_sample_sampled_at").on(table.sampledAt)
6090
6444
  ]
6091
6445
  );
6092
6446
  var appLogSource = pgTable(
@@ -6420,7 +6774,8 @@ var directorySubmissionStatus = pgEnum(
6420
6774
  "rejected",
6421
6775
  "failed",
6422
6776
  "manual",
6423
- "captcha"
6777
+ "captcha",
6778
+ "skipped"
6424
6779
  ]
6425
6780
  );
6426
6781
  var directoryAccountStatus = pgEnum("directory_account_status", [
@@ -6444,6 +6799,13 @@ var linkDirectory = pgTable(
6444
6799
  automationLevel: directoryAutomationLevel("automation_level").notNull().default("auto"),
6445
6800
  enabled: boolean("enabled").notNull().default(false),
6446
6801
  formMapping: jsonb("form_mapping").$type(),
6802
+ /** Set when a submission for this directory reached live/submitted end-to-end. */
6803
+ provenAt: timestamp("proven_at", { withTimezone: true }),
6804
+ successCount: integer("success_count").notNull().default(0),
6805
+ failCount: integer("fail_count").notNull().default(0),
6806
+ /** Hard captcha (even after residential) — disable after CAPTCHA_STRIKE_LIMIT. */
6807
+ captchaStrike: integer("captcha_strike").notNull().default(0),
6808
+ lastOutcome: text("last_outcome"),
6447
6809
  lastCheckedAt: timestamp("last_checked_at", { withTimezone: true }),
6448
6810
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6449
6811
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
@@ -6451,7 +6813,8 @@ var linkDirectory = pgTable(
6451
6813
  (table) => [
6452
6814
  uniqueIndex("link_directory_submit_url_uidx").on(table.submitUrl),
6453
6815
  index("link_directory_enabled_dr_idx").on(table.enabled, table.dr),
6454
- index("link_directory_region_idx").on(table.region)
6816
+ index("link_directory_region_idx").on(table.region),
6817
+ index("link_directory_proven_at_idx").on(table.provenAt)
6455
6818
  ]
6456
6819
  );
6457
6820
  var linkBuildingClient = pgTable(
@@ -6463,19 +6826,26 @@ var linkBuildingClient = pgTable(
6463
6826
  mailboxAddress: text("mailbox_address"),
6464
6827
  refrontMailAccountId: text("refront_mail_account_id"),
6465
6828
  profile: jsonb("profile").$type().notNull().default({}),
6466
- weeklyPace: integer("weekly_pace").notNull().default(12),
6829
+ weeklyPace: integer("weekly_pace").notNull().default(35),
6467
6830
  /** Target package size (number of directory backlinks sold). */
6468
6831
  packageSize: integer("package_size").notNull().default(100),
6469
- autoSubmit: boolean("auto_submit").notNull().default(false),
6832
+ autoSubmit: boolean("auto_submit").notNull().default(true),
6470
6833
  startingDr: doublePrecision("starting_dr"),
6471
6834
  currentDr: doublePrecision("current_dr"),
6472
6835
  isActive: boolean("is_active").notNull().default(true),
6836
+ /** Opaque token for public customer progress reports (backlinking.eu). */
6837
+ reportToken: text("report_token"),
6838
+ /** External shop order id from backlinking.eu checkout. */
6839
+ shopOrderId: text("shop_order_id"),
6840
+ shopCustomerEmail: text("shop_customer_email"),
6473
6841
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6474
6842
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6475
6843
  },
6476
6844
  (table) => [
6477
6845
  uniqueIndex("link_building_client_website_uidx").on(table.websiteUrl),
6478
- index("link_building_client_active_idx").on(table.isActive)
6846
+ index("link_building_client_active_idx").on(table.isActive),
6847
+ uniqueIndex("link_building_client_report_token_uidx").on(table.reportToken),
6848
+ index("link_building_client_shop_order_idx").on(table.shopOrderId)
6479
6849
  ]
6480
6850
  );
6481
6851
  var directorySubmission = pgTable(
@@ -6583,6 +6953,78 @@ pgTable(
6583
6953
  )
6584
6954
  ]
6585
6955
  );
6956
+ var networkListingStatus = pgEnum("network_listing_status", [
6957
+ "pending",
6958
+ "queued",
6959
+ "placing",
6960
+ "live",
6961
+ "failed",
6962
+ "expired",
6963
+ "cancelled"
6964
+ ]);
6965
+ var networkSite = pgTable(
6966
+ "network_site",
6967
+ {
6968
+ id: uuid("id").primaryKey().defaultRandom(),
6969
+ name: text("name").notNull(),
6970
+ domain: text("domain").notNull(),
6971
+ websiteUrl: text("website_url").notNull(),
6972
+ dr: integer("dr").notNull().default(0),
6973
+ theme: text("theme").notNull().default("general"),
6974
+ /** Yearly listing price in euro cents. */
6975
+ priceYearCents: integer("price_year_cents").notNull().default(9900),
6976
+ categories: jsonb("categories").$type().notNull().default([]),
6977
+ /** GitHub repo for placement commits, e.g. MGSoftwareBV/catalogcompany.nl */
6978
+ repoFullName: text("repo_full_name"),
6979
+ /** Path in repo for partner/listing data (relative). */
6980
+ listingDataPath: text("listing_data_path"),
6981
+ dofollow: boolean("dofollow").notNull().default(true),
6982
+ enabled: boolean("enabled").notNull().default(false),
6983
+ maxListings: integer("max_listings").notNull().default(50),
6984
+ notes: text("notes"),
6985
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6986
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6987
+ },
6988
+ (table) => [
6989
+ uniqueIndex("network_site_domain_uidx").on(table.domain),
6990
+ index("network_site_enabled_dr_idx").on(table.enabled, table.dr),
6991
+ index("network_site_theme_idx").on(table.theme)
6992
+ ]
6993
+ );
6994
+ pgTable(
6995
+ "network_listing",
6996
+ {
6997
+ id: uuid("id").primaryKey().defaultRandom(),
6998
+ siteId: uuid("site_id").notNull().references(() => networkSite.id, { onDelete: "cascade" }),
6999
+ clientId: uuid("client_id").references(() => linkBuildingClient.id, {
7000
+ onDelete: "set null"
7001
+ }),
7002
+ customerName: text("customer_name").notNull(),
7003
+ customerEmail: text("customer_email").notNull(),
7004
+ targetUrl: text("target_url").notNull(),
7005
+ anchorText: text("anchor_text").notNull(),
7006
+ categorySlug: text("category_slug").notNull().default("general"),
7007
+ status: networkListingStatus("status").notNull().default("pending"),
7008
+ listingUrl: text("listing_url"),
7009
+ /** External shop order reference from backlinking.eu */
7010
+ shopOrderId: text("shop_order_id"),
7011
+ shopOrderItemId: text("shop_order_item_id"),
7012
+ priceCents: integer("price_cents").notNull().default(0),
7013
+ startsAt: timestamp("starts_at", { withTimezone: true }),
7014
+ renewsAt: timestamp("renews_at", { withTimezone: true }),
7015
+ placedAt: timestamp("placed_at", { withTimezone: true }),
7016
+ lastError: text("last_error"),
7017
+ log: jsonb("log").$type().notNull().default({}),
7018
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
7019
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
7020
+ },
7021
+ (table) => [
7022
+ index("network_listing_site_status_idx").on(table.siteId, table.status),
7023
+ index("network_listing_client_idx").on(table.clientId),
7024
+ index("network_listing_renews_at_idx").on(table.renewsAt),
7025
+ index("network_listing_shop_order_idx").on(table.shopOrderId)
7026
+ ]
7027
+ );
6586
7028
  pgTable("github_api_budget", {
6587
7029
  tokenKey: text("token_key").primaryKey(),
6588
7030
  windowStartedAt: timestamp("window_started_at", { withTimezone: true }).notNull().defaultNow(),
@@ -6611,6 +7053,7 @@ pgTable(
6611
7053
  requestCount: integer("request_count").notNull().default(0),
6612
7054
  errorCount: integer("error_count").notNull().default(0),
6613
7055
  cacheHitCount: integer("cache_hit_count").notNull().default(0),
7056
+ breakerHitCount: integer("breaker_hit_count").notNull().default(0),
6614
7057
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6615
7058
  },
6616
7059
  (table) => [
@@ -6655,11 +7098,66 @@ pgTable(
6655
7098
  index("github_webhook_secret_owner_idx").on(table.owner)
6656
7099
  ]
6657
7100
  );
7101
+ var gscOpportunityPass = pgTable(
7102
+ "gsc_opportunity_pass",
7103
+ {
7104
+ id: uuid("id").primaryKey().defaultRandom(),
7105
+ siteKey: text("site_key").notNull(),
7106
+ gscProperty: text("gsc_property").notNull(),
7107
+ passedAt: timestamp("passed_at", { withTimezone: true }).notNull().defaultNow(),
7108
+ kind: text("kind").notNull(),
7109
+ headline: text("headline"),
7110
+ windowFrom: date("window_from"),
7111
+ windowTo: date("window_to"),
7112
+ canvasPath: text("canvas_path"),
7113
+ createdBy: text("created_by"),
7114
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
7115
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
7116
+ },
7117
+ (table) => [
7118
+ index("idx_gsc_opportunity_pass_site_passed").on(
7119
+ table.siteKey,
7120
+ table.passedAt
7121
+ ),
7122
+ index("idx_gsc_opportunity_pass_kind").on(table.siteKey, table.kind)
7123
+ ]
7124
+ );
7125
+ pgTable(
7126
+ "gsc_opportunity_item",
7127
+ {
7128
+ id: uuid("id").primaryKey().defaultRandom(),
7129
+ passId: uuid("pass_id").notNull().references(() => gscOpportunityPass.id, { onDelete: "cascade" }),
7130
+ siteKey: text("site_key").notNull(),
7131
+ url: text("url").notNull(),
7132
+ queryCluster: text("query_cluster"),
7133
+ status: text("status").notNull().default("awaiting"),
7134
+ changeSummary: text("change_summary"),
7135
+ shippedAt: date("shipped_at"),
7136
+ expectEffectAfter: date("expect_effect_after"),
7137
+ sourceUrls: jsonb("source_urls").$type().notNull().default([]),
7138
+ notes: text("notes"),
7139
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
7140
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
7141
+ },
7142
+ (table) => [
7143
+ index("idx_gsc_opportunity_item_site_status").on(
7144
+ table.siteKey,
7145
+ table.status
7146
+ ),
7147
+ index("idx_gsc_opportunity_item_pass").on(table.passId),
7148
+ index("idx_gsc_opportunity_item_url").on(table.siteKey, table.url)
7149
+ ]
7150
+ );
6658
7151
  pgTable(
6659
7152
  "handoff_loop_run",
6660
7153
  {
6661
7154
  id: uuid("id").primaryKey().defaultRandom(),
6662
7155
  runSlug: text("run_slug").notNull(),
7156
+ /**
7157
+ * Skill pack that produced the run. Default is the plan-handoff loop;
7158
+ * quality leaves push mg-strict-truth / mg-seo-strict.
7159
+ */
7160
+ skill: text("skill").notNull().default("mg-plan-handoff-loop"),
6663
7161
  userId: text("user_id"),
6664
7162
  gitRepository: text("git_repository"),
6665
7163
  machine: text("machine"),
@@ -6676,12 +7174,23 @@ pgTable(
6676
7174
  checklistProposed: integer("checklist_proposed").notNull().default(0),
6677
7175
  checklistAccepted: integer("checklist_accepted").notNull().default(0),
6678
7176
  checklistVerified: integer("checklist_verified").notNull().default(0),
7177
+ /** Items resolved with evidence instead of a check (e.g. no browser session). */
7178
+ checklistNa: integer("checklist_na").notNull().default(0),
6679
7179
  inputTokens: bigint("input_tokens", { mode: "number" }).notNull().default(0),
6680
7180
  outputTokens: bigint("output_tokens", { mode: "number" }).notNull().default(0),
6681
7181
  cacheReadTokens: bigint("cache_read_tokens", { mode: "number" }).notNull().default(0),
6682
7182
  totalDurationMs: bigint("total_duration_ms", { mode: "number" }).notNull().default(0),
6683
7183
  retro: text("retro"),
6684
7184
  metrics: jsonb("metrics"),
7185
+ /**
7186
+ * Human end-verdict: good | mixed | bad. The orchestrator asks once at
7187
+ * Phase 4 and the AI Agents table offers a second capture point, so the
7188
+ * review skill can score runs against the user instead of the loop's own
7189
+ * self-report.
7190
+ */
7191
+ userVerdict: text("user_verdict"),
7192
+ userVerdictNote: text("user_verdict_note"),
7193
+ verdictAt: timestamp("verdict_at", { withTimezone: true }),
6685
7194
  reviewed: boolean("reviewed").notNull().default(false),
6686
7195
  reviewNotes: text("review_notes"),
6687
7196
  reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
@@ -6694,8 +7203,10 @@ pgTable(
6694
7203
  uniqueIndex("uq_handoff_loop_run_slug").on(table.runSlug),
6695
7204
  index("idx_handoff_loop_run_user").on(table.userId),
6696
7205
  index("idx_handoff_loop_run_outcome").on(table.outcome),
7206
+ index("idx_handoff_loop_run_skill").on(table.skill),
6697
7207
  index("idx_handoff_loop_run_reviewed").on(table.reviewed),
6698
- index("idx_handoff_loop_run_created").on(table.createdAt)
7208
+ index("idx_handoff_loop_run_created").on(table.createdAt),
7209
+ index("idx_handoff_loop_run_verdict").on(table.userVerdict)
6699
7210
  ]
6700
7211
  );
6701
7212
  var managedSiteSyncMode = pgEnum("managed_site_sync_mode", [
@@ -7052,6 +7563,8 @@ var TRANSIENT_GITHUB_STATUSES = /* @__PURE__ */ new Set([
7052
7563
  524
7053
7564
  ]);
7054
7565
  var DEFAULT_BUDGET_PER_MINUTE = 75;
7566
+ var DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS = 5 * 6e4;
7567
+ var DEFAULT_INTERACTIVE_STALE_MAX_AGE_MS = 30 * 6e4;
7055
7568
  var BUDGET_STATE_KEY = "github_api_budget_per_minute";
7056
7569
  var DEFAULT_BLOCK_MS = 15 * 60 * 1e3;
7057
7570
  var MAX_ATTEMPTS = 4;
@@ -7059,6 +7572,7 @@ var BASE_DELAY_MS = 750;
7059
7572
  var RATE_LIMIT_FLOOR = 50;
7060
7573
  var BUDGET_WAIT_ATTEMPTS = 4;
7061
7574
  var BUDGET_WAIT_CAP_MS = 15e3;
7575
+ var INTERACTIVE_BUDGET_WAIT_CAP_MS = 1e3;
7062
7576
  var GitHubGatewayBlockedError = class extends Error {
7063
7577
  blockedUntil;
7064
7578
  constructor(blockedUntil, message) {
@@ -7095,6 +7609,31 @@ function summarizeGitHubErrorBody(body, maxLen = 240) {
7095
7609
  function isTransientGitHubStatus(status) {
7096
7610
  return TRANSIENT_GITHUB_STATUSES.has(status);
7097
7611
  }
7612
+ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
7613
+ "ECONNRESET",
7614
+ "ECONNREFUSED",
7615
+ "ETIMEDOUT",
7616
+ "ENOTFOUND",
7617
+ "EAI_AGAIN",
7618
+ "ENETUNREACH",
7619
+ "EHOSTUNREACH",
7620
+ "UND_ERR_CONNECT_TIMEOUT",
7621
+ "UND_ERR_HEADERS_TIMEOUT",
7622
+ "UND_ERR_BODY_TIMEOUT",
7623
+ "UND_ERR_SOCKET"
7624
+ ]);
7625
+ function isTransientNetworkError(err) {
7626
+ if (!(err instanceof Error)) return false;
7627
+ const message = err.message.toLowerCase();
7628
+ if (message.includes("fetch failed") || message.includes("socket hang up") || message.includes("network") || message.includes("other side closed")) {
7629
+ return true;
7630
+ }
7631
+ const code = err.code;
7632
+ if (code && TRANSIENT_NETWORK_CODES.has(code)) return true;
7633
+ const cause = err.cause;
7634
+ if (cause && cause !== err) return isTransientNetworkError(cause);
7635
+ return false;
7636
+ }
7098
7637
  function hourBucket(now = /* @__PURE__ */ new Date()) {
7099
7638
  return new Date(
7100
7639
  Date.UTC(
@@ -7111,6 +7650,55 @@ function hourBucket(now = /* @__PURE__ */ new Date()) {
7111
7650
  function cacheKeyFor(url, tokenKey) {
7112
7651
  return createHash("sha256").update(`${tokenKey}:${url}`).digest("hex");
7113
7652
  }
7653
+ function toDate(value) {
7654
+ if (value == null) return null;
7655
+ const date3 = typeof value === "string" ? new Date(value) : value;
7656
+ return Number.isNaN(date3.getTime()) ? null : date3;
7657
+ }
7658
+ function isSoftCacheFresh(input) {
7659
+ const nowMs = input.nowMs ?? Date.now();
7660
+ const expiresAt = toDate(input.expiresAt ?? null);
7661
+ if (expiresAt && expiresAt.getTime() > nowMs) return true;
7662
+ if (input.maxAgeMs == null || input.maxAgeMs <= 0) return false;
7663
+ const updatedAt = toDate(input.updatedAt ?? null);
7664
+ if (!updatedAt) return false;
7665
+ return nowMs - updatedAt.getTime() <= input.maxAgeMs;
7666
+ }
7667
+ function canInteractiveSoftServe(input) {
7668
+ if (isSoftCacheFresh({
7669
+ expiresAt: input.expiresAt,
7670
+ updatedAt: input.updatedAt,
7671
+ maxAgeMs: input.maxAgeMs,
7672
+ nowMs: input.nowMs
7673
+ })) {
7674
+ return true;
7675
+ }
7676
+ const staleMaxAgeMs = input.staleMaxAgeMs ?? DEFAULT_INTERACTIVE_STALE_MAX_AGE_MS;
7677
+ if (staleMaxAgeMs <= 0) return false;
7678
+ return isSoftCacheFresh({
7679
+ expiresAt: null,
7680
+ updatedAt: input.updatedAt,
7681
+ maxAgeMs: staleMaxAgeMs,
7682
+ nowMs: input.nowMs
7683
+ });
7684
+ }
7685
+ function resolveMaxAgeMs(options, priority, useCache) {
7686
+ if (!useCache) return void 0;
7687
+ if (options.maxAgeMs !== void 0) {
7688
+ return options.maxAgeMs > 0 ? options.maxAgeMs : void 0;
7689
+ }
7690
+ if (priority === "interactive") return DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS;
7691
+ return void 0;
7692
+ }
7693
+ function cachedJsonResponse(body, status, cacheHeader) {
7694
+ return new Response(body, {
7695
+ status,
7696
+ headers: {
7697
+ "x-github-gateway-cache": cacheHeader,
7698
+ "content-type": "application/json"
7699
+ }
7700
+ });
7701
+ }
7114
7702
  async function isGithubSyncPaused() {
7115
7703
  try {
7116
7704
  const rows = await getDb().execute(sql`
@@ -7133,9 +7721,9 @@ async function getBlockedUntil(tokenKey) {
7133
7721
  );
7134
7722
  const raw = rows[0]?.blocked_until;
7135
7723
  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;
7724
+ const date3 = typeof raw === "string" ? new Date(raw) : raw;
7725
+ if (Number.isNaN(date3.getTime()) || date3.getTime() <= Date.now()) return null;
7726
+ return date3;
7139
7727
  }
7140
7728
  async function setGithubCircuitBreaker(tokenKey, blockedUntil) {
7141
7729
  await getDb().execute(sql`
@@ -7215,7 +7803,15 @@ async function consumeBudgetOrWait(input) {
7215
7803
  return;
7216
7804
  }
7217
7805
  if (input.priority === "interactive") {
7218
- await sleep(BASE_DELAY_MS);
7806
+ if (attempt >= 2) {
7807
+ await forceConsumeBudget(input.tokenKey);
7808
+ return;
7809
+ }
7810
+ const waitMs2 = Math.min(
7811
+ await msUntilBudgetWindowReset(input.tokenKey),
7812
+ INTERACTIVE_BUDGET_WAIT_CAP_MS
7813
+ );
7814
+ await sleep(waitMs2);
7219
7815
  continue;
7220
7816
  }
7221
7817
  if (attempt === BUDGET_WAIT_ATTEMPTS) {
@@ -7229,40 +7825,76 @@ async function consumeBudgetOrWait(input) {
7229
7825
  }
7230
7826
  throw new GitHubGatewayBudgetError(input.tokenKey);
7231
7827
  }
7828
+ async function forceConsumeBudget(tokenKey) {
7829
+ const updated = await getDb().execute(sql`
7830
+ UPDATE github_api_budget
7831
+ SET request_count = request_count + 1, updated_at = now()
7832
+ WHERE token_key = ${tokenKey}
7833
+ RETURNING request_count
7834
+ `);
7835
+ if (updated[0]) return;
7836
+ await getDb().execute(sql`
7837
+ INSERT INTO github_api_budget (token_key, window_started_at, request_count, updated_at)
7838
+ VALUES (${tokenKey}, now(), 1, now())
7839
+ ON CONFLICT (token_key) DO UPDATE SET
7840
+ request_count = github_api_budget.request_count + 1,
7841
+ updated_at = now()
7842
+ `);
7843
+ }
7232
7844
  async function recordStat(input) {
7233
7845
  const bucket = hourBucket().toISOString();
7234
7846
  await getDb().execute(sql`
7235
7847
  INSERT INTO github_api_stat (
7236
- token_key, job_id, hour_bucket, request_count, error_count, cache_hit_count, updated_at
7848
+ token_key, job_id, hour_bucket, request_count, error_count,
7849
+ cache_hit_count, breaker_hit_count, updated_at
7237
7850
  )
7238
7851
  VALUES (
7239
7852
  ${input.tokenKey},
7240
7853
  ${input.jobId},
7241
7854
  ${bucket}::timestamptz,
7242
- 1,
7855
+ ${input.breakerHit ? 0 : 1},
7243
7856
  ${input.error ? 1 : 0},
7244
7857
  ${input.cacheHit ? 1 : 0},
7858
+ ${input.breakerHit ? 1 : 0},
7245
7859
  now()
7246
7860
  )
7247
7861
  ON CONFLICT (token_key, job_id, hour_bucket) DO UPDATE SET
7248
- request_count = github_api_stat.request_count + 1,
7862
+ request_count = github_api_stat.request_count + ${input.breakerHit ? 0 : 1},
7249
7863
  error_count = github_api_stat.error_count + ${input.error ? 1 : 0},
7250
7864
  cache_hit_count = github_api_stat.cache_hit_count + ${input.cacheHit ? 1 : 0},
7865
+ breaker_hit_count = github_api_stat.breaker_hit_count + ${input.breakerHit ? 1 : 0},
7251
7866
  updated_at = now()
7252
7867
  `);
7253
7868
  }
7869
+ async function recordBreakerHit(input) {
7870
+ await recordStat({
7871
+ tokenKey: input.tokenKey,
7872
+ jobId: input.jobId,
7873
+ breakerHit: true
7874
+ });
7875
+ }
7254
7876
  async function loadEtagCache(key) {
7255
7877
  const rows = await getDb().execute(sql`
7256
- SELECT etag, body, status FROM github_api_cache
7878
+ SELECT etag, body, status, expires_at, updated_at
7879
+ FROM github_api_cache
7257
7880
  WHERE cache_key = ${key}
7258
7881
  LIMIT 1
7259
7882
  `);
7260
- return rows[0] ?? null;
7883
+ const row = rows[0];
7884
+ if (!row) return null;
7885
+ return {
7886
+ etag: row.etag,
7887
+ body: row.body,
7888
+ status: row.status,
7889
+ expiresAt: row.expires_at,
7890
+ updatedAt: row.updated_at
7891
+ };
7261
7892
  }
7262
7893
  async function saveEtagCache(input) {
7894
+ const expiresAt = input.maxAgeMs != null && input.maxAgeMs > 0 ? new Date(Date.now() + input.maxAgeMs).toISOString() : null;
7263
7895
  await getDb().execute(sql`
7264
7896
  INSERT INTO github_api_cache (
7265
- cache_key, etag, body, status, content_type, updated_at
7897
+ cache_key, etag, body, status, content_type, expires_at, updated_at
7266
7898
  )
7267
7899
  VALUES (
7268
7900
  ${input.key},
@@ -7270,6 +7902,7 @@ async function saveEtagCache(input) {
7270
7902
  ${input.body},
7271
7903
  ${input.status},
7272
7904
  ${input.contentType},
7905
+ ${expiresAt}::timestamptz,
7273
7906
  now()
7274
7907
  )
7275
7908
  ON CONFLICT (cache_key) DO UPDATE SET
@@ -7277,6 +7910,7 @@ async function saveEtagCache(input) {
7277
7910
  body = EXCLUDED.body,
7278
7911
  status = EXCLUDED.status,
7279
7912
  content_type = EXCLUDED.content_type,
7913
+ expires_at = EXCLUDED.expires_at,
7280
7914
  updated_at = now()
7281
7915
  `);
7282
7916
  }
@@ -7319,21 +7953,65 @@ function tripBreakerFromResponse(response) {
7319
7953
  }
7320
7954
  async function githubGatewayFetch(url, init, options) {
7321
7955
  const priority = options.priority ?? "background";
7322
- const budgetPerMinute = await resolveGithubBudgetPerMinute(
7323
- options.budgetPerMinute
7324
- );
7325
7956
  const method = (init.method ?? "GET").toUpperCase();
7326
7957
  const useCache = options.useEtagCache !== false && method === "GET";
7958
+ const maxAgeMs = resolveMaxAgeMs(options, priority, useCache);
7327
7959
  if (priority !== "interactive" && await isGithubSyncPaused()) {
7328
7960
  throw new GitHubGatewayPausedError();
7329
7961
  }
7962
+ const key = useCache ? cacheKeyFor(url, options.tokenKey) : null;
7963
+ if (key && !options.bypassSoftCache) {
7964
+ const soft = await loadEtagCache(key);
7965
+ const softOk = soft?.body != null && (priority === "interactive" ? canInteractiveSoftServe({
7966
+ expiresAt: soft.expiresAt,
7967
+ updatedAt: soft.updatedAt,
7968
+ maxAgeMs
7969
+ }) : isSoftCacheFresh({
7970
+ expiresAt: soft.expiresAt,
7971
+ updatedAt: soft.updatedAt,
7972
+ maxAgeMs
7973
+ }));
7974
+ if (softOk && soft?.body != null) {
7975
+ const isFresh = isSoftCacheFresh({
7976
+ expiresAt: soft.expiresAt,
7977
+ updatedAt: soft.updatedAt,
7978
+ maxAgeMs
7979
+ });
7980
+ await recordStat({
7981
+ tokenKey: options.tokenKey,
7982
+ jobId: options.jobId,
7983
+ cacheHit: true
7984
+ });
7985
+ if (priority === "interactive" && !isFresh) {
7986
+ void githubGatewayFetch(url, init, {
7987
+ ...options,
7988
+ bypassSoftCache: true,
7989
+ priority: "background",
7990
+ maxAgeMs: maxAgeMs ?? DEFAULT_INTERACTIVE_CACHE_MAX_AGE_MS
7991
+ }).catch(() => {
7992
+ });
7993
+ }
7994
+ return cachedJsonResponse(soft.body, soft.status ?? 200, "soft");
7995
+ }
7996
+ }
7997
+ const budgetPerMinute = await resolveGithubBudgetPerMinute(
7998
+ options.budgetPerMinute
7999
+ );
7330
8000
  const blocked = await getBlockedUntil(options.tokenKey);
7331
8001
  if (blocked) {
7332
8002
  if (priority === "interactive") {
7333
8003
  if (blocked.getTime() - Date.now() > 6e4) {
8004
+ await recordBreakerHit({
8005
+ tokenKey: options.tokenKey,
8006
+ jobId: options.jobId
8007
+ });
7334
8008
  throw new GitHubGatewayBlockedError(blocked);
7335
8009
  }
7336
8010
  } else {
8011
+ await recordBreakerHit({
8012
+ tokenKey: options.tokenKey,
8013
+ jobId: options.jobId
8014
+ });
7337
8015
  throw new GitHubGatewayBlockedError(blocked);
7338
8016
  }
7339
8017
  }
@@ -7352,14 +8030,32 @@ async function githubGatewayFetch(url, init, options) {
7352
8030
  if (!headers.has("X-GitHub-Api-Version")) {
7353
8031
  headers.set("X-GitHub-Api-Version", "2022-11-28");
7354
8032
  }
7355
- const key = useCache ? cacheKeyFor(url, options.tokenKey) : null;
7356
8033
  if (key) {
7357
8034
  const cached = await loadEtagCache(key);
7358
8035
  if (cached?.etag) headers.set("If-None-Match", cached.etag);
7359
8036
  }
7360
8037
  let lastError = null;
7361
8038
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
7362
- const response = await fetch(url, { ...init, headers });
8039
+ let response;
8040
+ try {
8041
+ response = await fetch(url, {
8042
+ ...init,
8043
+ headers,
8044
+ signal: init.signal ?? AbortSignal.timeout(2e4)
8045
+ });
8046
+ } catch (err) {
8047
+ lastError = err instanceof Error ? err : new Error(String(err));
8048
+ await recordStat({
8049
+ tokenKey: options.tokenKey,
8050
+ jobId: options.jobId,
8051
+ error: true
8052
+ });
8053
+ if (!isTransientNetworkError(err) || attempt === MAX_ATTEMPTS) {
8054
+ throw lastError;
8055
+ }
8056
+ await sleep(BASE_DELAY_MS * 2 ** (attempt - 1));
8057
+ continue;
8058
+ }
7363
8059
  await updateRateLimitHeaders(options.tokenKey, response);
7364
8060
  if (response.status === 304 && key) {
7365
8061
  const cached = await loadEtagCache(key);
@@ -7368,13 +8064,11 @@ async function githubGatewayFetch(url, init, options) {
7368
8064
  jobId: options.jobId,
7369
8065
  cacheHit: true
7370
8066
  });
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
- });
8067
+ return cachedJsonResponse(
8068
+ cached?.body ?? "",
8069
+ cached?.status ?? 200,
8070
+ "hit"
8071
+ );
7378
8072
  }
7379
8073
  if (response.status === 403 || response.status === 429 || isTransientGitHubStatus(response.status)) {
7380
8074
  const bodyText = summarizeGitHubErrorBody(
@@ -7393,6 +8087,10 @@ async function githubGatewayFetch(url, init, options) {
7393
8087
  error: true
7394
8088
  });
7395
8089
  if (attempt === MAX_ATTEMPTS || secondary) {
8090
+ await recordBreakerHit({
8091
+ tokenKey: options.tokenKey,
8092
+ jobId: options.jobId
8093
+ });
7396
8094
  throw new GitHubGatewayBlockedError(until, lastError.message);
7397
8095
  }
7398
8096
  const retryAfter = Number(response.headers.get("retry-after"));
@@ -7409,7 +8107,8 @@ async function githubGatewayFetch(url, init, options) {
7409
8107
  etag,
7410
8108
  body,
7411
8109
  status: response.status,
7412
- contentType: response.headers.get("content-type")
8110
+ contentType: response.headers.get("content-type"),
8111
+ maxAgeMs
7413
8112
  });
7414
8113
  }
7415
8114
  await recordStat({
@@ -8840,7 +9539,7 @@ function getEncryptionKey() {
8840
9539
  throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
8841
9540
  return buf;
8842
9541
  }
8843
- function encrypt(text19) {
9542
+ function encrypt(text22) {
8844
9543
  const key = getEncryptionKey();
8845
9544
  const iv = randomBytes(ENC_IV_LENGTH);
8846
9545
  const cipher = createCipheriv(
@@ -8848,7 +9547,7 @@ function encrypt(text19) {
8848
9547
  new Uint8Array(key),
8849
9548
  new Uint8Array(iv)
8850
9549
  );
8851
- let encrypted = cipher.update(text19, "utf8", "hex");
9550
+ let encrypted = cipher.update(text22, "utf8", "hex");
8852
9551
  encrypted += cipher.final("hex");
8853
9552
  const authTag = cipher.getAuthTag();
8854
9553
  return Buffer.concat([
@@ -9568,10 +10267,10 @@ async function r2GetObjectRange(bucket, key, range) {
9568
10267
  const body = result.Body;
9569
10268
  if (!body?.transformToString)
9570
10269
  throw new Error("R2 returned no readable body");
9571
- const text19 = await body.transformToString();
10270
+ const text22 = await body.transformToString();
9572
10271
  const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
9573
10272
  return `${header}
9574
- ${text19}`;
10273
+ ${text22}`;
9575
10274
  } catch (e) {
9576
10275
  throw r2WrapError(bucket, key, e);
9577
10276
  }
@@ -9934,15 +10633,15 @@ async function sftpRead(opts, filePath, proxy, options) {
9934
10633
  clearTimeout(timer);
9935
10634
  cleanup?.();
9936
10635
  cleanup = void 0;
9937
- const text19 = Buffer.concat(
10636
+ const text22 = Buffer.concat(
9938
10637
  chunks.map((ch) => new Uint8Array(ch))
9939
10638
  ).toString("utf-8");
9940
10639
  if (!isWholeFileRequest) {
9941
10640
  const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
9942
10641
  resolve(`${header}
9943
- ${text19}`);
10642
+ ${text22}`);
9944
10643
  } else {
9945
- resolve(text19);
10644
+ resolve(text22);
9946
10645
  }
9947
10646
  });
9948
10647
  rs.on("error", (e) => {
@@ -9975,6 +10674,8 @@ var RESPONSE_MAX_BYTES = 8192;
9975
10674
  var NO_FOOTER_TOOLS = /* @__PURE__ */ new Set();
9976
10675
  var RAW_JSON_TOOLS = /* @__PURE__ */ new Set([
9977
10676
  "get_mg_dashboard_commits",
10677
+ "get_mg_dashboard_file",
10678
+ "get_mg_dashboard_diff",
9978
10679
  "extract_article",
9979
10680
  // Content family (2026-DASMG-040/041/042) — compact JSON consumed verbatim.
9980
10681
  // Consolidated action-based tools (the Cursor surface); each proxies to the
@@ -9990,7 +10691,9 @@ var RAW_JSON_TOOLS = /* @__PURE__ */ new Set([
9990
10691
  "managed-site",
9991
10692
  "content-sync",
9992
10693
  // Skill registry metadata only (no file bodies) — must stay valid JSON.
9993
- "cursor-skill"
10694
+ "cursor-skill",
10695
+ // GSC opportunity store (pass + items)
10696
+ "gsc-opportunity"
9994
10697
  ]);
9995
10698
  var TOOL_CACHE_TTL_MS = {
9996
10699
  "list-servers": 6e4,
@@ -10091,11 +10794,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
10091
10794
  if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
10092
10795
  return e.names;
10093
10796
  }
10094
- function truncateForLLM(text19, maxBytes) {
10095
- const totalBytes = Buffer.byteLength(text19, "utf8");
10797
+ function truncateForLLM(text22, maxBytes) {
10798
+ const totalBytes = Buffer.byteLength(text22, "utf8");
10096
10799
  if (totalBytes <= maxBytes)
10097
- return { text: text19, truncated: false, totalBytes, shownBytes: totalBytes };
10098
- const buf = Buffer.from(text19, "utf8");
10800
+ return { text: text22, truncated: false, totalBytes, shownBytes: totalBytes };
10801
+ const buf = Buffer.from(text22, "utf8");
10099
10802
  let cut = maxBytes;
10100
10803
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
10101
10804
  const head = buf.subarray(0, cut).toString("utf8");
@@ -10126,10 +10829,10 @@ function postprocessResult(result, meta) {
10126
10829
  if (!result.content?.length) return result;
10127
10830
  if (RAW_JSON_TOOLS.has(meta.toolName)) return result;
10128
10831
  const block = result.content[0];
10129
- let text19 = String(block.text ?? "");
10130
- const trunc = truncateForLLM(text19, RESPONSE_MAX_BYTES);
10832
+ let text22 = String(block.text ?? "");
10833
+ const trunc = truncateForLLM(text22, RESPONSE_MAX_BYTES);
10131
10834
  if (trunc.truncated) {
10132
- text19 = trunc.text + "\n\n... " + buildTruncationHint(
10835
+ text22 = trunc.text + "\n\n... " + buildTruncationHint(
10133
10836
  meta.toolName,
10134
10837
  meta.args,
10135
10838
  trunc.totalBytes,
@@ -10143,11 +10846,11 @@ function postprocessResult(result, meta) {
10143
10846
  const parts = [`took ${tookStr}`, sizeStr];
10144
10847
  if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
10145
10848
  if (meta.cached) parts.push("cached");
10146
- text19 = `${text19}
10849
+ text22 = `${text22}
10147
10850
 
10148
10851
  [${parts.join(", ")}]`;
10149
10852
  }
10150
- return { ...result, content: [{ ...block, text: text19 }] };
10853
+ return { ...result, content: [{ ...block, text: text22 }] };
10151
10854
  }
10152
10855
  function buildPipelineScript(commands, shell, marker, stopOnError) {
10153
10856
  if (shell === "powershell") {
@@ -10933,11 +11636,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
10933
11636
  applied_by TEXT
10934
11637
  );
10935
11638
  `.trim();
10936
- function normaliseMigrationSql(sql29) {
10937
- return sql29.replace(/\r\n/g, "\n").trim() + "\n";
11639
+ function normaliseMigrationSql(sql31) {
11640
+ return sql31.replace(/\r\n/g, "\n").trim() + "\n";
10938
11641
  }
10939
- function migrationSha256(sql29) {
10940
- return createHash("sha256").update(sql29.replace(/\r\n/g, "\n"), "utf8").digest("hex");
11642
+ function migrationSha256(sql31) {
11643
+ return createHash("sha256").update(sql31.replace(/\r\n/g, "\n"), "utf8").digest("hex");
10941
11644
  }
10942
11645
  function dollarQuoteTag(value) {
10943
11646
  let tag = "_mcp";
@@ -12143,6 +12846,7 @@ var TOOLS = [
12143
12846
  required: ["actor", "dateFrom", "dateTo"]
12144
12847
  }
12145
12848
  },
12849
+ ...GITHUB_CODE_ACCESS_TOOLS,
12146
12850
  {
12147
12851
  name: "extract_article",
12148
12852
  description: "Extract readable article/content data from ONE PUBLIC URL, with a browser-render fallback for pages that block a normal fetch (403) or only render with JavaScript (ticket 2026-DASMG-039). Returns title, author, publishedAt, source, summary, facts[], optional rawText (capped), canonicalUrl, the `extractionMethod` used (http | browser | jsonld | rss | amp | metadata_only) and an explicit `limitations` array (e.g. blocked_without_browser, paywall_detected, partial_content, rawtext_truncated). PUBLIC URLs only \u2014 SSRF-guarded (private/loopback/metadata addresses rejected), no paywall bypass, no credentials; the source/canonical URL is always preserved. Use it to turn a news/article link into safe factual source material for agents; it never auto-publishes or rewrites. For several sources + cross-checking, use `research` with action=topic.",
@@ -12803,6 +13507,87 @@ var TOOLS = [
12803
13507
  }
12804
13508
  },
12805
13509
  // ----- Shared Cursor skills (metadata only; install via downloads ZIP) -----
13510
+ {
13511
+ name: "gsc-opportunity",
13512
+ 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).',
13513
+ inputSchema: {
13514
+ type: "object",
13515
+ properties: {
13516
+ action: {
13517
+ type: "string",
13518
+ enum: ["list", "upsert", "close"],
13519
+ description: "list | upsert | close"
13520
+ },
13521
+ siteKey: {
13522
+ type: "string",
13523
+ description: "Stable site id, e.g. solarfast (required for all actions)."
13524
+ },
13525
+ status: {
13526
+ type: "string",
13527
+ enum: ["awaiting", "monitor", "pending_reindex", "skipped", "closed"],
13528
+ description: "action=list: single status filter."
13529
+ },
13530
+ statuses: {
13531
+ type: "array",
13532
+ items: {
13533
+ type: "string",
13534
+ enum: ["awaiting", "monitor", "pending_reindex", "skipped", "closed"]
13535
+ },
13536
+ description: "action=list: multi status filter."
13537
+ },
13538
+ kind: {
13539
+ type: "string",
13540
+ enum: ["analysis", "ctr_polish", "reindex", "cls", "other"],
13541
+ description: "action=list/upsert: pass kind."
13542
+ },
13543
+ includeClosed: {
13544
+ type: "boolean",
13545
+ description: "action=list: include closed items (default false)."
13546
+ },
13547
+ limit: { type: "number", description: "action=list: max items (default 50)." },
13548
+ offset: { type: "number", description: "action=list: pagination offset." },
13549
+ gscProperty: {
13550
+ type: "string",
13551
+ description: "action=upsert: e.g. sc-domain:solarfast.nl"
13552
+ },
13553
+ passId: {
13554
+ type: "string",
13555
+ description: "action=upsert: update existing pass uuid."
13556
+ },
13557
+ headline: { type: "string", description: "action=upsert: pass headline." },
13558
+ windowFrom: { type: "string", description: "action=upsert: YYYY-MM-DD." },
13559
+ windowTo: { type: "string", description: "action=upsert: YYYY-MM-DD." },
13560
+ canvasPath: { type: "string", description: "action=upsert: optional canvas path." },
13561
+ createdBy: { type: "string", description: "action=upsert: agent/user label." },
13562
+ passedAt: {
13563
+ type: "string",
13564
+ description: "action=upsert: ISO datetime for the pass."
13565
+ },
13566
+ items: {
13567
+ type: "array",
13568
+ description: "action=upsert: items [{url, queryCluster?, status?, changeSummary?, shippedAt?, expectEffectAfter?, sourceUrls?, notes?, id?}]",
13569
+ items: { type: "object" }
13570
+ },
13571
+ id: {
13572
+ type: "string",
13573
+ description: "action=close: item uuid (preferred)."
13574
+ },
13575
+ url: {
13576
+ type: "string",
13577
+ description: "action=close: item url when id omitted."
13578
+ },
13579
+ queryCluster: {
13580
+ type: "string",
13581
+ description: "action=close: optional cluster disambiguator."
13582
+ },
13583
+ notes: {
13584
+ type: "string",
13585
+ description: "action=close/upsert item notes."
13586
+ }
13587
+ },
13588
+ required: ["action", "siteKey"]
13589
+ }
13590
+ },
12806
13591
  {
12807
13592
  name: "cursor-skill",
12808
13593
  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 +13618,7 @@ var TOOLS = [
12833
13618
  // ----- Trigger.dev -----
12834
13619
  ...TRIGGER_TOOLS
12835
13620
  ];
12836
- var MCP_VERSION = "7.4.13";
13621
+ var MCP_VERSION = "7.4.14";
12837
13622
  async function handleListTools() {
12838
13623
  if (!authContext) return { tools: TOOLS };
12839
13624
  const allowedTools = authContext.allowedTools;
@@ -12974,6 +13759,12 @@ async function executeToolCall(name, a, _serverId) {
12974
13759
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
12975
13760
  };
12976
13761
  }
13762
+ case "get_mg_dashboard_file":
13763
+ case "get_mg_dashboard_diff":
13764
+ return handleGithubCodeAccessTool(name, a, {
13765
+ dashboardBaseUrl,
13766
+ apiKey: apiKey ?? ""
13767
+ });
12977
13768
  // ----- Public-web extraction (article tools) -----
12978
13769
  case "extract_article": {
12979
13770
  const url = typeof a.url === "string" ? a.url.trim() : "";
@@ -13050,6 +13841,39 @@ async function executeToolCall(name, a, _serverId) {
13050
13841
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
13051
13842
  };
13052
13843
  }
13844
+ case "gsc-opportunity": {
13845
+ const action = typeof a.action === "string" ? a.action.trim() : "";
13846
+ if (!action) {
13847
+ return {
13848
+ content: [{ type: "text", text: "Error: action is required" }]
13849
+ };
13850
+ }
13851
+ const { action: _action, ...params } = a;
13852
+ void _action;
13853
+ const res = await fetch(`${dashboardBaseUrl}/api/tools/gsc-opportunity`, {
13854
+ method: "POST",
13855
+ headers: {
13856
+ "content-type": "application/json",
13857
+ authorization: `Bearer ${apiKey}`
13858
+ },
13859
+ body: JSON.stringify({ action, params })
13860
+ });
13861
+ if (!res.ok) {
13862
+ const detail = await res.text().catch(() => "");
13863
+ return {
13864
+ content: [
13865
+ {
13866
+ type: "text",
13867
+ text: `Error: gsc-opportunity failed (${res.status}). ${detail.slice(0, 400)}`
13868
+ }
13869
+ ]
13870
+ };
13871
+ }
13872
+ const data = await res.json();
13873
+ return {
13874
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
13875
+ };
13876
+ }
13053
13877
  // ----- Team memory -----
13054
13878
  case "search-team-memory": {
13055
13879
  const query = typeof a.query === "string" ? a.query.trim() : "";
@@ -13091,7 +13915,7 @@ async function executeToolCall(name, a, _serverId) {
13091
13915
  ]
13092
13916
  };
13093
13917
  }
13094
- const lines = data.hits.map((hit, index16) => {
13918
+ const lines = data.hits.map((hit, index19) => {
13095
13919
  const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
13096
13920
  const repo = hit.repo ?? "unknown repo";
13097
13921
  const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
@@ -13101,7 +13925,7 @@ async function executeToolCall(name, a, _serverId) {
13101
13925
  ...Array.isArray(hit.tech) ? hit.tech.slice(0, 4) : []
13102
13926
  ].filter(Boolean);
13103
13927
  const tags = facets.length > 0 ? ` \xB7 ${facets.join(", ")}` : "";
13104
- return `${index16 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
13928
+ return `${index19 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
13105
13929
  id: ${hit.id}
13106
13930
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
13107
13931
  });
@@ -13308,12 +14132,12 @@ Searchable via search-team-memory once indexing completes (usually immediate).`
13308
14132
  ]
13309
14133
  };
13310
14134
  }
13311
- const lines = data.hits.map((hit, index16) => {
14135
+ const lines = data.hits.map((hit, index19) => {
13312
14136
  const sim = hit.similarity !== null ? ` \xB7 ${Math.round(hit.similarity * 100)}% match` : "";
13313
14137
  const tags = hit.tags.length ? ` \xB7 ${hit.tags.slice(0, 5).join(", ")}` : "";
13314
14138
  const sources = hit.sources.length ? `
13315
14139
  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}
14140
+ return `${index19 + 1}. [${hit.status} \xB7 updated ${hit.pageUpdatedOn ?? "unknown"}${sim}${tags}] ${hit.title}
13317
14141
  slug: ${hit.slug}${sources}
13318
14142
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 400)}`;
13319
14143
  });
@@ -13370,8 +14194,8 @@ Call get-wiki-page with a slug for the full page; follow its sources (team-memor
13370
14194
  const formatSource = (s) => {
13371
14195
  const base = `${s.type}:${s.ref}`;
13372
14196
  if (s.title) {
13373
- const date2 = s.date ? ` (${s.date})` : "";
13374
- return `${base} \u2014 "${s.title}"${date2}`;
14197
+ const date3 = s.date ? ` (${s.date})` : "";
14198
+ return `${base} \u2014 "${s.title}"${date3}`;
13375
14199
  }
13376
14200
  return base;
13377
14201
  };
@@ -14570,8 +15394,8 @@ ${sample.join("\n")}${files.length > 5 ? `
14570
15394
  };
14571
15395
  const filtered = sortRows(applyFilter(only.rows));
14572
15396
  if (format === "json") {
14573
- const text20 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
14574
- return { content: [{ type: "text", text: text20 }] };
15397
+ const text23 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
15398
+ return { content: [{ type: "text", text: text23 }] };
14575
15399
  }
14576
15400
  if (groupByProject) {
14577
15401
  const groups = /* @__PURE__ */ new Map();
@@ -14598,8 +15422,8 @@ ${sample.join("\n")}${files.length > 5 ? `
14598
15422
  }
14599
15423
  const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
14600
15424
  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 }] };
15425
+ const text22 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
15426
+ return { content: [{ type: "text", text: text22 }] };
14603
15427
  }
14604
15428
  if (format === "json") {
14605
15429
  const lines = [];