@ateam-ai/mcp 0.4.88 → 0.4.90
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/package.json +2 -2
- package/src/tools.js +208 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.90",
|
|
4
4
|
"mcpName": "io.github.ariekogan/ateam-mcp",
|
|
5
5
|
"description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"start:http": "node src/index.js --http",
|
|
14
14
|
"dev": "node --watch src/index.js",
|
|
15
15
|
"dev:http": "node --watch src/index.js --http",
|
|
16
|
-
"test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs && node test/actor-binding.test.mjs && node test/spec-topics.test.mjs && node test/example-types.test.mjs && node --test test/deploy-status-truth.test.mjs"
|
|
16
|
+
"test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs && node test/actor-binding.test.mjs && node test/spec-topics.test.mjs && node test/example-types.test.mjs && node --test test/deploy-status-truth.test.mjs && node --test test/connector-source-provenance.test.mjs"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"mcp",
|
package/src/tools.js
CHANGED
|
@@ -1856,7 +1856,9 @@ export const tools = [
|
|
|
1856
1856
|
name: "ateam_get_connector_source",
|
|
1857
1857
|
core: true,
|
|
1858
1858
|
description:
|
|
1859
|
-
`Read
|
|
1859
|
+
`Read a connector's AUTHORED source — the code the Builder holds as the source of record (its authored store, or GitHub). Returns a file manifest, or one file's content with path:'<file>'. Use this BEFORE patching or rewriting a connector, so you make surgical fixes instead of blind full rewrites. Every answer carries `+
|
|
1860
|
+
`\`provenance\` (authored_fs | github) so you know which store you are reading.\n\n` +
|
|
1861
|
+
`This does NOT return what Core is currently RUNNING. Those are different questions and used to share one answer: a connector could be deployed and healthy while no authored copy of it existed anywhere, and this tool would hand back the runtime bytes as though they were the source. If there is no authored source you get AUTHORED_SOURCE_MISSING, not a silent substitute. To see the deployed copy use ateam_get_deployed_connector_source; to adopt it as authored source use ateam_recover_connector_source.`,
|
|
1860
1862
|
inputSchema: {
|
|
1861
1863
|
type: "object",
|
|
1862
1864
|
properties: {
|
|
@@ -1876,6 +1878,45 @@ export const tools = [
|
|
|
1876
1878
|
required: ["solution_id", "connector_id"],
|
|
1877
1879
|
},
|
|
1878
1880
|
},
|
|
1881
|
+
{
|
|
1882
|
+
name: "ateam_get_deployed_connector_source",
|
|
1883
|
+
core: true,
|
|
1884
|
+
description:
|
|
1885
|
+
`Read the DEPLOYED copy of a connector — the files ADAS Core is actually running. This is a runtime projection of the last successful deploy, NOT the source of record: it can differ from the authored source, and it may exist for a connector the Builder cannot reproduce at all. Answers carry authored_source_of_record:false so that is never in doubt.\n\n` +
|
|
1886
|
+
`Use it to diagnose ("what is actually running?"), to compare against ateam_get_connector_source, or to inspect a connector whose authored source is missing before deciding whether to adopt it with ateam_recover_connector_source. Do NOT copy bytes out of here and re-upload them as if you had authored them — that launders a runtime copy into the source of record and hides the fact that the real source was lost.`,
|
|
1887
|
+
inputSchema: {
|
|
1888
|
+
type: "object",
|
|
1889
|
+
properties: {
|
|
1890
|
+
solution_id: { type: "string", description: "The solution ID" },
|
|
1891
|
+
connector_id: { type: "string", description: "The connector ID to inspect" },
|
|
1892
|
+
path: {
|
|
1893
|
+
type: "string",
|
|
1894
|
+
description: "Optional. Read ONE file. Omit for a manifest (paths + sizes) — a whole connector exceeds the output limit and truncates.",
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
required: ["solution_id", "connector_id"],
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
name: "ateam_recover_connector_source",
|
|
1902
|
+
core: true,
|
|
1903
|
+
description:
|
|
1904
|
+
`ADOPT the deployed copy of a connector as its AUTHORED source. The only sanctioned Core→Builder direction, and deliberately explicit: it is never part of a deploy.\n\n` +
|
|
1905
|
+
`Use it when a connector is running in Core but has no authored source (ateam_get_connector_source returns AUTHORED_SOURCE_MISSING) — the running bytes may be the only surviving copy of real work. The recovered files are STAMPED as recovered_from_core with a timestamp, so a reconstruction is never later mistaken for code someone wrote. Refuses with AUTHORED_SOURCE_EXISTS if authored source is already present; pass force:true only after comparing both copies and deciding the deployed one is the keeper.\n\n` +
|
|
1906
|
+
`Binaries and files over 512KB cannot round-trip and are reported under not_recovered — they stay missing. Push the result to GitHub afterwards so the recovered source is not held in one place only.`,
|
|
1907
|
+
inputSchema: {
|
|
1908
|
+
type: "object",
|
|
1909
|
+
properties: {
|
|
1910
|
+
solution_id: { type: "string", description: "The solution ID" },
|
|
1911
|
+
connector_id: { type: "string", description: "The connector ID to recover" },
|
|
1912
|
+
force: {
|
|
1913
|
+
type: "boolean",
|
|
1914
|
+
description: "Overwrite EXISTING authored source with Core's deployed copy. Default false. Only after comparing the two — the deployed copy may be older than what was authored.",
|
|
1915
|
+
},
|
|
1916
|
+
},
|
|
1917
|
+
required: ["solution_id", "connector_id"],
|
|
1918
|
+
},
|
|
1919
|
+
},
|
|
1879
1920
|
{
|
|
1880
1921
|
name: "ateam_get_metrics",
|
|
1881
1922
|
monitoring: { safe: true, cost: "cheap", latency_ms_p95: 1000, output: "bounded", poll_interval_s: 30 },
|
|
@@ -2574,6 +2615,8 @@ const TENANT_TOOLS = new Set([
|
|
|
2574
2615
|
"ateam_chain_status",
|
|
2575
2616
|
"ateam_get_widget_catalog",
|
|
2576
2617
|
"ateam_get_connector_source",
|
|
2618
|
+
"ateam_get_deployed_connector_source",
|
|
2619
|
+
"ateam_recover_connector_source",
|
|
2577
2620
|
"ateam_get_metrics",
|
|
2578
2621
|
"ateam_diff",
|
|
2579
2622
|
"ateam_verify_consistency",
|
|
@@ -3127,7 +3170,12 @@ function chainTreeOf(resp) {
|
|
|
3127
3170
|
};
|
|
3128
3171
|
}
|
|
3129
3172
|
|
|
3130
|
-
|
|
3173
|
+
// Exported for tests. handleToolCall below is the runtime entry point and stays
|
|
3174
|
+
// the only one production code should use; reaching a handler directly lets a
|
|
3175
|
+
// test EXECUTE it instead of asserting against this file's source text, which
|
|
3176
|
+
// is the difference between proving behaviour and matching a string that a
|
|
3177
|
+
// rename would quietly satisfy.
|
|
3178
|
+
export const handlers = {
|
|
3131
3179
|
ateam_bootstrap: async () => ({
|
|
3132
3180
|
runtime: {
|
|
3133
3181
|
ateam_mcp_version: MCP_VERSION,
|
|
@@ -3272,7 +3320,7 @@ const handlers = {
|
|
|
3272
3320
|
},
|
|
3273
3321
|
advanced_tools: {
|
|
3274
3322
|
_note: "These tools are available but hidden from the default tool list. Call them by name when you need fine-grained control.",
|
|
3275
|
-
debugging: ["ateam_get_execution_logs", "ateam_connector_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"],
|
|
3323
|
+
debugging: ["ateam_get_execution_logs", "ateam_connector_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source", "ateam_get_deployed_connector_source"],
|
|
3276
3324
|
manual_lifecycle: ["ateam_validate_skill", "ateam_validate_solution", "ateam_deploy_solution", "ateam_deploy_skill", "ateam_deploy_connector", "ateam_update", "ateam_redeploy"],
|
|
3277
3325
|
async_testing: ["ateam_test_status", "ateam_test_abort"],
|
|
3278
3326
|
other: ["ateam_upload_connector_files", "ateam_solution_chat"],
|
|
@@ -3646,6 +3694,12 @@ const handlers = {
|
|
|
3646
3694
|
if (!mcp_store) {
|
|
3647
3695
|
try {
|
|
3648
3696
|
const ghStatus = await get(`/deploy/solutions/${solutionId}/github/status`, sid);
|
|
3697
|
+
// repo_url only says a REPO EXISTS. It has never said the repo carries
|
|
3698
|
+
// this solution's connector source, and treating the two as the same
|
|
3699
|
+
// claim is how a connector with nothing in the repo used to slip
|
|
3700
|
+
// through Phase 0 and then vanish from connectors[] below. The real
|
|
3701
|
+
// per-connector answer comes from pull-bundle, and it is acted on
|
|
3702
|
+
// there — this stays a cheap "is GitHub worth asking at all?" gate.
|
|
3649
3703
|
if (ghStatus?.repo_url) {
|
|
3650
3704
|
github = true;
|
|
3651
3705
|
}
|
|
@@ -3690,13 +3744,30 @@ const handlers = {
|
|
|
3690
3744
|
// shape to the connector id and dedupe, so a mis-keyed mcp_store can never
|
|
3691
3745
|
// manufacture file-path connectors. (Core also rejects "/"-bearing ids at
|
|
3692
3746
|
// its boundary as defense-in-depth.)
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3747
|
+
//
|
|
3748
|
+
// B5 — A CONNECTOR WITH NO SOURCE IN THE REPO MUST NOT VANISH.
|
|
3749
|
+
// Synthesizing purely from mcp_store keys means a declared connector the
|
|
3750
|
+
// repo does not carry is simply ABSENT from connectors[], so nothing
|
|
3751
|
+
// validates it, nothing reports it, and the deploy proceeds as though it
|
|
3752
|
+
// were never part of the solution. Phase 0 made this likelier by
|
|
3753
|
+
// treating "the repo exists" (repo_url) as "the repo has the source" —
|
|
3754
|
+
// two different claims.
|
|
3755
|
+
//
|
|
3756
|
+
// pull-bundle now answers per connector, so union those ids in. A
|
|
3757
|
+
// connector listed here still deploys when its authored source lives in
|
|
3758
|
+
// the Builder's store; what it can no longer do is disappear.
|
|
3759
|
+
const missingSource = Array.isArray(pullResult.connectors_missing_source)
|
|
3760
|
+
? pullResult.connectors_missing_source : [];
|
|
3761
|
+
const unreadable = Array.isArray(pullResult.connectors_unreadable)
|
|
3762
|
+
? pullResult.connectors_unreadable : [];
|
|
3763
|
+
if (!connectors?.length && (Object.keys(effectiveMcpStore).length > 0 || missingSource.length > 0)) {
|
|
3764
|
+
const connIds = [...new Set([
|
|
3765
|
+
...Object.keys(effectiveMcpStore).map((k) => {
|
|
3696
3766
|
const m = String(k).match(/^connectors\/([^/]+)\//);
|
|
3697
3767
|
return m ? m[1] : k;
|
|
3698
|
-
})
|
|
3699
|
-
|
|
3768
|
+
}),
|
|
3769
|
+
...missingSource.map((c) => (typeof c === "string" ? c : c?.id)).filter(Boolean),
|
|
3770
|
+
])];
|
|
3700
3771
|
connectors = connIds.map((id) => ({
|
|
3701
3772
|
id,
|
|
3702
3773
|
name: id,
|
|
@@ -3710,6 +3781,12 @@ const handlers = {
|
|
|
3710
3781
|
connectors_found: pullResult.connectors_found || 0,
|
|
3711
3782
|
files_loaded: pullResult.files_loaded || 0,
|
|
3712
3783
|
connectors_synthesized: connectors?.length || 0,
|
|
3784
|
+
// Named, not swallowed. The repo existing said nothing about these.
|
|
3785
|
+
...(missingSource.length > 0 && {
|
|
3786
|
+
connectors_missing_source: missingSource,
|
|
3787
|
+
note: "These connectors are declared but the repo carries no source for them. They are still deployed if the Builder holds their authored source; if it does not, validation refuses and ateam_get_connector_source / ateam_recover_connector_source tell you which.",
|
|
3788
|
+
}),
|
|
3789
|
+
...(unreadable.length > 0 && { connectors_unreadable: unreadable }),
|
|
3713
3790
|
});
|
|
3714
3791
|
} catch (err) {
|
|
3715
3792
|
return {
|
|
@@ -3849,10 +3926,29 @@ const handlers = {
|
|
|
3849
3926
|
}
|
|
3850
3927
|
|
|
3851
3928
|
// Phase 2.5: Restart connectors that have source code (upload triggers stop+start)
|
|
3929
|
+
//
|
|
3930
|
+
// A runtime:"device" connector is SKIPPED. It has authored source — the RN
|
|
3931
|
+
// bundle (rn-src/, package.json, the esbuild config) — so it looks like an
|
|
3932
|
+
// ordinary connector to a loop that only asks "does it have files?". But
|
|
3933
|
+
// there is no server process to restart: the bundle ships to the phone.
|
|
3934
|
+
// Uploading it here 409s on the merge and then health marked the connector
|
|
3935
|
+
// "error", failing a deploy whose connector was working as designed.
|
|
3936
|
+
//
|
|
3937
|
+
// Classified from the DECLARED connectors[], which the caller authored —
|
|
3938
|
+
// not by asking Core. Same ownership rule the Builder now follows.
|
|
3939
|
+
const deviceConnectorIds = new Set(
|
|
3940
|
+
(connectors || [])
|
|
3941
|
+
.filter((c) => c && typeof c === "object" && c.runtime === "device" && c.id)
|
|
3942
|
+
.map((c) => c.id),
|
|
3943
|
+
);
|
|
3852
3944
|
if (effectiveMcpStore && Object.keys(effectiveMcpStore).length > 0) {
|
|
3853
3945
|
const connectorResults = [];
|
|
3854
3946
|
for (const [connId, files] of Object.entries(effectiveMcpStore)) {
|
|
3855
3947
|
if (!Array.isArray(files) || files.length === 0) continue;
|
|
3948
|
+
if (deviceConnectorIds.has(connId)) {
|
|
3949
|
+
connectorResults.push({ id: connId, ok: true, tools: 0, skipped: "device_runtime" });
|
|
3950
|
+
continue;
|
|
3951
|
+
}
|
|
3856
3952
|
try {
|
|
3857
3953
|
const uploadResult = await post(
|
|
3858
3954
|
`/deploy/solutions/${solutionId}/connectors/${connId}/upload`,
|
|
@@ -5247,8 +5343,46 @@ const handlers = {
|
|
|
5247
5343
|
},
|
|
5248
5344
|
|
|
5249
5345
|
ateam_get_connector_source: async ({ solution_id, connector_id, path }, sid) => {
|
|
5250
|
-
|
|
5346
|
+
let data;
|
|
5347
|
+
try {
|
|
5348
|
+
data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/source`, sid);
|
|
5349
|
+
} catch (err) {
|
|
5350
|
+
// AUTHORED_SOURCE_MISSING is a real, actionable answer — not a lookup
|
|
5351
|
+
// failure. Returning the raw 404 would send a caller hunting for a wrong
|
|
5352
|
+
// solution_id, when the true state is "Core may be running this connector
|
|
5353
|
+
// and nobody can reproduce it". Say that, and name the two tools that act
|
|
5354
|
+
// on it, instead of leaving the agent to improvise a rewrite.
|
|
5355
|
+
let parsed = null;
|
|
5356
|
+
try { parsed = JSON.parse(err.body || "{}"); } catch { /* not JSON */ }
|
|
5357
|
+
if (err.status === 404 && parsed?.code === "AUTHORED_SOURCE_MISSING") {
|
|
5358
|
+
return {
|
|
5359
|
+
ok: false,
|
|
5360
|
+
code: "AUTHORED_SOURCE_MISSING",
|
|
5361
|
+
connector_id,
|
|
5362
|
+
error: parsed.error,
|
|
5363
|
+
deployed_in_core: parsed.deployed_in_core === true,
|
|
5364
|
+
next: parsed.deployed_in_core
|
|
5365
|
+
? [
|
|
5366
|
+
`ateam_get_deployed_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — see what Core is actually running`,
|
|
5367
|
+
`ateam_recover_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — adopt it as authored source, stamped as recovered`,
|
|
5368
|
+
]
|
|
5369
|
+
: [`ateam_create_connector — nothing has been authored for this connector yet`],
|
|
5370
|
+
warning: parsed.deployed_in_core
|
|
5371
|
+
? "Do NOT write a replacement from memory. A running connector's code is recoverable; an improvised rewrite silently replaces working code with a guess."
|
|
5372
|
+
: undefined,
|
|
5373
|
+
};
|
|
5374
|
+
}
|
|
5375
|
+
throw err;
|
|
5376
|
+
}
|
|
5251
5377
|
const files = Array.isArray(data?.files) ? data.files : [];
|
|
5378
|
+
// Provenance travels with every answer. Without it a caller cannot tell the
|
|
5379
|
+
// authored store from GitHub — and historically could not tell either from
|
|
5380
|
+
// Core's runtime copy, which is how a lost source looked like a present one.
|
|
5381
|
+
const prov = {
|
|
5382
|
+
provenance: data?.provenance,
|
|
5383
|
+
...(data?.scheme && { scheme: data.scheme }),
|
|
5384
|
+
authored_source_of_record: data?.authored_source_of_record !== false,
|
|
5385
|
+
};
|
|
5252
5386
|
// A whole connector's source easily exceeds the ~50KB tool-output ceiling and
|
|
5253
5387
|
// truncates (you couldn't read the file you needed). So: no `path` → return a
|
|
5254
5388
|
// FILE MANIFEST (paths + sizes, no content — small); with `path` → return just
|
|
@@ -5257,6 +5391,7 @@ const handlers = {
|
|
|
5257
5391
|
return {
|
|
5258
5392
|
ok: true,
|
|
5259
5393
|
connector_id,
|
|
5394
|
+
...prov,
|
|
5260
5395
|
files: files.map((f) => ({ path: f.path, bytes: (f.content || "").length, encoding: f.encoding || "utf8" })),
|
|
5261
5396
|
total_bytes: files.reduce((n, f) => n + (f.content || "").length, 0),
|
|
5262
5397
|
hint: "Large source is not returned inline. Call again with path:'<file>' to read one file (e.g. path:'server.js').",
|
|
@@ -5265,9 +5400,71 @@ const handlers = {
|
|
|
5265
5400
|
const norm = String(path).replace(/^\.?\//, "");
|
|
5266
5401
|
const file = files.find((f) => f.path === path || f.path === norm || f.path.replace(/^\.?\//, "") === norm);
|
|
5267
5402
|
if (!file) {
|
|
5268
|
-
return { ok: false, connector_id, error: `file '${path}' not found`, available: files.map((f) => f.path) };
|
|
5403
|
+
return { ok: false, connector_id, ...prov, error: `file '${path}' not found`, available: files.map((f) => f.path) };
|
|
5404
|
+
}
|
|
5405
|
+
return { ok: true, connector_id, ...prov, path: file.path, encoding: file.encoding || "utf8", content: file.content };
|
|
5406
|
+
},
|
|
5407
|
+
|
|
5408
|
+
ateam_get_deployed_connector_source: async ({ solution_id, connector_id, path }, sid) => {
|
|
5409
|
+
const data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/deployed-source`, sid);
|
|
5410
|
+
const files = Array.isArray(data?.files) ? data.files : [];
|
|
5411
|
+
// The label rides on EVERY response shape, including the error one. A
|
|
5412
|
+
// caller that reads one file out of here must not be able to forget which
|
|
5413
|
+
// store it came from — that forgetting is the whole defect this splits.
|
|
5414
|
+
const label = {
|
|
5415
|
+
provenance: data?.provenance || "core_runtime",
|
|
5416
|
+
authored_source_of_record: false,
|
|
5417
|
+
note: "DEPLOYED copy from Core, not authored source. It reflects the last successful deploy and may differ from what the Builder can reproduce.",
|
|
5418
|
+
};
|
|
5419
|
+
if (!path) {
|
|
5420
|
+
return {
|
|
5421
|
+
ok: true,
|
|
5422
|
+
connector_id,
|
|
5423
|
+
...label,
|
|
5424
|
+
files: files.map((f) => ({ path: f.path, bytes: (f.content || "").length, encoding: f.encoding || "utf8" })),
|
|
5425
|
+
total_bytes: files.reduce((n, f) => n + (f.content || "").length, 0),
|
|
5426
|
+
hint: "Call again with path:'<file>' to read one file.",
|
|
5427
|
+
};
|
|
5428
|
+
}
|
|
5429
|
+
const norm = String(path).replace(/^\.?\//, "");
|
|
5430
|
+
const file = files.find((f) => f.path === path || f.path === norm || f.path.replace(/^\.?\//, "") === norm);
|
|
5431
|
+
if (!file) {
|
|
5432
|
+
return { ok: false, connector_id, ...label, error: `file '${path}' not found`, available: files.map((f) => f.path) };
|
|
5433
|
+
}
|
|
5434
|
+
return { ok: true, connector_id, ...label, path: file.path, encoding: file.encoding || "utf8", content: file.content };
|
|
5435
|
+
},
|
|
5436
|
+
|
|
5437
|
+
ateam_recover_connector_source: async ({ solution_id, connector_id, force = false }, sid) => {
|
|
5438
|
+
try {
|
|
5439
|
+
const data = await post(
|
|
5440
|
+
`/deploy/solutions/${solution_id}/connectors/${connector_id}/recover-from-core`,
|
|
5441
|
+
{ force: force === true },
|
|
5442
|
+
sid,
|
|
5443
|
+
);
|
|
5444
|
+
return data;
|
|
5445
|
+
} catch (err) {
|
|
5446
|
+
// The refusal is the point of the tool, so report it as a decision the
|
|
5447
|
+
// caller has to make rather than as a failure it should retry past.
|
|
5448
|
+
let parsed = null;
|
|
5449
|
+
try { parsed = JSON.parse(err.body || "{}"); } catch { /* not JSON */ }
|
|
5450
|
+
if (err.status === 409 && parsed?.code === "AUTHORED_SOURCE_EXISTS") {
|
|
5451
|
+
return {
|
|
5452
|
+
ok: false,
|
|
5453
|
+
code: "AUTHORED_SOURCE_EXISTS",
|
|
5454
|
+
connector_id,
|
|
5455
|
+
error: parsed.error,
|
|
5456
|
+
next: [
|
|
5457
|
+
`ateam_get_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — the authored copy`,
|
|
5458
|
+
`ateam_get_deployed_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — the deployed copy`,
|
|
5459
|
+
"Compare them. Only if the deployed copy is the one to keep, call again with force:true.",
|
|
5460
|
+
],
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
if (err.status === 404 && parsed?.code === "NOTHING_TO_RECOVER") {
|
|
5464
|
+
return { ok: false, code: "NOTHING_TO_RECOVER", connector_id, error: parsed.error };
|
|
5465
|
+
}
|
|
5466
|
+
throw err;
|
|
5269
5467
|
}
|
|
5270
|
-
return { ok: true, connector_id, path: file.path, encoding: file.encoding || "utf8", content: file.content };
|
|
5271
5468
|
},
|
|
5272
5469
|
|
|
5273
5470
|
// Render + write CLAUDE.md into the solution's GitHub repo.
|