@mgsoftwarebv/mg-dashboard-mcp 7.0.8 → 7.0.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 CHANGED
@@ -21,8 +21,7 @@ import { once } from 'events';
21
21
  import { lookup } from 'dns/promises';
22
22
  import { connect } from 'tls';
23
23
  import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, bigint } from 'drizzle-orm/pg-core';
24
- import { tasks } from '@trigger.dev/sdk/v3';
25
- import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
24
+ import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
26
25
 
27
26
  var __defProp = Object.defineProperty;
28
27
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -684,10 +683,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
684
683
  "trigger-run": "ci_cd"
685
684
  };
686
685
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
687
- const sql29 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
686
+ const sql23 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
688
687
  const cmd = [
689
688
  `PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
690
- `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null | tr -d '[:space:]')`,
689
+ `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null | tr -d '[:space:]')`,
691
690
  'echo "$PORT|$KEY"'
692
691
  ].join(" && ");
693
692
  const result = await sshExec2(conn, cmd, proxy);
@@ -708,8 +707,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
708
707
  return { port, apiKey: apiKey2 };
709
708
  }
710
709
  async function fetchRunLogs(runId, conn, proxy, sshExec2) {
711
- 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`;
712
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
710
+ const sql23 = `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`;
711
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
713
712
  const result = await sshExec2(conn, cmd, proxy);
714
713
  const output = result.stdout.trim();
715
714
  if (!output) return "";
