@bridge_gpt/mcp-server 0.2.12 → 0.2.13
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 +63 -1
- package/build/conductor/doctor.js +78 -1
- package/build/conductor/epic-runtime.js +126 -54
- package/build/conductor/epic-state.js +20 -5
- package/build/conductor/local-merge.js +212 -0
- package/build/conductor/pr-ci-producer.js +12 -2
- package/build/conductor/store.js +7 -4
- package/build/conductor/taxonomy.js +4 -0
- package/build/conductor-bin.js +476 -137
- package/build/doctor.js +3 -0
- package/build/index.js +1818 -441
- package/build/init.js +57 -0
- package/build/mcp-profile.js +33 -30
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +151 -0
- package/build/sfcc/config.js +39 -0
- package/build/sfcc/credentials.js +136 -0
- package/build/sfcc/ocapi-shape.js +77 -0
- package/build/sfcc/output.js +39 -0
- package/build/sfcc/permissions.js +136 -0
- package/build/sfcc/reads-custom-object-def.js +119 -0
- package/build/sfcc/reads-site-preference.js +158 -0
- package/build/sfcc/reads-system-object.js +162 -0
- package/build/sfcc/register.js +73 -0
- package/build/sfcc/setup-status.js +114 -0
- package/build/sfcc/tool-wrapper.js +70 -0
- package/build/start-tickets-conductor.js +9 -1
- package/build/start-tickets.js +47 -4
- package/build/version.generated.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC system-object introspection read tools (BAPI-401, T1–T3).
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* system_object_list — GET /system_object_definitions
|
|
6
|
+
* system_object_get — GET /system_object_definitions/{type}
|
|
7
|
+
* system_object_attribute_search — POST /system_object_definitions/{type}/attribute_definition_search
|
|
8
|
+
*
|
|
9
|
+
* All tools are read-only, run behind the T1 call-time gate, and route
|
|
10
|
+
* oversized payloads through the sfcc/output.ts truncate-and-save seam.
|
|
11
|
+
*/
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { ocapiGet, ocapiPost } from "./client.js";
|
|
15
|
+
import { withSfccGate } from "./tool-wrapper.js";
|
|
16
|
+
import { normalizeOcapiBody } from "./ocapi-shape.js";
|
|
17
|
+
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Annotations (SPEC: read-only against developer sandboxes)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const READ_ANNOTATIONS = {
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
};
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Input schemas
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const systemObjectListInput = z.object({
|
|
31
|
+
count: z.number().optional().describe("Maximum number of system object types to return."),
|
|
32
|
+
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
33
|
+
});
|
|
34
|
+
const systemObjectGetInput = z.object({
|
|
35
|
+
object_type: z.string().describe("System object type identifier, e.g. \"Product\" or \"Order\"."),
|
|
36
|
+
expand_attribute_definitions: z.boolean().optional().describe("When true, include the full attribute definition list in the response. " +
|
|
37
|
+
"May produce a large payload — auto-saved locally when oversized."),
|
|
38
|
+
});
|
|
39
|
+
const systemObjectAttributeSearchInput = z.object({
|
|
40
|
+
object_type: z.string().describe("System object type to search within, e.g. \"Order\"."),
|
|
41
|
+
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for a text search across id and display_name, " +
|
|
42
|
+
"or a structured OCAPI query object (term_query, filtered_query, etc.)."),
|
|
43
|
+
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
44
|
+
count: z.number().optional().describe("Maximum number of results to return."),
|
|
45
|
+
sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
|
|
46
|
+
});
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
function safeTimestamp() {
|
|
51
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
52
|
+
}
|
|
53
|
+
function safeType(objectType) {
|
|
54
|
+
return encodeURIComponent(objectType).replace(/%/g, "_");
|
|
55
|
+
}
|
|
56
|
+
function textResult(text) {
|
|
57
|
+
return { content: [{ type: "text", text }] };
|
|
58
|
+
}
|
|
59
|
+
async function saveAndReturn(text, dir, filename) {
|
|
60
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
61
|
+
return textResult(output);
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Handlers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
function buildSystemObjectListHandler(gateDeps, getDocsDir) {
|
|
67
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
68
|
+
const { count, start } = systemObjectListInput.parse(args);
|
|
69
|
+
const queryParams = {};
|
|
70
|
+
if (count !== undefined)
|
|
71
|
+
queryParams.count = String(count);
|
|
72
|
+
if (start !== undefined)
|
|
73
|
+
queryParams.start = String(start);
|
|
74
|
+
const queryStr = Object.keys(queryParams).length > 0
|
|
75
|
+
? "?" + new URLSearchParams(queryParams).toString()
|
|
76
|
+
: "";
|
|
77
|
+
const result = await ocapiGet(`/system_object_definitions${queryStr}`, credentials);
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
|
|
80
|
+
}
|
|
81
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
82
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
83
|
+
const dir = path.join(await getDocsDir(), "sfcc");
|
|
84
|
+
return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function buildSystemObjectGetHandler(gateDeps, getDocsDir) {
|
|
88
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
89
|
+
const { object_type, expand_attribute_definitions } = systemObjectGetInput.parse(args);
|
|
90
|
+
const encodedType = encodeURIComponent(object_type);
|
|
91
|
+
const expandParam = expand_attribute_definitions === true
|
|
92
|
+
? "?expand=attribute_definitions"
|
|
93
|
+
: "";
|
|
94
|
+
const result = await ocapiGet(`/system_object_definitions/${encodedType}${expandParam}`, credentials);
|
|
95
|
+
if (!result.ok) {
|
|
96
|
+
return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
|
|
97
|
+
}
|
|
98
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
99
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
100
|
+
const dir = path.join(await getDocsDir(), "sfcc");
|
|
101
|
+
return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
105
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
106
|
+
const { object_type, query, start, count, sorts } = systemObjectAttributeSearchInput.parse(args);
|
|
107
|
+
const encodedType = encodeURIComponent(object_type);
|
|
108
|
+
// Coerce plain string queries into OCAPI text_query shape
|
|
109
|
+
const resolvedQuery = typeof query === "string"
|
|
110
|
+
? { text_query: { fields: ["id", "display_name"], search_phrase: query } }
|
|
111
|
+
: query;
|
|
112
|
+
const postBody = { query: resolvedQuery };
|
|
113
|
+
if (start !== undefined)
|
|
114
|
+
postBody.start = start;
|
|
115
|
+
if (count !== undefined)
|
|
116
|
+
postBody.count = count;
|
|
117
|
+
if (sorts !== undefined)
|
|
118
|
+
postBody.sorts = sorts;
|
|
119
|
+
const result = await ocapiPost(`/system_object_definitions/${encodedType}/attribute_definition_search`, postBody, credentials);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
return textResult(JSON.stringify({ error: `OCAPI error`, status: result.status, body: result.body }, null, 2));
|
|
122
|
+
}
|
|
123
|
+
const normalized = normalizeOcapiBody(result.body);
|
|
124
|
+
const text = JSON.stringify(normalized, null, 2);
|
|
125
|
+
const dir = path.join(await getDocsDir(), "sfcc");
|
|
126
|
+
return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Register the three SFCC system-object introspection read tools.
|
|
131
|
+
*
|
|
132
|
+
* Called from `registerSfccTools` — no direct `index.ts` edits needed.
|
|
133
|
+
*/
|
|
134
|
+
export function registerSystemObjectReadTools(registerTool, deps) {
|
|
135
|
+
const { gateDeps, getDocsDir } = deps;
|
|
136
|
+
registerTool("system_object_list", {
|
|
137
|
+
description: "List all system object types from the developer sandbox via " +
|
|
138
|
+
"GET /system_object_definitions. Read-only. " +
|
|
139
|
+
"Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). " +
|
|
140
|
+
"Oversized outputs are auto-saved locally and previewed inline.",
|
|
141
|
+
inputSchema: systemObjectListInput,
|
|
142
|
+
annotations: READ_ANNOTATIONS,
|
|
143
|
+
}, buildSystemObjectListHandler(gateDeps, getDocsDir));
|
|
144
|
+
registerTool("system_object_get", {
|
|
145
|
+
description: "Retrieve a system object type from the developer sandbox. Read-only. " +
|
|
146
|
+
"GET /system_object_definitions/{type}. " +
|
|
147
|
+
"Prefer system_object_attribute_search for targeted attribute lookups — " +
|
|
148
|
+
"use this for a full type dump or expanded attribute list " +
|
|
149
|
+
"(expand_attribute_definitions=true). Oversized payloads are auto-saved locally.",
|
|
150
|
+
inputSchema: systemObjectGetInput,
|
|
151
|
+
annotations: READ_ANNOTATIONS,
|
|
152
|
+
}, buildSystemObjectGetHandler(gateDeps, getDocsDir));
|
|
153
|
+
registerTool("system_object_attribute_search", {
|
|
154
|
+
description: "Search attribute definitions for a system object type. Read-only. " +
|
|
155
|
+
"POST /system_object_definitions/{type}/attribute_definition_search. " +
|
|
156
|
+
"Prefer over system_object_get+expand for targeted c_ attribute lookups. " +
|
|
157
|
+
"Pass a plain string for text search or a structured OCAPI query object. " +
|
|
158
|
+
"Oversized results are auto-saved locally.",
|
|
159
|
+
inputSchema: systemObjectAttributeSearchInput,
|
|
160
|
+
annotations: READ_ANNOTATIONS,
|
|
161
|
+
}, buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir));
|
|
162
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC MCP tool registration entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Exports `registerSfccTools(registerTool, deps)` so index.ts can wire all
|
|
5
|
+
* SFCC tools with a single call adjacent to `registerConductorTools`. All
|
|
6
|
+
* SFCC business logic lives inside the sfcc/ module; index.ts stays a
|
|
7
|
+
* thin registration/composition layer.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { buildSfccSetupStatusHandler } from "./setup-status.js";
|
|
11
|
+
import { withSfccGate } from "./tool-wrapper.js";
|
|
12
|
+
import { checkPermissionsTool } from "./permissions.js";
|
|
13
|
+
import { registerSystemObjectReadTools } from "./reads-system-object.js";
|
|
14
|
+
import { registerSfccCustomObjectDefReadTools } from "./reads-custom-object-def.js";
|
|
15
|
+
import { registerSitePreferenceTools } from "./reads-site-preference.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Registration
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Register all SFCC tools through the host's registerTool wrapper.
|
|
21
|
+
*
|
|
22
|
+
* Call this once in index.ts, immediately after `registerConductorTools(registerTool)`.
|
|
23
|
+
* Sub-task tickets (2–4) add their read tools here; they never touch index.ts.
|
|
24
|
+
*/
|
|
25
|
+
export function registerSfccTools(registerTool, deps) {
|
|
26
|
+
const gateDeps = {
|
|
27
|
+
buildGetUrl: deps.buildGetUrl,
|
|
28
|
+
getGetHeaders: deps.getGetHeaders,
|
|
29
|
+
repoName: deps.repoName,
|
|
30
|
+
};
|
|
31
|
+
// sfcc_setup_status — aggregate prerequisite reporter.
|
|
32
|
+
registerTool("sfcc_setup_status", {
|
|
33
|
+
description: "Report on every SFCC prerequisite: Bridge API key, repo name, version config, " +
|
|
34
|
+
"dw.json presence/uniqueness, and AM token acquisition. Always-registered; " +
|
|
35
|
+
"returns status without requiring full SFCC configuration to be complete.",
|
|
36
|
+
inputSchema: z.object({}),
|
|
37
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
38
|
+
}, buildSfccSetupStatusHandler(deps.buildGetUrl, deps.getGetHeaders, deps.repoName, deps.getResolvedApiKey));
|
|
39
|
+
// check_permissions — OCAPI access probe + settings JSON printer.
|
|
40
|
+
const gatedCheckPermissions = withSfccGate(gateDeps, async (_args, credentials) => checkPermissionsTool(credentials));
|
|
41
|
+
registerTool("check_permissions", {
|
|
42
|
+
description: "Probe SFCC OCAPI access via GET /system_object_definitions. " +
|
|
43
|
+
"On 200: reports OK and the detected OCAPI version. " +
|
|
44
|
+
"On 401/403: prints the exact OCAPI Settings JSON to paste in Business Manager " +
|
|
45
|
+
"(split read-only vs. write/import grants).",
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
instance: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Explicit sandbox hostname to use instead of dw.json auto-detection."),
|
|
51
|
+
}),
|
|
52
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
53
|
+
}, gatedCheckPermissions);
|
|
54
|
+
// Heavy read tools are gated behind ACTIVE_GROUPS.has("sfcc") (BAPI-453).
|
|
55
|
+
// sfcc_setup_status and check_permissions above remain always-on (D-4 decision).
|
|
56
|
+
if (deps.includeReadTools) {
|
|
57
|
+
// system_object_list, system_object_get, system_object_attribute_search (BAPI-401 T1–T3)
|
|
58
|
+
registerSystemObjectReadTools(registerTool, {
|
|
59
|
+
gateDeps,
|
|
60
|
+
getDocsDir: deps.getDocsDir,
|
|
61
|
+
});
|
|
62
|
+
// custom_object_definition_list, custom_object_definition_get (BAPI-402 T4–T5)
|
|
63
|
+
registerSfccCustomObjectDefReadTools(registerTool, {
|
|
64
|
+
gateDeps,
|
|
65
|
+
getDocsDir: deps.getDocsDir,
|
|
66
|
+
});
|
|
67
|
+
// site_preference_get, site_preference_search (BAPI-403 T6 — optional)
|
|
68
|
+
registerSitePreferenceTools(registerTool, {
|
|
69
|
+
gateDeps,
|
|
70
|
+
getDocsDir: deps.getDocsDir,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sfcc_setup_status — SFCC prerequisite aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Reports on every prerequisite needed to use SFCC tools, completely free of
|
|
5
|
+
* secret values. Each check runs independently so a failure in one does not
|
|
6
|
+
* prevent the others from running.
|
|
7
|
+
*/
|
|
8
|
+
import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
|
|
9
|
+
import { resolveSfccCredentials } from "./credentials.js";
|
|
10
|
+
import { getAmToken } from "./client.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Implementation
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Aggregate every SFCC prerequisite into one status report.
|
|
16
|
+
*
|
|
17
|
+
* Checks (in order):
|
|
18
|
+
* 1. Bridge API key resolved
|
|
19
|
+
* 2. Repo name set (REPO_NAME)
|
|
20
|
+
* 3. version config field is an SFCC version
|
|
21
|
+
* 4. dw.json found / instance unambiguous
|
|
22
|
+
* 5. AM token acquisition
|
|
23
|
+
*
|
|
24
|
+
* Never throws; each check is caught independently so partial states are
|
|
25
|
+
* always reported. Output is completely secret-free.
|
|
26
|
+
*/
|
|
27
|
+
export async function sfccSetupStatusTool(deps) {
|
|
28
|
+
const lines = ["## SFCC Setup Status\n"];
|
|
29
|
+
// 1. Bridge API key
|
|
30
|
+
const apiKeyOk = Boolean(deps.apiKey);
|
|
31
|
+
lines.push(`1. Bridge API Key: ${apiKeyOk ? "✓ Resolved" : "✗ Missing (set BAPI_API_KEY)"}`);
|
|
32
|
+
// 2. Repo name
|
|
33
|
+
const repoOk = Boolean(deps.repoName);
|
|
34
|
+
lines.push(`2. Repo Name: ${repoOk ? `✓ Set (${deps.repoName})` : "✗ Not set (set BAPI_REPO_NAME)"}`);
|
|
35
|
+
// 3. SFCC version config
|
|
36
|
+
let versionStatus = "✗ Not set";
|
|
37
|
+
let resolvedVersion = null;
|
|
38
|
+
if (apiKeyOk && repoOk) {
|
|
39
|
+
try {
|
|
40
|
+
resolvedVersion = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
|
|
41
|
+
if (resolvedVersion === null) {
|
|
42
|
+
versionStatus = "✗ Not set (configure the 'version' field in Bridge API project settings)";
|
|
43
|
+
}
|
|
44
|
+
else if (!SFCC_VERSIONS.includes(resolvedVersion)) {
|
|
45
|
+
versionStatus = `✗ '${resolvedVersion}' is not an SFCC version (expected: ${SFCC_VERSIONS.join(", ")})`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
versionStatus = `✓ '${resolvedVersion}'`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
versionStatus = "✗ Could not read (Bridge API error)";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
versionStatus = "— Skipped (Bridge API not configured)";
|
|
57
|
+
}
|
|
58
|
+
lines.push(`3. SFCC Version: ${versionStatus}`);
|
|
59
|
+
// 4. dw.json / credential resolution
|
|
60
|
+
let credStatus = "✗ Missing";
|
|
61
|
+
let resolvedCredentials = null;
|
|
62
|
+
try {
|
|
63
|
+
const result = await resolveSfccCredentials();
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
// Report source and instance ID only — never the secret values.
|
|
66
|
+
resolvedCredentials = {
|
|
67
|
+
hostname: result.credentials.hostname.split(".")[0] ?? result.credentials.hostname,
|
|
68
|
+
source: result.credentials.source,
|
|
69
|
+
};
|
|
70
|
+
credStatus = `✓ Found (${resolvedCredentials.source})`;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
credStatus = `✗ ${result.error}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
credStatus = `✗ Resolution error: ${err instanceof Error ? err.message : String(err)}`;
|
|
78
|
+
}
|
|
79
|
+
lines.push(`4. dw.json / Credentials: ${credStatus}`);
|
|
80
|
+
// 5. AM token acquisition (only if credentials were resolved)
|
|
81
|
+
let tokenStatus = "— Skipped (credentials not available)";
|
|
82
|
+
if (resolvedCredentials) {
|
|
83
|
+
try {
|
|
84
|
+
const credResult = await resolveSfccCredentials();
|
|
85
|
+
if (credResult.ok) {
|
|
86
|
+
await getAmToken(credResult.credentials);
|
|
87
|
+
tokenStatus = `✓ Token acquired for instance ${resolvedCredentials.hostname}`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
tokenStatus = "✗ Credentials not resolved";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Sanitize: strip any token/secret-looking content from error message.
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
// The error from getAmToken is already sanitized (includes only HTTP status + instance ID).
|
|
97
|
+
tokenStatus = `✗ ${msg}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.push(`5. AM Token: ${tokenStatus}`);
|
|
101
|
+
lines.push("\nRun `check_permissions` to probe OCAPI access once steps 1–5 are all green.");
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the sfcc_setup_status handler bound to the host's runtime deps.
|
|
108
|
+
*/
|
|
109
|
+
export function buildSfccSetupStatusHandler(buildGetUrl, getGetHeaders, repoName, getApiKey) {
|
|
110
|
+
return async (_args) => {
|
|
111
|
+
const apiKey = await getApiKey();
|
|
112
|
+
return sfccSetupStatusTool({ buildGetUrl, getGetHeaders, repoName, apiKey });
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call-time SFCC configuration gate.
|
|
3
|
+
*
|
|
4
|
+
* Wraps every SFCC tool handler. Evaluated at call time (NOT startup) so all
|
|
5
|
+
* SFCC tools are always registered — the gate decides at invocation whether the
|
|
6
|
+
* repo is configured for SFCC and has valid local credentials.
|
|
7
|
+
*
|
|
8
|
+
* The "not configured" response distinguishes three failure classes:
|
|
9
|
+
* (a) Bridge-auth/config failure — could not read /config-field/version.
|
|
10
|
+
* (b) version-not-SFCC — version is set but is not an SFCC version.
|
|
11
|
+
* (c) missing/ambiguous dw.json — credential resolution failed.
|
|
12
|
+
*/
|
|
13
|
+
import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
|
|
14
|
+
import { resolveSfccCredentials } from "./credentials.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Not-configured envelope helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function notConfigured(failureClass, message) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
error: "NOT_CONFIGURED",
|
|
25
|
+
status: 503,
|
|
26
|
+
failure_class: failureClass,
|
|
27
|
+
message,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Gate
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Wrap an SFCC tool handler with the call-time version + credential gate.
|
|
38
|
+
*
|
|
39
|
+
* The returned handler:
|
|
40
|
+
* 1. Reads /config-field/version via Bridge API.
|
|
41
|
+
* 2. Validates the value is an SFCC version.
|
|
42
|
+
* 3. Resolves dw.json credentials.
|
|
43
|
+
* 4. Calls the inner handler only if all checks pass.
|
|
44
|
+
*/
|
|
45
|
+
export function withSfccGate(deps, handler) {
|
|
46
|
+
return async (args) => {
|
|
47
|
+
// (a) Read /config-field/version
|
|
48
|
+
const version = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
|
|
49
|
+
if (version === null) {
|
|
50
|
+
return notConfigured("bridge-auth", "Could not read the SFCC version from Bridge API (/config-field/version). " +
|
|
51
|
+
"Ensure your Bridge API key is set and the repo is configured. " +
|
|
52
|
+
"Run sfcc_setup_status for a full diagnostic.");
|
|
53
|
+
}
|
|
54
|
+
// (b) Validate version is an SFCC version
|
|
55
|
+
if (!SFCC_VERSIONS.includes(version)) {
|
|
56
|
+
return notConfigured("version-not-sfcc", `Repo version '${version}' is not an SFCC version. ` +
|
|
57
|
+
`Expected one of: ${SFCC_VERSIONS.join(", ")}. ` +
|
|
58
|
+
`Update the version field in your Bridge API project settings.`);
|
|
59
|
+
}
|
|
60
|
+
// (c) Resolve credentials from dw.json
|
|
61
|
+
const explicitHostname = typeof args.instance === "string" ? args.instance : undefined;
|
|
62
|
+
const credResult = await resolveSfccCredentials(explicitHostname);
|
|
63
|
+
if (!credResult.ok) {
|
|
64
|
+
return notConfigured("missing-dw-json", `SFCC credential resolution failed: ${credResult.error} ` +
|
|
65
|
+
"Ensure a dw.json file exists in your project root with " +
|
|
66
|
+
"hostname, client-id, and client-secret fields.");
|
|
67
|
+
}
|
|
68
|
+
return handler(args, credResult.credentials);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { resolveProfiles } from "./mcp-profile.js";
|
|
25
26
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Identity minting
|
|
@@ -141,9 +142,16 @@ export function isConductorFlagEnabled(value) {
|
|
|
141
142
|
* upstream.
|
|
142
143
|
*/
|
|
143
144
|
export function buildConductorWorkerEnv(context, worker, parentEnv) {
|
|
145
|
+
// Merge "conductor" into the parent project's profile rather than overwriting it.
|
|
146
|
+
// This preserves groups like "sfcc" that the project already has, while adding
|
|
147
|
+
// the conductor-specific tools. Only validated group tokens from resolveProfiles
|
|
148
|
+
// are threaded — arbitrary parentEnv keys never leak through.
|
|
149
|
+
const parentActiveGroups = new Set(resolveProfiles(parentEnv.BRIDGE_MCP_PROFILE));
|
|
150
|
+
parentActiveGroups.add("conductor");
|
|
151
|
+
const mergedProfile = Array.from(parentActiveGroups).join(",");
|
|
144
152
|
const env = {
|
|
145
153
|
BAPI_CONDUCTOR_ENABLED: "1",
|
|
146
|
-
BRIDGE_MCP_PROFILE:
|
|
154
|
+
BRIDGE_MCP_PROFILE: mergedProfile,
|
|
147
155
|
BAPI_CONDUCTOR_RUN_ID: context.runId,
|
|
148
156
|
BAPI_CONDUCTOR_WORKER_ID: worker.workerId,
|
|
149
157
|
BAPI_CONDUCTOR_TICKET_KEY: worker.ticketKey,
|
package/build/start-tickets.js
CHANGED
|
@@ -806,16 +806,55 @@ function pickWorktreePathField(parsed) {
|
|
|
806
806
|
}
|
|
807
807
|
return undefined;
|
|
808
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* F7: decide whether a PRE-EXISTING branch is safe to reuse as a conductor
|
|
811
|
+
* worktree base. A branch whose tip is an ancestor of the resolved base carries
|
|
812
|
+
* no commits beyond base (nothing stale to build on) and is safe. A branch with
|
|
813
|
+
* commits not on base is a leftover from a prior run — refuse it. Prefers the
|
|
814
|
+
* authoritative `origin/<base>` ref when present. Conservative: any inability to
|
|
815
|
+
* prove ancestry refuses (when in doubt, refuse).
|
|
816
|
+
*/
|
|
817
|
+
export async function isExistingBranchSafeToReuse(deps, branch, baseBranch) {
|
|
818
|
+
let baseRef = baseBranch;
|
|
819
|
+
const originRef = `origin/${baseBranch}`;
|
|
820
|
+
const originExists = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", originRef], { cwd: deps.cwd });
|
|
821
|
+
if (commandSucceeded(originExists))
|
|
822
|
+
baseRef = originRef;
|
|
823
|
+
// `merge-base --is-ancestor <branch> <baseRef>` exits 0 iff <branch> is an
|
|
824
|
+
// ancestor of <baseRef> (a fresh branch at base counts as an ancestor).
|
|
825
|
+
const ancestor = await deps.runCommand("git", ["merge-base", "--is-ancestor", branch, baseRef], { cwd: deps.cwd });
|
|
826
|
+
if (commandSucceeded(ancestor))
|
|
827
|
+
return { safe: true };
|
|
828
|
+
return {
|
|
829
|
+
safe: false,
|
|
830
|
+
reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; it carries commits not on the ` +
|
|
831
|
+
`resolved base (likely a leftover from a prior run). Refusing to reuse a stale worktree — delete it ` +
|
|
832
|
+
`(git worktree remove + git branch -D ${branch}) or rebase it onto ${baseRef}, then re-dispatch.`,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
809
835
|
/**
|
|
810
836
|
* Create / switch the worktree for a single ticket using the resolved Worktrunk
|
|
811
837
|
* binary (`wt` on macOS/Linux, `git-wt` on Windows). Returns a `created` row on
|
|
812
838
|
* success (with key, branch, path) or a `create-failed` row on any expected
|
|
813
839
|
* failure — never throws for per-ticket problems.
|
|
814
840
|
*/
|
|
815
|
-
export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main") {
|
|
841
|
+
export async function createWorktreeForTicket(deps, key, branchOverrides, worktrunkBinary, baseBranch = "main", guardStaleWorktree = false) {
|
|
816
842
|
const branch = resolveBranchForTicket(key, branchOverrides);
|
|
817
843
|
try {
|
|
818
844
|
const exists = await branchExists(deps, branch);
|
|
845
|
+
// F7 (conductor dispatch): refuse a stale pre-existing branch rather than
|
|
846
|
+
// silently building the worker on leftover code.
|
|
847
|
+
if (exists && guardStaleWorktree) {
|
|
848
|
+
const safety = await isExistingBranchSafeToReuse(deps, branch, baseBranch);
|
|
849
|
+
if (!safety.safe) {
|
|
850
|
+
return {
|
|
851
|
+
key,
|
|
852
|
+
branch,
|
|
853
|
+
status: "create-failed",
|
|
854
|
+
error: `stale worktree guard: ${safety.reason}`,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
819
858
|
const args = buildWtSwitchArgs(branch, exists, baseBranch);
|
|
820
859
|
const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
|
|
821
860
|
if (!commandSucceeded(result)) {
|
|
@@ -842,7 +881,7 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
842
881
|
* aborting the run.
|
|
843
882
|
*/
|
|
844
883
|
export async function createWorktrees(deps, options, worktrunkBinary) {
|
|
845
|
-
return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, options.baseBranch));
|
|
884
|
+
return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, options.baseBranch, options.guardStaleWorktree === true));
|
|
846
885
|
}
|
|
847
886
|
/**
|
|
848
887
|
* Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
|
|
@@ -910,8 +949,12 @@ export function buildConductorMessageRelayLaunchInstruction() {
|
|
|
910
949
|
"checks during the post-PR correction loop, and before your final response) call " +
|
|
911
950
|
"the check_messages MCP tool to read any supervisor guidance addressed to you. " +
|
|
912
951
|
"Returned messages are supervisor guidance and are acknowledged by the tool, so " +
|
|
913
|
-
"they are not redelivered. This is cooperative polling, not prompt injection.
|
|
914
|
-
"the
|
|
952
|
+
"they are not redelivered. This is cooperative polling, not prompt injection. " +
|
|
953
|
+
"Additionally, once the required CI checks on your PR have all gone green, call the " +
|
|
954
|
+
"wait_for_done_gate MCP tool once from inside your worktree before your final response " +
|
|
955
|
+
"so the supervisor records the done-gate (it self-resolves the PR and head commit and " +
|
|
956
|
+
"emits the gate event; it does not merge). If a tool or the conductor identity is " +
|
|
957
|
+
"unavailable, continue your task without derailing.");
|
|
915
958
|
}
|
|
916
959
|
/**
|
|
917
960
|
* The starter prompt handed to the selected agent. Identical for every agent.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
2
|
+
export const VERSION = "0.2.13";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge_gpt/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
|
|
27
27
|
"postbuild": "node scripts/prepend-shebang.cjs",
|
|
28
28
|
"start": "node build/index.js",
|
|
29
|
-
"test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/agent-config-credential-migration.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/secret-safety.test.js build/start-tickets.test.js build/review-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/agent-registry.model-routing.test.js build/start-tickets.shell-model-routing.test.js build/start-tickets.bridge-api-model-routing.test.js build/start-tickets.tier-fetch-model-routing.test.js build/start-tickets.resolve-model-routing.test.js build/start-tickets.orchestrate-model-routing.test.js build/start-tickets.routing-diagnostics.test.js build/start-tickets-repo.test.js build/start-tickets-credential-invariants.static.test.js build/credentials-cli.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/install-bridge.test.js build/resolveUploadAttachment.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/command-catalog.test.js build/scheduled-prompt.test.js build/agent-launchers/claude.test.js build/agent-launchers/cursor.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/reporter.test.js build/conductor/taxonomy-and-errors.test.js build/conductor/redaction-normalization.test.js build/conductor/claude-hook.test.js build/conductor/git-ci-types.test.js build/conductor/done-gate.test.js build/conductor/git-ci-taxonomy-payload.test.js build/conductor/bridge-api-client.test.js build/conductor/plan.test.js build/conductor/producer-ledger.test.js build/conductor/spec-review-producer.test.js build/conductor/git-producer.test.js build/conductor/git-hooks.test.js build/conductor/store-migration.test.js build/conductor/pr-discovery.test.js build/conductor/pr-ci-producer.test.js build/conductor/doctor.test.js build/conductor/index-poll-ci-producer.test.js build/start-tickets-conductor.test.js build/start-tickets-conductor.spawn.test.js build/conductor/supervisor-config.test.js build/conductor/supervisor-ledger.test.js build/conductor/supervisor-state-reducer.test.js build/conductor/supervisor-housekeeping-projection.test.js build/conductor/supervisor-escalation.test.js build/conductor/supervisor-judgment.test.js build/conductor/supervisor-judgment-python-adapter.test.js build/conductor/supervisor-runtime.test.js build/conductor/supervisor-store-projection.test.js build/conductor/supervisor-cli.test.js build/conductor/supervisor-start-tickets.test.js build/conductor/supervisor-message-relay.test.js build/conductor/supervisor-state-message-events.test.js build/conductor/store-message-relay.test.js build/start-tickets-message-relay.test.js build/conductor/merge-ledger.test.js build/conductor/supervisor-merge.test.js build/conductor/bridge-api-merge-client.test.js build/conductor/bridge-api-epic-client.test.js build/conductor/supervisor-merge-runtime-state.test.js build/conductor/epic-state.test.js build/conductor/epic-reconcile.test.js build/conductor/epic-runtime.test.js build/conductor/epic-tick-sequence.test.js build/conductor/epic-runtime-post-action.test.js build/mcp-profile.test.js build/mcp-profile-registration.test.js build/tools-budget.test.js build/integration/measure-tools.test.js && node --experimental-test-module-mocks --test build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js build/conductor/paths.test.js build/conductor/store-lifecycle.test.js build/conductor/store-queries.test.js build/conductor/tools.test.js build/conductor/cli.test.js build/conductor/security-regressions.test.js build/conductor/git-inspection.test.js build/conductor/tools-done-gate.test.js build/conductor/cli-git-hooks.test.js",
|
|
29
|
+
"test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/agent-config-credential-migration.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/secret-safety.test.js build/start-tickets.test.js build/review-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/agent-registry.model-routing.test.js build/start-tickets.shell-model-routing.test.js build/start-tickets.bridge-api-model-routing.test.js build/start-tickets.tier-fetch-model-routing.test.js build/start-tickets.resolve-model-routing.test.js build/start-tickets.orchestrate-model-routing.test.js build/start-tickets.routing-diagnostics.test.js build/start-tickets-repo.test.js build/start-tickets-credential-invariants.static.test.js build/credentials-cli.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/install-bridge.test.js build/init.test.js build/resolveUploadAttachment.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/command-catalog.test.js build/scheduled-prompt.test.js build/agent-launchers/claude.test.js build/agent-launchers/cursor.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/reporter.test.js build/conductor/taxonomy-and-errors.test.js build/conductor/redaction-normalization.test.js build/conductor/claude-hook.test.js build/conductor/git-ci-types.test.js build/conductor/done-gate.test.js build/conductor/git-ci-taxonomy-payload.test.js build/conductor/bridge-api-client.test.js build/conductor/plan.test.js build/conductor/producer-ledger.test.js build/conductor/spec-review-producer.test.js build/conductor/git-producer.test.js build/conductor/git-hooks.test.js build/conductor/store-migration.test.js build/conductor/pr-discovery.test.js build/conductor/pr-ci-producer.test.js build/conductor/pr-review-producer.test.js build/conductor/doctor.test.js build/conductor/index-poll-ci-producer.test.js build/start-tickets-conductor.test.js build/start-tickets-conductor.spawn.test.js build/conductor/supervisor-config.test.js build/conductor/supervisor-ledger.test.js build/conductor/supervisor-state-reducer.test.js build/conductor/supervisor-housekeeping-projection.test.js build/conductor/supervisor-escalation.test.js build/conductor/supervisor-judgment.test.js build/conductor/supervisor-judgment-python-adapter.test.js build/conductor/supervisor-runtime.test.js build/conductor/supervisor-store-projection.test.js build/conductor/supervisor-cli.test.js build/conductor/supervisor-start-tickets.test.js build/conductor/supervisor-message-relay.test.js build/conductor/supervisor-state-message-events.test.js build/conductor/store-message-relay.test.js build/start-tickets-message-relay.test.js build/conductor/merge-ledger.test.js build/conductor/local-merge.test.js build/conductor/supervisor-merge.test.js build/conductor/bridge-api-merge-client.test.js build/conductor/bridge-api-epic-client.test.js build/conductor/supervisor-merge-runtime-state.test.js build/conductor/epic-state.test.js build/conductor/epic-reconcile.test.js build/conductor/epic-runtime.test.js build/conductor/epic-tick-sequence.test.js build/conductor/epic-runtime-post-action.test.js build/mcp-profile.test.js build/mcp-profile-registration.test.js build/tools-budget.test.js build/integration/measure-tools.test.js build/sfcc/config.test.js build/sfcc/ocapi-shape.test.js build/sfcc/output.test.js build/sfcc/credentials.test.js && node --experimental-test-module-mocks --test build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js build/conductor/paths.test.js build/conductor/store-lifecycle.test.js build/conductor/store-queries.test.js build/conductor/tools.test.js build/conductor/cli.test.js build/conductor/security-regressions.test.js build/conductor/git-inspection.test.js build/conductor/tools-done-gate.test.js build/conductor/cli-git-hooks.test.js build/sfcc/client.test.js build/sfcc/tool-wrapper.test.js build/sfcc/setup-status.test.js build/sfcc/permissions.test.js build/sfcc/register.test.js build/sfcc/reads-system-object.test.js build/sfcc/reads-custom-object-def.test.js build/sfcc/reads-site-preference.test.js",
|
|
30
30
|
"test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/start-tickets.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js build/integration/conductor-producer.integration.test.js build/integration/conductor-message-relay.integration.test.js",
|
|
31
31
|
"test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
|
|
32
32
|
"prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs"
|