sneakoscope 4.6.2 → 4.6.3

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/README.md CHANGED
@@ -49,7 +49,7 @@ sks seo-geo-optimizer apply latest --mode seo --apply --json
49
49
  sks seo-geo-optimizer audit --mode geo --target package --offline --json
50
50
  ```
51
51
 
52
- > 📋 **Current release: `v4.6.2`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
52
+ > 📋 **Current release: `v4.6.3`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
53
53
 
54
54
  ## 🍥 Parallelism, UX, And Integrations
55
55
 
@@ -659,7 +659,7 @@ npm install -g .
659
659
  sks doctor --fix
660
660
  ```
661
661
 
662
- If PATH or npm has duplicate global installs, `sks doctor --fix` keeps one global npm install and removes duplicate global `sneakoscope` installs. The Sneakoscope source checkout is exempt so local development files are not removed.
662
+ If PATH or npm has duplicate global installs, `sks doctor --fix` keeps one global npm install, removes duplicate global `sneakoscope` installs, and clears the npm cache so old downloaded package versions do not accumulate. The Sneakoscope source checkout is exempt so local development files are not removed.
663
663
 
664
664
  ### SKS keeps asking to update after a global update
665
665
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "4.6.2"
79
+ version = "4.6.3"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "4.6.2"
3
+ version = "4.6.3"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 4.6.2"),
7
+ Some("--version") => println!("sks-rs 4.6.3"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '4.6.2';
2
+ const FAST_PACKAGE_VERSION = '4.6.3';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -501,7 +501,7 @@ async function runDoctor(args = [], root, doctorFix) {
501
501
  warnings_suppressed: false,
502
502
  blockers: [err?.message || String(err)]
503
503
  }));
504
- const globalSksInstallCleanup = flag(args, '--fix') && !flag(args, '--local-only') && deepDiagnostics
504
+ const globalSksInstallCleanup = flag(args, '--fix') && !flag(args, '--local-only')
505
505
  ? await cleanDuplicateGlobalSksInstalls({ root, fix: true }).catch((err) => ({ schema: 'sks.global-sks-install-cleanup.v1', ok: false, fix: true, error: err?.message || String(err), blockers: ['global_sks_install_cleanup_exception'] }))
506
506
  : null;
507
507
  const { detectImagegenCapability } = await import('../core/imagegen/imagegen-capability.js');
@@ -879,6 +879,8 @@ async function runDoctor(args = [], root, doctorFix) {
879
879
  }
880
880
  if (globalSksInstallCleanup) {
881
881
  console.log(`Global SKS installs: kept ${globalSksInstallCleanup.kept?.length ?? 0}, removed ${globalSksInstallCleanup.removed?.filter((entry) => entry.ok).length ?? 0}, source repo exempt ${globalSksInstallCleanup.candidates?.filter((entry) => entry.source_repo_exempt).length ?? 0}`);
882
+ if (globalSksInstallCleanup.npm_cache)
883
+ console.log(`NPM cache cleanup: ${globalSksInstallCleanup.npm_cache.status}`);
882
884
  }
