@mgsoftwarebv/mg-dashboard-mcp 7.4.8 → 7.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +930 -86
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { sql } from 'drizzle-orm';
|
|
|
20
20
|
import { once } from 'events';
|
|
21
21
|
import { lookup } from 'dns/promises';
|
|
22
22
|
import { connect } from 'tls';
|
|
23
|
-
import { pgTable, timestamp,
|
|
23
|
+
import { pgTable, timestamp, uuid, jsonb, boolean, text, pgEnum, integer, uniqueIndex, index, check, bigint, doublePrecision, primaryKey, foreignKey, date, real } from 'drizzle-orm/pg-core';
|
|
24
24
|
import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
25
25
|
|
|
26
26
|
var __defProp = Object.defineProperty;
|
|
@@ -703,10 +703,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
|
|
|
703
703
|
"trigger-env": "settings"
|
|
704
704
|
};
|
|
705
705
|
async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
706
|
-
const
|
|
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`;
|
|
707
707
|
const cmd = [
|
|
708
708
|
`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 "${
|
|
709
|
+
`ROW=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null | tr -d '[:space:]')`,
|
|
710
710
|
'echo "$PORT|$ROW"'
|
|
711
711
|
].join(" && ");
|
|
712
712
|
const result = await sshExec2(conn, cmd, proxy);
|
|
@@ -728,8 +728,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
|
728
728
|
return { port, apiKey: apiKey2, projectRef: projectRef || "" };
|
|
729
729
|
}
|
|
730
730
|
async function fetchRunLogs(runId, conn, proxy, sshExec2) {
|
|
731
|
-
const
|
|
732
|
-
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${
|
|
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`;
|
|
733
733
|
const result = await sshExec2(conn, cmd, proxy);
|
|
734
734
|
const output = result.stdout.trim();
|
|
735
735
|
if (!output) return "";
|
|
@@ -811,8 +811,8 @@ async function handleTriggerTool(name, args2, deps) {
|
|
|
811
811
|
switch (name) {
|
|
812
812
|
// -----------------------------------------------------------------
|
|
813
813
|
case "trigger-list": {
|
|
814
|
-
const
|
|
815
|
-
const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${
|
|
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`;
|
|
816
816
|
const result = await sshExec2(conn, cmd, proxy);
|
|
817
817
|
const output = result.stdout.trim();
|
|
818
818
|
if (!output) {
|
|
@@ -990,9 +990,9 @@ ${raw2.substring(0, 500)}` }] };
|
|
|
990
990
|
return { content: [{ type: "text", text: `No environment variables for ${project}/${env}.` }] };
|
|
991
991
|
}
|
|
992
992
|
const lines = vars.map((v) => `${v.isSecret ? "[secret]" : " "} ${v.name}`).sort();
|
|
993
|
-
const
|
|
993
|
+
const text16 = `Env vars for ${project}/${env} (${vars.length}) \u2014 values hidden, use action="get" with a key:
|
|
994
994
|
` + "-".repeat(50) + "\n" + lines.join("\n");
|
|
995
|
-
return { content: [{ type: "text", text:
|
|
995
|
+
return { content: [{ type: "text", text: text16 }] };
|
|
996
996
|
}
|
|
997
997
|
if (action === "get") {
|
|
998
998
|
const key2 = String(args2.key ?? "");
|
|
@@ -1079,9 +1079,9 @@ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
|
|
|
1079
1079
|
return { content: [{ type: "text", text: `Invalid API response:
|
|
1080
1080
|
${rawJson.substring(0, 500)}` }] };
|
|
1081
1081
|
}
|
|
1082
|
-
let
|
|
1083
|
-
if (logs)
|
|
1084
|
-
return { content: [{ type: "text", text:
|
|
1082
|
+
let text16 = formatRunDetail(run);
|
|
1083
|
+
if (logs) text16 += "\n\n--- Logs ---\n" + logs;
|
|
1084
|
+
return { content: [{ type: "text", text: text16 }] };
|
|
1085
1085
|
}
|
|
1086
1086
|
async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
|
|
1087
1087
|
const pollInterval = 3e3;
|
|
@@ -1103,10 +1103,10 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
|
|
|
1103
1103
|
continue;
|
|
1104
1104
|
}
|
|
1105
1105
|
if (TERMINAL_STATUSES.has(run.status)) {
|
|
1106
|
-
let
|
|
1106
|
+
let text16 = formatRunDetail(run);
|
|
1107
1107
|
const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
|
|
1108
|
-
if (logs)
|
|
1109
|
-
return { content: [{ type: "text", text:
|
|
1108
|
+
if (logs) text16 += "\n\n--- Logs ---\n" + logs;
|
|
1109
|
+
return { content: [{ type: "text", text: text16 }] };
|
|
1110
1110
|
}
|
|
1111
1111
|
}
|
|
1112
1112
|
return {
|
|
@@ -3793,10 +3793,10 @@ var ZodObject = class _ZodObject extends ZodType {
|
|
|
3793
3793
|
// }) as any;
|
|
3794
3794
|
// return merged;
|
|
3795
3795
|
// }
|
|
3796
|
-
catchall(
|
|
3796
|
+
catchall(index13) {
|
|
3797
3797
|
return new _ZodObject({
|
|
3798
3798
|
...this._def,
|
|
3799
|
-
catchall:
|
|
3799
|
+
catchall: index13
|
|
3800
3800
|
});
|
|
3801
3801
|
}
|
|
3802
3802
|
pick(mask) {
|
|
@@ -4114,9 +4114,9 @@ function mergeValues(a, b) {
|
|
|
4114
4114
|
return { valid: false };
|
|
4115
4115
|
}
|
|
4116
4116
|
const newArray = [];
|
|
4117
|
-
for (let
|
|
4118
|
-
const itemA = a[
|
|
4119
|
-
const itemB = b[
|
|
4117
|
+
for (let index13 = 0; index13 < a.length; index13++) {
|
|
4118
|
+
const itemA = a[index13];
|
|
4119
|
+
const itemB = b[index13];
|
|
4120
4120
|
const sharedValue = mergeValues(itemA, itemB);
|
|
4121
4121
|
if (!sharedValue.valid) {
|
|
4122
4122
|
return { valid: false };
|
|
@@ -4322,10 +4322,10 @@ var ZodMap = class extends ZodType {
|
|
|
4322
4322
|
}
|
|
4323
4323
|
const keyType = this._def.keyType;
|
|
4324
4324
|
const valueType = this._def.valueType;
|
|
4325
|
-
const pairs = [...ctx.data.entries()].map(([key, value],
|
|
4325
|
+
const pairs = [...ctx.data.entries()].map(([key, value], index13) => {
|
|
4326
4326
|
return {
|
|
4327
|
-
key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [
|
|
4328
|
-
value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [
|
|
4327
|
+
key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index13, "key"])),
|
|
4328
|
+
value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index13, "value"]))
|
|
4329
4329
|
};
|
|
4330
4330
|
});
|
|
4331
4331
|
if (ctx.common.async) {
|
|
@@ -5247,6 +5247,8 @@ var users = pgTable("user", {
|
|
|
5247
5247
|
emailVerified: boolean("email_verified").notNull().default(false),
|
|
5248
5248
|
roleId: uuid("role_id"),
|
|
5249
5249
|
permissions: jsonb("permissions"),
|
|
5250
|
+
/** Linked Refront user id (matched by email during refront-crm-sync). */
|
|
5251
|
+
refrontUserId: uuid("refront_user_id"),
|
|
5250
5252
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
5251
5253
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
5252
5254
|
});
|
|
@@ -6150,7 +6152,8 @@ var directorySubmissionStatus = pgEnum(
|
|
|
6150
6152
|
"live",
|
|
6151
6153
|
"rejected",
|
|
6152
6154
|
"failed",
|
|
6153
|
-
"manual"
|
|
6155
|
+
"manual",
|
|
6156
|
+
"captcha"
|
|
6154
6157
|
]
|
|
6155
6158
|
);
|
|
6156
6159
|
var directoryAccountStatus = pgEnum("directory_account_status", [
|
|
@@ -6194,6 +6197,8 @@ var linkBuildingClient = pgTable(
|
|
|
6194
6197
|
refrontMailAccountId: text("refront_mail_account_id"),
|
|
6195
6198
|
profile: jsonb("profile").$type().notNull().default({}),
|
|
6196
6199
|
weeklyPace: integer("weekly_pace").notNull().default(12),
|
|
6200
|
+
/** Target package size (number of directory backlinks sold). */
|
|
6201
|
+
packageSize: integer("package_size").notNull().default(100),
|
|
6197
6202
|
autoSubmit: boolean("auto_submit").notNull().default(false),
|
|
6198
6203
|
startingDr: doublePrecision("starting_dr"),
|
|
6199
6204
|
currentDr: doublePrecision("current_dr"),
|
|
@@ -6206,7 +6211,7 @@ var linkBuildingClient = pgTable(
|
|
|
6206
6211
|
index("link_building_client_active_idx").on(table.isActive)
|
|
6207
6212
|
]
|
|
6208
6213
|
);
|
|
6209
|
-
pgTable(
|
|
6214
|
+
var directorySubmission = pgTable(
|
|
6210
6215
|
"directory_submission",
|
|
6211
6216
|
{
|
|
6212
6217
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -6221,6 +6226,7 @@ pgTable(
|
|
|
6221
6226
|
submittedAt: timestamp("submitted_at", { withTimezone: true }),
|
|
6222
6227
|
verifiedAt: timestamp("verified_at", { withTimezone: true }),
|
|
6223
6228
|
lastError: text("last_error"),
|
|
6229
|
+
fixAttempts: integer("fix_attempts").notNull().default(0),
|
|
6224
6230
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6225
6231
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6226
6232
|
},
|
|
@@ -6277,6 +6283,78 @@ pgTable(
|
|
|
6277
6283
|
index("directory_account_status_idx").on(table.status)
|
|
6278
6284
|
]
|
|
6279
6285
|
);
|
|
6286
|
+
pgTable(
|
|
6287
|
+
"link_building_agent_job",
|
|
6288
|
+
{
|
|
6289
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6290
|
+
submissionId: uuid("submission_id").references(
|
|
6291
|
+
() => directorySubmission.id,
|
|
6292
|
+
{ onDelete: "set null" }
|
|
6293
|
+
),
|
|
6294
|
+
hostId: uuid("host_id").notNull(),
|
|
6295
|
+
commandId: uuid("command_id"),
|
|
6296
|
+
status: text("status").$type().notNull().default("pending"),
|
|
6297
|
+
requestJson: jsonb("request_json").$type().notNull().default({}),
|
|
6298
|
+
resultJson: jsonb("result_json").$type(),
|
|
6299
|
+
browserSessionId: text("browser_session_id"),
|
|
6300
|
+
hostName: text("host_name"),
|
|
6301
|
+
errorMessage: text("error_message"),
|
|
6302
|
+
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
6303
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
6304
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6305
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6306
|
+
},
|
|
6307
|
+
(table) => [
|
|
6308
|
+
index("idx_link_building_agent_job_status_created").on(
|
|
6309
|
+
table.status,
|
|
6310
|
+
table.createdAt
|
|
6311
|
+
),
|
|
6312
|
+
index("idx_link_building_agent_job_submission").on(table.submissionId),
|
|
6313
|
+
index("idx_link_building_agent_job_host_status").on(
|
|
6314
|
+
table.hostId,
|
|
6315
|
+
table.status
|
|
6316
|
+
)
|
|
6317
|
+
]
|
|
6318
|
+
);
|
|
6319
|
+
pgTable("github_api_budget", {
|
|
6320
|
+
tokenKey: text("token_key").primaryKey(),
|
|
6321
|
+
windowStartedAt: timestamp("window_started_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6322
|
+
requestCount: integer("request_count").notNull().default(0),
|
|
6323
|
+
blockedUntil: timestamp("blocked_until", { withTimezone: true }),
|
|
6324
|
+
lastRemaining: integer("last_remaining"),
|
|
6325
|
+
lastResetAt: timestamp("last_reset_at", { withTimezone: true }),
|
|
6326
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6327
|
+
});
|
|
6328
|
+
pgTable("github_api_cache", {
|
|
6329
|
+
cacheKey: text("cache_key").primaryKey(),
|
|
6330
|
+
etag: text("etag"),
|
|
6331
|
+
body: text("body"),
|
|
6332
|
+
status: integer("status").notNull().default(200),
|
|
6333
|
+
contentType: text("content_type"),
|
|
6334
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
6335
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6336
|
+
});
|
|
6337
|
+
pgTable(
|
|
6338
|
+
"github_api_stat",
|
|
6339
|
+
{
|
|
6340
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6341
|
+
tokenKey: text("token_key").notNull(),
|
|
6342
|
+
jobId: text("job_id").notNull().default("unknown"),
|
|
6343
|
+
hourBucket: timestamp("hour_bucket", { withTimezone: true }).notNull(),
|
|
6344
|
+
requestCount: integer("request_count").notNull().default(0),
|
|
6345
|
+
errorCount: integer("error_count").notNull().default(0),
|
|
6346
|
+
cacheHitCount: integer("cache_hit_count").notNull().default(0),
|
|
6347
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6348
|
+
},
|
|
6349
|
+
(table) => [
|
|
6350
|
+
uniqueIndex("github_api_stat_token_job_hour_key").on(
|
|
6351
|
+
table.tokenKey,
|
|
6352
|
+
table.jobId,
|
|
6353
|
+
table.hourBucket
|
|
6354
|
+
),
|
|
6355
|
+
index("idx_github_api_stat_hour").on(table.hourBucket)
|
|
6356
|
+
]
|
|
6357
|
+
);
|
|
6280
6358
|
var githubToken = pgTable(
|
|
6281
6359
|
"github_token",
|
|
6282
6360
|
{
|
|
@@ -6448,6 +6526,34 @@ pgTable(
|
|
|
6448
6526
|
index("idx_refront_project_code").on(table.code)
|
|
6449
6527
|
]
|
|
6450
6528
|
);
|
|
6529
|
+
var refrontMember = pgTable(
|
|
6530
|
+
"refront_member",
|
|
6531
|
+
{
|
|
6532
|
+
/** Refront user id. */
|
|
6533
|
+
id: uuid("id").primaryKey(),
|
|
6534
|
+
name: text("name"),
|
|
6535
|
+
role: text("role"),
|
|
6536
|
+
teamId: text("team_id"),
|
|
6537
|
+
teamName: text("team_name"),
|
|
6538
|
+
/** True when the member has no explicit project assignments in Refront. */
|
|
6539
|
+
unrestricted: boolean("unrestricted").notNull().default(true),
|
|
6540
|
+
/** Derived wiki customer folder prefixes (empty when unrestricted). */
|
|
6541
|
+
wikiFolders: text("wiki_folders").array().notNull().default([]),
|
|
6542
|
+
syncedAt: timestamp("synced_at", { withTimezone: true }).notNull(),
|
|
6543
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6544
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6545
|
+
},
|
|
6546
|
+
(table) => [index("idx_refront_member_team").on(table.teamId)]
|
|
6547
|
+
);
|
|
6548
|
+
pgTable(
|
|
6549
|
+
"refront_member_project_access",
|
|
6550
|
+
{
|
|
6551
|
+
memberId: uuid("member_id").notNull().references(() => refrontMember.id, { onDelete: "cascade" }),
|
|
6552
|
+
projectId: uuid("project_id").notNull(),
|
|
6553
|
+
syncedAt: timestamp("synced_at", { withTimezone: true }).notNull()
|
|
6554
|
+
},
|
|
6555
|
+
(table) => [primaryKey({ columns: [table.memberId, table.projectId] })]
|
|
6556
|
+
);
|
|
6451
6557
|
pgTable(
|
|
6452
6558
|
"refront_source_sync_state",
|
|
6453
6559
|
{
|
|
@@ -6538,7 +6644,587 @@ pgTable(
|
|
|
6538
6644
|
}).onDelete("cascade")
|
|
6539
6645
|
]
|
|
6540
6646
|
);
|
|
6647
|
+
pgTable(
|
|
6648
|
+
"wiki_page",
|
|
6649
|
+
{
|
|
6650
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6651
|
+
slug: text("slug").notNull().unique(),
|
|
6652
|
+
title: text("title").notNull().default(""),
|
|
6653
|
+
summary: text("summary"),
|
|
6654
|
+
content: text("content").notNull().default(""),
|
|
6655
|
+
status: text("status").notNull().default("draft"),
|
|
6656
|
+
pageUpdatedOn: date("page_updated_on"),
|
|
6657
|
+
reviewedBy: text("reviewed_by"),
|
|
6658
|
+
reviewedOn: date("reviewed_on"),
|
|
6659
|
+
lastVerifiedOn: date("last_verified_on"),
|
|
6660
|
+
verifyCount: integer("verify_count").notNull().default(0),
|
|
6661
|
+
verifyNote: text("verify_note"),
|
|
6662
|
+
dirty: boolean("dirty").notNull().default(false),
|
|
6663
|
+
vaultSyncedAt: timestamp("vault_synced_at", { withTimezone: true }),
|
|
6664
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6665
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6666
|
+
},
|
|
6667
|
+
(table) => [
|
|
6668
|
+
index("idx_wiki_page_status").on(table.status),
|
|
6669
|
+
index("idx_wiki_page_last_verified").on(table.lastVerifiedOn),
|
|
6670
|
+
index("idx_wiki_page_dirty").on(table.dirty)
|
|
6671
|
+
]
|
|
6672
|
+
);
|
|
6673
|
+
pgTable(
|
|
6674
|
+
"wiki_change_log",
|
|
6675
|
+
{
|
|
6676
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6677
|
+
authorJob: text("author_job").notNull(),
|
|
6678
|
+
summary: text("summary").notNull().default(""),
|
|
6679
|
+
slugs: text("slugs").array().notNull().default([]),
|
|
6680
|
+
syncedAt: timestamp("synced_at", { withTimezone: true }),
|
|
6681
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
6682
|
+
},
|
|
6683
|
+
(table) => [index("idx_wiki_change_log_created").on(table.createdAt)]
|
|
6684
|
+
);
|
|
6685
|
+
pgTable(
|
|
6686
|
+
"wiki_page_flag",
|
|
6687
|
+
{
|
|
6688
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6689
|
+
pageSlug: text("page_slug").notNull(),
|
|
6690
|
+
kind: text("kind").notNull(),
|
|
6691
|
+
detail: text("detail").notNull().default(""),
|
|
6692
|
+
sourceRef: text("source_ref"),
|
|
6693
|
+
createdBy: text("created_by"),
|
|
6694
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6695
|
+
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
6696
|
+
resolvedBy: text("resolved_by")
|
|
6697
|
+
},
|
|
6698
|
+
(table) => [index("idx_wiki_page_flag_created").on(table.createdAt)]
|
|
6699
|
+
);
|
|
6700
|
+
pgTable(
|
|
6701
|
+
"wiki_page_evidence",
|
|
6702
|
+
{
|
|
6703
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6704
|
+
pageSlug: text("page_slug").notNull(),
|
|
6705
|
+
evidenceType: text("evidence_type").notNull(),
|
|
6706
|
+
evidenceRef: text("evidence_ref").notNull(),
|
|
6707
|
+
matchScore: real("match_score"),
|
|
6708
|
+
matchReason: text("match_reason"),
|
|
6709
|
+
snippet: text("snippet"),
|
|
6710
|
+
status: text("status").notNull().default("pending"),
|
|
6711
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6712
|
+
resolvedAt: timestamp("resolved_at", { withTimezone: true })
|
|
6713
|
+
},
|
|
6714
|
+
(table) => [
|
|
6715
|
+
uniqueIndex("wiki_page_evidence_page_slug_evidence_type_evidence_ref_key").on(
|
|
6716
|
+
table.pageSlug,
|
|
6717
|
+
table.evidenceType,
|
|
6718
|
+
table.evidenceRef
|
|
6719
|
+
),
|
|
6720
|
+
index("idx_wiki_page_evidence_created").on(table.createdAt)
|
|
6721
|
+
]
|
|
6722
|
+
);
|
|
6723
|
+
pgTable(
|
|
6724
|
+
"wiki_page_read",
|
|
6725
|
+
{
|
|
6726
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6727
|
+
slug: text("slug").notNull(),
|
|
6728
|
+
caller: text("caller"),
|
|
6729
|
+
includedArchive: boolean("included_archive").notNull().default(false),
|
|
6730
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
6731
|
+
},
|
|
6732
|
+
(table) => [
|
|
6733
|
+
index("idx_wiki_page_read_slug").on(table.slug, table.createdAt),
|
|
6734
|
+
index("idx_wiki_page_read_created").on(table.createdAt)
|
|
6735
|
+
]
|
|
6736
|
+
);
|
|
6737
|
+
pgTable(
|
|
6738
|
+
"wiki_verify_event",
|
|
6739
|
+
{
|
|
6740
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6741
|
+
pageSlug: text("page_slug").notNull(),
|
|
6742
|
+
outcome: text("outcome").notNull(),
|
|
6743
|
+
runMode: text("run_mode"),
|
|
6744
|
+
note: text("note"),
|
|
6745
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
6746
|
+
},
|
|
6747
|
+
(table) => [
|
|
6748
|
+
index("idx_wiki_verify_event_created").on(table.createdAt),
|
|
6749
|
+
index("idx_wiki_verify_event_outcome_created").on(
|
|
6750
|
+
table.outcome,
|
|
6751
|
+
table.createdAt
|
|
6752
|
+
)
|
|
6753
|
+
]
|
|
6754
|
+
);
|
|
6755
|
+
var wikiActor = pgTable(
|
|
6756
|
+
"wiki_actor",
|
|
6757
|
+
{
|
|
6758
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6759
|
+
displayName: text("display_name").notNull(),
|
|
6760
|
+
kind: text("kind").notNull().default("external"),
|
|
6761
|
+
notes: text("notes"),
|
|
6762
|
+
wikiPageSlug: text("wiki_page_slug"),
|
|
6763
|
+
refrontCustomerId: uuid("refront_customer_id"),
|
|
6764
|
+
refrontMemberId: uuid("refront_member_id"),
|
|
6765
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6766
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6767
|
+
},
|
|
6768
|
+
(table) => [
|
|
6769
|
+
index("idx_wiki_actor_kind").on(table.kind),
|
|
6770
|
+
index("idx_wiki_actor_wiki_page_slug").on(table.wikiPageSlug)
|
|
6771
|
+
]
|
|
6772
|
+
);
|
|
6773
|
+
pgTable(
|
|
6774
|
+
"wiki_actor_identity",
|
|
6775
|
+
{
|
|
6776
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6777
|
+
actorId: uuid("actor_id").notNull().references(() => wikiActor.id, { onDelete: "cascade" }),
|
|
6778
|
+
identityHash: text("identity_hash").notNull(),
|
|
6779
|
+
kind: text("kind").notNull(),
|
|
6780
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
6781
|
+
},
|
|
6782
|
+
(table) => [
|
|
6783
|
+
uniqueIndex("wiki_actor_identity_kind_hash").on(
|
|
6784
|
+
table.kind,
|
|
6785
|
+
table.identityHash
|
|
6786
|
+
),
|
|
6787
|
+
index("idx_wiki_actor_identity_actor").on(table.actorId)
|
|
6788
|
+
]
|
|
6789
|
+
);
|
|
6790
|
+
pgTable(
|
|
6791
|
+
"wiki_actor_affiliation",
|
|
6792
|
+
{
|
|
6793
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6794
|
+
actorId: uuid("actor_id").notNull().references(() => wikiActor.id, { onDelete: "cascade" }),
|
|
6795
|
+
customerFolder: text("customer_folder"),
|
|
6796
|
+
refrontCustomerId: uuid("refront_customer_id"),
|
|
6797
|
+
refrontProjectId: uuid("refront_project_id"),
|
|
6798
|
+
affiliationKey: text("affiliation_key").notNull().unique(),
|
|
6799
|
+
signalCount: integer("signal_count").notNull().default(1),
|
|
6800
|
+
firstSeen: timestamp("first_seen", { withTimezone: true }).notNull().defaultNow(),
|
|
6801
|
+
lastSeen: timestamp("last_seen", { withTimezone: true }).notNull().defaultNow(),
|
|
6802
|
+
sampleSources: text("sample_sources").array().notNull().default([]),
|
|
6803
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
6804
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6805
|
+
},
|
|
6806
|
+
(table) => [
|
|
6807
|
+
index("idx_wiki_actor_affiliation_actor").on(table.actorId),
|
|
6808
|
+
index("idx_wiki_actor_affiliation_customer_folder").on(
|
|
6809
|
+
table.customerFolder
|
|
6810
|
+
)
|
|
6811
|
+
]
|
|
6812
|
+
);
|
|
6813
|
+
pgTable(
|
|
6814
|
+
"intake_identity_unresolved",
|
|
6815
|
+
{
|
|
6816
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
6817
|
+
identityHash: text("identity_hash").notNull(),
|
|
6818
|
+
kind: text("kind").notNull(),
|
|
6819
|
+
seenCount: integer("seen_count").notNull().default(1),
|
|
6820
|
+
sampleSignalIds: uuid("sample_signal_ids").array().notNull().default([]),
|
|
6821
|
+
sampleContext: text("sample_context").array().notNull().default([]),
|
|
6822
|
+
sampleSources: text("sample_sources").array().notNull().default([]),
|
|
6823
|
+
lastSeen: timestamp("last_seen", { withTimezone: true }).notNull().defaultNow(),
|
|
6824
|
+
insightFiledAt: timestamp("insight_filed_at", { withTimezone: true }),
|
|
6825
|
+
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
6826
|
+
resolvedActorId: uuid("resolved_actor_id").references(() => wikiActor.id, {
|
|
6827
|
+
onDelete: "set null"
|
|
6828
|
+
}),
|
|
6829
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
6830
|
+
},
|
|
6831
|
+
(table) => [
|
|
6832
|
+
uniqueIndex("intake_identity_unresolved_kind_hash").on(
|
|
6833
|
+
table.kind,
|
|
6834
|
+
table.identityHash
|
|
6835
|
+
)
|
|
6836
|
+
]
|
|
6837
|
+
);
|
|
6541
6838
|
var GITHUB_API = "https://api.github.com";
|
|
6839
|
+
var TRANSIENT_GITHUB_STATUSES = /* @__PURE__ */ new Set([
|
|
6840
|
+
429,
|
|
6841
|
+
502,
|
|
6842
|
+
503,
|
|
6843
|
+
504,
|
|
6844
|
+
520,
|
|
6845
|
+
521,
|
|
6846
|
+
522,
|
|
6847
|
+
523,
|
|
6848
|
+
524
|
|
6849
|
+
]);
|
|
6850
|
+
var DEFAULT_BUDGET_PER_MINUTE = 75;
|
|
6851
|
+
var BUDGET_STATE_KEY = "github_api_budget_per_minute";
|
|
6852
|
+
var DEFAULT_BLOCK_MS = 15 * 60 * 1e3;
|
|
6853
|
+
var MAX_ATTEMPTS = 4;
|
|
6854
|
+
var BASE_DELAY_MS = 750;
|
|
6855
|
+
var RATE_LIMIT_FLOOR = 50;
|
|
6856
|
+
var BUDGET_WAIT_ATTEMPTS = 4;
|
|
6857
|
+
var BUDGET_WAIT_CAP_MS = 15e3;
|
|
6858
|
+
var GitHubGatewayBlockedError = class extends Error {
|
|
6859
|
+
blockedUntil;
|
|
6860
|
+
constructor(blockedUntil, message) {
|
|
6861
|
+
super(
|
|
6862
|
+
message ?? `GitHub API circuit breaker open until ${blockedUntil.toISOString()}`
|
|
6863
|
+
);
|
|
6864
|
+
this.name = "GitHubGatewayBlockedError";
|
|
6865
|
+
this.blockedUntil = blockedUntil;
|
|
6866
|
+
}
|
|
6867
|
+
};
|
|
6868
|
+
var GitHubGatewayPausedError = class extends Error {
|
|
6869
|
+
constructor() {
|
|
6870
|
+
super("GitHub sync is paused (github_sync_paused app_setting)");
|
|
6871
|
+
this.name = "GitHubGatewayPausedError";
|
|
6872
|
+
}
|
|
6873
|
+
};
|
|
6874
|
+
var GitHubGatewayBudgetError = class extends Error {
|
|
6875
|
+
constructor(tokenKey) {
|
|
6876
|
+
super(`GitHub API budget exhausted for token ${tokenKey}`);
|
|
6877
|
+
this.name = "GitHubGatewayBudgetError";
|
|
6878
|
+
}
|
|
6879
|
+
};
|
|
6880
|
+
function sleep(ms) {
|
|
6881
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6882
|
+
}
|
|
6883
|
+
function summarizeGitHubErrorBody(body, maxLen = 240) {
|
|
6884
|
+
const trimmed = body.trim();
|
|
6885
|
+
if (!trimmed) return "";
|
|
6886
|
+
const looksLikeHtml = trimmed.startsWith("<!") || trimmed.startsWith("<html") || /<title>\s*Unicorn!/i.test(trimmed);
|
|
6887
|
+
if (looksLikeHtml) return "[GitHub HTML error page truncated]";
|
|
6888
|
+
if (trimmed.length <= maxLen) return trimmed;
|
|
6889
|
+
return `${trimmed.slice(0, maxLen)}\u2026`;
|
|
6890
|
+
}
|
|
6891
|
+
function isTransientGitHubStatus(status) {
|
|
6892
|
+
return TRANSIENT_GITHUB_STATUSES.has(status);
|
|
6893
|
+
}
|
|
6894
|
+
function hourBucket(now = /* @__PURE__ */ new Date()) {
|
|
6895
|
+
return new Date(
|
|
6896
|
+
Date.UTC(
|
|
6897
|
+
now.getUTCFullYear(),
|
|
6898
|
+
now.getUTCMonth(),
|
|
6899
|
+
now.getUTCDate(),
|
|
6900
|
+
now.getUTCHours(),
|
|
6901
|
+
0,
|
|
6902
|
+
0,
|
|
6903
|
+
0
|
|
6904
|
+
)
|
|
6905
|
+
);
|
|
6906
|
+
}
|
|
6907
|
+
function cacheKeyFor(url, tokenKey) {
|
|
6908
|
+
return createHash("sha256").update(`${tokenKey}:${url}`).digest("hex");
|
|
6909
|
+
}
|
|
6910
|
+
async function isGithubSyncPaused() {
|
|
6911
|
+
try {
|
|
6912
|
+
const rows = await getDb().execute(sql`
|
|
6913
|
+
SELECT value FROM wiki_sync_state
|
|
6914
|
+
WHERE key = 'github_sync_paused'
|
|
6915
|
+
LIMIT 1
|
|
6916
|
+
`);
|
|
6917
|
+
return Boolean(rows[0]?.value?.paused);
|
|
6918
|
+
} catch {
|
|
6919
|
+
return false;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
async function getBlockedUntil(tokenKey) {
|
|
6923
|
+
const rows = await getDb().execute(
|
|
6924
|
+
sql`
|
|
6925
|
+
SELECT blocked_until FROM github_api_budget
|
|
6926
|
+
WHERE token_key = ${tokenKey}
|
|
6927
|
+
LIMIT 1
|
|
6928
|
+
`
|
|
6929
|
+
);
|
|
6930
|
+
const raw = rows[0]?.blocked_until;
|
|
6931
|
+
if (!raw) return null;
|
|
6932
|
+
const date2 = typeof raw === "string" ? new Date(raw) : raw;
|
|
6933
|
+
if (Number.isNaN(date2.getTime()) || date2.getTime() <= Date.now()) return null;
|
|
6934
|
+
return date2;
|
|
6935
|
+
}
|
|
6936
|
+
async function setGithubCircuitBreaker(tokenKey, blockedUntil) {
|
|
6937
|
+
await getDb().execute(sql`
|
|
6938
|
+
INSERT INTO github_api_budget (token_key, blocked_until, updated_at)
|
|
6939
|
+
VALUES (${tokenKey}, ${blockedUntil.toISOString()}::timestamptz, now())
|
|
6940
|
+
ON CONFLICT (token_key) DO UPDATE SET
|
|
6941
|
+
blocked_until = EXCLUDED.blocked_until,
|
|
6942
|
+
updated_at = now()
|
|
6943
|
+
`);
|
|
6944
|
+
}
|
|
6945
|
+
async function tryConsumeBudget(tokenKey, budgetPerMinute) {
|
|
6946
|
+
const updated = await getDb().execute(sql`
|
|
6947
|
+
UPDATE github_api_budget
|
|
6948
|
+
SET
|
|
6949
|
+
window_started_at = CASE
|
|
6950
|
+
WHEN window_started_at < now() - interval '60 seconds'
|
|
6951
|
+
THEN now()
|
|
6952
|
+
ELSE window_started_at
|
|
6953
|
+
END,
|
|
6954
|
+
request_count = CASE
|
|
6955
|
+
WHEN window_started_at < now() - interval '60 seconds'
|
|
6956
|
+
THEN 1
|
|
6957
|
+
ELSE request_count + 1
|
|
6958
|
+
END,
|
|
6959
|
+
updated_at = now()
|
|
6960
|
+
WHERE token_key = ${tokenKey}
|
|
6961
|
+
AND (
|
|
6962
|
+
window_started_at < now() - interval '60 seconds'
|
|
6963
|
+
OR request_count < ${budgetPerMinute}
|
|
6964
|
+
)
|
|
6965
|
+
RETURNING request_count
|
|
6966
|
+
`);
|
|
6967
|
+
if (updated[0]) return true;
|
|
6968
|
+
const inserted = await getDb().execute(sql`
|
|
6969
|
+
INSERT INTO github_api_budget (token_key, window_started_at, request_count, updated_at)
|
|
6970
|
+
VALUES (${tokenKey}, now(), 1, now())
|
|
6971
|
+
ON CONFLICT (token_key) DO NOTHING
|
|
6972
|
+
RETURNING request_count
|
|
6973
|
+
`);
|
|
6974
|
+
if (inserted[0]) return true;
|
|
6975
|
+
return false;
|
|
6976
|
+
}
|
|
6977
|
+
async function msUntilBudgetWindowReset(tokenKey) {
|
|
6978
|
+
const rows = await getDb().execute(sql`
|
|
6979
|
+
SELECT window_started_at FROM github_api_budget
|
|
6980
|
+
WHERE token_key = ${tokenKey}
|
|
6981
|
+
LIMIT 1
|
|
6982
|
+
`);
|
|
6983
|
+
const raw = rows[0]?.window_started_at;
|
|
6984
|
+
if (!raw) return 1e3;
|
|
6985
|
+
const started = typeof raw === "string" ? new Date(raw).getTime() : raw.getTime();
|
|
6986
|
+
if (!Number.isFinite(started)) return 1e3;
|
|
6987
|
+
const resetAt = started + 6e4;
|
|
6988
|
+
return Math.max(250, resetAt - Date.now());
|
|
6989
|
+
}
|
|
6990
|
+
async function resolveGithubBudgetPerMinute(override) {
|
|
6991
|
+
if (typeof override === "number" && override > 0) return Math.floor(override);
|
|
6992
|
+
const fromEnv = Number(process.env.GITHUB_API_BUDGET_PER_MINUTE);
|
|
6993
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0) return Math.floor(fromEnv);
|
|
6994
|
+
try {
|
|
6995
|
+
const rows = await getDb().execute(sql`
|
|
6996
|
+
SELECT value FROM wiki_sync_state
|
|
6997
|
+
WHERE key = ${BUDGET_STATE_KEY}
|
|
6998
|
+
LIMIT 1
|
|
6999
|
+
`);
|
|
7000
|
+
const perMinute = rows[0]?.value?.perMinute;
|
|
7001
|
+
if (typeof perMinute === "number" && perMinute > 0) {
|
|
7002
|
+
return Math.floor(perMinute);
|
|
7003
|
+
}
|
|
7004
|
+
} catch {
|
|
7005
|
+
}
|
|
7006
|
+
return DEFAULT_BUDGET_PER_MINUTE;
|
|
7007
|
+
}
|
|
7008
|
+
async function consumeBudgetOrWait(input) {
|
|
7009
|
+
for (let attempt = 1; attempt <= BUDGET_WAIT_ATTEMPTS; attempt++) {
|
|
7010
|
+
if (await tryConsumeBudget(input.tokenKey, input.budgetPerMinute)) {
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
if (input.priority === "interactive") {
|
|
7014
|
+
await sleep(BASE_DELAY_MS);
|
|
7015
|
+
continue;
|
|
7016
|
+
}
|
|
7017
|
+
if (attempt === BUDGET_WAIT_ATTEMPTS) {
|
|
7018
|
+
throw new GitHubGatewayBudgetError(input.tokenKey);
|
|
7019
|
+
}
|
|
7020
|
+
const waitMs = Math.min(
|
|
7021
|
+
await msUntilBudgetWindowReset(input.tokenKey),
|
|
7022
|
+
BUDGET_WAIT_CAP_MS
|
|
7023
|
+
);
|
|
7024
|
+
await sleep(waitMs);
|
|
7025
|
+
}
|
|
7026
|
+
throw new GitHubGatewayBudgetError(input.tokenKey);
|
|
7027
|
+
}
|
|
7028
|
+
async function recordStat(input) {
|
|
7029
|
+
const bucket = hourBucket().toISOString();
|
|
7030
|
+
await getDb().execute(sql`
|
|
7031
|
+
INSERT INTO github_api_stat (
|
|
7032
|
+
token_key, job_id, hour_bucket, request_count, error_count, cache_hit_count, updated_at
|
|
7033
|
+
)
|
|
7034
|
+
VALUES (
|
|
7035
|
+
${input.tokenKey},
|
|
7036
|
+
${input.jobId},
|
|
7037
|
+
${bucket}::timestamptz,
|
|
7038
|
+
1,
|
|
7039
|
+
${input.error ? 1 : 0},
|
|
7040
|
+
${input.cacheHit ? 1 : 0},
|
|
7041
|
+
now()
|
|
7042
|
+
)
|
|
7043
|
+
ON CONFLICT (token_key, job_id, hour_bucket) DO UPDATE SET
|
|
7044
|
+
request_count = github_api_stat.request_count + 1,
|
|
7045
|
+
error_count = github_api_stat.error_count + ${input.error ? 1 : 0},
|
|
7046
|
+
cache_hit_count = github_api_stat.cache_hit_count + ${input.cacheHit ? 1 : 0},
|
|
7047
|
+
updated_at = now()
|
|
7048
|
+
`);
|
|
7049
|
+
}
|
|
7050
|
+
async function loadEtagCache(key) {
|
|
7051
|
+
const rows = await getDb().execute(sql`
|
|
7052
|
+
SELECT etag, body, status FROM github_api_cache
|
|
7053
|
+
WHERE cache_key = ${key}
|
|
7054
|
+
LIMIT 1
|
|
7055
|
+
`);
|
|
7056
|
+
return rows[0] ?? null;
|
|
7057
|
+
}
|
|
7058
|
+
async function saveEtagCache(input) {
|
|
7059
|
+
await getDb().execute(sql`
|
|
7060
|
+
INSERT INTO github_api_cache (
|
|
7061
|
+
cache_key, etag, body, status, content_type, updated_at
|
|
7062
|
+
)
|
|
7063
|
+
VALUES (
|
|
7064
|
+
${input.key},
|
|
7065
|
+
${input.etag},
|
|
7066
|
+
${input.body},
|
|
7067
|
+
${input.status},
|
|
7068
|
+
${input.contentType},
|
|
7069
|
+
now()
|
|
7070
|
+
)
|
|
7071
|
+
ON CONFLICT (cache_key) DO UPDATE SET
|
|
7072
|
+
etag = EXCLUDED.etag,
|
|
7073
|
+
body = EXCLUDED.body,
|
|
7074
|
+
status = EXCLUDED.status,
|
|
7075
|
+
content_type = EXCLUDED.content_type,
|
|
7076
|
+
updated_at = now()
|
|
7077
|
+
`);
|
|
7078
|
+
}
|
|
7079
|
+
async function updateRateLimitHeaders(tokenKey, response) {
|
|
7080
|
+
const remaining = Number(response.headers.get("x-ratelimit-remaining"));
|
|
7081
|
+
const reset = Number(response.headers.get("x-ratelimit-reset"));
|
|
7082
|
+
if (!Number.isFinite(remaining) && !Number.isFinite(reset)) return;
|
|
7083
|
+
await getDb().execute(sql`
|
|
7084
|
+
INSERT INTO github_api_budget (
|
|
7085
|
+
token_key, last_remaining, last_reset_at, updated_at
|
|
7086
|
+
)
|
|
7087
|
+
VALUES (
|
|
7088
|
+
${tokenKey},
|
|
7089
|
+
${Number.isFinite(remaining) ? remaining : null},
|
|
7090
|
+
${Number.isFinite(reset) ? sql`to_timestamp(${reset})` : sql`NULL`},
|
|
7091
|
+
now()
|
|
7092
|
+
)
|
|
7093
|
+
ON CONFLICT (token_key) DO UPDATE SET
|
|
7094
|
+
last_remaining = COALESCE(EXCLUDED.last_remaining, github_api_budget.last_remaining),
|
|
7095
|
+
last_reset_at = COALESCE(EXCLUDED.last_reset_at, github_api_budget.last_reset_at),
|
|
7096
|
+
updated_at = now()
|
|
7097
|
+
`);
|
|
7098
|
+
if (Number.isFinite(remaining) && remaining < RATE_LIMIT_FLOOR) {
|
|
7099
|
+
const until = Number.isFinite(reset) ? new Date(reset * 1e3) : new Date(Date.now() + DEFAULT_BLOCK_MS);
|
|
7100
|
+
if (until.getTime() > Date.now()) {
|
|
7101
|
+
await setGithubCircuitBreaker(tokenKey, until);
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
function tripBreakerFromResponse(response) {
|
|
7106
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
7107
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
7108
|
+
return new Date(Date.now() + retryAfter * 1e3);
|
|
7109
|
+
}
|
|
7110
|
+
const reset = Number(response.headers.get("x-ratelimit-reset"));
|
|
7111
|
+
if (Number.isFinite(reset) && reset * 1e3 > Date.now()) {
|
|
7112
|
+
return new Date(reset * 1e3);
|
|
7113
|
+
}
|
|
7114
|
+
return new Date(Date.now() + DEFAULT_BLOCK_MS);
|
|
7115
|
+
}
|
|
7116
|
+
async function githubGatewayFetch(url, init, options) {
|
|
7117
|
+
const priority = options.priority ?? "background";
|
|
7118
|
+
const budgetPerMinute = await resolveGithubBudgetPerMinute(
|
|
7119
|
+
options.budgetPerMinute
|
|
7120
|
+
);
|
|
7121
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
7122
|
+
const useCache = options.useEtagCache !== false && method === "GET";
|
|
7123
|
+
if (priority !== "interactive" && await isGithubSyncPaused()) {
|
|
7124
|
+
throw new GitHubGatewayPausedError();
|
|
7125
|
+
}
|
|
7126
|
+
const blocked = await getBlockedUntil(options.tokenKey);
|
|
7127
|
+
if (blocked) {
|
|
7128
|
+
if (priority === "interactive") {
|
|
7129
|
+
if (blocked.getTime() - Date.now() > 6e4) {
|
|
7130
|
+
throw new GitHubGatewayBlockedError(blocked);
|
|
7131
|
+
}
|
|
7132
|
+
} else {
|
|
7133
|
+
throw new GitHubGatewayBlockedError(blocked);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
await consumeBudgetOrWait({
|
|
7137
|
+
tokenKey: options.tokenKey,
|
|
7138
|
+
budgetPerMinute,
|
|
7139
|
+
priority
|
|
7140
|
+
});
|
|
7141
|
+
const headers = new Headers(init.headers);
|
|
7142
|
+
if (!headers.has("Authorization")) {
|
|
7143
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
7144
|
+
}
|
|
7145
|
+
if (!headers.has("Accept")) {
|
|
7146
|
+
headers.set("Accept", "application/vnd.github+json");
|
|
7147
|
+
}
|
|
7148
|
+
if (!headers.has("X-GitHub-Api-Version")) {
|
|
7149
|
+
headers.set("X-GitHub-Api-Version", "2022-11-28");
|
|
7150
|
+
}
|
|
7151
|
+
const key = useCache ? cacheKeyFor(url, options.tokenKey) : null;
|
|
7152
|
+
if (key) {
|
|
7153
|
+
const cached = await loadEtagCache(key);
|
|
7154
|
+
if (cached?.etag) headers.set("If-None-Match", cached.etag);
|
|
7155
|
+
}
|
|
7156
|
+
let lastError = null;
|
|
7157
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
7158
|
+
const response = await fetch(url, { ...init, headers });
|
|
7159
|
+
await updateRateLimitHeaders(options.tokenKey, response);
|
|
7160
|
+
if (response.status === 304 && key) {
|
|
7161
|
+
const cached = await loadEtagCache(key);
|
|
7162
|
+
await recordStat({
|
|
7163
|
+
tokenKey: options.tokenKey,
|
|
7164
|
+
jobId: options.jobId,
|
|
7165
|
+
cacheHit: true
|
|
7166
|
+
});
|
|
7167
|
+
return new Response(cached?.body ?? "", {
|
|
7168
|
+
status: cached?.status ?? 200,
|
|
7169
|
+
headers: {
|
|
7170
|
+
"x-github-gateway-cache": "hit",
|
|
7171
|
+
"content-type": "application/json"
|
|
7172
|
+
}
|
|
7173
|
+
});
|
|
7174
|
+
}
|
|
7175
|
+
if (response.status === 403 || response.status === 429 || isTransientGitHubStatus(response.status)) {
|
|
7176
|
+
const bodyText = summarizeGitHubErrorBody(
|
|
7177
|
+
await response.clone().text().catch(() => "")
|
|
7178
|
+
);
|
|
7179
|
+
const secondary = response.status === 403 && /secondary rate limit|abuse detection/i.test(bodyText);
|
|
7180
|
+
if (response.status === 429 || secondary || isTransientGitHubStatus(response.status)) {
|
|
7181
|
+
const until = tripBreakerFromResponse(response);
|
|
7182
|
+
await setGithubCircuitBreaker(options.tokenKey, until);
|
|
7183
|
+
lastError = new Error(
|
|
7184
|
+
`GitHub ${response.status}${bodyText ? `: ${bodyText}` : ""}`
|
|
7185
|
+
);
|
|
7186
|
+
await recordStat({
|
|
7187
|
+
tokenKey: options.tokenKey,
|
|
7188
|
+
jobId: options.jobId,
|
|
7189
|
+
error: true
|
|
7190
|
+
});
|
|
7191
|
+
if (attempt === MAX_ATTEMPTS || secondary) {
|
|
7192
|
+
throw new GitHubGatewayBlockedError(until, lastError.message);
|
|
7193
|
+
}
|
|
7194
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
7195
|
+
const delayMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
7196
|
+
await sleep(delayMs);
|
|
7197
|
+
continue;
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
if (key && response.ok) {
|
|
7201
|
+
const etag = response.headers.get("etag");
|
|
7202
|
+
const body = await response.clone().text();
|
|
7203
|
+
await saveEtagCache({
|
|
7204
|
+
key,
|
|
7205
|
+
etag,
|
|
7206
|
+
body,
|
|
7207
|
+
status: response.status,
|
|
7208
|
+
contentType: response.headers.get("content-type")
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
await recordStat({
|
|
7212
|
+
tokenKey: options.tokenKey,
|
|
7213
|
+
jobId: options.jobId,
|
|
7214
|
+
error: !response.ok
|
|
7215
|
+
});
|
|
7216
|
+
return response;
|
|
7217
|
+
}
|
|
7218
|
+
throw lastError ?? new Error("GitHub request failed after retries");
|
|
7219
|
+
}
|
|
7220
|
+
function gatewayOptions(token) {
|
|
7221
|
+
return {
|
|
7222
|
+
tokenKey: "default",
|
|
7223
|
+
token,
|
|
7224
|
+
jobId: "release-profile-quick-create",
|
|
7225
|
+
priority: "background"
|
|
7226
|
+
};
|
|
7227
|
+
}
|
|
6542
7228
|
var ReleaseProfileQuickCreateError = class extends Error {
|
|
6543
7229
|
constructor(message, code = "INTERNAL_SERVER_ERROR") {
|
|
6544
7230
|
super(message);
|
|
@@ -6565,18 +7251,20 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
|
|
|
6565
7251
|
"Content-Type": "application/json"
|
|
6566
7252
|
};
|
|
6567
7253
|
async function fetchContents(path) {
|
|
6568
|
-
const res = await
|
|
7254
|
+
const res = await githubGatewayFetch(
|
|
6569
7255
|
`${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
|
|
6570
|
-
{ headers }
|
|
7256
|
+
{ headers },
|
|
7257
|
+
gatewayOptions(githubToken2)
|
|
6571
7258
|
);
|
|
6572
7259
|
if (!res.ok) return null;
|
|
6573
7260
|
const data = await res.json();
|
|
6574
7261
|
return Array.isArray(data) ? data : null;
|
|
6575
7262
|
}
|
|
6576
7263
|
async function fileExists(path) {
|
|
6577
|
-
const res = await
|
|
7264
|
+
const res = await githubGatewayFetch(
|
|
6578
7265
|
`${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
|
|
6579
|
-
{ headers, method: "HEAD" }
|
|
7266
|
+
{ headers, method: "HEAD" },
|
|
7267
|
+
gatewayOptions(githubToken2)
|
|
6580
7268
|
);
|
|
6581
7269
|
return res.ok;
|
|
6582
7270
|
}
|
|
@@ -6713,31 +7401,36 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
|
|
|
6713
7401
|
Authorization: `Bearer ${githubToken2}`,
|
|
6714
7402
|
"Content-Type": "application/json"
|
|
6715
7403
|
};
|
|
6716
|
-
const checkRes = await
|
|
7404
|
+
const checkRes = await githubGatewayFetch(
|
|
6717
7405
|
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${branchName}`,
|
|
6718
|
-
{ headers }
|
|
7406
|
+
{ headers },
|
|
7407
|
+
gatewayOptions(githubToken2)
|
|
6719
7408
|
);
|
|
6720
7409
|
if (checkRes.ok) return;
|
|
6721
7410
|
let sha;
|
|
6722
|
-
const sourceRes = await
|
|
7411
|
+
const sourceRes = await githubGatewayFetch(
|
|
6723
7412
|
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
|
|
6724
|
-
{ headers }
|
|
7413
|
+
{ headers },
|
|
7414
|
+
gatewayOptions(githubToken2)
|
|
6725
7415
|
);
|
|
6726
7416
|
if (sourceRes.ok) {
|
|
6727
7417
|
const data = await sourceRes.json();
|
|
6728
7418
|
sha = data.object?.sha;
|
|
6729
7419
|
} else {
|
|
6730
|
-
const repoRes = await
|
|
6731
|
-
|
|
6732
|
-
|
|
7420
|
+
const repoRes = await githubGatewayFetch(
|
|
7421
|
+
`${GITHUB_API}/repos/${repoFullName}`,
|
|
7422
|
+
{ headers },
|
|
7423
|
+
gatewayOptions(githubToken2)
|
|
7424
|
+
);
|
|
6733
7425
|
if (!repoRes.ok) {
|
|
6734
7426
|
throw new Error(`Could not access repository ${repoFullName}`);
|
|
6735
7427
|
}
|
|
6736
7428
|
const repo = await repoRes.json();
|
|
6737
7429
|
const defaultBranch = repo.default_branch ?? "main";
|
|
6738
|
-
const defaultRes = await
|
|
7430
|
+
const defaultRes = await githubGatewayFetch(
|
|
6739
7431
|
`${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
|
|
6740
|
-
{ headers }
|
|
7432
|
+
{ headers },
|
|
7433
|
+
gatewayOptions(githubToken2)
|
|
6741
7434
|
);
|
|
6742
7435
|
if (!defaultRes.ok) {
|
|
6743
7436
|
throw new Error(`Could not resolve a source branch for ${repoFullName}`);
|
|
@@ -6748,13 +7441,14 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
|
|
|
6748
7441
|
if (!sha) {
|
|
6749
7442
|
throw new Error(`Could not determine SHA to create branch ${branchName}`);
|
|
6750
7443
|
}
|
|
6751
|
-
const createRes = await
|
|
7444
|
+
const createRes = await githubGatewayFetch(
|
|
6752
7445
|
`${GITHUB_API}/repos/${repoFullName}/git/refs`,
|
|
6753
7446
|
{
|
|
6754
7447
|
method: "POST",
|
|
6755
7448
|
headers,
|
|
6756
7449
|
body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha })
|
|
6757
|
-
}
|
|
7450
|
+
},
|
|
7451
|
+
gatewayOptions(githubToken2)
|
|
6758
7452
|
);
|
|
6759
7453
|
if (!createRes.ok && createRes.status !== 422) {
|
|
6760
7454
|
const body = await createRes.text().catch(() => "");
|
|
@@ -7941,7 +8635,7 @@ function getEncryptionKey() {
|
|
|
7941
8635
|
throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
|
|
7942
8636
|
return buf;
|
|
7943
8637
|
}
|
|
7944
|
-
function encrypt(
|
|
8638
|
+
function encrypt(text16) {
|
|
7945
8639
|
const key = getEncryptionKey();
|
|
7946
8640
|
const iv = randomBytes(ENC_IV_LENGTH);
|
|
7947
8641
|
const cipher = createCipheriv(
|
|
@@ -7949,7 +8643,7 @@ function encrypt(text13) {
|
|
|
7949
8643
|
new Uint8Array(key),
|
|
7950
8644
|
new Uint8Array(iv)
|
|
7951
8645
|
);
|
|
7952
|
-
let encrypted = cipher.update(
|
|
8646
|
+
let encrypted = cipher.update(text16, "utf8", "hex");
|
|
7953
8647
|
encrypted += cipher.final("hex");
|
|
7954
8648
|
const authTag = cipher.getAuthTag();
|
|
7955
8649
|
return Buffer.concat([
|
|
@@ -8669,10 +9363,10 @@ async function r2GetObjectRange(bucket, key, range) {
|
|
|
8669
9363
|
const body = result.Body;
|
|
8670
9364
|
if (!body?.transformToString)
|
|
8671
9365
|
throw new Error("R2 returned no readable body");
|
|
8672
|
-
const
|
|
9366
|
+
const text16 = await body.transformToString();
|
|
8673
9367
|
const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
|
|
8674
9368
|
return `${header}
|
|
8675
|
-
${
|
|
9369
|
+
${text16}`;
|
|
8676
9370
|
} catch (e) {
|
|
8677
9371
|
throw r2WrapError(bucket, key, e);
|
|
8678
9372
|
}
|
|
@@ -9035,15 +9729,15 @@ async function sftpRead(opts, filePath, proxy, options) {
|
|
|
9035
9729
|
clearTimeout(timer);
|
|
9036
9730
|
cleanup?.();
|
|
9037
9731
|
cleanup = void 0;
|
|
9038
|
-
const
|
|
9732
|
+
const text16 = Buffer.concat(
|
|
9039
9733
|
chunks.map((ch) => new Uint8Array(ch))
|
|
9040
9734
|
).toString("utf-8");
|
|
9041
9735
|
if (!isWholeFileRequest) {
|
|
9042
9736
|
const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
|
|
9043
9737
|
resolve(`${header}
|
|
9044
|
-
${
|
|
9738
|
+
${text16}`);
|
|
9045
9739
|
} else {
|
|
9046
|
-
resolve(
|
|
9740
|
+
resolve(text16);
|
|
9047
9741
|
}
|
|
9048
9742
|
});
|
|
9049
9743
|
rs.on("error", (e) => {
|
|
@@ -9096,7 +9790,7 @@ var TOOL_CACHE_TTL_MS = {
|
|
|
9096
9790
|
"docker-list": 3e4
|
|
9097
9791
|
};
|
|
9098
9792
|
var TOOL_CACHE = /* @__PURE__ */ new Map();
|
|
9099
|
-
function
|
|
9793
|
+
function cacheKeyFor2(name, args2) {
|
|
9100
9794
|
const { noCache: _ignored, ...rest } = args2;
|
|
9101
9795
|
const sorted = Object.fromEntries(
|
|
9102
9796
|
Object.entries(rest).sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -9107,7 +9801,7 @@ function getCachedTool(name, args2) {
|
|
|
9107
9801
|
const ttl = TOOL_CACHE_TTL_MS[name];
|
|
9108
9802
|
if (!ttl || ttl <= 0) return void 0;
|
|
9109
9803
|
if (args2.noCache === true) return void 0;
|
|
9110
|
-
const k =
|
|
9804
|
+
const k = cacheKeyFor2(name, args2);
|
|
9111
9805
|
const e = TOOL_CACHE.get(k);
|
|
9112
9806
|
if (!e) return void 0;
|
|
9113
9807
|
if (e.expiresAt < Date.now()) {
|
|
@@ -9120,7 +9814,7 @@ function setCachedTool(name, args2, value) {
|
|
|
9120
9814
|
const ttl = TOOL_CACHE_TTL_MS[name];
|
|
9121
9815
|
if (!ttl || ttl <= 0) return;
|
|
9122
9816
|
if (args2.noCache === true) return;
|
|
9123
|
-
TOOL_CACHE.set(
|
|
9817
|
+
TOOL_CACHE.set(cacheKeyFor2(name, args2), {
|
|
9124
9818
|
value,
|
|
9125
9819
|
expiresAt: Date.now() + ttl
|
|
9126
9820
|
});
|
|
@@ -9190,11 +9884,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
|
|
|
9190
9884
|
if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
|
|
9191
9885
|
return e.names;
|
|
9192
9886
|
}
|
|
9193
|
-
function truncateForLLM(
|
|
9194
|
-
const totalBytes = Buffer.byteLength(
|
|
9887
|
+
function truncateForLLM(text16, maxBytes) {
|
|
9888
|
+
const totalBytes = Buffer.byteLength(text16, "utf8");
|
|
9195
9889
|
if (totalBytes <= maxBytes)
|
|
9196
|
-
return { text:
|
|
9197
|
-
const buf = Buffer.from(
|
|
9890
|
+
return { text: text16, truncated: false, totalBytes, shownBytes: totalBytes };
|
|
9891
|
+
const buf = Buffer.from(text16, "utf8");
|
|
9198
9892
|
let cut = maxBytes;
|
|
9199
9893
|
while (cut > 0 && (buf[cut] & 192) === 128) cut--;
|
|
9200
9894
|
const head = buf.subarray(0, cut).toString("utf8");
|
|
@@ -9225,10 +9919,10 @@ function postprocessResult(result, meta) {
|
|
|
9225
9919
|
if (!result.content?.length) return result;
|
|
9226
9920
|
if (RAW_JSON_TOOLS.has(meta.toolName)) return result;
|
|
9227
9921
|
const block = result.content[0];
|
|
9228
|
-
let
|
|
9229
|
-
const trunc = truncateForLLM(
|
|
9922
|
+
let text16 = String(block.text ?? "");
|
|
9923
|
+
const trunc = truncateForLLM(text16, RESPONSE_MAX_BYTES);
|
|
9230
9924
|
if (trunc.truncated) {
|
|
9231
|
-
|
|
9925
|
+
text16 = trunc.text + "\n\n... " + buildTruncationHint(
|
|
9232
9926
|
meta.toolName,
|
|
9233
9927
|
meta.args,
|
|
9234
9928
|
trunc.totalBytes,
|
|
@@ -9242,11 +9936,11 @@ function postprocessResult(result, meta) {
|
|
|
9242
9936
|
const parts = [`took ${tookStr}`, sizeStr];
|
|
9243
9937
|
if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
|
|
9244
9938
|
if (meta.cached) parts.push("cached");
|
|
9245
|
-
|
|
9939
|
+
text16 = `${text16}
|
|
9246
9940
|
|
|
9247
9941
|
[${parts.join(", ")}]`;
|
|
9248
9942
|
}
|
|
9249
|
-
return { ...result, content: [{ ...block, text:
|
|
9943
|
+
return { ...result, content: [{ ...block, text: text16 }] };
|
|
9250
9944
|
}
|
|
9251
9945
|
function buildPipelineScript(commands, shell, marker, stopOnError) {
|
|
9252
9946
|
if (shell === "powershell") {
|
|
@@ -10032,11 +10726,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
|
|
|
10032
10726
|
applied_by TEXT
|
|
10033
10727
|
);
|
|
10034
10728
|
`.trim();
|
|
10035
|
-
function normaliseMigrationSql(
|
|
10036
|
-
return
|
|
10729
|
+
function normaliseMigrationSql(sql29) {
|
|
10730
|
+
return sql29.replace(/\r\n/g, "\n").trim() + "\n";
|
|
10037
10731
|
}
|
|
10038
|
-
function migrationSha256(
|
|
10039
|
-
return createHash("sha256").update(
|
|
10732
|
+
function migrationSha256(sql29) {
|
|
10733
|
+
return createHash("sha256").update(sql29.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
10040
10734
|
}
|
|
10041
10735
|
function dollarQuoteTag(value) {
|
|
10042
10736
|
let tag = "_mcp";
|
|
@@ -10302,7 +10996,7 @@ var DNS_MIN_INTERVAL_MS = 750;
|
|
|
10302
10996
|
var DNS_RETRY_BACKOFF_MS = 1500;
|
|
10303
10997
|
var dnsZoneQueue = /* @__PURE__ */ new Map();
|
|
10304
10998
|
var dnsLastPutAt = /* @__PURE__ */ new Map();
|
|
10305
|
-
function
|
|
10999
|
+
function sleep2(ms) {
|
|
10306
11000
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10307
11001
|
}
|
|
10308
11002
|
async function withDnsZoneLock(domain, fn) {
|
|
@@ -10311,7 +11005,7 @@ async function withDnsZoneLock(domain, fn) {
|
|
|
10311
11005
|
const run = previous.then(async () => {
|
|
10312
11006
|
const last = dnsLastPutAt.get(key) ?? 0;
|
|
10313
11007
|
const wait = DNS_MIN_INTERVAL_MS - (Date.now() - last);
|
|
10314
|
-
if (wait > 0) await
|
|
11008
|
+
if (wait > 0) await sleep2(wait);
|
|
10315
11009
|
return fn();
|
|
10316
11010
|
});
|
|
10317
11011
|
const queued = run.finally(() => {
|
|
@@ -11753,6 +12447,32 @@ var TOOLS = [
|
|
|
11753
12447
|
}
|
|
11754
12448
|
}
|
|
11755
12449
|
},
|
|
12450
|
+
{
|
|
12451
|
+
name: "submit-team-memory",
|
|
12452
|
+
description: "Manually push a curated problem\u2192solution note into the team's searchable memory (agent_memory). Use ONLY when the user explicitly asks to push/share context to team memory (e.g. no agent-host daemon). Do NOT auto-submit. Do NOT dump raw chat. English; no secrets/PII. Pass repo for the current project. Distinct from wiki intake \u2014 this is searchable via search-team-memory.",
|
|
12453
|
+
inputSchema: {
|
|
12454
|
+
type: "object",
|
|
12455
|
+
properties: {
|
|
12456
|
+
title: {
|
|
12457
|
+
type: "string",
|
|
12458
|
+
description: "Short English title for the note (max 300 chars)."
|
|
12459
|
+
},
|
|
12460
|
+
content: {
|
|
12461
|
+
type: "string",
|
|
12462
|
+
description: "Problem / root cause / solution / context markdown (min ~40 chars, max ~24k stored)."
|
|
12463
|
+
},
|
|
12464
|
+
repo: {
|
|
12465
|
+
type: "string",
|
|
12466
|
+
description: "Git repository, e.g. 'github.com/MGSoftwareBV/fenicks' or 'github.com/MGSoftwareBV/mg-boiler'."
|
|
12467
|
+
},
|
|
12468
|
+
externalRef: {
|
|
12469
|
+
type: "string",
|
|
12470
|
+
description: "Optional client dedupe key; re-submitting the same ref upserts the row."
|
|
12471
|
+
}
|
|
12472
|
+
},
|
|
12473
|
+
required: ["title", "content", "repo"]
|
|
12474
|
+
}
|
|
12475
|
+
},
|
|
11756
12476
|
{
|
|
11757
12477
|
name: "search-wiki",
|
|
11758
12478
|
description: "Search the team's curated knowledge wiki (the distilled layer ABOVE team memory): compiled pages about how systems actually work, customer setups/quirks, and known recurring issues with their standard fixes. Check this BEFORE search-team-memory \u2014 it is smaller, curated, and deduplicated, so a hit here is usually the complete answer. Every page lists its `sources` (team-memory ids, ticket numbers, commits): follow those with get-team-memory for full raw detail instead of broad searches. Fall back to search-team-memory when nothing relevant comes back or the page's `updated` date looks stale.",
|
|
@@ -11777,13 +12497,17 @@ var TOOLS = [
|
|
|
11777
12497
|
},
|
|
11778
12498
|
{
|
|
11779
12499
|
name: "get-wiki-page",
|
|
11780
|
-
description: "Fetch one full knowledge-wiki page (markdown + frontmatter + sources) by the `slug` from a search-wiki result.
|
|
12500
|
+
description: "Fetch one full knowledge-wiki page (markdown + frontmatter + sources) by the `slug` from a search-wiki result. Active sources are capped (~10) and enriched with title/date for team-memory refs so you can pick 1\u20132 relevant transcripts via get-team-memory instead of fetching blindly. If the page itself does not answer your question, call again with `include_all_sources: true` to see the full active list plus the archive, then fetch only the refs you need. If an archived source held the real answer, flag the page with kind `archive-solution` + that `source_ref` so the reviewer can promote it.",
|
|
11781
12501
|
inputSchema: {
|
|
11782
12502
|
type: "object",
|
|
11783
12503
|
properties: {
|
|
11784
12504
|
slug: {
|
|
11785
12505
|
type: "string",
|
|
11786
12506
|
description: "Page slug from a search-wiki hit, e.g. 'wiki/engineering/team-memory'."
|
|
12507
|
+
},
|
|
12508
|
+
include_all_sources: {
|
|
12509
|
+
type: "boolean",
|
|
12510
|
+
description: "Return every active source plus the archived history (also enriched). Default false \u2014 compact active list only."
|
|
11787
12511
|
}
|
|
11788
12512
|
},
|
|
11789
12513
|
required: ["slug"]
|
|
@@ -11791,7 +12515,7 @@ var TOOLS = [
|
|
|
11791
12515
|
},
|
|
11792
12516
|
{
|
|
11793
12517
|
name: "flag-wiki-page",
|
|
11794
|
-
description: "Report that a knowledge-wiki page was WRONG, outdated or incomplete when you actually used it during real work. This is the wiki's feedback loop: open flags jump the daily reviewer queue and show in the backoffice review panel, so the page gets fixed instead of misleading the next agent. Call it right after discovering the mismatch, with a concrete `detail` of what was wrong and what turned out to be true.",
|
|
12518
|
+
description: "Report that a knowledge-wiki page was WRONG, outdated or incomplete when you actually used it during real work. This is the wiki's feedback loop: open flags jump the daily reviewer queue and show in the backoffice review panel, so the page gets fixed instead of misleading the next agent. Call it right after discovering the mismatch, with a concrete `detail` of what was wrong and what turned out to be true. Use kind `archive-solution` (with `source_ref`) when the page itself missed the answer but an archived source had it \u2014 the reviewer will promote that source and update the page.",
|
|
11795
12519
|
inputSchema: {
|
|
11796
12520
|
type: "object",
|
|
11797
12521
|
properties: {
|
|
@@ -11806,13 +12530,18 @@ var TOOLS = [
|
|
|
11806
12530
|
"stale",
|
|
11807
12531
|
"duplicate",
|
|
11808
12532
|
"unverified",
|
|
11809
|
-
"obsolete-candidate"
|
|
12533
|
+
"obsolete-candidate",
|
|
12534
|
+
"archive-solution"
|
|
11810
12535
|
],
|
|
11811
|
-
description: "feedback (default) = wrong/incomplete in practice; stale = outdated vs current reality; duplicate = covers the same thing as another page; obsolete-candidate = the described system/practice appears no longer in use."
|
|
12536
|
+
description: "feedback (default) = wrong/incomplete in practice; stale = outdated vs current reality; duplicate = covers the same thing as another page; obsolete-candidate = the described system/practice appears no longer in use; archive-solution = answer was found in an archived source (pass source_ref)."
|
|
11812
12537
|
},
|
|
11813
12538
|
detail: {
|
|
11814
12539
|
type: "string",
|
|
11815
12540
|
description: "What was wrong and what is actually true (be concrete: the claim, the observed reality, ticket/commit refs if any). Min 5 chars."
|
|
12541
|
+
},
|
|
12542
|
+
source_ref: {
|
|
12543
|
+
type: "string",
|
|
12544
|
+
description: "For archive-solution: the archived source that held the answer, e.g. 'team-memory:<uuid>' or 'ticket:2026-DASMG-032'."
|
|
11816
12545
|
}
|
|
11817
12546
|
},
|
|
11818
12547
|
required: ["slug", "detail"]
|
|
@@ -12127,7 +12856,7 @@ async function executeToolCall(name, a, _serverId) {
|
|
|
12127
12856
|
]
|
|
12128
12857
|
};
|
|
12129
12858
|
}
|
|
12130
|
-
const lines = data.hits.map((hit,
|
|
12859
|
+
const lines = data.hits.map((hit, index13) => {
|
|
12131
12860
|
const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
|
|
12132
12861
|
const repo = hit.repo ?? "unknown repo";
|
|
12133
12862
|
const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
|
|
@@ -12137,7 +12866,7 @@ async function executeToolCall(name, a, _serverId) {
|
|
|
12137
12866
|
...Array.isArray(hit.tech) ? hit.tech.slice(0, 4) : []
|
|
12138
12867
|
].filter(Boolean);
|
|
12139
12868
|
const tags = facets.length > 0 ? ` \xB7 ${facets.join(", ")}` : "";
|
|
12140
|
-
return `${
|
|
12869
|
+
return `${index13 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
|
|
12141
12870
|
id: ${hit.id}
|
|
12142
12871
|
${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
|
|
12143
12872
|
});
|
|
@@ -12239,6 +12968,71 @@ ${facets.join(", ")}` : ""),
|
|
|
12239
12968
|
]
|
|
12240
12969
|
};
|
|
12241
12970
|
}
|
|
12971
|
+
// ----- Team memory: manual curated submit (no daemon) -----
|
|
12972
|
+
case "submit-team-memory": {
|
|
12973
|
+
const title = typeof a.title === "string" ? a.title.trim() : "";
|
|
12974
|
+
const content = typeof a.content === "string" ? a.content.trim() : "";
|
|
12975
|
+
const repo = typeof a.repo === "string" ? a.repo.trim() : "";
|
|
12976
|
+
const externalRef = typeof a.externalRef === "string" ? a.externalRef.trim() : void 0;
|
|
12977
|
+
if (!title || !content || !repo) {
|
|
12978
|
+
return {
|
|
12979
|
+
content: [
|
|
12980
|
+
{
|
|
12981
|
+
type: "text",
|
|
12982
|
+
text: "Error: title, content, and repo are required."
|
|
12983
|
+
}
|
|
12984
|
+
]
|
|
12985
|
+
};
|
|
12986
|
+
}
|
|
12987
|
+
if (content.length < 40) {
|
|
12988
|
+
return {
|
|
12989
|
+
content: [
|
|
12990
|
+
{
|
|
12991
|
+
type: "text",
|
|
12992
|
+
text: "Error: content must be at least 40 characters (curated problem\u2192solution, not a one-liner)."
|
|
12993
|
+
}
|
|
12994
|
+
]
|
|
12995
|
+
};
|
|
12996
|
+
}
|
|
12997
|
+
const res = await fetch(`${dashboardBaseUrl}/api/team-memory/submit`, {
|
|
12998
|
+
method: "POST",
|
|
12999
|
+
headers: {
|
|
13000
|
+
"content-type": "application/json",
|
|
13001
|
+
authorization: `Bearer ${apiKey}`
|
|
13002
|
+
},
|
|
13003
|
+
body: JSON.stringify({
|
|
13004
|
+
title,
|
|
13005
|
+
content,
|
|
13006
|
+
gitRepository: repo,
|
|
13007
|
+
...externalRef ? { externalRef } : {}
|
|
13008
|
+
})
|
|
13009
|
+
});
|
|
13010
|
+
if (!res.ok) {
|
|
13011
|
+
const detail = await res.text().catch(() => "");
|
|
13012
|
+
return {
|
|
13013
|
+
content: [
|
|
13014
|
+
{
|
|
13015
|
+
type: "text",
|
|
13016
|
+
text: `Error: team-memory submit failed (${res.status}). ${detail.slice(0, 400)}`
|
|
13017
|
+
}
|
|
13018
|
+
]
|
|
13019
|
+
};
|
|
13020
|
+
}
|
|
13021
|
+
const data = await res.json();
|
|
13022
|
+
return {
|
|
13023
|
+
content: [
|
|
13024
|
+
{
|
|
13025
|
+
type: "text",
|
|
13026
|
+
text: `Submitted to team memory${data.upserted ? "" : " (unchanged \u2014 identical content)"}.
|
|
13027
|
+
id: ${data.id}
|
|
13028
|
+
localConversationId: ${data.localConversationId}
|
|
13029
|
+
repo: ${repo}
|
|
13030
|
+
summarized: ${data.summarized}, embedded: ${data.embedded}
|
|
13031
|
+
Searchable via search-team-memory once indexing completes (usually immediate).`
|
|
13032
|
+
}
|
|
13033
|
+
]
|
|
13034
|
+
};
|
|
13035
|
+
}
|
|
12242
13036
|
// ----- Knowledge wiki: search -----
|
|
12243
13037
|
case "search-wiki": {
|
|
12244
13038
|
const query = typeof a.query === "string" ? a.query.trim() : "";
|
|
@@ -12279,12 +13073,12 @@ ${facets.join(", ")}` : ""),
|
|
|
12279
13073
|
]
|
|
12280
13074
|
};
|
|
12281
13075
|
}
|
|
12282
|
-
const lines = data.hits.map((hit,
|
|
13076
|
+
const lines = data.hits.map((hit, index13) => {
|
|
12283
13077
|
const sim = hit.similarity !== null ? ` \xB7 ${Math.round(hit.similarity * 100)}% match` : "";
|
|
12284
13078
|
const tags = hit.tags.length ? ` \xB7 ${hit.tags.slice(0, 5).join(", ")}` : "";
|
|
12285
13079
|
const sources = hit.sources.length ? `
|
|
12286
13080
|
sources: ${hit.sources.slice(0, 6).map((s) => `${s.type}:${s.ref}`).join(", ")}` : "";
|
|
12287
|
-
return `${
|
|
13081
|
+
return `${index13 + 1}. [${hit.status} \xB7 updated ${hit.pageUpdatedOn ?? "unknown"}${sim}${tags}] ${hit.title}
|
|
12288
13082
|
slug: ${hit.slug}${sources}
|
|
12289
13083
|
${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 400)}`;
|
|
12290
13084
|
});
|
|
@@ -12306,13 +13100,17 @@ Call get-wiki-page with a slug for the full page; follow its sources (team-memor
|
|
|
12306
13100
|
if (!slug) {
|
|
12307
13101
|
return { content: [{ type: "text", text: "Error: slug is required" }] };
|
|
12308
13102
|
}
|
|
13103
|
+
const includeAllSources = a.include_all_sources === true;
|
|
12309
13104
|
const res = await fetch(`${dashboardBaseUrl}/api/wiki/page`, {
|
|
12310
13105
|
method: "POST",
|
|
12311
13106
|
headers: {
|
|
12312
13107
|
"content-type": "application/json",
|
|
12313
13108
|
authorization: `Bearer ${apiKey}`
|
|
12314
13109
|
},
|
|
12315
|
-
body: JSON.stringify({
|
|
13110
|
+
body: JSON.stringify({
|
|
13111
|
+
slug,
|
|
13112
|
+
include_all_sources: includeAllSources
|
|
13113
|
+
})
|
|
12316
13114
|
});
|
|
12317
13115
|
if (!res.ok) {
|
|
12318
13116
|
const detail = await res.text().catch(() => "");
|
|
@@ -12334,14 +13132,52 @@ Call get-wiki-page with a slug for the full page; follow its sources (team-memor
|
|
|
12334
13132
|
};
|
|
12335
13133
|
}
|
|
12336
13134
|
const page = data.page;
|
|
12337
|
-
const
|
|
12338
|
-
|
|
13135
|
+
const formatSource = (s) => {
|
|
13136
|
+
const base = `${s.type}:${s.ref}`;
|
|
13137
|
+
if (s.title) {
|
|
13138
|
+
const date2 = s.date ? ` (${s.date})` : "";
|
|
13139
|
+
return `${base} \u2014 "${s.title}"${date2}`;
|
|
13140
|
+
}
|
|
13141
|
+
return base;
|
|
13142
|
+
};
|
|
13143
|
+
const totalActive = page.sourcesTotalActive ?? page.sources.length;
|
|
13144
|
+
const totalArchived = page.sourcesTotalArchived ?? 0;
|
|
13145
|
+
const sourceLines = page.sources.map(formatSource);
|
|
13146
|
+
let sourcesBlock = "";
|
|
13147
|
+
if (sourceLines.length > 0) {
|
|
13148
|
+
const header = page.sourcesTruncated ? `Sources (${sourceLines.length} of ${totalActive} active):` : `Sources (${totalActive} active):`;
|
|
13149
|
+
sourcesBlock = `
|
|
13150
|
+
${header}
|
|
13151
|
+
${sourceLines.map((l) => `- ${l}`).join("\n")}`;
|
|
13152
|
+
if (page.sourcesTruncated || !includeAllSources && totalArchived > 0) {
|
|
13153
|
+
const moreActive = Math.max(0, totalActive - sourceLines.length);
|
|
13154
|
+
const parts = [];
|
|
13155
|
+
if (moreActive > 0) parts.push(`+${moreActive} more active`);
|
|
13156
|
+
if (totalArchived > 0) parts.push(`+${totalArchived} archived`);
|
|
13157
|
+
sourcesBlock += `
|
|
13158
|
+
\u2026 ${parts.join(", ")} (call again with include_all_sources=true). If an archived source answers your question, flag-wiki-page kind=archive-solution with that source_ref.`;
|
|
13159
|
+
}
|
|
13160
|
+
} else if (totalArchived > 0 && !includeAllSources) {
|
|
13161
|
+
sourcesBlock = `
|
|
13162
|
+
Sources: none active, ${totalArchived} archived (call with include_all_sources=true to list them).`;
|
|
13163
|
+
}
|
|
13164
|
+
const activeKeys = new Set(
|
|
13165
|
+
page.sources.map((s) => `${s.type}:${s.ref}`)
|
|
13166
|
+
);
|
|
13167
|
+
const archiveOnly = (page.sourcesArchive ?? []).filter(
|
|
13168
|
+
(s) => !activeKeys.has(`${s.type}:${s.ref}`)
|
|
13169
|
+
);
|
|
13170
|
+
if (includeAllSources && archiveOnly.length > 0) {
|
|
13171
|
+
sourcesBlock += `
|
|
13172
|
+
Archived sources (${archiveOnly.length}):
|
|
13173
|
+
` + archiveOnly.map((s) => `- ${formatSource(s)}`).join("\n");
|
|
13174
|
+
}
|
|
12339
13175
|
return {
|
|
12340
13176
|
content: [
|
|
12341
13177
|
{
|
|
12342
13178
|
type: "text",
|
|
12343
13179
|
text: `# ${page.title}
|
|
12344
|
-
[${page.slug} \xB7 ${page.status} \xB7 updated ${page.pageUpdatedOn ?? "unknown"}` + (page.tags.length ? ` \xB7 ${page.tags.join(", ")}` : "") + `]${
|
|
13180
|
+
[${page.slug} \xB7 ${page.status} \xB7 updated ${page.pageUpdatedOn ?? "unknown"}` + (page.tags.length ? ` \xB7 ${page.tags.join(", ")}` : "") + `]${sourcesBlock}
|
|
12345
13181
|
|
|
12346
13182
|
` + page.content
|
|
12347
13183
|
}
|
|
@@ -12367,15 +13203,22 @@ Sources: ${page.sources.map((s) => `${s.type}:${s.ref}`).join(", ")}` : "";
|
|
|
12367
13203
|
"stale",
|
|
12368
13204
|
"duplicate",
|
|
12369
13205
|
"unverified",
|
|
12370
|
-
"obsolete-candidate"
|
|
13206
|
+
"obsolete-candidate",
|
|
13207
|
+
"archive-solution"
|
|
12371
13208
|
].includes(a.kind) ? a.kind : "feedback";
|
|
13209
|
+
const sourceRef = typeof a.source_ref === "string" ? a.source_ref.trim() : "";
|
|
12372
13210
|
const res = await fetch(`${dashboardBaseUrl}/api/wiki/flag`, {
|
|
12373
13211
|
method: "POST",
|
|
12374
13212
|
headers: {
|
|
12375
13213
|
"content-type": "application/json",
|
|
12376
13214
|
authorization: `Bearer ${apiKey}`
|
|
12377
13215
|
},
|
|
12378
|
-
body: JSON.stringify({
|
|
13216
|
+
body: JSON.stringify({
|
|
13217
|
+
slug,
|
|
13218
|
+
kind,
|
|
13219
|
+
detail,
|
|
13220
|
+
...sourceRef ? { source_ref: sourceRef } : {}
|
|
13221
|
+
})
|
|
12379
13222
|
});
|
|
12380
13223
|
if (!res.ok) {
|
|
12381
13224
|
const errDetail = await res.text().catch(() => "");
|
|
@@ -12399,11 +13242,12 @@ Sources: ${page.sources.map((s) => `${s.type}:${s.ref}`).join(", ")}` : "";
|
|
|
12399
13242
|
]
|
|
12400
13243
|
};
|
|
12401
13244
|
}
|
|
13245
|
+
const dedupeNote = data.flag.deduped ? " (already had an open flag for this source \u2014 reused)" : "";
|
|
12402
13246
|
return {
|
|
12403
13247
|
content: [
|
|
12404
13248
|
{
|
|
12405
13249
|
type: "text",
|
|
12406
|
-
text: `Flagged ${data.flag.slug} (${kind}). The daily wiki reviewer will pick it up next run; it also shows in the backoffice review queue.`
|
|
13250
|
+
text: `Flagged ${data.flag.slug} (${kind}${sourceRef ? `, ${sourceRef}` : ""})${dedupeNote}. The daily wiki reviewer will pick it up next run; it also shows in the backoffice review queue.`
|
|
12407
13251
|
}
|
|
12408
13252
|
]
|
|
12409
13253
|
};
|
|
@@ -13491,8 +14335,8 @@ ${sample.join("\n")}${files.length > 5 ? `
|
|
|
13491
14335
|
};
|
|
13492
14336
|
const filtered = sortRows(applyFilter(only.rows));
|
|
13493
14337
|
if (format === "json") {
|
|
13494
|
-
const
|
|
13495
|
-
return { content: [{ type: "text", text:
|
|
14338
|
+
const text17 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
|
|
14339
|
+
return { content: [{ type: "text", text: text17 }] };
|
|
13496
14340
|
}
|
|
13497
14341
|
if (groupByProject) {
|
|
13498
14342
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -13519,8 +14363,8 @@ ${sample.join("\n")}${files.length > 5 ? `
|
|
|
13519
14363
|
}
|
|
13520
14364
|
const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
|
|
13521
14365
|
const body = filtered.map((r) => `${fmtRow(r)} ${r.Project || ""}`);
|
|
13522
|
-
const
|
|
13523
|
-
return { content: [{ type: "text", text:
|
|
14366
|
+
const text16 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
|
|
14367
|
+
return { content: [{ type: "text", text: text16 }] };
|
|
13524
14368
|
}
|
|
13525
14369
|
if (format === "json") {
|
|
13526
14370
|
const lines = [];
|
|
@@ -15044,7 +15888,7 @@ ${lines.join("\n")}`
|
|
|
15044
15888
|
);
|
|
15045
15889
|
let current = await fetchZone();
|
|
15046
15890
|
if (current.data.records.length === 0) {
|
|
15047
|
-
await
|
|
15891
|
+
await sleep2(DNS_RETRY_BACKOFF_MS);
|
|
15048
15892
|
current = await fetchZone();
|
|
15049
15893
|
}
|
|
15050
15894
|
let nextRecords;
|
|
@@ -15138,7 +15982,7 @@ ${lines.join("\n")}`
|
|
|
15138
15982
|
);
|
|
15139
15983
|
} catch (err) {
|
|
15140
15984
|
if (!isTransientMijnhostError(err)) throw err;
|
|
15141
|
-
await
|
|
15985
|
+
await sleep2(DNS_RETRY_BACKOFF_MS);
|
|
15142
15986
|
const refetched = await fetchZone();
|
|
15143
15987
|
const stillNeedsApply = action === "create" ? !refetched.data.records.some(
|
|
15144
15988
|
(r) => r.type === type && r.name === dnsName && r.value === (typeof a.value === "string" ? a.value : "")
|