@mytegroupinc/myte-core 0.0.38 → 0.0.40
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 +7 -6
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/mytecody-cli.js +185 -14
- package/package.json +1 -1
- package/lib/mytecody-async-responses-bridge.js +0 -431
package/README.md
CHANGED
|
@@ -28,16 +28,17 @@ This package exists so the public wrapper can stay small and versioned cleanly.
|
|
|
28
28
|
- Public package documentation is intentionally minimal. Internal rollout and design notes are not part of the npm package contract.
|
|
29
29
|
- `mytecody` is a model-agnostic MyteCody team launcher. It supports `doctor`,
|
|
30
30
|
`doctor --probe-gateway`, `update --dry-run`, and `update`. On first coding
|
|
31
|
-
run, it installs the branded MyteCody engine
|
|
32
|
-
into a user-local Myte cache after signature/hash
|
|
33
|
-
requires
|
|
31
|
+
run, it installs the branded MyteCody engine and signed client assets from
|
|
32
|
+
the Myte release manifest into a user-local Myte cache after signature/hash
|
|
33
|
+
checks. Coding execution requires those assets, a reachable Myte AI Cody
|
|
34
|
+
gateway, and `MYTEAI_API_KEY`.
|
|
34
35
|
- `mytecody doctor` reports both the signed MyteCody engine state and this npm
|
|
35
36
|
package version. `mytecody update` updates the engine only. Use
|
|
36
37
|
`npm install -g myte@latest` to update the launcher and the Myte API CLI
|
|
37
38
|
commands shipped by npm.
|
|
38
|
-
- The package does not bundle the large MyteCody engine binary
|
|
39
|
-
|
|
40
|
-
notices.
|
|
39
|
+
- The package does not bundle the large MyteCody engine binary or the async
|
|
40
|
+
Responses bridge implementation. See `THIRD_PARTY_NOTICES.md` and
|
|
41
|
+
`TRADEMARKS.md` for Codex lineage and Myte brand notices.
|
|
41
42
|
|
|
42
43
|
## Agent Usage Contract
|
|
43
44
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -13,9 +13,9 @@ Source lineage tracked for this slice:
|
|
|
13
13
|
- License: Apache-2.0
|
|
14
14
|
- Upstream: https://github.com/openai/codex
|
|
15
15
|
|
|
16
|
-
The MyteCody engine
|
|
17
|
-
through a Myte release manifest. This package verifies
|
|
18
|
-
before installing
|
|
16
|
+
The MyteCody engine and signed client assets are distributed separately from
|
|
17
|
+
this npm package through a Myte release manifest. This package verifies release
|
|
18
|
+
artifacts before installing them into the user's Myte-owned local cache.
|
|
19
19
|
|
|
20
20
|
Myte names, marks, logos, terminal branding, and product design are not part of
|
|
21
21
|
the OpenAI Codex project and remain Myte-owned brand assets.
|
package/mytecody-cli.js
CHANGED
|
@@ -12,9 +12,6 @@ const {
|
|
|
12
12
|
normalizeMyteAiBase,
|
|
13
13
|
} = require("./lib/ai-gateway");
|
|
14
14
|
const { createMyteSplash } = require("./lib/mytecody-splash");
|
|
15
|
-
const {
|
|
16
|
-
startMyteCodyAsyncResponsesBridge,
|
|
17
|
-
} = require("./lib/mytecody-async-responses-bridge");
|
|
18
15
|
|
|
19
16
|
const PACKAGE_NAME = "myte";
|
|
20
17
|
const PACKAGE_VERSION = require("./package.json").version;
|
|
@@ -154,13 +151,21 @@ function installRoot() {
|
|
|
154
151
|
return path.join(os.homedir(), ".myte", "cody");
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
function currentInstallRoot() {
|
|
155
|
+
return path.join(installRoot(), "current");
|
|
156
|
+
}
|
|
157
|
+
|
|
157
158
|
function currentClientManifestPath() {
|
|
158
|
-
return path.join(
|
|
159
|
+
return path.join(currentInstallRoot(), "manifest.json");
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
function currentEnginePath() {
|
|
162
163
|
const executable = process.platform === "win32" ? "mytecody-engine.exe" : "mytecody-engine";
|
|
163
|
-
return path.join(
|
|
164
|
+
return path.join(currentInstallRoot(), "bin", executable);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function currentBridgePath() {
|
|
168
|
+
return path.join(currentInstallRoot(), "lib", "mytecody-async-responses-bridge.js");
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
function codexHome() {
|
|
@@ -187,6 +192,100 @@ function installedClientCommand() {
|
|
|
187
192
|
return { cmd: enginePath, args: [], source: "myte-installed-engine" };
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
function installedClientUsable() {
|
|
196
|
+
return Boolean(installedClientCommand() && fs.existsSync(currentBridgePath()) && readCurrentClientManifest());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadSignedBridge() {
|
|
200
|
+
const bridgePath = currentBridgePath();
|
|
201
|
+
if (!fs.existsSync(bridgePath)) {
|
|
202
|
+
throw new Error("signed MyteCody inference bridge asset is missing; run `mytecody update`.");
|
|
203
|
+
}
|
|
204
|
+
const bridge = require(bridgePath);
|
|
205
|
+
if (!bridge || typeof bridge.startMyteCodyAsyncResponsesBridge !== "function") {
|
|
206
|
+
throw new Error("signed MyteCody inference bridge asset is invalid.");
|
|
207
|
+
}
|
|
208
|
+
return bridge;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function releaseAssetsForPlatform(manifest, artifact) {
|
|
212
|
+
const assets = [];
|
|
213
|
+
const collect = (value, source) => {
|
|
214
|
+
if (!value) return;
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
value.forEach((item, index) => {
|
|
217
|
+
if (item && typeof item === "object") {
|
|
218
|
+
assets.push({ ...item, source, index });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (typeof value === "object") {
|
|
224
|
+
Object.entries(value).forEach(([name, item], index) => {
|
|
225
|
+
if (item && typeof item === "object") {
|
|
226
|
+
assets.push({ name: item.name || name, ...item, source, index });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
collect(manifest && manifest.client_assets, "manifest.client_assets");
|
|
233
|
+
collect(artifact && artifact.assets, "artifact.assets");
|
|
234
|
+
|
|
235
|
+
return assets
|
|
236
|
+
.map((asset) => ({
|
|
237
|
+
...asset,
|
|
238
|
+
install_path: asset.install_path || asset.path || "",
|
|
239
|
+
}))
|
|
240
|
+
.filter((asset) => asset && asset.url && asset.sha256 && asset.install_path);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function assertSafeReleaseAssetInstallPath(installPath) {
|
|
244
|
+
const raw = String(installPath || "").replace(/\\/g, "/").trim();
|
|
245
|
+
if (!raw) throw new Error("release asset install_path is required");
|
|
246
|
+
if (path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw)) {
|
|
247
|
+
throw new Error(`release asset install_path must be relative: ${installPath}`);
|
|
248
|
+
}
|
|
249
|
+
const parts = raw.split("/").filter(Boolean);
|
|
250
|
+
if (!parts.length || parts.includes("..")) {
|
|
251
|
+
throw new Error(`release asset install_path is unsafe: ${installPath}`);
|
|
252
|
+
}
|
|
253
|
+
return path.join(currentInstallRoot(), ...parts);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function currentReleaseAssetRecordByPath(current) {
|
|
257
|
+
const map = new Map();
|
|
258
|
+
for (const asset of Array.isArray(current && current.assets) ? current.assets : []) {
|
|
259
|
+
if (asset && asset.install_path) {
|
|
260
|
+
map.set(String(asset.install_path).replace(/\\/g, "/"), asset);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return map;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function installedReleaseAssetsMatchManifest(manifest, artifact) {
|
|
267
|
+
const assets = releaseAssetsForPlatform(manifest, artifact);
|
|
268
|
+
if (!assets.length) return true;
|
|
269
|
+
const current = readCurrentClientManifest();
|
|
270
|
+
const currentAssets = currentReleaseAssetRecordByPath(current);
|
|
271
|
+
|
|
272
|
+
for (const asset of assets) {
|
|
273
|
+
const installPath = String(asset.install_path || "").replace(/\\/g, "/");
|
|
274
|
+
const currentAsset = currentAssets.get(installPath);
|
|
275
|
+
if (!currentAsset) return false;
|
|
276
|
+
if (String(currentAsset.sha256 || "").toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
|
|
280
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
281
|
+
const installedSha = sha256Hex(fs.readFileSync(targetPath));
|
|
282
|
+
if (installedSha.toLowerCase() !== String(asset.installed_sha256 || asset.sha256).toLowerCase()) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
190
289
|
function installedClientMatchesManifest(manifest, artifact) {
|
|
191
290
|
const current = readCurrentClientManifest();
|
|
192
291
|
if (!current || !installedClientCommand()) return false;
|
|
@@ -207,6 +306,8 @@ function installedClientMatchesManifest(manifest, artifact) {
|
|
|
207
306
|
}
|
|
208
307
|
}
|
|
209
308
|
|
|
309
|
+
if (!installedReleaseAssetsMatchManifest(manifest, artifact)) return false;
|
|
310
|
+
|
|
210
311
|
return true;
|
|
211
312
|
}
|
|
212
313
|
|
|
@@ -271,8 +372,10 @@ function commonStatus(args, envPath) {
|
|
|
271
372
|
platform: platformKey(),
|
|
272
373
|
install_root: installRoot(),
|
|
273
374
|
engine_path: currentEnginePath(),
|
|
375
|
+
bridge_path: currentBridgePath(),
|
|
274
376
|
client_manifest: currentClientManifestPath(),
|
|
275
377
|
client_installed: Boolean(current && installedClientCommand()),
|
|
378
|
+
bridge_installed: fs.existsSync(currentBridgePath()),
|
|
276
379
|
client_version: current && current.version ? current.version : null,
|
|
277
380
|
},
|
|
278
381
|
};
|
|
@@ -517,11 +620,11 @@ function localPathFromArtifactUrl(urlValue) {
|
|
|
517
620
|
return null;
|
|
518
621
|
}
|
|
519
622
|
|
|
520
|
-
async function readArtifactBytes(artifact, { progress } = {}) {
|
|
623
|
+
async function readArtifactBytes(artifact, { progress, label = "MyteCody engine" } = {}) {
|
|
521
624
|
const urlValue = artifact && artifact.url ? String(artifact.url) : "";
|
|
522
625
|
const localPath = localPathFromArtifactUrl(urlValue);
|
|
523
626
|
if (localPath) {
|
|
524
|
-
if (progress) progress(
|
|
627
|
+
if (progress) progress(`reading local ${label} artifact`);
|
|
525
628
|
return fs.readFileSync(localPath);
|
|
526
629
|
}
|
|
527
630
|
const headers = {};
|
|
@@ -529,7 +632,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
529
632
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
530
633
|
if (progress) {
|
|
531
634
|
const expectedSize = Number(artifact && artifact.size_bytes ? artifact.size_bytes : 0);
|
|
532
|
-
progress(`downloading
|
|
635
|
+
progress(`downloading ${label} (${formatBytes(expectedSize)})`);
|
|
533
636
|
}
|
|
534
637
|
const response = await fetch(urlValue, { method: "GET", headers });
|
|
535
638
|
if (!response.ok) {
|
|
@@ -538,7 +641,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
538
641
|
}
|
|
539
642
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
540
643
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
541
|
-
if (progress) progress(`downloaded
|
|
644
|
+
if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
|
|
542
645
|
return bytes;
|
|
543
646
|
}
|
|
544
647
|
|
|
@@ -556,13 +659,13 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
556
659
|
if (progress && total > 0) {
|
|
557
660
|
const pct = Math.min(100, Math.floor((received / total) * 100));
|
|
558
661
|
if (pct >= lastPct + 10 || pct === 100) {
|
|
559
|
-
progress(`downloading
|
|
662
|
+
progress(`downloading ${label} ${pct}% (${formatBytes(received)} / ${formatBytes(total)})`);
|
|
560
663
|
lastPct = pct;
|
|
561
664
|
}
|
|
562
665
|
}
|
|
563
666
|
}
|
|
564
667
|
const bytes = Buffer.concat(chunks);
|
|
565
|
-
if (progress) progress(`downloaded
|
|
668
|
+
if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
|
|
566
669
|
return bytes;
|
|
567
670
|
}
|
|
568
671
|
|
|
@@ -621,6 +724,34 @@ function installArtifactBytes(bytes, manifest, artifact) {
|
|
|
621
724
|
return installedManifest;
|
|
622
725
|
}
|
|
623
726
|
|
|
727
|
+
async function installReleaseAssets(manifest, artifact, { progress } = {}) {
|
|
728
|
+
const assets = releaseAssetsForPlatform(manifest, artifact);
|
|
729
|
+
const installed = [];
|
|
730
|
+
for (const asset of assets) {
|
|
731
|
+
const name = String(asset.name || path.basename(String(asset.install_path || "")) || "client asset");
|
|
732
|
+
const bytes = await readArtifactBytes(asset, { progress, label: `MyteCody ${name}` });
|
|
733
|
+
const digest = sha256Hex(bytes);
|
|
734
|
+
if (digest.toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
|
|
735
|
+
throw new Error(`Release asset SHA-256 mismatch for ${name}: expected ${asset.sha256}, got ${digest}`);
|
|
736
|
+
}
|
|
737
|
+
const installBytes = artifactBytesForInstall(bytes, asset);
|
|
738
|
+
const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
|
|
739
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
740
|
+
fs.writeFileSync(targetPath, installBytes);
|
|
741
|
+
installed.push({
|
|
742
|
+
name,
|
|
743
|
+
install_path: String(asset.install_path || "").replace(/\\/g, "/"),
|
|
744
|
+
url: asset.url,
|
|
745
|
+
sha256: asset.sha256,
|
|
746
|
+
format: artifactFormat(asset),
|
|
747
|
+
size_bytes: bytes.length,
|
|
748
|
+
installed_sha256: sha256Hex(installBytes),
|
|
749
|
+
installed_size_bytes: installBytes.length,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return installed;
|
|
753
|
+
}
|
|
754
|
+
|
|
624
755
|
async function fetchJson(url, { headers = {}, timeoutMs = 8000 } = {}) {
|
|
625
756
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
626
757
|
const timeoutId =
|
|
@@ -920,7 +1051,8 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
|
|
|
920
1051
|
let bridge = null;
|
|
921
1052
|
try {
|
|
922
1053
|
progress("opening Myte inference bridge");
|
|
923
|
-
|
|
1054
|
+
const signedBridge = loadSignedBridge();
|
|
1055
|
+
bridge = await signedBridge.startMyteCodyAsyncResponsesBridge({
|
|
924
1056
|
gatewayRoot: gatewayRoot(args),
|
|
925
1057
|
token,
|
|
926
1058
|
});
|
|
@@ -969,7 +1101,8 @@ async function runDoctor(args, envPath) {
|
|
|
969
1101
|
if (args["probe-gateway"]) {
|
|
970
1102
|
payload.gateway.probe = await probeGateway(args);
|
|
971
1103
|
}
|
|
972
|
-
payload.ready_for_coding =
|
|
1104
|
+
payload.ready_for_coding =
|
|
1105
|
+
payload.auth.present && payload.release.client_installed && payload.release.bridge_installed;
|
|
973
1106
|
if (payload.gateway.probe) {
|
|
974
1107
|
payload.ready_for_coding = Boolean(payload.ready_for_coding && payload.gateway.probe.ok);
|
|
975
1108
|
}
|
|
@@ -998,6 +1131,7 @@ async function runDoctor(args, envPath) {
|
|
|
998
1131
|
console.log(`package update: ${payload.package.check_status}`);
|
|
999
1132
|
}
|
|
1000
1133
|
console.log(`client: ${payload.release.client_installed ? payload.release.client_version : "not installed"}`);
|
|
1134
|
+
console.log(`bridge: ${payload.release.bridge_installed ? "installed" : "not installed"}`);
|
|
1001
1135
|
console.log(`install: ${payload.release.install_root}`);
|
|
1002
1136
|
console.log("");
|
|
1003
1137
|
console.log("Coding requires the Myte AI gateway and a Myte AI key.");
|
|
@@ -1015,6 +1149,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1015
1149
|
const artifact = manifest ? artifactForPlatform(manifest) : null;
|
|
1016
1150
|
const signature = manifest ? signatureAccepted(manifest, args) : { ok: false, signature: { status: "not-checked", verified: false } };
|
|
1017
1151
|
const artifactMetadata = manifest ? validateArtifactMetadata(artifact) : { status: "not-checked", ok: false };
|
|
1152
|
+
const releaseAssets = manifest ? releaseAssetsForPlatform(manifest, artifact) : [];
|
|
1018
1153
|
const payload = {
|
|
1019
1154
|
ok: true,
|
|
1020
1155
|
dry_run: isDryRun,
|
|
@@ -1028,6 +1163,13 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1028
1163
|
trusted_unsigned: Boolean(signature.trusted_unsigned),
|
|
1029
1164
|
},
|
|
1030
1165
|
artifact: artifactMetadata,
|
|
1166
|
+
release_assets: releaseAssets.map((asset) => ({
|
|
1167
|
+
name: asset.name || null,
|
|
1168
|
+
install_path: asset.install_path,
|
|
1169
|
+
url: asset.url || null,
|
|
1170
|
+
format: asset.format || null,
|
|
1171
|
+
sha256_present: Boolean(asset.sha256),
|
|
1172
|
+
})),
|
|
1031
1173
|
};
|
|
1032
1174
|
|
|
1033
1175
|
if (!isDryRun) {
|
|
@@ -1046,6 +1188,11 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1046
1188
|
throw new Error(`Artifact SHA-256 mismatch: expected ${artifact.sha256}, got ${digest}`);
|
|
1047
1189
|
}
|
|
1048
1190
|
const installed = installArtifactBytes(bytes, manifest, artifact);
|
|
1191
|
+
const installedAssets = await installReleaseAssets(manifest, artifact, { progress });
|
|
1192
|
+
if (installedAssets.length) {
|
|
1193
|
+
installed.assets = installedAssets;
|
|
1194
|
+
fs.writeFileSync(currentClientManifestPath(), JSON.stringify(installed, null, 2), "utf8");
|
|
1195
|
+
}
|
|
1049
1196
|
payload.installed = {
|
|
1050
1197
|
ok: true,
|
|
1051
1198
|
version: installed.version,
|
|
@@ -1055,6 +1202,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1055
1202
|
installed_sha256: installed.artifact.installed_sha256,
|
|
1056
1203
|
installed_size_bytes: installed.artifact.installed_size_bytes,
|
|
1057
1204
|
format: installed.artifact.format,
|
|
1205
|
+
assets: installedAssets,
|
|
1058
1206
|
};
|
|
1059
1207
|
payload.release = {
|
|
1060
1208
|
...payload.release,
|
|
@@ -1082,9 +1230,31 @@ async function ensureBrandedEngineInstalled(args = {}, envPath = null, { progres
|
|
|
1082
1230
|
delete updateArgs.json;
|
|
1083
1231
|
|
|
1084
1232
|
const source = manifestUrl(updateArgs);
|
|
1085
|
-
|
|
1233
|
+
let manifestResult;
|
|
1234
|
+
try {
|
|
1235
|
+
manifestResult = await readManifest(source, { fetchManifest: true, progress });
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (isUrl(source) && installedClientUsable()) {
|
|
1238
|
+
return {
|
|
1239
|
+
ok: true,
|
|
1240
|
+
installed: false,
|
|
1241
|
+
reason: "cached-engine-manifest-unavailable",
|
|
1242
|
+
manifest_status: "fetch-failed",
|
|
1243
|
+
error: error && error.message ? error.message : String(error),
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1086
1248
|
const manifest = manifestResult.manifest;
|
|
1087
1249
|
if (!manifest) {
|
|
1250
|
+
if (isUrl(source) && installedClientUsable()) {
|
|
1251
|
+
return {
|
|
1252
|
+
ok: true,
|
|
1253
|
+
installed: false,
|
|
1254
|
+
reason: "cached-engine-manifest-unavailable",
|
|
1255
|
+
manifest_status: manifestResult.status,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1088
1258
|
return { ok: false, installed: false, reason: "manifest-unavailable", manifest_status: manifestResult.status };
|
|
1089
1259
|
}
|
|
1090
1260
|
const signature = signatureAccepted(manifest, updateArgs);
|
|
@@ -1185,6 +1355,7 @@ module.exports = {
|
|
|
1185
1355
|
codexProviderArgs,
|
|
1186
1356
|
codyInferenceBase,
|
|
1187
1357
|
codyGatewayUrl,
|
|
1358
|
+
currentBridgePath,
|
|
1188
1359
|
currentClientManifestPath,
|
|
1189
1360
|
currentEnginePath,
|
|
1190
1361
|
ensureBrandedEngineInstalled,
|
package/package.json
CHANGED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const http = require("node:http");
|
|
4
|
-
const crypto = require("node:crypto");
|
|
5
|
-
|
|
6
|
-
const HOST = "127.0.0.1";
|
|
7
|
-
const PATCH_TOOL_NAME = "apply_patch";
|
|
8
|
-
|
|
9
|
-
function sendJson(res, status, payload, headers = {}) {
|
|
10
|
-
res.writeHead(status, {
|
|
11
|
-
"content-type": "application/json",
|
|
12
|
-
"cache-control": "no-store",
|
|
13
|
-
...headers,
|
|
14
|
-
});
|
|
15
|
-
res.end(JSON.stringify(payload));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readJsonBody(req) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const chunks = [];
|
|
21
|
-
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
22
|
-
req.on("error", reject);
|
|
23
|
-
req.on("end", () => {
|
|
24
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
25
|
-
if (!text.trim()) return resolve(null);
|
|
26
|
-
try {
|
|
27
|
-
resolve(JSON.parse(text));
|
|
28
|
-
} catch (error) {
|
|
29
|
-
reject(new Error(`invalid JSON body: ${error.message || error}`));
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stableJson(value) {
|
|
36
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
37
|
-
if (value && typeof value === "object") {
|
|
38
|
-
return `{${Object.keys(value)
|
|
39
|
-
.sort()
|
|
40
|
-
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
41
|
-
.join(",")}}`;
|
|
42
|
-
}
|
|
43
|
-
return JSON.stringify(value);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function requestHash(value) {
|
|
47
|
-
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseJsonMaybe(value) {
|
|
51
|
-
if (value && typeof value === "object") return value;
|
|
52
|
-
if (typeof value !== "string") return null;
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(value);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function extractPatchArgument(argumentsValue) {
|
|
61
|
-
const parsed = parseJsonMaybe(argumentsValue);
|
|
62
|
-
if (parsed && typeof parsed.patch === "string") return parsed.patch;
|
|
63
|
-
if (parsed && typeof parsed.input === "string") return parsed.input;
|
|
64
|
-
if (
|
|
65
|
-
typeof argumentsValue === "string" &&
|
|
66
|
-
argumentsValue.includes("*** Begin Patch") &&
|
|
67
|
-
argumentsValue.includes("*** End Patch")
|
|
68
|
-
) {
|
|
69
|
-
return argumentsValue;
|
|
70
|
-
}
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function normalizeOutputItem(item) {
|
|
75
|
-
if (!item || typeof item !== "object") return null;
|
|
76
|
-
const normalized = { ...item };
|
|
77
|
-
if (
|
|
78
|
-
normalized.type === "function_call" &&
|
|
79
|
-
normalized.arguments &&
|
|
80
|
-
typeof normalized.arguments !== "string"
|
|
81
|
-
) {
|
|
82
|
-
normalized.arguments = JSON.stringify(normalized.arguments);
|
|
83
|
-
}
|
|
84
|
-
return normalized;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function functionCallToCustomPatchCall(item) {
|
|
88
|
-
if (!item || item.type !== "function_call" || item.name !== PATCH_TOOL_NAME) return null;
|
|
89
|
-
const patch = extractPatchArgument(item.arguments);
|
|
90
|
-
if (!patch) return null;
|
|
91
|
-
const callId = item.call_id || item.id || `call_myte_patch_${Date.now()}`;
|
|
92
|
-
return {
|
|
93
|
-
id: item.id || callId,
|
|
94
|
-
type: "custom_tool_call",
|
|
95
|
-
status: item.status || "completed",
|
|
96
|
-
call_id: callId,
|
|
97
|
-
name: PATCH_TOOL_NAME,
|
|
98
|
-
input: patch,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function adaptOutputItemForCodex(rawItem) {
|
|
103
|
-
const item = normalizeOutputItem(rawItem);
|
|
104
|
-
if (!item) return null;
|
|
105
|
-
return functionCallToCustomPatchCall(item) || item;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function adaptResponsePayloadForCodex(payload) {
|
|
109
|
-
if (!payload || typeof payload !== "object") return payload;
|
|
110
|
-
const output = Array.isArray(payload.output)
|
|
111
|
-
? payload.output.map(adaptOutputItemForCodex).filter(Boolean)
|
|
112
|
-
: payload.output;
|
|
113
|
-
return {
|
|
114
|
-
...payload,
|
|
115
|
-
model: "myte",
|
|
116
|
-
output,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function sse(res, event) {
|
|
121
|
-
res.write(`event: ${event.type}\n`);
|
|
122
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function chunkText(text, size = 4096) {
|
|
126
|
-
const value = String(text || "");
|
|
127
|
-
const chunks = [];
|
|
128
|
-
for (let index = 0; index < value.length; index += size) {
|
|
129
|
-
chunks.push(value.slice(index, index + size));
|
|
130
|
-
}
|
|
131
|
-
return chunks.length ? chunks : [""];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function emitOutputItem(res, item, outputIndex) {
|
|
135
|
-
if (item && item.type === "custom_tool_call" && item.name === PATCH_TOOL_NAME) {
|
|
136
|
-
const itemId = item.id || item.call_id || `call_myte_patch_${Date.now()}`;
|
|
137
|
-
const callId = item.call_id || itemId;
|
|
138
|
-
const input = item.input || "";
|
|
139
|
-
sse(res, {
|
|
140
|
-
type: "response.output_item.added",
|
|
141
|
-
output_index: outputIndex,
|
|
142
|
-
item: {
|
|
143
|
-
...item,
|
|
144
|
-
id: itemId,
|
|
145
|
-
call_id: callId,
|
|
146
|
-
input: "",
|
|
147
|
-
status: "in_progress",
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
for (const delta of chunkText(input)) {
|
|
151
|
-
sse(res, {
|
|
152
|
-
type: "response.custom_tool_call_input.delta",
|
|
153
|
-
output_index: outputIndex,
|
|
154
|
-
item_id: itemId,
|
|
155
|
-
call_id: callId,
|
|
156
|
-
delta,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
sse(res, {
|
|
160
|
-
type: "response.custom_tool_call_input.done",
|
|
161
|
-
output_index: outputIndex,
|
|
162
|
-
item_id: itemId,
|
|
163
|
-
call_id: callId,
|
|
164
|
-
input,
|
|
165
|
-
});
|
|
166
|
-
sse(res, {
|
|
167
|
-
type: "response.output_item.done",
|
|
168
|
-
output_index: outputIndex,
|
|
169
|
-
item: {
|
|
170
|
-
...item,
|
|
171
|
-
id: itemId,
|
|
172
|
-
call_id: callId,
|
|
173
|
-
input,
|
|
174
|
-
status: "completed",
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
sse(res, {
|
|
181
|
-
type: "response.output_item.done",
|
|
182
|
-
output_index: outputIndex,
|
|
183
|
-
item,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function retryAfterMs(headers, fallbackMs) {
|
|
188
|
-
const headerValue =
|
|
189
|
-
headers && typeof headers.get === "function" ? Number(headers.get("retry-after")) : 0;
|
|
190
|
-
if (Number.isFinite(headerValue) && headerValue > 0) {
|
|
191
|
-
return Math.max(250, Math.min(15000, headerValue * 1000));
|
|
192
|
-
}
|
|
193
|
-
return fallbackMs;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function sleep(ms) {
|
|
197
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function fetchJson(url, options = {}) {
|
|
201
|
-
const response = await fetch(url, options);
|
|
202
|
-
const text = await response.text();
|
|
203
|
-
let body = {};
|
|
204
|
-
if (text.trim()) {
|
|
205
|
-
try {
|
|
206
|
-
body = JSON.parse(text);
|
|
207
|
-
} catch {
|
|
208
|
-
body = { raw_text: text };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return { response, body, text };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function absoluteGatewayUrl(root, pathOrUrl) {
|
|
215
|
-
const value = String(pathOrUrl || "");
|
|
216
|
-
if (/^https?:\/\//i.test(value)) return value;
|
|
217
|
-
const base = String(root || "").replace(/\/+$/, "");
|
|
218
|
-
const tail = value.startsWith("/") ? value : `/${value}`;
|
|
219
|
-
return `${base}${tail}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function resultPayloadFromJobResult(body) {
|
|
223
|
-
if (body && typeof body === "object" && body.object === "response") return body;
|
|
224
|
-
if (body && typeof body === "object" && body.payload && typeof body.payload === "object") {
|
|
225
|
-
return body.payload;
|
|
226
|
-
}
|
|
227
|
-
return body;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function submitAndDrainResponseJob(body, options) {
|
|
231
|
-
const gatewayRoot = String(options.gatewayRoot || "").replace(/\/+$/, "");
|
|
232
|
-
const token = String(options.token || "").trim();
|
|
233
|
-
if (!gatewayRoot) throw new Error("missing Myte Cody gateway root");
|
|
234
|
-
if (!token) throw new Error("missing MYTEAI_API_KEY");
|
|
235
|
-
|
|
236
|
-
const bridgeRequestId = `codybridge_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
237
|
-
const requestBody = {
|
|
238
|
-
...body,
|
|
239
|
-
request_id: body.request_id || bridgeRequestId,
|
|
240
|
-
stream: false,
|
|
241
|
-
};
|
|
242
|
-
const timeoutMs = Number(options.timeoutMs || process.env.MYTE_CODY_ASYNC_TIMEOUT_MS || 900000);
|
|
243
|
-
const startedAt = Date.now();
|
|
244
|
-
const submitUrl = `${gatewayRoot}/cody/v1/jobs/responses`;
|
|
245
|
-
const authHeaders = {
|
|
246
|
-
authorization: `Bearer ${token}`,
|
|
247
|
-
accept: "application/json",
|
|
248
|
-
};
|
|
249
|
-
const idempotencyKey = `mytecody-${bridgeRequestId}-${requestHash(requestBody).slice(0, 16)}`;
|
|
250
|
-
const submitted = await fetchJson(submitUrl, {
|
|
251
|
-
method: "POST",
|
|
252
|
-
headers: {
|
|
253
|
-
...authHeaders,
|
|
254
|
-
"content-type": "application/json",
|
|
255
|
-
"idempotency-key": idempotencyKey,
|
|
256
|
-
},
|
|
257
|
-
body: JSON.stringify(requestBody),
|
|
258
|
-
});
|
|
259
|
-
if (!submitted.response.ok && submitted.response.status !== 202) {
|
|
260
|
-
throw new Error(
|
|
261
|
-
`MyteCody async submit failed (${submitted.response.status}): ${submitted.text.slice(0, 500)}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const submitBody = submitted.body || {};
|
|
266
|
-
const jobId = submitBody.job_id || submitBody.id;
|
|
267
|
-
const poll = submitBody.poll || {};
|
|
268
|
-
let resultUrl = absoluteGatewayUrl(
|
|
269
|
-
gatewayRoot,
|
|
270
|
-
poll.result_url || (jobId ? `/cody/v1/jobs/${jobId}/result` : ""),
|
|
271
|
-
);
|
|
272
|
-
const statusUrl = absoluteGatewayUrl(
|
|
273
|
-
gatewayRoot,
|
|
274
|
-
poll.status_url || (jobId ? `/cody/v1/jobs/${jobId}` : ""),
|
|
275
|
-
);
|
|
276
|
-
if (!jobId || !resultUrl) {
|
|
277
|
-
return resultPayloadFromJobResult(submitBody);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let waitMs = retryAfterMs(submitted.response.headers, 500);
|
|
281
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
282
|
-
const separator = resultUrl.includes("?") ? "&" : "?";
|
|
283
|
-
const result = await fetchJson(`${resultUrl}${separator}consume=1`, {
|
|
284
|
-
method: "GET",
|
|
285
|
-
headers: authHeaders,
|
|
286
|
-
});
|
|
287
|
-
if (result.response.status === 200) return resultPayloadFromJobResult(result.body);
|
|
288
|
-
if (result.response.status !== 202) {
|
|
289
|
-
throw new Error(
|
|
290
|
-
`MyteCody async result failed (${result.response.status}): ${result.text.slice(0, 500)}`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
const status = result.body || {};
|
|
294
|
-
if (status.status === "failed" || status.status === "cancelled" || status.status === "expired") {
|
|
295
|
-
throw new Error(`MyteCody async job ${jobId} ended with status ${status.status}`);
|
|
296
|
-
}
|
|
297
|
-
resultUrl = absoluteGatewayUrl(gatewayRoot, status.poll?.result_url || resultUrl);
|
|
298
|
-
waitMs = retryAfterMs(result.response.headers, waitMs);
|
|
299
|
-
if (typeof options.onPoll === "function") {
|
|
300
|
-
options.onPoll({
|
|
301
|
-
job_id: jobId,
|
|
302
|
-
status: status.status || "queued",
|
|
303
|
-
status_url: statusUrl,
|
|
304
|
-
result_url: resultUrl,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
await sleep(waitMs);
|
|
308
|
-
}
|
|
309
|
-
throw new Error(`MyteCody async job ${jobId} timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function handleResponses(req, res, body, options) {
|
|
313
|
-
const responseId = `resp_myte_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
314
|
-
const wantsStream = body && body.stream !== false;
|
|
315
|
-
if (!wantsStream) {
|
|
316
|
-
const payload = adaptResponsePayloadForCodex(await submitAndDrainResponseJob(body, options));
|
|
317
|
-
return sendJson(res, 200, payload);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
res.writeHead(200, {
|
|
321
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
322
|
-
"cache-control": "no-cache, no-transform",
|
|
323
|
-
connection: "keep-alive",
|
|
324
|
-
"x-myte-cody-async-bridge": "1",
|
|
325
|
-
});
|
|
326
|
-
sse(res, {
|
|
327
|
-
type: "response.created",
|
|
328
|
-
response: { id: responseId, object: "response", status: "in_progress" },
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const keepalive = setInterval(() => {
|
|
332
|
-
res.write(": keepalive\n\n");
|
|
333
|
-
}, 10000);
|
|
334
|
-
let closed = false;
|
|
335
|
-
res.on("close", () => {
|
|
336
|
-
closed = true;
|
|
337
|
-
clearInterval(keepalive);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const payload = adaptResponsePayloadForCodex(await submitAndDrainResponseJob(body, options));
|
|
342
|
-
if (closed) return;
|
|
343
|
-
const output = Array.isArray(payload && payload.output) ? payload.output : [];
|
|
344
|
-
output.forEach((item, index) => emitOutputItem(res, item, index));
|
|
345
|
-
sse(res, {
|
|
346
|
-
type: "response.completed",
|
|
347
|
-
response: {
|
|
348
|
-
...payload,
|
|
349
|
-
id: responseId,
|
|
350
|
-
object: payload && payload.object ? payload.object : "response",
|
|
351
|
-
status: "completed",
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
clearInterval(keepalive);
|
|
355
|
-
res.end();
|
|
356
|
-
} catch (error) {
|
|
357
|
-
clearInterval(keepalive);
|
|
358
|
-
if (!closed) {
|
|
359
|
-
sse(res, {
|
|
360
|
-
type: "response.failed",
|
|
361
|
-
response: {
|
|
362
|
-
id: responseId,
|
|
363
|
-
error: {
|
|
364
|
-
code: "myte_cody_async_bridge_failed",
|
|
365
|
-
message: error && error.message ? error.message : String(error),
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
res.end();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function handle(req, res, options) {
|
|
375
|
-
try {
|
|
376
|
-
const url = new URL(req.url, `http://${HOST}`);
|
|
377
|
-
if (req.method === "GET" && (url.pathname === "/health" || url.pathname === "/v1/health")) {
|
|
378
|
-
return sendJson(res, 200, { ok: true, service: "myte-cody-async-responses-bridge" });
|
|
379
|
-
}
|
|
380
|
-
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
381
|
-
return sendJson(res, 200, {
|
|
382
|
-
object: "list",
|
|
383
|
-
data: [{ id: "myte", object: "model", owned_by: "myte" }],
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (req.method === "POST" && (url.pathname === "/v1/responses" || url.pathname === "/responses")) {
|
|
387
|
-
const body = await readJsonBody(req);
|
|
388
|
-
if (!body || typeof body !== "object") {
|
|
389
|
-
return sendJson(res, 400, { error: { message: "JSON object body required" } });
|
|
390
|
-
}
|
|
391
|
-
return handleResponses(req, res, body, options);
|
|
392
|
-
}
|
|
393
|
-
return sendJson(res, 404, { error: { message: "not found" } });
|
|
394
|
-
} catch (error) {
|
|
395
|
-
return sendJson(res, 500, {
|
|
396
|
-
error: { message: error && error.message ? error.message : String(error) },
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async function startMyteCodyAsyncResponsesBridge(options = {}) {
|
|
402
|
-
const server = http.createServer((req, res) => {
|
|
403
|
-
handle(req, res, options);
|
|
404
|
-
});
|
|
405
|
-
await new Promise((resolve, reject) => {
|
|
406
|
-
server.once("error", reject);
|
|
407
|
-
server.listen(0, HOST, resolve);
|
|
408
|
-
});
|
|
409
|
-
const address = server.address();
|
|
410
|
-
const port = address && typeof address === "object" ? address.port : 0;
|
|
411
|
-
let closed = false;
|
|
412
|
-
return {
|
|
413
|
-
host: HOST,
|
|
414
|
-
port,
|
|
415
|
-
baseUrl: `http://${HOST}:${port}/v1`,
|
|
416
|
-
close: () => {
|
|
417
|
-
if (closed) return Promise.resolve();
|
|
418
|
-
closed = true;
|
|
419
|
-
return new Promise((resolve) => {
|
|
420
|
-
server.close(() => resolve());
|
|
421
|
-
});
|
|
422
|
-
},
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
module.exports = {
|
|
427
|
-
adaptResponsePayloadForCodex,
|
|
428
|
-
extractPatchArgument,
|
|
429
|
-
startMyteCodyAsyncResponsesBridge,
|
|
430
|
-
submitAndDrainResponseJob,
|
|
431
|
-
};
|