883
885
  if (!ready.ready && ready.next_actions?.length) {
884
886
  console.log('What still needs you:');
@@ -12,18 +12,22 @@ export async function cleanDuplicateGlobalSksInstalls(opts = {}) {
12
12
  const planned = planGlobalSksInstallCleanup(candidates, { sourceRoot });
13
13
  const removed = [];
14
14
  const blockers = [...planned.blockers];
15
+ const cleanupContract = opts.fix === true && npmBin
16
+ ? createRequestedScopeContract({
17
+ route: 'doctor',
18
+ userRequest: 'sks doctor --fix global SKS install and npm cache cleanup',
19
+ projectRoot: sourceRoot,
20
+ overrides: { package_install: true }
21
+ })
22
+ : null;
23
+ const guardContext = cleanupContract && npmBin
24
+ ? guardContextForRoute(sourceRoot, cleanupContract, 'sks doctor --fix global SKS install and npm cache cleanup')
25
+ : null;
15
26
  if (opts.fix === true && planned.removable.length > 0) {
16
- if (!npmBin) {
27
+ if (!npmBin || !guardContext) {
17
28
  blockers.push('npm_not_found_for_duplicate_global_sks_cleanup');
18
29
  }
19
30
  else {
20
- const cleanupContract = createRequestedScopeContract({
21
- route: 'doctor',
22
- userRequest: 'sks doctor --fix duplicate global SKS cleanup',
23
- projectRoot: sourceRoot,
24
- overrides: { package_install: true }
25
- });
26
- const guardContext = guardContextForRoute(sourceRoot, cleanupContract, 'sks doctor --fix duplicate global SKS cleanup');
27
31
  for (const candidate of planned.removable) {
28
32
  if (!candidate.prefix) {
29
33
  blockers.push(`duplicate_global_sks_missing_prefix:${candidate.package_root || candidate.bin || 'unknown'}`);
@@ -44,9 +48,12 @@ export async function cleanDuplicateGlobalSksInstalls(opts = {}) {
44
48
  }
45
49
  }
46
50
  }
51
+ const npmCache = await cleanNpmCache({ fix: opts.fix === true, npmBin, env, guardContext });
52
+ if (opts.fix === true && !npmCache.ok)
53
+ blockers.push(...npmCache.blockers);
47
54
  return {
48
55
  schema: 'sks.global-sks-install-cleanup.v1',
49
- ok: blockers.length === 0,
56
+ ok: blockers.length === 0 && npmCache.ok,
50
57
  fix: opts.fix === true,
51
58
  source_root: sourceRoot,
52
59
  package: 'sneakoscope',
@@ -54,6 +61,7 @@ export async function cleanDuplicateGlobalSksInstalls(opts = {}) {
54
61
  kept: planned.kept,
55
62
  removable: planned.removable,
56
63
  removed,
64
+ npm_cache: npmCache,
57
65
  blockers
58
66
  };
59
67
  }
@@ -217,6 +225,62 @@ function dedupeCandidates(candidates) {
217
225
  function scoreCandidate(candidate) {
218
226
  return (candidate.bin ? 2 : 0) + (candidate.prefix ? 1 : 0);
219
227
  }
228
+ async function cleanNpmCache(opts) {
229
+ const args = ['cache', 'clean', '--force', '--silent'];
230
+ const command = opts.npmBin ? [opts.npmBin, ...args].join(' ') : null;
231
+ if (!opts.fix) {
232
+ return {
233
+ schema: 'sks.npm-cache-cleanup.v1',
234
+ ok: true,
235
+ fix: false,
236
+ status: 'skipped',
237
+ command,
238
+ code: null,
239
+ scope: 'npm_cache_all_packages',
240
+ stdout_tail: '',
241
+ stderr_tail: '',
242
+ blockers: []
243
+ };
244
+ }
245
+ if (!opts.npmBin || !opts.guardContext) {
246
+ return {
247
+ schema: 'sks.npm-cache-cleanup.v1',
248
+ ok: false,
249
+ fix: true,
250
+ status: 'npm_missing',
251
+ command: null,
252
+ code: null,
253
+ scope: 'npm_cache_all_packages',
254
+ stdout_tail: '',
255
+ stderr_tail: '',
256
+ blockers: ['npm_not_found_for_npm_cache_cleanup']
257
+ };
258
+ }
259
+ const result = await guardedPackageInstall(opts.guardContext, 'npm cache clean --force', {
260
+ confirmed: true,
261
+ command: opts.npmBin,
262
+ args,
263
+ env: opts.env,
264
+ timeoutMs: 60_000,
265
+ maxOutputBytes: 16 * 1024
266
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
267
+ const ok = result.code === 0;
268
+ return {
269
+ schema: 'sks.npm-cache-cleanup.v1',
270
+ ok,
271
+ fix: true,
272
+ status: ok ? 'cleaned' : 'failed',
273
+ command,
274
+ code: result.code,
275
+ scope: 'npm_cache_all_packages',
276
+ stdout_tail: tail(result.stdout),
277
+ stderr_tail: tail(result.stderr),
278
+ blockers: ok ? [] : ['npm_cache_clean_failed']
279
+ };
280
+ }
281
+ function tail(text) {
282
+ return String(text || '').slice(-2000);
283
+ }
220
284
  async function realpathOrSelf(value) {
221
285
  try {
222
286
  return await fsp.realpath(value);
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '4.6.2';
8
+ export const PACKAGE_VERSION = '4.6.3';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -17,10 +17,10 @@ export async function prepareMadDbMission(input) {
17
17
  const blockers = [...target.blockers];
18
18
  let profile;
19
19
  if (!target.project_ref) {
20
- profile = await createMadDbRuntimeProfile({ root: input.root, missionId: id, cycleId, projectRef: 'missing-project-ref', runtimeSessionId });
20
+ profile = await createMadDbRuntimeProfile({ root: input.root, missionId: id, cycleId, projectRef: 'missing-project-ref', runtimeSessionId, mcpUrl: target.mcp_url });
21
21
  }
22
22
  else {
23
- profile = await createMadDbRuntimeProfile({ root: input.root, missionId: id, cycleId, projectRef: target.project_ref, runtimeSessionId });
23
+ profile = await createMadDbRuntimeProfile({ root: input.root, missionId: id, cycleId, projectRef: target.project_ref, runtimeSessionId, mcpUrl: target.mcp_url });
24
24
  }
25
25
  const capability = await createMadDbCapability(input.root, {
26
26
  missionId: id,
@@ -54,7 +54,7 @@ export async function prepareMadDbMission(input) {
54
54
  command: '$MAD-DB',
55
55
  mode: 'MADDB',
56
56
  task: input.task,
57
- target: { ...target, project_ref: target.project_ref ? `<hash:${target.project_ref_hash}>` : null },
57
+ target: redactTarget(target),
58
58
  capability_file: 'mad-db-capability.json',
59
59
  runtime_profile_manifest: 'mad-db/runtime/runtime-profile-manifest.json',
60
60
  tool_inventory: toolInventory ? 'mad-db/runtime/tool-inventory.json' : null
@@ -118,6 +118,8 @@ export async function runMadDbCycle(input) {
118
118
  await writeJsonAtomic(path.join(missionDir(input.root, prepared.mission_id), 'mad-db', 'runtime', 'tool-inventory.json'), inventory);
119
119
  if (!inventory.ok) {
120
120
  blockers.push('mad_db_execute_sql_or_apply_migration_unavailable');
121
+ if (inventory.error_kind)
122
+ blockers.push(inventory.error_kind);
121
123
  throw new Error('mad_db_tool_inventory_failed');
122
124
  }
123
125
  await activateMadDbCapability(input.root, prepared.mission_id);
@@ -154,6 +156,8 @@ export async function runMadDbCycle(input) {
154
156
  });
155
157
  if (!execution.ok)
156
158
  blockers.push('mad_db_tool_execution_failed');
159
+ if (!execution.ok && execution.error_kind)
160
+ blockers.push(execution.error_kind);
157
161
  if (execution.ok && input.verifySql) {
158
162
  const verifyStart = Date.now();
159
163
  readBack = await runReadBackChecks({
@@ -227,10 +231,7 @@ async function clearMadDbCurrentState(root, missionId, ok, restoration) {
227
231
  function redactCycleResult(result) {
228
232
  return {
229
233
  ...result,
230
- target: {
231
- ...result.target,
232
- project_ref: result.target.project_ref ? '<redacted>' : null
233
- }
234
+ target: redactTarget(result.target)
234
235
  };
235
236
  }
236
237
  async function resolveSqlInput(input) {
@@ -252,13 +253,21 @@ async function recreateProfileFromPrepared(root, prepared) {
252
253
  profile_path: prepared.runtime_profile.profile_path,
253
254
  profile_sha256: prepared.runtime_profile.profile_sha256,
254
255
  server_url_redacted: prepared.runtime_profile.server_url_redacted,
255
- server_url: `https://mcp.supabase.com/mcp?project_ref=${encodeURIComponent(projectRef)}&features=database`,
256
+ server_url: prepared.target.mcp_url || `https://mcp.supabase.com/mcp?project_ref=${encodeURIComponent(projectRef)}&features=database`,
257
+ server_url_source: prepared.target.mcp_url ? 'explicit_mcp_url' : 'generated_project_ref',
256
258
  features: ['database'],
257
259
  write_capable: true,
258
260
  normal_config_hash_before: prepared.runtime_profile.normal_config_hash_before,
259
261
  created_at: prepared.runtime_profile.created_at
260
262
  };
261
263
  }
264
+ function redactTarget(target) {
265
+ return {
266
+ ...target,
267
+ project_ref: target.project_ref ? `<hash:${target.project_ref_hash}>` : null,
268
+ mcp_url: target.mcp_url ? '<redacted>' : null
269
+ };
270
+ }
262
271
  export async function madDbRouteIdentityProof(root, missionId) {
263
272
  const capability = await readMadDbCapability(root, missionId);
264
273
  const state = await readJson(path.join(root, '.sneakoscope', 'state', 'current.json'), {});
@@ -33,10 +33,15 @@ export class MadDbMcpExecutor {
33
33
  execute_sql_available: hasTool(names, 'execute_sql'),
34
34
  apply_migration_available: hasTool(names, 'apply_migration'),
35
35
  duration_ms: Math.round(performance.now() - started),
36
- error_digest: null
36
+ error_digest: null,
37
+ error_summary: null,
38
+ error_kind: null,
39
+ retry_guidance: null
37
40
  };
38
41
  }
39
42
  catch (err) {
43
+ const summary = summarizeMadDbError(err);
44
+ const kind = classifyMadDbError(summary);
40
45
  return {
41
46
  schema: 'sks.mad-db-tool-inventory.v1',
42
47
  checked_at: nowIso(),
@@ -45,7 +50,10 @@ export class MadDbMcpExecutor {
45
50
  execute_sql_available: false,
46
51
  apply_migration_available: false,
47
52
  duration_ms: Math.round(performance.now() - started),
48
- error_digest: sha256(redactError(err)).slice(0, 32)
53
+ error_digest: sha256(summary).slice(0, 32),
54
+ error_summary: summary,
55
+ error_kind: kind,
56
+ retry_guidance: madDbRetryGuidance(kind)
49
57
  };
50
58
  }
51
59
  }
@@ -77,21 +85,29 @@ export class MadDbMcpExecutor {
77
85
  result_digest: sha256(JSON.stringify(redactToolResult(result))).slice(0, 32),
78
86
  row_count: extractRowCount(result),
79
87
  is_error: result.isError === true,
80
- duration_ms: Math.round(performance.now() - started)
88
+ duration_ms: Math.round(performance.now() - started),
89
+ error_summary: result.isError === true ? summarizeMadDbToolError(result) : null,
90
+ error_kind: result.isError === true ? classifyMadDbError(summarizeMadDbToolError(result)) : null,
91
+ retry_guidance: result.isError === true ? madDbRetryGuidance(classifyMadDbError(summarizeMadDbToolError(result))) : null
81
92
  };
82
93
  }
83
94
  catch (err) {
84
95
  lastError = err;
85
96
  }
86
97
  }
98
+ const summary = summarizeMadDbError(lastError);
99
+ const kind = classifyMadDbError(summary);
87
100
  return {
88
101
  schema: 'sks.mad-db-tool-result.v1',
89
102
  ok: false,
90
103
  tool_name: tool,
91
- result_digest: sha256(redactError(lastError)).slice(0, 32),
104
+ result_digest: sha256(summary).slice(0, 32),
92
105
  row_count: null,
93
106
  is_error: true,
94
- duration_ms: 0
107
+ duration_ms: 0,
108
+ error_summary: summary,
109
+ error_kind: kind,
110
+ retry_guidance: madDbRetryGuidance(kind)
95
111
  };
96
112
  }
97
113
  }
@@ -112,9 +128,45 @@ function authHeaders() {
112
128
  return undefined;
113
129
  return { Authorization: `Bearer ${token}` };
114
130
  }
115
- function redactError(err) {
131
+ export function summarizeMadDbError(err) {
116
132
  const text = err instanceof Error ? `${err.name}:${err.message}` : String(err);
117
- return text.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>').replace(/(token|password|secret|apikey)=([^&\s]+)/gi, '$1=<redacted>');
133
+ return redactErrorText(text).slice(0, 1200);
134
+ }
135
+ function summarizeMadDbToolError(result) {
136
+ return redactErrorText(JSON.stringify(redactToolResult(result))).slice(0, 1200);
137
+ }
138
+ function redactErrorText(text) {
139
+ return String(text || '')
140
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>')
141
+ .replace(/(token|password|secret|apikey)=([^&\s]+)/gi, '$1=<redacted>')
142
+ .replace(/(postgres(?:ql)?:\/\/[^:\s/]+:)([^@\s]+)(@)/gi, '$1<redacted>$3');
143
+ }
144
+ export function classifyMadDbError(summary) {
145
+ const text = String(summary || '').toLowerCase();
146
+ if (/read[_ -]?only|read only|permission denied.+read/i.test(text))
147
+ return 'supabase_mcp_read_only_transport';
148
+ if (/timeout|timed out|etimedout|i\/o timeout|aborterror|deadline/i.test(text))
149
+ return 'supabase_sql_plane_timeout';
150
+ if (/connection terminated|econnreset|unexpected eof|socket hang up|server closed/i.test(text))
151
+ return 'supabase_sql_plane_connection_interrupted';
152
+ if (/sasl|password authentication failed|invalid login|authentication failed|sqlstate 28p01/i.test(text))
153
+ return 'supabase_sql_plane_auth_failed';
154
+ if (/fetch failed|enotfound|econnrefused|network|dns/i.test(text))
155
+ return 'supabase_mcp_transport_unreachable';
156
+ return 'supabase_mcp_tool_error';
157
+ }
158
+ export function madDbRetryGuidance(kind) {
159
+ if (kind === 'supabase_mcp_read_only_transport')
160
+ return 'The active MadDB cycle must use the mission-local write-capable MCP URL. Pass --mcp-url or SKS_MAD_DB_MCP_URL if a project-local read-only Supabase MCP config is shadowing the intended transport.';
161
+ if (kind === 'supabase_sql_plane_timeout')
162
+ return 'Retry with an explicit Supabase pooler/session/direct MCP transport if available; Supabase CLI docs also support --db-url for CLI migration paths when pooler/direct connectivity differs.';
163
+ if (kind === 'supabase_sql_plane_connection_interrupted')
164
+ return 'Treat this as a SQL-plane connectivity interruption, not a DB safety denial. Retry after checking Supabase project health, pooler/direct connection mode, and network reachability.';
165
+ if (kind === 'supabase_sql_plane_auth_failed')
166
+ return 'Check SUPABASE_ACCESS_TOKEN and the project/database credentials used by the Supabase MCP server; do not retry destructive SQL until auth is corrected.';
167
+ if (kind === 'supabase_mcp_transport_unreachable')
168
+ return 'Check access to https://mcp.supabase.com/mcp or pass a trusted local MCP URL with --mcp-url for the active MadDB cycle.';
169
+ return 'Inspect error_summary in mad-db-result.json; the failure occurred after MadDB capability gating, so do not report it as a safety-gate denial.';
118
170
  }
119
171
  function redactToolResult(value) {
120
172
  if (value == null)
@@ -5,7 +5,7 @@ import { missionDir } from '../mission.js';
5
5
  export async function createMadDbRuntimeProfile(input) {
6
6
  const dir = path.join(missionDir(input.root, input.missionId), 'mad-db', 'runtime');
7
7
  await fs.mkdir(dir, { recursive: true });
8
- const url = madDbMcpUrl(input.projectRef);
8
+ const url = madDbMcpUrl(input.projectRef, input.mcpUrl);
9
9
  const text = [
10
10
  '[mcp_servers.supabase_mad_db]',
11
11
  `url = "${url}"`,
@@ -26,6 +26,7 @@ export async function createMadDbRuntimeProfile(input) {
26
26
  profile_sha256: profileHash,
27
27
  server_url_redacted: redactSupabaseUrl(url),
28
28
  server_url: url,
29
+ server_url_source: input.mcpUrl ? 'explicit_mcp_url' : 'generated_project_ref',
29
30
  features: ['database'],
30
31
  write_capable: true,
31
32
  normal_config_hash_before: normalHash,
@@ -74,7 +75,15 @@ export function redactedRuntimeProfile(profile) {
74
75
  const { server_url: _serverUrl, ...rest } = profile;
75
76
  return rest;
76
77
  }
77
- export function madDbMcpUrl(projectRef) {
78
+ export function madDbMcpUrl(projectRef, explicitUrl) {
79
+ if (explicitUrl) {
80
+ const parsed = new URL(explicitUrl);
81
+ if (projectRef && !parsed.searchParams.get('project_ref'))
82
+ parsed.searchParams.set('project_ref', projectRef);
83
+ parsed.searchParams.set('features', 'database');
84
+ parsed.searchParams.delete('read_only');
85
+ return parsed.toString();
86
+ }
78
87
  const params = new URLSearchParams();
79
88
  params.set('project_ref', projectRef);
80
89
  params.set('features', 'database');
@@ -1,10 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { exists, readText, sha256 } from '../fsx.js';
3
+ import { redactSupabaseUrl } from './mad-db-runtime-profile.js';
3
4
  export async function resolveMadDbTarget(root, input = {}) {
4
5
  const args = input.args || [];
5
6
  const explicit = input.projectRef || readOption(args, '--project-ref', '') || process.env.SKS_MAD_DB_PROJECT_REF || process.env.SKS_MAD_DB_E2E_PROJECT_REF || '';
6
7
  const candidates = explicit ? [explicit] : await projectRefCandidates(root);
7
8
  const projectRef = explicit || (candidates.length === 1 ? candidates[0] || '' : '');
9
+ const explicitMcpUrl = readOption(args, '--mcp-url', '') || process.env.SKS_MAD_DB_MCP_URL || process.env.SKS_MAD_DB_SUPABASE_MCP_URL || '';
10
+ const normalizedMcp = normalizeExplicitMcpUrl(explicitMcpUrl, projectRef || null);
8
11
  const target = normalizeTarget(input.target || readOption(args, '--target', '') || process.env.SKS_MAD_DB_TARGET || process.env.SKS_MAD_DB_E2E_TARGET || 'production');
9
12
  const allowedSchemas = input.allowedSchemas?.length
10
13
  ? input.allowedSchemas
@@ -12,10 +15,16 @@ export async function resolveMadDbTarget(root, input = {}) {
12
15
  const blockers = [];
13
16
  if (!projectRef)
14
17
  blockers.push(candidates.length > 1 ? 'mad_db_project_ref_ambiguous' : 'mad_db_project_ref_missing');
18
+ if (normalizedMcp.blocker)
19
+ blockers.push(normalizedMcp.blocker);
15
20
  return {
16
21
  schema: 'sks.mad-db-target.v1',
17
22
  project_ref: projectRef || null,
18
23
  project_ref_hash: projectRef ? sha256(projectRef).slice(0, 16) : null,
24
+ mcp_url: normalizedMcp.url,
25
+ mcp_url_hash: normalizedMcp.url ? sha256(normalizedMcp.url).slice(0, 16) : null,
26
+ mcp_url_redacted: normalizedMcp.url ? redactSupabaseUrl(normalizedMcp.url) : null,
27
+ mcp_url_source: normalizedMcp.url ? 'explicit_or_environment' : 'default_generated',
19
28
  target_environment: target,
20
29
  allowed_schemas: allowedSchemas.length ? allowedSchemas : ['public'],
21
30
  source: explicit ? 'explicit_or_environment' : candidates.length === 1 ? 'managed_config_single_candidate' : 'unresolved',
@@ -23,6 +32,28 @@ export async function resolveMadDbTarget(root, input = {}) {
23
32
  candidates: candidates.map((candidate) => `${sha256(candidate).slice(0, 8)}:${candidate.slice(0, 2)}...`)
24
33
  };
25
34
  }
35
+ function normalizeExplicitMcpUrl(value, projectRef) {
36
+ if (!value)
37
+ return { url: null, blocker: null };
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(value);
41
+ }
42
+ catch {
43
+ return { url: null, blocker: 'mad_db_mcp_url_invalid' };
44
+ }
45
+ const isSupabaseRemote = parsed.protocol === 'https:' && parsed.hostname === 'mcp.supabase.com' && parsed.pathname === '/mcp';
46
+ const isLocalMcp = parsed.protocol === 'http:' && /^localhost$|^127\.0\.0\.1$/.test(parsed.hostname) && parsed.pathname.endsWith('/mcp');
47
+ if (!isSupabaseRemote && !isLocalMcp)
48
+ return { url: null, blocker: 'mad_db_mcp_url_untrusted_host' };
49
+ if (parsed.searchParams.get('read_only') === 'true')
50
+ return { url: null, blocker: 'mad_db_mcp_url_read_only_conflict' };
51
+ if (projectRef && !parsed.searchParams.get('project_ref'))
52
+ parsed.searchParams.set('project_ref', projectRef);
53
+ parsed.searchParams.set('features', 'database');
54
+ parsed.searchParams.delete('read_only');
55
+ return { url: parsed.toString(), blocker: null };
56
+ }
26
57
  export async function projectRootHash(root) {
27
58
  return sha256(path.resolve(root)).slice(0, 24);
28
59
  }
@@ -389,7 +389,7 @@ export async function detectEffectiveSksVersion(options = {}) {
389
389
  const pathCandidate = candidates.find((candidate) => candidate.source.startsWith('PATH:'))?.version || null;
390
390
  const npmGlobalCandidate = candidates.find((candidate) => candidate.source.startsWith('npm-global:'))?.version || null;
391
391
  const packageRootCandidate = candidates.find((candidate) => candidate.source === 'packageRoot:package.json')?.version || null;
392
- const current = highestPackageVersion(candidates.map((candidate) => candidate.version));
392
+ const current = effectiveInstalledVersion(candidates);
393
393
  return {
394
394
  current,
395
395
  runtime_current: PACKAGE_VERSION,
@@ -533,6 +533,16 @@ function updateNowError(install, newBinary, newVersionDoctor, migrationCurrent)
533
533
  return 'project update migration receipt was not current';
534
534
  return 'update failed';
535
535
  }
536
+ function effectiveInstalledVersion(candidates) {
537
+ const firstBySource = (source) => candidates.find((candidate) => candidate.source === source)?.version || null;
538
+ const firstByPrefix = (prefix) => candidates.find((candidate) => candidate.source.startsWith(prefix))?.version || null;
539
+ return firstBySource('env:SKS_INSTALLED_SKS_VERSION')
540
+ || firstByPrefix('npm-global:')
541
+ || firstByPrefix('PATH:')
542
+ || firstBySource('runtime')
543
+ || firstBySource('packageRoot:package.json')
544
+ || highestPackageVersion(candidates.map((candidate) => candidate.version));
545
+ }
536
546
  function highestPackageVersion(versions) {
537
547
  return versions
538
548
  .filter((version) => typeof version === 'string' && version.length > 0)
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '4.6.2';
1
+ export const PACKAGE_VERSION = '4.6.3';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createMission } from '../core/mission.js';
6
+ import { classifyMadDbError, madDbRetryGuidance, summarizeMadDbError } from '../core/mad-db/mad-db-executor.js';
7
+ import { createMadDbRuntimeProfile, madDbMcpUrl } from '../core/mad-db/mad-db-runtime-profile.js';
8
+ import { resolveMadDbTarget } from '../core/mad-db/mad-db-target.js';
9
+ import { assertGate, emitGate } from './sks-1-18-gate-lib.js';
10
+ const timeoutSummary = summarizeMadDbError(new Error('failed to connect to postgres: dial tcp 1.2.3.4:6543: i/o timeout token=secret-token'));
11
+ assertGate(classifyMadDbError(timeoutSummary) === 'supabase_sql_plane_timeout', 'timeout errors must be classified as SQL-plane timeout', { timeoutSummary });
12
+ assertGate(!timeoutSummary.includes('secret-token'), 'error summary must redact token query values', { timeoutSummary });
13
+ assertGate(madDbRetryGuidance('supabase_sql_plane_timeout').includes('--db-url'), 'timeout guidance should mention explicit db-url fallback evidence from Supabase CLI docs');
14
+ const readOnlyKind = classifyMadDbError('MCP server denied apply_migration because this connection is read only');
15
+ assertGate(readOnlyKind === 'supabase_mcp_read_only_transport', 'read-only transport errors must be separated from SQL-plane connectivity', { readOnlyKind });
16
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-mad-db-transport-'));
17
+ await fs.mkdir(path.join(root, '.codex'), { recursive: true });
18
+ await fs.writeFile(path.join(root, '.codex', 'config.toml'), '[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=localref&read_only=true&features=database,docs"\n', 'utf8');
19
+ const target = await resolveMadDbTarget(root, {
20
+ args: ['--project-ref', 'explicitref', '--mcp-url', 'https://mcp.supabase.com/mcp?features=docs']
21
+ });
22
+ assertGate(target.blockers.length === 0, 'explicit trusted MCP URL without read_only should be accepted', target);
23
+ assertGate(target.mcp_url?.includes('project_ref=explicitref'), 'explicit MCP URL should inherit project_ref when omitted', target);
24
+ assertGate(target.mcp_url?.includes('features=database'), 'explicit MCP URL should force database feature for MadDB SQL-plane execution', target);
25
+ assertGate(!target.mcp_url?.includes('read_only=true'), 'active MadDB explicit MCP URL must not remain read-only', target);
26
+ const conflict = await resolveMadDbTarget(root, {
27
+ args: ['--project-ref', 'explicitref', '--mcp-url', 'https://mcp.supabase.com/mcp?read_only=true']
28
+ });
29
+ assertGate(conflict.blockers.includes('mad_db_mcp_url_read_only_conflict'), 'read-only explicit MCP URL must be rejected in active MadDB', conflict);
30
+ const mission = await createMission(root, { mode: 'mad-db', prompt: 'transport diagnostics fixture' });
31
+ const profile = await createMadDbRuntimeProfile({
32
+ root,
33
+ missionId: mission.id,
34
+ cycleId: 'cycle-fixture',
35
+ projectRef: 'explicitref',
36
+ runtimeSessionId: 'session-fixture',
37
+ mcpUrl: target.mcp_url
38
+ });
39
+ assertGate(profile.server_url_source === 'explicit_mcp_url', 'runtime profile must record explicit MCP URL source', profile);
40
+ assertGate(profile.server_url === target.mcp_url, 'runtime profile must use normalized explicit MCP URL', { profile, target });
41
+ assertGate(madDbMcpUrl('abc123') === 'https://mcp.supabase.com/mcp?project_ref=abc123&features=database', 'generated MCP URL should stay project scoped and database-feature scoped');
42
+ emitGate('mad-db:supabase-transport-diagnostics', {
43
+ timeout_kind: classifyMadDbError(timeoutSummary),
44
+ read_only_kind: readOnlyKind,
45
+ mcp_url_source: profile.server_url_source
46
+ });
47
+ //# sourceMappingURL=mad-db-supabase-transport-diagnostics-check.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "4.6.2",
4
+ "version": "4.6.3",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -745,6 +745,7 @@
745
745
  "mad-db:runtime-profile": "node ./dist/scripts/mad-db-runtime-profile-lifecycle-check.js",
746
746
  "mad-db:skill-policy": "node ./dist/scripts/mad-db-skill-policy-snapshot-check.js",
747
747
  "mad-db:policy-v2": "node ./dist/scripts/mad-db-policy-v2-check.js",
748
+ "mad-db:supabase-transport-diagnostics": "node ./dist/scripts/mad-db-supabase-transport-diagnostics-check.js",
748
749
  "release:speed-summary": "node ./dist/scripts/release-speed-summary-check.js",
749
750
  "release:speed-summary:check": "node ./dist/scripts/release-speed-summary-check.js",
750
751
  "naruto:ssot-routing": "node ./dist/scripts/naruto-ssot-routing-check.js",
@@ -778,7 +779,7 @@
778
779
  "mad-db:lifecycle-hook-decision": "node ./dist/scripts/mad-db-lifecycle-hook-decision-check.js",
779
780
  "mad-db:mcp-result-lifecycle": "node ./dist/scripts/mad-db-mcp-result-lifecycle-check.js",
780
781
  "mad-db:operation-lifecycle-blackbox": "node ./dist/scripts/mad-db-operation-lifecycle-blackbox.js",
781
- "mad-db:unit": "npm run mad-db:capability && npm run mad-db:command && npm run mad-db:mad-command && npm run mad-db:priority-resolver && npm run mad-db:ledger && npm run mad-db:one-cycle-consumption && npm run mad-db:safety-conflict-matrix && npm run mad-db:one-cycle-bounded && npm run mad-db:operation-lifecycle-ledger && npm run mad-db:route-identity && npm run mad-db:hook-idempotency && npm run mad-db:direct-apply-migration-hook && npm run mad-db:parallel-lifecycle && npm run mad-db:runtime-profile && npm run mad-db:skill-policy && npm run mad-db:policy-v2 && npm run mad-db:lifecycle-hook-decision && npm run mad-db:mcp-result-lifecycle && npm run mad-db:operation-lifecycle-blackbox",
782
+ "mad-db:unit": "npm run mad-db:capability && npm run mad-db:command && npm run mad-db:mad-command && npm run mad-db:priority-resolver && npm run mad-db:ledger && npm run mad-db:one-cycle-consumption && npm run mad-db:safety-conflict-matrix && npm run mad-db:one-cycle-bounded && npm run mad-db:operation-lifecycle-ledger && npm run mad-db:route-identity && npm run mad-db:hook-idempotency && npm run mad-db:direct-apply-migration-hook && npm run mad-db:parallel-lifecycle && npm run mad-db:runtime-profile && npm run mad-db:skill-policy && npm run mad-db:policy-v2 && npm run mad-db:supabase-transport-diagnostics && npm run mad-db:lifecycle-hook-decision && npm run mad-db:mcp-result-lifecycle && npm run mad-db:operation-lifecycle-blackbox",
782
783
  "mad-db:real-e2e": "node ./dist/scripts/mad-db-real-supabase-e2e.js --require-real",
783
784
  "mad-db:all": "npm run build && npm run mad-db:unit",
784
785
  "mad-db:release": "npm run mad-db:all && npm run mad-db:real-e2e",