@bridge_gpt/mcp-server 0.2.37 → 0.2.39
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 +193 -16
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +423 -0
- package/build/conduct-epic/cli.js +1732 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +197 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/ocapi-shape.js +23 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +6 -2
- package/build/sfcc/reads-custom-object-def.js +33 -21
- package/build/sfcc/reads-site-preference.js +20 -7
- package/build/sfcc/reads-system-object.js +11 -5
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/write-result.js +16 -7
- package/build/sfcc/writes-custom-object-def.js +12 -4
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
|
@@ -27,6 +27,38 @@ export const OCAPI_WRITE_RESOURCE_IDS = [
|
|
|
27
27
|
"/custom_object_definitions/**",
|
|
28
28
|
"/site_preferences/**",
|
|
29
29
|
];
|
|
30
|
+
/**
|
|
31
|
+
* The exact HTTP methods each resource family needs, derived from the methods
|
|
32
|
+
* the nine shipped write handlers actually call (BAPI-817).
|
|
33
|
+
*
|
|
34
|
+
* Previously every family was granted `["get","put","patch","delete"]`. **No
|
|
35
|
+
* shipped handler performs a DELETE** — the write surface is create/update only
|
|
36
|
+
* (`ocapiPut`, `ocapiPatch`, `ocapiPatchDirect` in `client.ts`; verified across
|
|
37
|
+
* `writes-system-object.ts`, `writes-custom-object-def.ts`, and
|
|
38
|
+
* `writes-site-preference.ts`). Granting `delete` handed an operator's OCAPI
|
|
39
|
+
* client a destructive capability that Bridge cannot even invoke, on a grant
|
|
40
|
+
* they were told to paste once and forget. The narrower set below is what the
|
|
41
|
+
* code demonstrably needs and nothing more.
|
|
42
|
+
*
|
|
43
|
+
* `get` is retained where a write path genuinely reads: `ocapiPatch` performs a
|
|
44
|
+
* GET-then-If-Match ETag round trip before the PATCH, so removing `get` would
|
|
45
|
+
* break every update tool.
|
|
46
|
+
*
|
|
47
|
+
* /system_object_definitions get — the check_permissions probe.
|
|
48
|
+
* /system_object_definitions/** put, patch (+get for ETag) — attribute and
|
|
49
|
+
* attribute-group create/update, assignment,
|
|
50
|
+
* custom preference create.
|
|
51
|
+
* /custom_object_definitions/** put, patch (+get for ETag) — attribute
|
|
52
|
+
* definition create/update.
|
|
53
|
+
* /site_preferences/** patch (+get) — site_preference_values_set,
|
|
54
|
+
* a direct PATCH with no ETag round trip.
|
|
55
|
+
*/
|
|
56
|
+
export const OCAPI_WRITE_RESOURCE_METHODS = {
|
|
57
|
+
"/system_object_definitions": ["get"],
|
|
58
|
+
"/system_object_definitions/**": ["get", "put", "patch"],
|
|
59
|
+
"/custom_object_definitions/**": ["get", "put", "patch"],
|
|
60
|
+
"/site_preferences/**": ["get", "patch"],
|
|
61
|
+
};
|
|
30
62
|
// ---------------------------------------------------------------------------
|
|
31
63
|
// Grant JSON builders
|
|
32
64
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +76,7 @@ export function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder =
|
|
|
44
76
|
client_id: clientIdPlaceholder,
|
|
45
77
|
resources: OCAPI_WRITE_RESOURCE_IDS.map((resource_id) => ({
|
|
46
78
|
resource_id,
|
|
47
|
-
methods: [
|
|
79
|
+
methods: [...(OCAPI_WRITE_RESOURCE_METHODS[resource_id] ?? [])],
|
|
48
80
|
read_attributes: "(**)",
|
|
49
81
|
write_attributes: "(**)",
|
|
50
82
|
})),
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Secondary OCAPI instance-CONTEXT check for writes (BAPI-582 foundation).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* ⚠️ **This is NOT the sandbox security boundary.** It validates a
|
|
5
|
+
* caller-supplied *input string*, not the instance the request will reach. The
|
|
6
|
+
* hostname boundary lives in `tool-wrapper.ts`
|
|
7
|
+
* (`withSfccTargetBoundary` / `isApprovedSfccSandboxHost`), runs at
|
|
8
|
+
* registration level for all twenty SFCC tools, and validates the hostname
|
|
9
|
+
* credential resolution actually selected.
|
|
10
|
+
*
|
|
11
|
+
* The distinction is the entire subject of BAPI-817. This guard was previously
|
|
12
|
+
* described — in the README and in its own comments — as the thing that made
|
|
13
|
+
* SFCC writes sandbox-only. It never could be: `instance` is an argument the
|
|
14
|
+
* caller chooses, the target came from `dw.json`/`SFCC_HOSTNAME`, and the two
|
|
15
|
+
* were never compared. Worse, the guard **defaulted an omitted `instance` to
|
|
16
|
+
* `"sandbox"` and passed**, so it approved its single most common call shape
|
|
17
|
+
* while the request went to whatever host was configured. Nine destructive
|
|
18
|
+
* tools sat behind that.
|
|
19
|
+
*
|
|
20
|
+
* The behavior below is deliberately UNCHANGED, and is kept only as
|
|
21
|
+
* defence-in-depth on the OCAPI instance-context input: it rejects a caller who
|
|
22
|
+
* explicitly asks for `"staging"` or `"production"` context, which is still
|
|
23
|
+
* worth refusing. What changed is what it is allowed to *mean*. Its acceptance
|
|
24
|
+
* no longer authorizes anything — a write reaching this guard has already had
|
|
25
|
+
* its target hostname independently approved, and omitting `instance` cannot
|
|
26
|
+
* bypass that approval because the hostname check does not consult `instance`
|
|
27
|
+
* at all.
|
|
28
|
+
*
|
|
29
|
+
* SFCC write handlers still call `rejectIfNotSandboxForWrite` immediately after
|
|
30
|
+
* input parsing and BEFORE any payload construction or OCAPI mutation. A
|
|
31
|
+
* non-null return value is the pre-formatted MCP rejection to return directly;
|
|
32
|
+
* a null return means the effective instance context is `sandbox` and the
|
|
33
|
+
* handler may proceed.
|
|
9
34
|
*
|
|
10
35
|
* BAPI-766 moved the rejection's *representation* to the shared SFCC formatter
|
|
11
36
|
* so the guardrail fails at the protocol level like every other SFCC failure.
|
|
@@ -17,13 +42,17 @@ import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
|
17
42
|
// Guard
|
|
18
43
|
// ---------------------------------------------------------------------------
|
|
19
44
|
/**
|
|
20
|
-
* Reject
|
|
45
|
+
* Reject a write whose OCAPI instance CONTEXT is not `sandbox`.
|
|
46
|
+
*
|
|
47
|
+
* The effective context defaults to `"sandbox"` when `undefined` (some schemas
|
|
48
|
+
* default the value elsewhere), so an omitted instance is allowed here. That
|
|
49
|
+
* permissive default is safe only because it is no longer load-bearing: the
|
|
50
|
+
* resolved-hostname boundary has already approved the target before any handler
|
|
51
|
+
* runs, and it never consults this argument. Returning `null` therefore means
|
|
52
|
+
* "this input is not objectionable", never "this target is a sandbox".
|
|
21
53
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* elsewhere), so an omitted instance is allowed. Any other value returns the
|
|
25
|
-
* unified validation failure naming the rejected instance; a sandbox returns
|
|
26
|
-
* `null` so the caller proceeds.
|
|
54
|
+
* Any other value returns the unified validation failure naming the rejected
|
|
55
|
+
* context.
|
|
27
56
|
*
|
|
28
57
|
* `details` carries non-sensitive guard metadata only — the rejected value is
|
|
29
58
|
* caller-supplied.
|
|
@@ -6,15 +6,19 @@
|
|
|
6
6
|
* their own 403/fault/success response logic, guaranteeing a consistent surface
|
|
7
7
|
* across every write tool.
|
|
8
8
|
*
|
|
9
|
-
* Successful results
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
9
|
+
* Successful results never gain `isError`, but their echoed `body` is redacted
|
|
10
|
+
* (BAPI-769): an attribute-definition write's success body can carry the same
|
|
11
|
+
* `default_value` a read can, so it is withheld via `read-body.ts`'s
|
|
12
|
+
* `redactSfccBody` before serialization. Every other *failure* branch —
|
|
13
|
+
* including the 403 write-grant remediation, which used to be prose — returns
|
|
14
|
+
* the universal nested envelope from `sfcc-result.ts`, so `result.isError ===
|
|
15
|
+
* true` is total across a write tool's post-transport failures as well as its
|
|
16
|
+
* pre-transport ones.
|
|
14
17
|
*/
|
|
15
18
|
import { writeGrantForbiddenResult } from "./write-grants.js";
|
|
16
19
|
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
17
20
|
import { formatSfccWriteFailure } from "./sfcc-result.js";
|
|
21
|
+
import { redactSfccBody } from "./read-body.js";
|
|
18
22
|
/** Wrap text in the standard MCP text-result shape (success path only). */
|
|
19
23
|
function textResult(text) {
|
|
20
24
|
return { content: [{ type: "text", text }] };
|
|
@@ -54,7 +58,12 @@ function ocapiWriteFaultMessage(body, status) {
|
|
|
54
58
|
*
|
|
55
59
|
* - `403` → the unified failure carrying the paste-ready write-grant
|
|
56
60
|
* remediation as structured data (via write-grants.ts).
|
|
57
|
-
* - success → JSON with `status`, `outcome`, and `body`; no `isError`.
|
|
61
|
+
* - success → JSON with `status`, `outcome`, and `body`; no `isError`. `body`
|
|
62
|
+
* is redacted (BAPI-769) before serialization: a successful
|
|
63
|
+
* attribute-definition PUT/PATCH echoes the updated resource in the same
|
|
64
|
+
* shape a GET returns, so it can carry a real `default_value` exactly like a
|
|
65
|
+
* read can — `redactSfccBody` withholds it here the same way `readSfccBody`
|
|
66
|
+
* withholds it on the read path. Every other key survives untouched.
|
|
58
67
|
* - other failure → the unified `OCAPI_WRITE_ERROR` failure, source `ocapi`,
|
|
59
68
|
* with the structured fault mapping and the upstream body under
|
|
60
69
|
* `error.details`.
|
|
@@ -76,7 +85,7 @@ export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion
|
|
|
76
85
|
return textResult(JSON.stringify({
|
|
77
86
|
status: result.status,
|
|
78
87
|
outcome: result.outcome,
|
|
79
|
-
body: result.body,
|
|
88
|
+
body: redactSfccBody(result.body),
|
|
80
89
|
}));
|
|
81
90
|
}
|
|
82
91
|
return formatSfccWriteFailure({
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* Both tools write ATTRIBUTE DEFINITIONS on an already-known custom object type.
|
|
9
9
|
* Custom object TYPE creation is intentionally NOT attempted: OCAPI cannot
|
|
10
10
|
* create custom object types (that is a v2 metadata-import capability), so the
|
|
11
|
-
* type must pre-exist.
|
|
11
|
+
* type must pre-exist. OCAPI also cannot enumerate custom object type IDs
|
|
12
|
+
* directly — see reads-custom-object-def.ts for the `system_object_list` +
|
|
13
|
+
* `projection: "full"` discovery path a caller can use to find one before
|
|
14
|
+
* writing to it. Mirrors reads-custom-object-def.ts.
|
|
12
15
|
*
|
|
13
16
|
* Every handler is call-time gated by `withSfccGate`, sandbox-guarded via
|
|
14
17
|
* `rejectIfNotSandboxForWrite`, and routes its OCAPI result through
|
|
@@ -25,13 +28,18 @@ import { WRITE_ANNOTATIONS, encodedSegment, preTransportErrorEnvelope, unexpecte
|
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// Input schemas
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
// OCAPI instance CONTEXT, not a target selector: the instance actually
|
|
32
|
+
// contacted is decided by the resolved-hostname boundary (BAPI-817), which this
|
|
33
|
+
// value cannot influence. "omit for sandbox" is gone because omitting it never
|
|
34
|
+
// selected anything.
|
|
35
|
+
const INSTANCE_DESCRIBE = "OCAPI instance context. Omit or pass \"sandbox\"; any other value is " +
|
|
36
|
+
"rejected before OCAPI is called.";
|
|
30
37
|
const createCustomObjectAttributeDefinitionInput = z.object({
|
|
31
38
|
object_type: z
|
|
32
39
|
.string()
|
|
33
40
|
.describe('Known custom object type identifier (must already exist). OCAPI cannot ' +
|
|
34
|
-
"
|
|
41
|
+
"create types, and cannot enumerate type IDs directly — only attribute " +
|
|
42
|
+
"definitions on a known type."),
|
|
35
43
|
attribute_id: z
|
|
36
44
|
.string()
|
|
37
45
|
.describe("URL attribute-definition id. If the body also carries `id`, it must match."),
|
|
@@ -28,8 +28,13 @@ import { WRITE_ANNOTATIONS, encodedSegment, preTransportErrorEnvelope, unexpecte
|
|
|
28
28
|
// Mirror reads-site-preference.ts so callers see the same instance enum, while
|
|
29
29
|
// the write remains sandbox-only (enforced by the guard + the literal path).
|
|
30
30
|
const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]);
|
|
31
|
+
// OCAPI instance CONTEXT only. The trailing "Defaults to 'sandbox'" is gone:
|
|
32
|
+
// it read as though the default selected a safe target, when the target is
|
|
33
|
+
// decided by the resolved-hostname boundary (BAPI-817) and this value cannot
|
|
34
|
+
// influence it. The forced `/sandbox` path segment below remains as an extra
|
|
35
|
+
// safeguard on the OCAPI route.
|
|
31
36
|
const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; any other " +
|
|
32
|
-
"value is rejected before OCAPI is called.
|
|
37
|
+
"value is rejected before OCAPI is called.";
|
|
33
38
|
const sitePreferenceValuesSetInput = z.object({
|
|
34
39
|
group: z.string().describe("Custom site preference group id, e.g. 'LLMIntegration'."),
|
|
35
40
|
instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
|
|
@@ -50,11 +50,20 @@ function preferenceObjectTypeForScope(scope) {
|
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Input schemas
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Optional OCAPI instance CONTEXT — not a target selector.
|
|
55
|
+
*
|
|
56
|
+
* The old description ("Sandbox-only: omit (defaults to sandbox) or pass
|
|
57
|
+
* \"sandbox\"") told the model that omitting this field selected a sandbox. It
|
|
58
|
+
* never did: the host came from `dw.json`/`SFCC_HOSTNAME`, and omitting the
|
|
59
|
+
* field only made `write-guard.ts` default to `"sandbox"` and pass. Which
|
|
60
|
+
* instance is actually contacted is decided by the resolved-hostname boundary in
|
|
61
|
+
* `tool-wrapper.ts` (BAPI-817), which this value cannot influence.
|
|
62
|
+
*/
|
|
54
63
|
const instanceSchema = z
|
|
55
64
|
.string()
|
|
56
65
|
.optional()
|
|
57
|
-
.describe("
|
|
66
|
+
.describe("OCAPI instance context. Omit or pass \"sandbox\".");
|
|
58
67
|
const createAttributeDefinitionInput = z.object({
|
|
59
68
|
object_type: z.string().describe("System object type, e.g. \"Product\"."),
|
|
60
69
|
attribute_id: z.string().describe("Attribute id (URL id); the body id must match it."),
|
package/build/sfcc/writes.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* populated by BAPI-584).
|
|
4
4
|
*
|
|
5
5
|
* This is the registration seam that write-surface tickets register through.
|
|
6
|
-
* `register.ts` wires
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
6
|
+
* `register.ts` wires it behind its OWN `includeWriteTools` gate, driven by the
|
|
7
|
+
* `sfcc-write` profile group (BAPI-817). The nine tools here are destructive, so
|
|
8
|
+
* they are no longer default-on with the reads: enabling `sfcc` registers the
|
|
9
|
+
* read profile and nothing from this module.
|
|
9
10
|
*
|
|
10
11
|
* Each write handler follows the foundation recipe:
|
|
11
12
|
* 1. Parse inputs.
|
|
@@ -25,7 +26,8 @@ import { registerSitePreferenceWriteTools, SITE_PREFERENCE_WRITE_TOOL_NAMES, } f
|
|
|
25
26
|
* Write tool names registered by this seam (centralized inventory). Extended by
|
|
26
27
|
* each write-surface ticket — BAPI-583 adds the six system-object write tools,
|
|
27
28
|
* BAPI-584 adds the custom-object attribute-definition and site-preference value
|
|
28
|
-
* write tools.
|
|
29
|
+
* write tools. These nine have SHIPPED; `registration-inventory.ts` reuses this
|
|
30
|
+
* array as the `sfcc-write` profile inventory rather than duplicating it.
|
|
29
31
|
*/
|
|
30
32
|
export const SFCC_WRITE_TOOL_NAMES = [
|
|
31
33
|
...SYSTEM_OBJECT_WRITE_TOOL_NAMES,
|
|
@@ -47,14 +49,17 @@ export const SFCC_WRITE_TOOL_NAMES = [
|
|
|
47
49
|
* 4. Call `ocapiPut` or `ocapiPatch` (client.ts) for transport.
|
|
48
50
|
* 5. Route the result through `formatOcapiWriteToolResult` (write-result.ts).
|
|
49
51
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
52
|
+
* Every handler registered here crosses three checks before it can mutate
|
|
53
|
+
* anything: the resolved-hostname sandbox boundary (registration-level,
|
|
54
|
+
* `tool-wrapper.ts`), the call-time `withSfccGate` version check, and the
|
|
55
|
+
* secondary instance-context guard in `write-guard.ts`. Only the first of those
|
|
56
|
+
* decides which instance is contacted.
|
|
53
57
|
*
|
|
54
58
|
* BAPI-583 registers the system-object write surface here; BAPI-584 adds the
|
|
55
59
|
* custom-object attribute-definition and site-preference value write surfaces
|
|
56
60
|
* alongside it. Later write-surface tickets add their modules the same way,
|
|
57
|
-
* without touching `register.ts`/`index.ts
|
|
61
|
+
* without touching `register.ts`/`index.ts` — a new tool added here is
|
|
62
|
+
* automatically inside the `sfcc-write` group and inside the target boundary.
|
|
58
63
|
*/
|
|
59
64
|
export function registerSfccWriteTools(registerTool, deps) {
|
|
60
65
|
// System-object attribute/group definition writes (BAPI-583).
|
|
@@ -18,7 +18,7 @@ import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./creden
|
|
|
18
18
|
import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
|
|
19
19
|
import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
|
|
20
20
|
import { probeWorktreeCommandAssets } from "./command-assets-doctor.js";
|
|
21
|
-
import {
|
|
21
|
+
import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
// Constants (moved here from start-tickets.ts so both consumers share them)
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -531,12 +531,11 @@ export async function resolveMainWorktreePathForDoctor(deps) {
|
|
|
531
531
|
* which is why the advice below is phrased as "delete, or move aside". Keep every
|
|
532
532
|
* such identifier out of this file, even inside a comment like this one.
|
|
533
533
|
*/
|
|
534
|
-
const CLAUDE_MCP_SHADOWING_HINT =
|
|
535
|
-
"
|
|
536
|
-
"
|
|
537
|
-
"
|
|
538
|
-
|
|
539
|
-
"(doctor still reports it).";
|
|
534
|
+
const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the 'bridge-api' MCP ` +
|
|
535
|
+
"registration in ~/.claude.json (use the matching scope if it is registered at user scope, " +
|
|
536
|
+
"or under the projects entry for this repository/worktree). This is ADVISORY: workers load " +
|
|
537
|
+
"MCP servers with --strict-mcp-config from their own worktree registration, so this entry " +
|
|
538
|
+
"does not reach a worker and does not block a conductor run.";
|
|
540
539
|
const CLAUDE_MCP_SHADOWING_INSTALL_HINTS = {
|
|
541
540
|
darwin: CLAUDE_MCP_SHADOWING_HINT,
|
|
542
541
|
linux: CLAUDE_MCP_SHADOWING_HINT,
|
|
@@ -547,17 +546,25 @@ const CLAUDE_MCP_SHADOWING_INSTALL_HINTS = {
|
|
|
547
546
|
* configuration register a `bridge-api` MCP server that could shadow a worker's
|
|
548
547
|
* provisioned registration?
|
|
549
548
|
*
|
|
549
|
+
* ADVISORY SINCE BAPI-790. Every outcome reports `found: true`, so this never
|
|
550
|
+
* fails the prerequisite section or the exit code; a confirmed collision is
|
|
551
|
+
* surfaced through the DETAIL, which names the scope and carries the exact
|
|
552
|
+
* remediation command. The downgrade is not a softening of the diagnostic — it
|
|
553
|
+
* reflects that strict flag-scoped worker MCP loading means the collision can no
|
|
554
|
+
* longer reach a worker (inventory Finding 6), so reporting it as a broken
|
|
555
|
+
* prerequisite would tell an operator to fix something that is not breaking
|
|
556
|
+
* anything.
|
|
557
|
+
*
|
|
550
558
|
* Status mapping:
|
|
551
559
|
* - no registration, or only acceptable provisioned shims → `found: true`
|
|
552
|
-
* - a CONFIRMED foreign registration → `found:
|
|
560
|
+
* - a CONFIRMED foreign registration → `found: true` + advisory detail
|
|
553
561
|
* - unreadable / malformed config, or an unresolved main
|
|
554
562
|
* worktree path → `found: true` + detail
|
|
555
563
|
*
|
|
556
|
-
*
|
|
557
|
-
* `BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* condition an operator ran doctor to find.
|
|
564
|
+
* Reporting stays deliberately independent of the retired
|
|
565
|
+
* `BAPI_CONDUCTOR_ALLOW_CLAUDE_MCP_SHADOWING` override: whether that variable is
|
|
566
|
+
* exported has never had any bearing on whether the collision exists, and now it
|
|
567
|
+
* has no bearing on anything at all.
|
|
561
568
|
*/
|
|
562
569
|
export function claudeMcpShadowingDescriptor() {
|
|
563
570
|
return {
|
|
@@ -589,8 +596,11 @@ export function claudeMcpShadowingDescriptor() {
|
|
|
589
596
|
degraded.push("the main worktree path could not be resolved; only user and current-worktree scopes were checked");
|
|
590
597
|
}
|
|
591
598
|
if (inspection.findings.length > 0) {
|
|
592
|
-
|
|
593
|
-
|
|
599
|
+
// Advisory (BAPI-790): reported in full, with the remediation command,
|
|
600
|
+
// but `found: true` so it never fails the prerequisite or the exit code.
|
|
601
|
+
const findings = inspection.findings.map((finding) => `advisory: ${formatClaudeMcpShadowFinding(finding)} — not loaded by a worker under ` +
|
|
602
|
+
`strict MCP loading; clear it with \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\``);
|
|
603
|
+
return { found: true, detail: [...findings, ...degraded].join("; ") };
|
|
594
604
|
}
|
|
595
605
|
return {
|
|
596
606
|
found: true,
|
package/build/start-tickets.js
CHANGED
|
@@ -59,6 +59,10 @@ import os from "node:os";
|
|
|
59
59
|
import path from "path";
|
|
60
60
|
import { VERSION } from "./version.generated.js";
|
|
61
61
|
import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
62
|
+
// BAPI-814/E1b: the read-only index-branch override probe. `getIndexBranch` is
|
|
63
|
+
// reused from the conduct-epic client rather than re-wrapped, so both conductor
|
|
64
|
+
// paths read the one repository-wide override through the same client.
|
|
65
|
+
import { getIndexBranch } from "./conduct-epic/bridge-client.js";
|
|
62
66
|
import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
|
|
63
67
|
import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
|
|
64
68
|
import { provisionCommandsForCreatedWorktrees, } from "./command-provisioning.js";
|
|
@@ -173,6 +177,7 @@ export function parseStartTicketsArgs(argv) {
|
|
|
173
177
|
let maxParallelRaw;
|
|
174
178
|
let agentName = DEFAULT_AGENT_NAME;
|
|
175
179
|
let baseBranch = "main";
|
|
180
|
+
let baseBranchExplicit = false;
|
|
176
181
|
let conductorEnabled = false;
|
|
177
182
|
let workflow = "implement";
|
|
178
183
|
let reviewRoundsRaw;
|
|
@@ -342,6 +347,7 @@ export function parseStartTicketsArgs(argv) {
|
|
|
342
347
|
return { status: "error", message: `Invalid --base-branch value: ${error}` };
|
|
343
348
|
}
|
|
344
349
|
baseBranch = trimmed;
|
|
350
|
+
baseBranchExplicit = true;
|
|
345
351
|
continue;
|
|
346
352
|
}
|
|
347
353
|
if (arg === "--dry-run") {
|
|
@@ -449,6 +455,7 @@ export function parseStartTicketsArgs(argv) {
|
|
|
449
455
|
branchOverrides,
|
|
450
456
|
agentName,
|
|
451
457
|
baseBranch,
|
|
458
|
+
...(baseBranchExplicit ? { baseBranchExplicit: true } : {}),
|
|
452
459
|
conductorEnabled,
|
|
453
460
|
workflow,
|
|
454
461
|
reviewRounds,
|
|
@@ -506,16 +513,19 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
|
|
|
506
513
|
// instruction to the resume-mode worker's prompt.
|
|
507
514
|
resumeMode = false, workflow = "implement", reviewRounds,
|
|
508
515
|
// BAPI-586: the effective run base branch (incl. epic.base_branch override).
|
|
509
|
-
// Injected as BAPI_BASE_BRANCH
|
|
510
|
-
// and
|
|
516
|
+
// Injected as BAPI_BASE_BRANCH so the spawned worker's PR targets it — for
|
|
517
|
+
// conductor runs always, and (BAPI-801) for plain runs whose base is a
|
|
518
|
+
// non-default branch — and threaded (BAPI-593) into the spawned workflow command.
|
|
511
519
|
baseBranch) {
|
|
512
520
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
513
521
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
514
522
|
}
|
|
515
523
|
const platform = deps.platform;
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
524
|
+
// BAPI-801: both conductor and plain dispatch carry the PR-base ENVIRONMENT
|
|
525
|
+
// contract (see resolvePrBaseBranchEnvValue). Only the conductor launch
|
|
526
|
+
// INSTRUCTION remains conductor-only — a plain worker's prompt stays bare and
|
|
527
|
+
// learns the base from the env plus the create-PR instruction files.
|
|
528
|
+
const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
|
|
519
529
|
return {
|
|
520
530
|
ok: true,
|
|
521
531
|
config: {
|
|
@@ -523,7 +533,8 @@ baseBranch) {
|
|
|
523
533
|
worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
|
|
524
534
|
// Inject the resolved repo identity so the spawned worktree session never
|
|
525
535
|
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
526
|
-
// (BAPI-586) the run base so the
|
|
536
|
+
// (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
|
|
537
|
+
// against it. The base assignment stays outermost.
|
|
527
538
|
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch), repoName, platform), prBaseBranch, platform),
|
|
528
539
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
529
540
|
},
|
|
@@ -555,14 +566,40 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
|
|
|
555
566
|
return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
|
|
556
567
|
}
|
|
557
568
|
/**
|
|
558
|
-
* BAPI-
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
569
|
+
* BAPI-801: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell should
|
|
570
|
+
* carry — the single rule shared by the real spawn builder
|
|
571
|
+
* ({@link resolveStartTicketsPlatformConfig}) and the dry-run preview builder
|
|
572
|
+
* ({@link getDryRunPlatformDetails}), so the preview can never drift from what
|
|
573
|
+
* is actually spawned.
|
|
574
|
+
*
|
|
575
|
+
* - Conductor runs export whatever base was resolved, `main` included — BAPI-586
|
|
576
|
+
* behavior, unchanged.
|
|
577
|
+
* - Plain runs export only a non-empty base other than the CLI default `main`.
|
|
578
|
+
* This mirrors the `--base-branch` threading rule in {@link buildAgentPrompt},
|
|
579
|
+
* keeps today's plain spawn strings byte-stable on the default base, and costs
|
|
580
|
+
* nothing: the create-PR fallback tier is `main` anyway.
|
|
581
|
+
*
|
|
582
|
+
* Returns `null` when nothing should be exported;
|
|
583
|
+
* {@link prependBaseBranchEnvAssignment} is fail-open on that value.
|
|
584
|
+
*/
|
|
585
|
+
function resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch) {
|
|
586
|
+
if (conductorEnabled)
|
|
587
|
+
return baseBranch ?? null;
|
|
588
|
+
if (!baseBranch || baseBranch === "main")
|
|
589
|
+
return null;
|
|
590
|
+
return baseBranch;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* BAPI-586/BAPI-801: prepend a `BAPI_BASE_BRANCH` environment assignment to a
|
|
594
|
+
* spawned worker's shell command so the worker opens its PR against the run base
|
|
595
|
+
* instead of inferring the repo default branch. Conductor workers additionally
|
|
596
|
+
* receive the paired launch instruction; plain workers (BAPI-801) carry the same
|
|
597
|
+
* env and learn the contract from the create-PR instruction files, which resolve
|
|
598
|
+
* `$BAPI_BASE_BRANCH` first. Platform correct — `$env:VAR = '…'; …` on
|
|
599
|
+
* PowerShell, `export VAR='…' && …` on POSIX — with the value quoted by the same
|
|
600
|
+
* escaper used for the rest of the command. Fail-open: a null/empty `baseBranch`
|
|
601
|
+
* returns the command unchanged (the caller's rule already decided not to
|
|
602
|
+
* export — see {@link resolvePrBaseBranchEnvValue}).
|
|
566
603
|
*/
|
|
567
604
|
export function prependBaseBranchEnvAssignment(command, baseBranch, platform = "darwin") {
|
|
568
605
|
if (!baseBranch)
|
|
@@ -1061,6 +1098,9 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1061
1098
|
parts.push(buildConductorMessageRelayLaunchInstruction());
|
|
1062
1099
|
// BAPI-586: conductor implementation workers must open their PR against the
|
|
1063
1100
|
// run base (injected as BAPI_BASE_BRANCH), never the repo default branch.
|
|
1101
|
+
// The launch INSTRUCTION stays conductor-only. BAPI-801 gave plain runs the
|
|
1102
|
+
// same env, but their prompt stays bare — the create-PR instruction files
|
|
1103
|
+
// read $BAPI_BASE_BRANCH for them.
|
|
1064
1104
|
parts.push(buildPrBaseContractLaunchInstruction());
|
|
1065
1105
|
}
|
|
1066
1106
|
if (opts.resumeMode)
|
|
@@ -1723,11 +1763,13 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
1723
1763
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
1724
1764
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
1725
1765
|
// now passes the previewed tier's alias so `--model` shows in the preview.
|
|
1726
|
-
// The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1766
|
+
// The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix,
|
|
1767
|
+
// and (BAPI-801) the PR base as a BAPI_BASE_BRANCH prefix through the SAME
|
|
1768
|
+
// resolvePrBaseBranchEnvValue rule and the same outermost ordering the real
|
|
1769
|
+
// spawn builder uses — dry-run shares both environment renderings with real
|
|
1770
|
+
// spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
|
|
1771
|
+
// real spawn — dry-run is never special-cased — so the preview is exact.
|
|
1772
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
|
|
1731
1773
|
};
|
|
1732
1774
|
}
|
|
1733
1775
|
/**
|
|
@@ -1769,10 +1811,17 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
|
|
|
1769
1811
|
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
|
|
1770
1812
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
1771
1813
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
1814
|
+
// BAPI-801: one concise secondary detail naming the PR base, emitted only when
|
|
1815
|
+
// the shared rule actually exports BAPI_BASE_BRANCH. It sits AFTER the rendered
|
|
1816
|
+
// spawn command so the copyable command stays the primary scan target, and it
|
|
1817
|
+
// is ordinary routing detail — never warning-like. An ordinary plain run on
|
|
1818
|
+
// `main` exports nothing and so keeps its existing output density verbatim.
|
|
1819
|
+
const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
|
|
1772
1820
|
return [
|
|
1773
1821
|
`DRY-RUN: ${key} -> branch=${branch}`,
|
|
1774
1822
|
`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
|
|
1775
1823
|
`DRY-RUN: ${agentInvocation}`,
|
|
1824
|
+
...(prBaseBranch ? [`DRY-RUN: PR base: ${prBaseBranch}`] : []),
|
|
1776
1825
|
...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
|
|
1777
1826
|
];
|
|
1778
1827
|
}
|
|
@@ -2792,8 +2841,10 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2792
2841
|
const platformConfig = resolveStartTicketsPlatformConfig(deps, agent, options.autoApprove, options.conductorEnabled ?? false, resolvedRepoName,
|
|
2793
2842
|
// BAPI-494: resume-mode dispatches get the full-suite remediation finalize prompt.
|
|
2794
2843
|
options.resumeMode ?? false, options.workflow, options.reviewRounds,
|
|
2795
|
-
// BAPI-586: the effective run base (already carries any
|
|
2796
|
-
// override applied above) so
|
|
2844
|
+
// BAPI-586/BAPI-801: the effective run base (already carries any
|
|
2845
|
+
// epic.base_branch override applied above) so the spawned worker gets
|
|
2846
|
+
// BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
|
|
2847
|
+
// is non-default.
|
|
2797
2848
|
options.baseBranch);
|
|
2798
2849
|
if (!platformConfig.ok)
|
|
2799
2850
|
return { ok: false, error: platformConfig.error };
|
|
@@ -2940,6 +2991,54 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2940
2991
|
const rows = await spawnTabsFn(deps, routed, terminal, platformConfig.config.buildAgentShellCommand);
|
|
2941
2992
|
return { ok: true, rows };
|
|
2942
2993
|
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Warn when an implicit base branch could be resolved through an active
|
|
2996
|
+
* repository-wide index-branch override (BAPI-814/E1b).
|
|
2997
|
+
*
|
|
2998
|
+
* Informational only, and deliberately narrow:
|
|
2999
|
+
*
|
|
3000
|
+
* - **Only for an IMPLICIT base.** An explicit `--base-branch` is the caller's
|
|
3001
|
+
* own decision and is unaffected by the override, so the probe is skipped
|
|
3002
|
+
* entirely — no request is made and no warning is printed.
|
|
3003
|
+
* - **Only with resolvable credentials.** `start-tickets` creates worktrees and
|
|
3004
|
+
* opens tabs; it does not require Bridge access to do its job. "No credential"
|
|
3005
|
+
* is therefore an ordinary state for this command, not a diagnostic, and
|
|
3006
|
+
* staying silent keeps an unrelated command from growing a credential
|
|
3007
|
+
* complaint. A lookup that FAILS after credentials resolved is different, and
|
|
3008
|
+
* does produce one bounded, sanitized line.
|
|
3009
|
+
* - **Never a refusal.** Nothing here changes the exit code, the base branch, or
|
|
3010
|
+
* any spawn behavior.
|
|
3011
|
+
*
|
|
3012
|
+
* Warnings go to `errorLog` (stderr), so machine-readable stdout stays clean.
|
|
3013
|
+
*/
|
|
3014
|
+
async function warnOnIndexOverrideForImplicitBase(deps, options, errorLog, fetchImpl) {
|
|
3015
|
+
if (options.baseBranchExplicit)
|
|
3016
|
+
return;
|
|
3017
|
+
const access = await resolveConductorBridgeApiAccess({
|
|
3018
|
+
env: deps.env,
|
|
3019
|
+
cwd: deps.cwd,
|
|
3020
|
+
homedir: os.homedir,
|
|
3021
|
+
platform: deps.platform,
|
|
3022
|
+
readFile: deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")),
|
|
3023
|
+
stat: (filePath) => stat(filePath).then((info) => ({ mode: info.mode })),
|
|
3024
|
+
});
|
|
3025
|
+
if (!access.ok)
|
|
3026
|
+
return;
|
|
3027
|
+
const result = await getIndexBranch(access.access, fetchImpl);
|
|
3028
|
+
if (!result.ok) {
|
|
3029
|
+
errorLog(`Warning: the repository's indexed-branch override could not be read: ${result.error}. ` +
|
|
3030
|
+
"Continuing — ticket startup is unaffected.");
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const override = result.value.override;
|
|
3034
|
+
if (override === null)
|
|
3035
|
+
return;
|
|
3036
|
+
errorLog(`Warning: ${access.access.repoName} has an ACTIVE index-branch override: ` +
|
|
3037
|
+
`'${override.override_branch}'. No --base-branch was given, so the implicit base — ` +
|
|
3038
|
+
"the base-branch config field and feature-branch provisioning — may resolve through " +
|
|
3039
|
+
"that override rather than the repository's usual base. Ticket startup is ALLOWED and " +
|
|
3040
|
+
"continues; pass --base-branch to choose explicitly.");
|
|
3041
|
+
}
|
|
2943
3042
|
/** Platform-specific guidance printed when one or more tabs fail to spawn. */
|
|
2944
3043
|
function spawnFailureHintForPlatform(platform) {
|
|
2945
3044
|
if (platform === "darwin") {
|
|
@@ -2995,6 +3094,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
2995
3094
|
errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and " +
|
|
2996
3095
|
"using premium model routing for every ticket.");
|
|
2997
3096
|
}
|
|
3097
|
+
// BAPI-814/E1b: informational only, ahead of the dry-run branch so both a
|
|
3098
|
+
// preview and a real run report the same repository-wide condition.
|
|
3099
|
+
await warnOnIndexOverrideForImplicitBase(deps, options, errorLog, overrides.fetchImpl ?? globalThis.fetch);
|
|
2998
3100
|
if (options.dryRun) {
|
|
2999
3101
|
// Resolve the repo identity for the preview so the dry-run command matches
|
|
3000
3102
|
// what the real spawn injects (see prependRepoNameEnvAssignment). The real
|
|
@@ -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.39";
|