@@ -791,8 +790,8 @@ async function handleTriggerTool(name, args2, deps) {
791
790
  switch (name) {
792
791
  // -----------------------------------------------------------------
793
792
  case "trigger-list": {
794
- const sql29 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
795
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
793
+ const sql23 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
794
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
796
795
  const result = await sshExec2(conn, cmd, proxy);
797
796
  const output = result.stdout.trim();
798
797
  if (!output) {
@@ -5746,563 +5745,7 @@ pgTable(
5746
5745
  index("github_webhook_secret_owner_idx").on(table.owner)
5747
5746
  ]
5748
5747
  );
5749
- function assertPipelinePayload(payload) {
5750
- if (!payload.stepIds?.length) {
5751
- throw new Error("Cannot trigger execute-pipeline: stepIds is empty");
5752
- }
5753
- }
5754
- async function triggerPipeline(payload) {
5755
- assertPipelinePayload(payload);
5756
- return tasks.trigger(
5757
- "execute-pipeline",
5758
- payload
5759
- );
5760
- }
5761
-
5762
- // ../jobs/src/github-changed-files.ts
5763
5748
  var GITHUB_API = "https://api.github.com";
5764
- async function fetchCompareChangedPaths(repoFullName, baseSha, headSha, token) {
5765
- if (baseSha === headSha) return [];
5766
- const res = await fetch(
5767
- `${GITHUB_API}/repos/${repoFullName}/compare/${baseSha}...${headSha}`,
5768
- {
5769
- headers: {
5770
- Accept: "application/vnd.github.v3+json",
5771
- Authorization: `Bearer ${token}`
5772
- }
5773
- }
5774
- );
5775
- if (!res.ok) {
5776
- const body = await res.text().catch(() => "");
5777
- throw new Error(
5778
- `GitHub compare ${baseSha.slice(0, 8)}...${headSha.slice(0, 8)} failed (${res.status}): ${body.slice(0, 200)}`
5779
- );
5780
- }
5781
- const detail = await res.json();
5782
- return (detail.files ?? []).map((file) => file.filename).filter((name) => Boolean(name));
5783
- }
5784
- function globToRegex(pattern) {
5785
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
5786
- return new RegExp(`^${escaped}$`);
5787
- }
5788
- function matchesPathPatterns(changedPaths, patterns) {
5789
- if (patterns.length === 0) return false;
5790
- const regexes = patterns.map(globToRegex);
5791
- return changedPaths.some((path) => regexes.some((regex) => regex.test(path)));
5792
- }
5793
- function parseScriptIdList(value) {
5794
- if (!Array.isArray(value)) return [];
5795
- return value.filter((item) => typeof item === "string");
5796
- }
5797
- async function fetchDeployRetryScripts(stageId) {
5798
- const rows = await getDb().execute(
5799
- sql`SELECT deploy_retry_scripts FROM release_profile_stage WHERE id = ${stageId} LIMIT 1`
5800
- );
5801
- return parseScriptIdList(rows[0]?.deploy_retry_scripts);
5802
- }
5803
- async function fetchLastSuccessfulScriptDeploySha(stageId, scriptId) {
5804
- const rows = await getDb().execute(
5805
- sql`SELECT r.trigger_sha
5806
- FROM release_step rs
5807
- JOIN release r ON r.id = rs.release_id
5808
- WHERE r.release_profile_stage_id = ${stageId}
5809
- AND rs.step_type = 'run_ssh_script'
5810
- AND rs.step_config->>'scriptId' = ${scriptId}
5811
- AND rs.status = 'success'
5812
- AND r.trigger_sha IS NOT NULL
5813
- ORDER BY rs.finished_at DESC NULLS LAST
5814
- LIMIT 1`
5815
- );
5816
- return rows[0]?.trigger_sha ?? null;
5817
- }
5818
-
5819
- // ../jobs/src/release-pipeline-config.ts
5820
- function shouldRunSshScript(script, context) {
5821
- if (script.always) return true;
5822
- if (!script.paths?.length) return false;
5823
- if (context.runAllConditional) return true;
5824
- if (context.latchedScriptIds?.includes(script.id)) return true;
5825
- const comparePaths = context.compareChangedPathsByScriptId?.[script.id];
5826
- if (comparePaths === null) return true;
5827
- if (comparePaths !== void 0) {
5828
- if (!comparePaths.length) return false;
5829
- return matchesPathPatterns(comparePaths, script.paths);
5830
- }
5831
- if (!context.changedPaths?.length) return false;
5832
- return matchesPathPatterns(context.changedPaths, script.paths);
5833
- }
5834
- function buildSkipReason(script) {
5835
- const patterns = script.paths?.join(", ") ?? "configured paths";
5836
- return `Skipped: no matching file changes (${patterns})`;
5837
- }
5838
- function parseReleasePipelineConfig(value) {
5839
- if (!value || typeof value !== "object") return null;
5840
- const config = value;
5841
- const hasSshScripts = Array.isArray(config.sshScripts) && config.sshScripts.length > 0;
5842
- const hasMigration = config.migration != null && typeof config.migration === "object";
5843
- const hasNpmPublish = Array.isArray(config.npmPublish) && config.npmPublish.length > 0;
5844
- if (!hasSshScripts && !hasMigration && !hasNpmPublish) return null;
5845
- return config;
5846
- }
5847
- function injectSshScriptSteps(baseSteps, config, context = {}) {
5848
- const scripts = config?.sshScripts;
5849
- if (!scripts?.length) return baseSteps;
5850
- const result = [];
5851
- for (const step of baseSteps) {
5852
- result.push(step);
5853
- for (const script of scripts) {
5854
- if (script.after !== step.type) continue;
5855
- const run = shouldRunSshScript(script, context);
5856
- result.push({
5857
- type: "run_ssh_script",
5858
- label: script.label,
5859
- stepConfig: {
5860
- scriptId: script.id,
5861
- script: script.script
5862
- },
5863
- initialStatus: run ? "pending" : "skipped",
5864
- skipReason: run ? void 0 : buildSkipReason(script)
5865
- });
5866
- }
5867
- }
5868
- return result;
5869
- }
5870
- function parseDeployRetryScripts(value) {
5871
- if (!Array.isArray(value)) return [];
5872
- return value.filter((item) => typeof item === "string");
5873
- }
5874
- async function resolveBuildStepListOptions(args2) {
5875
- const options = {};
5876
- const pipelineConfig = parseReleasePipelineConfig(
5877
- args2.profile.release_pipeline_config
5878
- );
5879
- if (!pipelineConfig?.sshScripts?.length) {
5880
- return options;
5881
- }
5882
- const latchedFromStage = args2.stageId ? await fetchDeployRetryScripts(args2.stageId) : parseDeployRetryScripts(args2.deployRetryScripts);
5883
- options.latchedScriptIds = latchedFromStage;
5884
- if (args2.isManual) {
5885
- options.runAllConditional = true;
5886
- return options;
5887
- }
5888
- if (!args2.triggerSha || !args2.profile.repo_full_name || !args2.githubToken) {
5889
- return options;
5890
- }
5891
- const compareChangedPathsByScriptId = {};
5892
- const conditionalScripts = pipelineConfig.sshScripts.filter(
5893
- (script) => !script.always && script.paths?.length
5894
- );
5895
- await Promise.all(
5896
- conditionalScripts.map(async (script) => {
5897
- if (latchedFromStage.includes(script.id)) return;
5898
- const stageId = args2.stageId;
5899
- const lastSuccessSha = stageId != null ? await fetchLastSuccessfulScriptDeploySha(stageId, script.id) : null;
5900
- if (!lastSuccessSha) {
5901
- compareChangedPathsByScriptId[script.id] = null;
5902
- return;
5903
- }
5904
- try {
5905
- compareChangedPathsByScriptId[script.id] = await fetchCompareChangedPaths(
5906
- args2.profile.repo_full_name,
5907
- lastSuccessSha,
5908
- args2.triggerSha,
5909
- args2.githubToken
5910
- );
5911
- } catch (err) {
5912
- console.warn(
5913
- `[release] compare failed for script ${script.id} (${args2.profile.repo_full_name}@${args2.triggerSha?.slice(0, 8)}):`,
5914
- err
5915
- );
5916
- compareChangedPathsByScriptId[script.id] = [];
5917
- }
5918
- })
5919
- );
5920
- options.compareChangedPathsByScriptId = compareChangedPathsByScriptId;
5921
- return options;
5922
- }
5923
-
5924
- // ../jobs/src/pm2-deploy-script.ts
5925
- var MG_DASHBOARD_REPO = "MGSoftwareBV/mg-dashboard";
5926
- var MG_BOILER_REPO = "MGSoftwareBV/mg-boiler";
5927
- function isBoilerPm2Build(buildFilters) {
5928
- return buildFilters.includes("@mgboiler/");
5929
- }
5930
- function shouldRunConventionTests(profile, ctx) {
5931
- const repo = profile.repo_full_name;
5932
- if (repo === MG_DASHBOARD_REPO || repo === MG_BOILER_REPO) return true;
5933
- return isBoilerPm2Build(ctx.buildFilters);
5934
- }
5935
- function npmPublishWatchPaths(config) {
5936
- if (config.paths?.length) return config.paths;
5937
- const dir = config.path.replace(/\/+$/, "");
5938
- return [`${dir}/**`];
5939
- }
5940
-
5941
- // ../jobs/src/build-step-list.ts
5942
- function buildStepList(stage, profile, options) {
5943
- const steps = [];
5944
- const dockerApps = (stage.stage_apps ?? []).filter(
5945
- (a) => a.enabled && a.deployMethod === "docker"
5946
- );
5947
- for (const _app of dockerApps) {
5948
- steps.push({ type: "deploy", label: "Deploy (Docker)" });
5949
- }
5950
- const pm2Apps = (stage.stage_apps ?? []).filter(
5951
- (a) => a.enabled && a.deployMethod === "pm2"
5952
- );
5953
- const pm2Servers = new Set(
5954
- pm2Apps.map((a) => a.deploymentServerId).filter(Boolean)
5955
- );
5956
- const boilerFilters = pm2Apps.map((a) => a.path ?? "").map((p) => p.match(/apps\/(backoffice|portal|web)/)?.[0]).filter((m) => Boolean(m)).map((m) => `--filter=@mgboiler/${m.replace("apps/", "")}`).join(" ");
5957
- const runConventions = profile?.repo_full_name != null && shouldRunConventionTests(
5958
- { repo_full_name: profile.repo_full_name },
5959
- { buildFilters: boilerFilters }
5960
- );
5961
- if (runConventions) {
5962
- for (const _server of pm2Servers) {
5963
- steps.push({ type: "run_conventions", label: "Convention tests" });
5964
- }
5965
- }
5966
- for (const _server of pm2Servers) {
5967
- steps.push({ type: "build_pm2", label: "Build (PM2)" });
5968
- }
5969
- if (stage.migrate_db) {
5970
- steps.push({ type: "migrate_db", label: "Apply Migrations" });
5971
- }
5972
- if (profile?.has_trigger_dev) {
5973
- steps.push({ type: "deploy_trigger_jobs" });
5974
- }
5975
- for (const _server of pm2Servers) {
5976
- steps.push({ type: "reload_pm2", label: "Reload PM2" });
5977
- }
5978
- const pipelineConfig = parseReleasePipelineConfig(
5979
- profile?.release_pipeline_config
5980
- );
5981
- for (const pkg of pipelineConfig?.npmPublish ?? []) {
5982
- if (!pkg.id || !pkg.path) continue;
5983
- steps.push({
5984
- type: "publish_npm",
5985
- label: pkg.label ?? `Publish npm (${pkg.path})`,
5986
- stepConfig: {
5987
- packageId: pkg.id,
5988
- packagePath: pkg.path,
5989
- watchPaths: npmPublishWatchPaths(pkg),
5990
- tokenEnvVar: pkg.tokenEnvVar
5991
- }
5992
- });
5993
- }
5994
- const withScripts = injectSshScriptSteps(steps, pipelineConfig, {
5995
- changedPaths: options?.changedPaths,
5996
- runAllConditional: options?.runAllConditional,
5997
- latchedScriptIds: options?.latchedScriptIds,
5998
- compareChangedPathsByScriptId: options?.compareChangedPathsByScriptId
5999
- });
6000
- if (stage.stage === "prod") {
6001
- withScripts.push({
6002
- type: "update_release_branch",
6003
- label: "Fast-forward main"
6004
- });
6005
- }
6006
- return withScripts;
6007
- }
6008
- async function insertReleaseSteps(releaseId, steps) {
6009
- const db2 = getDb();
6010
- const stepIds = [];
6011
- for (let i = 0; i < steps.length; i++) {
6012
- const step = steps[i];
6013
- const status = step.initialStatus ?? "pending";
6014
- const logs = status === "skipped" && step.skipReason ? step.skipReason : null;
6015
- const stepConfigJson = step.stepConfig ? JSON.stringify(step.stepConfig) : null;
6016
- const stepRows = await db2.execute(
6017
- stepConfigJson ? sql`INSERT INTO release_step (
6018
- release_id, step_type, step_order, status, step_label, step_config, logs
6019
- ) VALUES (
6020
- ${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null},
6021
- ${stepConfigJson}::jsonb, ${logs}
6022
- )
6023
- RETURNING id` : sql`INSERT INTO release_step (
6024
- release_id, step_type, step_order, status, step_label, logs
6025
- ) VALUES (
6026
- ${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null}, ${logs}
6027
- )
6028
- RETURNING id`
6029
- );
6030
- const id = stepRows[0]?.id;
6031
- if (id) stepIds.push(id);
6032
- }
6033
- return stepIds;
6034
- }
6035
- var ACTIVE_STATUSES = [
6036
- "pending",
6037
- "pending_approval",
6038
- "reviewing",
6039
- "running"
6040
- ];
6041
- async function cancelReleaseRow(releaseId, reason, allowedStatuses) {
6042
- const db2 = getDb();
6043
- const now = (/* @__PURE__ */ new Date()).toISOString();
6044
- const releaseResult = await db2.execute(sql`
6045
- UPDATE release
6046
- SET status = 'cancelled',
6047
- finished_at = ${now},
6048
- error_message = ${reason},
6049
- updated_at = ${now}
6050
- WHERE id = ${releaseId}
6051
- AND status IN ${allowedStatuses}
6052
- RETURNING id
6053
- `);
6054
- if (releaseResult.length === 0) return false;
6055
- await db2.execute(sql`
6056
- UPDATE release_step
6057
- SET status = 'skipped'
6058
- WHERE release_id = ${releaseId}
6059
- AND status = 'pending'
6060
- `);
6061
- return true;
6062
- }
6063
- async function cancelActiveReleasesForStage(stageId, reason) {
6064
- const db2 = getDb();
6065
- const active = await db2.execute(sql`
6066
- SELECT id FROM release
6067
- WHERE release_profile_stage_id = ${stageId}
6068
- AND status IN ${ACTIVE_STATUSES}
6069
- `);
6070
- const cancelled = [];
6071
- for (const row of active) {
6072
- if (await cancelReleaseRow(row.id, reason, ACTIVE_STATUSES)) {
6073
- cancelled.push(row.id);
6074
- }
6075
- }
6076
- return cancelled;
6077
- }
6078
- function isPostgresUniqueViolation(err) {
6079
- return typeof err === "object" && err !== null && "code" in err && err.code === "23505";
6080
- }
6081
- async function resolveUserDisplayName(userId) {
6082
- const rows = await getDb().execute(sql`
6083
- SELECT full_name, email
6084
- FROM "user"
6085
- WHERE id = ${userId}
6086
- LIMIT 1
6087
- `);
6088
- const user = rows[0];
6089
- if (!user) return null;
6090
- const fullName = user.full_name?.trim();
6091
- return fullName || user.email || null;
6092
- }
6093
-
6094
- // ../jobs/src/create-release-for-stage.ts
6095
- var GITHUB_API2 = "https://api.github.com";
6096
- var CreateReleaseError = class extends Error {
6097
- constructor(message, code = "INTERNAL_SERVER_ERROR") {
6098
- super(message);
6099
- this.code = code;
6100
- this.name = "CreateReleaseError";
6101
- }
6102
- };
6103
- async function resolveBranchHeadSha(repoFullName, branch, token) {
6104
- const headers = {
6105
- Accept: "application/vnd.github.v3+json",
6106
- Authorization: `Bearer ${token}`
6107
- };
6108
- const refRes = await fetch(
6109
- `${GITHUB_API2}/repos/${repoFullName}/git/ref/heads/${branch}`,
6110
- { headers }
6111
- );
6112
- if (!refRes.ok) {
6113
- throw new CreateReleaseError(
6114
- `Could not find branch ${branch} to deploy from`,
6115
- "BAD_REQUEST"
6116
- );
6117
- }
6118
- const refData = await refRes.json();
6119
- const sha = refData.object?.sha;
6120
- if (!sha) {
6121
- throw new CreateReleaseError(
6122
- `Could not determine SHA for branch ${branch}`,
6123
- "INTERNAL_SERVER_ERROR"
6124
- );
6125
- }
6126
- return sha;
6127
- }
6128
- async function createReleaseForStage(userId, params) {
6129
- const triggerType = params.triggerType ?? "manual";
6130
- const db2 = getDb();
6131
- const stageRows = await db2.execute(
6132
- sql`SELECT * FROM release_profile_stage WHERE id = ${params.stageId} LIMIT 1`
6133
- );
6134
- const stage = stageRows[0];
6135
- if (!stage) {
6136
- throw new CreateReleaseError("Stage not found", "NOT_FOUND");
6137
- }
6138
- const profileRows = await db2.execute(
6139
- sql`SELECT * FROM release_profile WHERE id = ${stage.release_profile_id} LIMIT 1`
6140
- );
6141
- const profile = profileRows[0];
6142
- if (!profile) {
6143
- throw new CreateReleaseError("Release profile not found", "NOT_FOUND");
6144
- }
6145
- if (!profile.enabled || !stage.enabled) {
6146
- throw new CreateReleaseError(
6147
- "Profile or stage is disabled",
6148
- "BAD_REQUEST"
6149
- );
6150
- }
6151
- const deployBranch = stage.trigger_branch?.trim() || stage.release_branch?.trim() || null;
6152
- if (!deployBranch) {
6153
- throw new CreateReleaseError(
6154
- "Stage has no release or trigger branch configured",
6155
- "BAD_REQUEST"
6156
- );
6157
- }
6158
- const { token } = await resolveReleaseGitHubToken(
6159
- profile.github_token_id ?? null
6160
- );
6161
- if (!token) {
6162
- throw new CreateReleaseError(
6163
- "GitHub token is not configured",
6164
- "PRECONDITION_FAILED"
6165
- );
6166
- }
6167
- const triggerSha = await resolveBranchHeadSha(
6168
- profile.repo_full_name,
6169
- deployBranch,
6170
- token
6171
- );
6172
- if (params.skipIfActiveSameSha) {
6173
- const existing = await db2.execute(sql`
6174
- SELECT
6175
- r.id,
6176
- r.status,
6177
- r.version,
6178
- r.trigger_author_name,
6179
- COUNT(rs.id)::int AS steps_count
6180
- FROM release r
6181
- LEFT JOIN release_step rs ON rs.release_id = r.id
6182
- WHERE r.release_profile_stage_id = ${params.stageId}
6183
- AND r.trigger_sha = ${triggerSha}
6184
- AND r.status IN ('pending', 'pending_approval', 'running', 'reviewing', 'success')
6185
- GROUP BY r.id
6186
- ORDER BY r.created_at DESC
6187
- LIMIT 1
6188
- `);
6189
- if (existing[0]) {
6190
- return {
6191
- id: existing[0].id,
6192
- status: existing[0].status ?? "running",
6193
- version: existing[0].version ?? null,
6194
- stepsCount: Number(existing[0].steps_count ?? 0),
6195
- authorName: existing[0].trigger_author_name ?? null
6196
- };
6197
- }
6198
- }
6199
- await cancelActiveReleasesForStage(
6200
- params.stageId,
6201
- "Superseded by newer manual trigger"
6202
- );
6203
- let triggerAuthorName = params.triggerAuthorName?.trim() || null;
6204
- if (!triggerAuthorName && userId) {
6205
- triggerAuthorName = await resolveUserDisplayName(userId);
6206
- }
6207
- const enabledSteps = buildStepList(
6208
- stage,
6209
- profile,
6210
- await resolveBuildStepListOptions({
6211
- profile,
6212
- triggerSha,
6213
- githubToken: token,
6214
- isManual: triggerType === "manual" || triggerType === "mcp",
6215
- stageId: params.stageId,
6216
- deployRetryScripts: stage.deploy_retry_scripts
6217
- })
6218
- );
6219
- if (enabledSteps.length === 0) {
6220
- throw new CreateReleaseError(
6221
- "No pipeline steps configured for this stage (enable a PM2/Docker stage_app or deployment_project_server)",
6222
- "PRECONDITION_FAILED"
6223
- );
6224
- }
6225
- const initialStatus = "running";
6226
- let release;
6227
- try {
6228
- const inserted = await db2.execute(sql`
6229
- INSERT INTO release (
6230
- release_profile_id, release_profile_stage_id, status, started_at,
6231
- version, version_type, trigger_type, skip_pr_flow, created_by,
6232
- trigger_kind, trigger_branch, trigger_sha, source_commit,
6233
- trigger_author_name
6234
- ) VALUES (
6235
- ${profile.id}, ${params.stageId}, ${initialStatus}, ${(/* @__PURE__ */ new Date()).toISOString()},
6236
- NULL, ${params.versionType}, ${triggerType}, ${params.skipPrFlow}, ${userId},
6237
- 'branch', ${deployBranch}, ${triggerSha}, ${triggerSha},
6238
- ${triggerAuthorName}
6239
- )
6240
- RETURNING *
6241
- `);
6242
- if (!inserted[0]) {
6243
- throw new Error("INSERT did not return a row");
6244
- }
6245
- release = inserted[0];
6246
- } catch (err) {
6247
- if (isPostgresUniqueViolation(err)) {
6248
- throw new CreateReleaseError(
6249
- "A release is already running for this stage.",
6250
- "CONFLICT"
6251
- );
6252
- }
6253
- throw new CreateReleaseError(
6254
- `Failed to create release: ${err instanceof Error ? err.message : String(err)}`,
6255
- "INTERNAL_SERVER_ERROR"
6256
- );
6257
- }
6258
- const stepIds = [];
6259
- try {
6260
- stepIds.push(...await insertReleaseSteps(release.id, enabledSteps));
6261
- } catch (err) {
6262
- const msg = err instanceof Error ? err.message : String(err);
6263
- await db2.execute(sql`
6264
- UPDATE release
6265
- SET status = 'failed',
6266
- error_message = ${`Failed to create steps: ${msg}`},
6267
- finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
6268
- WHERE id = ${release.id}
6269
- `);
6270
- throw new CreateReleaseError(
6271
- `Failed to create release steps: ${msg}`,
6272
- "INTERNAL_SERVER_ERROR"
6273
- );
6274
- }
6275
- try {
6276
- await triggerPipeline({
6277
- releaseId: release.id,
6278
- profileId: profile.id,
6279
- stageId: stage.id,
6280
- stepIds,
6281
- version: null
6282
- });
6283
- } catch (err) {
6284
- const msg = err instanceof Error ? err.message : String(err);
6285
- await db2.execute(sql`
6286
- UPDATE release
6287
- SET status = 'failed',
6288
- error_message = ${`Failed to start pipeline: ${msg}`},
6289
- finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
6290
- WHERE id = ${release.id}
6291
- `);
6292
- throw new CreateReleaseError(
6293
- `Failed to start pipeline: ${msg}`,
6294
- "INTERNAL_SERVER_ERROR"
6295
- );
6296
- }
6297
- return {
6298
- id: release.id,
6299
- status: initialStatus,
6300
- version: null,
6301
- stepsCount: enabledSteps.length,
6302
- authorName: triggerAuthorName
6303
- };
6304
- }
6305
- var GITHUB_API3 = "https://api.github.com";
6306
5749
  var ReleaseProfileQuickCreateError = class extends Error {
6307
5750
  constructor(message, code = "INTERNAL_SERVER_ERROR") {
6308
5751
  super(message);
@@ -6330,7 +5773,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
6330
5773
  };
6331
5774
  async function fetchContents(path) {
6332
5775
  const res = await fetch(
6333
- `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5776
+ `${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6334
5777
  { headers }
6335
5778
  );
6336
5779
  if (!res.ok) return null;
@@ -6339,7 +5782,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
6339
5782
  }
6340
5783
  async function fileExists(path) {
6341
5784
  const res = await fetch(
6342
- `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5785
+ `${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6343
5786
  { headers, method: "HEAD" }
6344
5787
  );
6345
5788
  return res.ok;
@@ -6478,20 +5921,20 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6478
5921
  "Content-Type": "application/json"
6479
5922
  };
6480
5923
  const checkRes = await fetch(
6481
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${branchName}`,
5924
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${branchName}`,
6482
5925
  { headers }
6483
5926
  );
6484
5927
  if (checkRes.ok) return;
6485
5928
  let sha;
6486
5929
  const sourceRes = await fetch(
6487
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
5930
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
6488
5931
  { headers }
6489
5932
  );
6490
5933
  if (sourceRes.ok) {
6491
5934
  const data = await sourceRes.json();
6492
5935
  sha = data.object?.sha;
6493
5936
  } else {
6494
- const repoRes = await fetch(`${GITHUB_API3}/repos/${repoFullName}`, {
5937
+ const repoRes = await fetch(`${GITHUB_API}/repos/${repoFullName}`, {
6495
5938
  headers
6496
5939
  });
6497
5940
  if (!repoRes.ok) {
@@ -6500,7 +5943,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6500
5943
  const repo = await repoRes.json();
6501
5944
  const defaultBranch = repo.default_branch ?? "main";
6502
5945
  const defaultRes = await fetch(
6503
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
5946
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
6504
5947
  { headers }
6505
5948
  );
6506
5949
  if (!defaultRes.ok) {
@@ -6513,7 +5956,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6513
5956
  throw new Error(`Could not determine SHA to create branch ${branchName}`);
6514
5957
  }
6515
5958
  const createRes = await fetch(
6516
- `${GITHUB_API3}/repos/${repoFullName}/git/refs`,
5959
+ `${GITHUB_API}/repos/${repoFullName}/git/refs`,
6517
5960
  {
6518
5961
  method: "POST",
6519
5962
  headers,
@@ -6759,6 +6202,7 @@ var apiKey = getArg2("api-key") || process.env.MG_DASHBOARD_API_KEY;
6759
6202
  var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
6760
6203
  var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
6761
6204
  var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
6205
+ var dashboardBaseUrl = (getArg2("dashboard-url") || process.env.MG_DASHBOARD_BASE_URL || "https://dashboard.mgsoftware.nl").replace(/\/$/, "");
6762
6206
  var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
6763
6207
  function isMijnhostViaSshProxyEnabled() {
6764
6208
  const arg = getArg2("mijnhost-via-ssh-proxy");
@@ -6975,7 +6419,6 @@ var TOOL_MODULE_MAP = {
6975
6419
  "docker-logs": "ssh_servers",
6976
6420
  "docker-exec": "ssh_servers",
6977
6421
  "docker-compose": "ssh_servers",
6978
- "wait-for": "ssh_servers",
6979
6422
  "db-discover": "ssh_servers",
6980
6423
  "db-tables": "ssh_servers",
6981
6424
  "db-query": "ssh_servers",
@@ -6985,10 +6428,8 @@ var TOOL_MODULE_MAP = {
6985
6428
  "env-list": "ci_cd",
6986
6429
  "env-get": "ci_cd",
6987
6430
  "env-store": "ci_cd",
6988
- "release-trigger": "ci_cd",
6989
6431
  "release-profile-list": "ci_cd",
6990
- "release-profile-create": "ci_cd",
6991
- "release-profile-update": "ci_cd",
6432
+ "release-profile": "ci_cd",
6992
6433
  "domain-list": "domains",
6993
6434
  "dns-list": "domains",
6994
6435
  "dns-record": "domains",
@@ -9550,11 +8991,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
9550
8991
  applied_by TEXT
9551
8992
  );
9552
8993
  `.trim();
9553
- function normaliseMigrationSql(sql29) {
9554
- return sql29.replace(/\r\n/g, "\n").trim() + "\n";
8994
+ function normaliseMigrationSql(sql23) {
8995
+ return sql23.replace(/\r\n/g, "\n").trim() + "\n";
9555
8996
  }
9556
- function migrationSha256(sql29) {
9557
- return createHash("sha256").update(normaliseMigrationSql(sql29), "utf8").digest("hex");
8997
+ function migrationSha256(sql23) {
8998
+ return createHash("sha256").update(normaliseMigrationSql(sql23), "utf8").digest("hex");
9558
8999
  }
9559
9000
  function dollarQuoteTag(value) {
9560
9001
  let tag = "_mcp";
@@ -10294,48 +9735,6 @@ var TOOLS = [
10294
9735
  required: ["serverId", "projectPath", "action"]
10295
9736
  }
10296
9737
  },
10297
- {
10298
- name: "wait-for",
10299
- description: 'Poll until a condition holds (or timeout). Replaces 30+ ad-hoc loops the LLM would otherwise build itself. Four condition types:\n- `url`: `target: "https://..."`, `until: 200` (HTTP status, default 200). 10s per probe.\n- `container-health`: `serverId`, `target: "container-name"`, `until: "healthy" | "running" | "exited"` (default "healthy"). Uses `docker inspect`.\n- `file-exists`: `serverId`, `target: "/path/to/file"` \u2014 SSH stat check.\n- `sftp-exists`: same as file-exists but with `bucket: "name"` instead of serverId for R2.\n\nReturns a per-attempt trail so you can see how long it took to converge.',
10300
- inputSchema: {
10301
- type: "object",
10302
- properties: {
10303
- type: {
10304
- type: "string",
10305
- enum: ["url", "container-health", "file-exists", "sftp-exists"],
10306
- description: "Condition type to wait on."
10307
- },
10308
- target: {
10309
- type: "string",
10310
- description: "Target identifier: URL, container name, file path, or R2 key (depends on type)."
10311
- },
10312
- until: {
10313
- description: 'Expected end-state (depends on type): for url an HTTP status (default 200), for container-health a state string (default "healthy"). Omit for file-exists/sftp-exists.'
10314
- },
10315
- serverId: {
10316
- type: "string",
10317
- description: "SSH server UUID (required for container-health and file-exists)."
10318
- },
10319
- bucket: {
10320
- type: "string",
10321
- description: "R2 bucket name (used with sftp-exists when checking an R2 object)."
10322
- },
10323
- path: {
10324
- type: "string",
10325
- description: "Alias for `target` for file-exists/sftp-exists."
10326
- },
10327
- intervalSeconds: {
10328
- type: "number",
10329
- description: "Poll interval in seconds (default 3, max 60)."
10330
- },
10331
- timeout: {
10332
- type: "number",
10333
- description: "Total deadline in seconds (default 120, max 1800)."
10334
- }
10335
- },
10336
- required: ["type", "target"]
10337
- }
10338
- },
10339
9738
  {
10340
9739
  name: "db-discover",
10341
9740
  description: 'Discover databases available on an SSH server. Use this first to find what is reachable before running other db-* tools.\n\nModes (pass via `include` array, default `["www"]`):\n- `"www"` \u2014 scan `/var/www` for WordPress / PrestaShop / Laravel / generic .env apps. Returns sitePath, db name, user, host, port.\n- `"postgres-containers"` \u2014 enumerate running Docker containers that expose `POSTGRES_USER` / `POSTGRES_DB` env vars (vanilla `postgres:*` / `pgvector/*` images). Returns container name, db, user, host port mapping, and whether a password is set. Use the returned (container, db, user) tuple directly with `db-query containerName=\u2026` or `db-apply-migration`.\n\nPass multiple modes to scan in parallel: `{ include: ["www", "postgres-containers"] }`.',
@@ -10594,33 +9993,6 @@ var TOOLS = [
10594
9993
  required: ["appName", "environment", "content"]
10595
9994
  }
10596
9995
  },
10597
- {
10598
- name: "release-trigger",
10599
- description: "Start a release/deploy pipeline for a release profile stage. Attributes the deploy to the MCP API key owner. Requires ci_cd permission.",
10600
- inputSchema: {
10601
- type: "object",
10602
- properties: {
10603
- releaseProfile: {
10604
- type: "string",
10605
- description: "Release profile name (usually matches the project folder name or git repo name, e.g. mg-dashboard)."
10606
- },
10607
- stage: {
10608
- type: "string",
10609
- description: "Stage slug or name (e.g. dev, production). Required when the profile has multiple enabled stages."
10610
- },
10611
- versionType: {
10612
- type: "string",
10613
- enum: ["major", "minor", "patch"],
10614
- description: "Semver bump type when versioning applies (default: minor)."
10615
- },
10616
- skipPrFlow: {
10617
- type: "boolean",
10618
- description: "Skip PR approval flow (default: false)."
10619
- }
10620
- },
10621
- required: ["releaseProfile"]
10622
- }
10623
- },
10624
9996
  {
10625
9997
  name: "release-profile-list",
10626
9998
  description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
@@ -10631,67 +10003,55 @@ var TOOLS = [
10631
10003
  }
10632
10004
  },
10633
10005
  {
10634
- name: "release-profile-create",
10635
- description: "One-step release profile creation: scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Deployment servers must be assigned afterwards in the dashboard (Releases \u2192 Profiles). Requires ci_cd permission.",
10006
+ name: "release-profile",
10007
+ description: 'Create or update a release profile. Pick the operation with `action`:\n- "create": one-step creation \u2014 scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Required: repoFullName. Optional: githubKey, name, releaseType. Assign deployment servers afterwards in the dashboard (Releases \u2192 Profiles).\n- "update": change settings of an existing profile located by `releaseProfile` (name or repo). Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Required: releaseProfile. Optional: name, enabled, releaseType, githubKey, useChangesBranch, hasTriggerDev, workDirectory.\n\nRequires ci_cd permission.',
10636
10008
  inputSchema: {
10637
10009
  type: "object",
10638
10010
  properties: {
10011
+ action: {
10012
+ type: "string",
10013
+ enum: ["create", "update"],
10014
+ description: "Which operation to perform."
10015
+ },
10639
10016
  repoFullName: {
10640
10017
  type: "string",
10641
- description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard).'
10018
+ description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard). Required for action="create".'
10642
10019
  },
10643
- githubKey: {
10020
+ releaseProfile: {
10644
10021
  type: "string",
10645
- description: 'Named GitHub key (github_token label or owner, e.g. "AVARC Solutions") used for all GitHub API calls for this profile. Omit for the default MG Software token.'
10022
+ description: 'Profile name or repo full name to locate the profile. Required for action="update".'
10646
10023
  },
10647
10024
  name: {
10648
10025
  type: "string",
10649
- description: 'Profile name override. Default: title-cased repo name (mg-dashboard \u2192 "Mg Dashboard").'
10026
+ description: 'Profile name. For create: name override (default: title-cased repo name, e.g. mg-dashboard \u2192 "Mg Dashboard"). For update: new profile name.'
10650
10027
  },
10651
- releaseType: {
10652
- type: "string",
10653
- enum: ["dev_only", "prod_only", "both"],
10654
- description: "Which stages to create (default: both = dev + prod)."
10655
- }
10656
- },
10657
- required: ["repoFullName"]
10658
- }
10659
- },
10660
- {
10661
- name: "release-profile-update",
10662
- description: "Update release profile settings. Locate the profile by name or repo. Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Requires ci_cd permission.",
10663
- inputSchema: {
10664
- type: "object",
10665
- properties: {
10666
- releaseProfile: {
10667
- type: "string",
10668
- description: "Profile name or repo full name to locate the profile."
10028
+ enabled: {
10029
+ type: "boolean",
10030
+ description: "Enable/disable the profile (update only)."
10669
10031
  },
10670
- name: { type: "string", description: "New profile name." },
10671
- enabled: { type: "boolean", description: "Enable/disable the profile." },
10672
10032
  releaseType: {
10673
10033
  type: "string",
10674
10034
  enum: ["dev_only", "prod_only", "both"],
10675
- description: "New release type \u2014 reconfigures stages accordingly."
10035
+ description: "Which stages exist (default for create: both = dev + prod). Changing it on update reconfigures the stages."
10676
10036
  },
10677
10037
  githubKey: {
10678
10038
  type: "string",
10679
- description: 'Named GitHub key (github_token label or owner). Pass "default" to reset to the default MG Software token.'
10039
+ description: 'Named GitHub key (github_token label or owner, e.g. "AVARC Solutions") used for all GitHub API calls for this profile. Omit for the default MG Software token. On update, pass "default" to reset to the default token.'
10680
10040
  },
10681
10041
  useChangesBranch: {
10682
10042
  type: "boolean",
10683
- description: "Use a long-lived -changes accumulation branch."
10043
+ description: "Use a long-lived -changes accumulation branch (update only)."
10684
10044
  },
10685
10045
  hasTriggerDev: {
10686
10046
  type: "boolean",
10687
- description: "Deploy Trigger.dev jobs during production releases."
10047
+ description: "Deploy Trigger.dev jobs during production releases (update only)."
10688
10048
  },
10689
10049
  workDirectory: {
10690
10050
  type: "string",
10691
- description: 'Working directory within the repo (e.g. ".").'
10051
+ description: 'Working directory within the repo (e.g. "."). Update only.'
10692
10052
  }
10693
10053
  },
10694
- required: ["releaseProfile"]
10054
+ required: ["action"]
10695
10055
  }
10696
10056
  },
10697
10057
  {
@@ -10790,6 +10150,34 @@ var TOOLS = [
10790
10150
  required: ["action", "domain", "type", "name"]
10791
10151
  }
10792
10152
  },
10153
+ // ----- Team memory -----
10154
+ {
10155
+ name: "search-team-memory",
10156
+ description: "Search the team's ENTIRE past Cursor history (every developer, every project) for how something was handled before. Use this FIRST, before investigating from scratch, whenever you: hit a non-trivial bug or error, are about to build something that may have been done before, need a project-specific convention/gotcha, or the user asks 'have we done X', 'how did we fix Y', or 'did we solve this already'. Hybrid semantic + keyword search over mirrored conversations. Returns ranked past chats with repo, date, a solution snippet and a similarity score. Phrase the query as the problem in natural language (e.g. 'release pipeline PM2 deploy fails with module not found').",
10157
+ inputSchema: {
10158
+ type: "object",
10159
+ properties: {
10160
+ query: {
10161
+ type: "string",
10162
+ description: "The problem/topic in natural language. Include error text, tool/module names, or symptoms for best recall."
10163
+ },
10164
+ repo: {
10165
+ type: "string",
10166
+ description: "Optional repo filter, e.g. 'github.com/MGSoftwareBV/mg-dashboard'. Omit to search across all projects."
10167
+ },
10168
+ scope: {
10169
+ type: "string",
10170
+ enum: ["team", "mine"],
10171
+ description: "'team' (default) searches everyone's history; 'mine' restricts to the calling user's own conversations."
10172
+ },
10173
+ limit: {
10174
+ type: "number",
10175
+ description: "Max results to return (1-25, default 8)."
10176
+ }
10177
+ },
10178
+ required: ["query"]
10179
+ }
10180
+ },
10793
10181
  // ----- Trigger.dev -----
10794
10182
  ...TRIGGER_TOOLS
10795
10183
  ];
@@ -10861,6 +10249,65 @@ async function executeToolCall(name, a, _serverId) {
10861
10249
  const ctx = authContext;
10862
10250
  try {
10863
10251
  switch (name) {
10252
+ // ----- Team memory -----
10253
+ case "search-team-memory": {
10254
+ const query = typeof a.query === "string" ? a.query.trim() : "";
10255
+ if (!query) {
10256
+ return { content: [{ type: "text", text: "Error: query is required" }] };
10257
+ }
10258
+ const res = await fetch(`${dashboardBaseUrl}/api/team-memory/search`, {
10259
+ method: "POST",
10260
+ headers: {
10261
+ "content-type": "application/json",
10262
+ authorization: `Bearer ${apiKey}`
10263
+ },
10264
+ body: JSON.stringify({
10265
+ query,
10266
+ repo: typeof a.repo === "string" ? a.repo : void 0,
10267
+ scope: a.scope === "mine" ? "mine" : "team",
10268
+ limit: typeof a.limit === "number" ? a.limit : void 0
10269
+ })
10270
+ });
10271
+ if (!res.ok) {
10272
+ const detail = await res.text().catch(() => "");
10273
+ return {
10274
+ content: [
10275
+ {
10276
+ type: "text",
10277
+ text: `Error: team-memory search failed (${res.status}). ${detail.slice(0, 300)}`
10278
+ }
10279
+ ]
10280
+ };
10281
+ }
10282
+ const data = await res.json();
10283
+ if (data.count === 0) {
10284
+ return {
10285
+ content: [
10286
+ {
10287
+ type: "text",
10288
+ text: `No prior team conversations matched "${query}". Proceed from scratch.`
10289
+ }
10290
+ ]
10291
+ };
10292
+ }
10293
+ const lines = data.hits.map((hit, index5) => {
10294
+ const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
10295
+ const repo = hit.repo ?? "unknown repo";
10296
+ const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
10297
+ return `${index5 + 1}. [${repo} \xB7 ${when}${sim}] ${hit.title}
10298
+ ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
10299
+ });
10300
+ return {
10301
+ content: [
10302
+ {
10303
+ type: "text",
10304
+ text: `Found ${data.count} prior conversation(s) (${data.mode} search) \u2014 reuse this before solving from scratch:
10305
+
10306
+ ` + lines.join("\n\n")
10307
+ }
10308
+ ]
10309
+ };
10310
+ }
10864
10311
  // ----- Servers -----
10865
10312
  case "list-servers": {
10866
10313
  const data = ctx.allowedServerIds !== null ? await db.execute(sql`
@@ -12156,163 +11603,6 @@ ${stderrOut}`);
12156
11603
  ${result.stderr}`);
12157
11604
  return { content: [{ type: "text", text: output.join("\n") }] };
12158
11605
  }
12159
- // ----- wait-for: poll until a condition holds (one tool, four types) -----
12160
- case "wait-for": {
12161
- const type = String(a.type || "");
12162
- if (!["url", "container-health", "file-exists", "sftp-exists"].includes(
12163
- type
12164
- )) {
12165
- return {
12166
- content: [
12167
- {
12168
- type: "text",
12169
- text: "Error: type must be one of: url, container-health, file-exists, sftp-exists"
12170
- }
12171
- ]
12172
- };
12173
- }
12174
- const timeoutSeconds = Math.max(
12175
- 1,
12176
- Math.min(1800, Number(a.timeout) || 120)
12177
- );
12178
- const intervalSeconds = Math.max(
12179
- 1,
12180
- Math.min(60, Number(a.intervalSeconds) || 3)
12181
- );
12182
- const deadline = Date.now() + timeoutSeconds * 1e3;
12183
- const trail = [];
12184
- const check = async () => {
12185
- if (type === "url") {
12186
- const target = String(a.target || "");
12187
- if (!target) return { ok: false, observed: "no target url" };
12188
- const expectStatus = a.until !== void 0 ? Number(a.until) : 200;
12189
- try {
12190
- const ctrl = new AbortController();
12191
- const t = setTimeout(() => ctrl.abort(), 1e4);
12192
- const res = await fetch(target, {
12193
- signal: ctrl.signal,
12194
- redirect: "manual"
12195
- });
12196
- clearTimeout(t);
12197
- const ok2 = res.status === expectStatus || expectStatus === 200 && res.status >= 200 && res.status < 300;
12198
- return { ok: ok2, observed: `HTTP ${res.status}` };
12199
- } catch (e) {
12200
- return {
12201
- ok: false,
12202
- observed: e instanceof Error ? e.message : String(e)
12203
- };
12204
- }
12205
- }
12206
- if (type === "container-health") {
12207
- const containerName = String(a.target || "").replace(
12208
- /[^a-zA-Z0-9._-]/g,
12209
- ""
12210
- );
12211
- const until = String(a.until || "healthy");
12212
- if (!containerName)
12213
- return { ok: false, observed: "no container name" };
12214
- if (!a.serverId)
12215
- return {
12216
- ok: false,
12217
- observed: "serverId is required for container-health"
12218
- };
12219
- const { conn: conn2, proxy: proxy2 } = await getServerConnection(
12220
- String(a.serverId)
12221
- );
12222
- const cmd2 = `docker inspect --format '{{json .State}}' ${containerName} 2>&1`;
12223
- const r2 = await sshExec(conn2, cmd2, proxy2);
12224
- if (r2.exitCode !== 0)
12225
- return {
12226
- ok: false,
12227
- observed: r2.stderr.trim().slice(0, 80) || "inspect failed"
12228
- };
12229
- try {
12230
- const state = JSON.parse(r2.stdout.trim());
12231
- const healthStatus = state.Health?.Status;
12232
- if (until === "healthy") {
12233
- return {
12234
- ok: healthStatus === "healthy",
12235
- observed: `Status=${state.Status} Health=${healthStatus || "no-check"}`
12236
- };
12237
- }
12238
- return {
12239
- ok: state.Status === until,
12240
- observed: `Status=${state.Status}`
12241
- };
12242
- } catch (e) {
12243
- return {
12244
- ok: false,
12245
- observed: "parse error: " + (e instanceof Error ? e.message : String(e))
12246
- };
12247
- }
12248
- }
12249
- const bucket = typeof a.bucket === "string" && a.bucket ? a.bucket : "";
12250
- const path = String(a.target || a.path || "");
12251
- if (!path) return { ok: false, observed: "no path" };
12252
- if (bucket) {
12253
- try {
12254
- await getR2Client().send(
12255
- new HeadObjectCommand({ Bucket: bucket, Key: r2Key(path) })
12256
- );
12257
- return { ok: true, observed: `r2://${bucket}/${path} exists` };
12258
- } catch {
12259
- return {
12260
- ok: false,
12261
- observed: `r2://${bucket}/${path} not found`
12262
- };
12263
- }
12264
- }
12265
- if (!a.serverId)
12266
- return {
12267
- ok: false,
12268
- observed: "serverId or bucket is required for file-exists"
12269
- };
12270
- const { conn, proxy } = await getServerConnection(String(a.serverId));
12271
- const safe = sanitizePath(path);
12272
- const cmd = `test -e ${posixQuote(safe)} && echo OK || echo MISSING`;
12273
- const r = await sshExec(conn, cmd, proxy);
12274
- const ok = r.stdout.trim() === "OK";
12275
- return { ok, observed: ok ? `${safe} exists` : `${safe} missing` };
12276
- };
12277
- let attempts = 0;
12278
- let lastObserved = "";
12279
- while (Date.now() < deadline) {
12280
- attempts++;
12281
- const r = await check();
12282
- lastObserved = r.observed;
12283
- trail.push(
12284
- `[${attempts}] ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${r.ok ? "OK" : ".."} ${r.observed}`
12285
- );
12286
- if (r.ok) {
12287
- return {
12288
- content: [
12289
- {
12290
- type: "text",
12291
- text: `Condition met after ${attempts} attempt(s) in ${((Date.now() - (deadline - timeoutSeconds * 1e3)) / 1e3).toFixed(1)}s.
12292
- Last observation: ${r.observed}
12293
-
12294
- --- trail ---
12295
- ${trail.join("\n")}`
12296
- }
12297
- ]
12298
- };
12299
- }
12300
- if (Date.now() + intervalSeconds * 1e3 >= deadline) break;
12301
- await new Promise((res) => setTimeout(res, intervalSeconds * 1e3));
12302
- }
12303
- return {
12304
- content: [
12305
- {
12306
- type: "text",
12307
- text: `Timed out after ${timeoutSeconds}s without seeing condition (${attempts} probe(s)).
12308
- Last observation: ${lastObserved}
12309
-
12310
- --- trail ---
12311
- ${trail.join("\n")}`
12312
- }
12313
- ]
12314
- };
12315
- }
12316
11606
  // ----- Database -----
12317
11607
  case "db-discover": {
12318
11608
  const { serverId, conn, proxy } = await getServerConnection(
@@ -13188,53 +12478,6 @@ LIMIT ${limit};
13188
12478
  }
13189
12479
  return { content: [{ type: "text", text: saveMsg }] };
13190
12480
  }
13191
- case "release-trigger": {
13192
- const releaseProfile = String(a.releaseProfile);
13193
- const stageFilter = a.stage ? String(a.stage) : null;
13194
- const versionType = String(a.versionType ?? "minor");
13195
- if (versionType !== "major" && versionType !== "minor" && versionType !== "patch") {
13196
- throw new Error(
13197
- `Invalid versionType "${versionType}". Expected major, minor, or patch.`
13198
- );
13199
- }
13200
- const skipPrFlow = a.skipPrFlow === true;
13201
- const resolved = await resolveReleaseTriggerStage(
13202
- releaseProfile,
13203
- stageFilter
13204
- );
13205
- try {
13206
- const result = await createReleaseForStage(authContext.userId, {
13207
- stageId: resolved.stageId,
13208
- versionType,
13209
- skipPrFlow,
13210
- triggerType: "mcp"
13211
- });
13212
- return {
13213
- content: [
13214
- {
13215
- type: "text",
13216
- text: JSON.stringify(
13217
- {
13218
- releaseId: result.id,
13219
- status: result.status,
13220
- stepsCount: result.stepsCount,
13221
- author: result.authorName,
13222
- profile: resolved.profileName,
13223
- stage: resolved.stageName
13224
- },
13225
- null,
13226
- 2
13227
- )
13228
- }
13229
- ]
13230
- };
13231
- } catch (err) {
13232
- if (err instanceof CreateReleaseError) {
13233
- throw new Error(err.message);
13234
- }
13235
- throw err;
13236
- }
13237
- }
13238
12481
  case "release-profile-list": {
13239
12482
  const profiles = await db.execute(sql`
13240
12483
  SELECT
@@ -13282,64 +12525,74 @@ ${lines.join("\n\n")}`
13282
12525
  ]
13283
12526
  };
13284
12527
  }
13285
- case "release-profile-create": {
13286
- const repoFullName = String(a.repoFullName ?? "").trim();
13287
- const githubTokenId = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
13288
- const releaseType = a.releaseType ? String(a.releaseType) : null;
13289
- if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
13290
- throw new Error(
13291
- `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
13292
- );
12528
+ case "release-profile": {
12529
+ const action = String(a.action);
12530
+ if (action !== "create" && action !== "update") {
12531
+ throw new Error("action must be one of: create, update");
13293
12532
  }
13294
- try {
13295
- const result = await quickCreateReleaseProfile(
13296
- authContext.userId,
13297
- {
13298
- repoFullName,
13299
- githubTokenId,
13300
- name: a.name ? String(a.name) : null,
13301
- releaseType
13302
- }
13303
- );
13304
- return {
13305
- content: [
12533
+ if (action === "create") {
12534
+ const repoFullName = String(a.repoFullName ?? "").trim();
12535
+ if (!repoFullName) {
12536
+ throw new Error('action="create" requires repoFullName');
12537
+ }
12538
+ const githubTokenId2 = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
12539
+ const releaseType2 = a.releaseType ? String(a.releaseType) : null;
12540
+ if (releaseType2 !== null && releaseType2 !== "dev_only" && releaseType2 !== "prod_only" && releaseType2 !== "both") {
12541
+ throw new Error(
12542
+ `Invalid releaseType "${releaseType2}". Expected dev_only, prod_only, or both.`
12543
+ );
12544
+ }
12545
+ try {
12546
+ const result = await quickCreateReleaseProfile(
12547
+ authContext.userId,
13306
12548
  {
13307
- type: "text",
13308
- text: JSON.stringify(
13309
- {
13310
- profileId: result.profile.id,
13311
- name: result.profile.name,
13312
- repo: repoFullName,
13313
- releaseType: releaseType ?? "both",
13314
- appsDetected: result.scan.apps.map((app) => ({
13315
- path: app.path,
13316
- label: app.label,
13317
- deployMethod: app.deployMethod
13318
- })),
13319
- stages: result.stages.map((s) => ({
13320
- stage: s.stage,
13321
- name: s.name,
13322
- releaseBranch: s.release_branch,
13323
- triggerMode: s.trigger_mode
13324
- })),
13325
- branchWarnings: result.branchWarnings,
13326
- hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
13327
- },
13328
- null,
13329
- 2
13330
- )
12549
+ repoFullName,
12550
+ githubTokenId: githubTokenId2,
12551
+ name: a.name ? String(a.name) : null,
12552
+ releaseType: releaseType2
13331
12553
  }
13332
- ]
13333
- };
13334
- } catch (err) {
13335
- if (err instanceof ReleaseProfileQuickCreateError) {
13336
- throw new Error(err.message);
12554
+ );
12555
+ return {
12556
+ content: [
12557
+ {
12558
+ type: "text",
12559
+ text: JSON.stringify(
12560
+ {
12561
+ profileId: result.profile.id,
12562
+ name: result.profile.name,
12563
+ repo: repoFullName,
12564
+ releaseType: releaseType2 ?? "both",
12565
+ appsDetected: result.scan.apps.map((app) => ({
12566
+ path: app.path,
12567
+ label: app.label,
12568
+ deployMethod: app.deployMethod
12569
+ })),
12570
+ stages: result.stages.map((s) => ({
12571
+ stage: s.stage,
12572
+ name: s.name,
12573
+ releaseBranch: s.release_branch,
12574
+ triggerMode: s.trigger_mode
12575
+ })),
12576
+ branchWarnings: result.branchWarnings,
12577
+ hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
12578
+ },
12579
+ null,
12580
+ 2
12581
+ )
12582
+ }
12583
+ ]
12584
+ };
12585
+ } catch (err) {
12586
+ if (err instanceof ReleaseProfileQuickCreateError) {
12587
+ throw new Error(err.message);
12588
+ }
12589
+ throw err;
13337
12590
  }
13338
- throw err;
13339
12591
  }
13340
- }
13341
- case "release-profile-update": {
13342
12592
  const identifier = String(a.releaseProfile ?? "").trim();
12593
+ if (!identifier) {
12594
+ throw new Error('action="update" requires releaseProfile');
12595
+ }
13343
12596
  const profileRows = await db.execute(sql`
13344
12597
  SELECT id, name FROM release_profile
13345
12598
  WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}