@aexol/spectral 0.9.231 → 0.9.240
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 +47 -0
- package/dist/agent/index.d.ts +32 -5
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +209 -36
- package/dist/backend/generated-images.d.ts +1 -1
- package/dist/backend/generated-images.d.ts.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -4
- package/dist/commands/desktop.d.ts +167 -0
- package/dist/commands/desktop.d.ts.map +1 -0
- package/dist/commands/desktop.js +540 -0
- package/dist/commands/serve.d.ts +9 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +90 -2
- package/dist/commands/update.d.ts +85 -5
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +288 -58
- package/dist/extensions/desktop-control/macos.d.ts.map +1 -1
- package/dist/extensions/desktop-control/macos.js +2 -2
- package/dist/extensions/logic-trace.d.ts +1 -1
- package/dist/extensions/logic-trace.d.ts.map +1 -1
- package/dist/extensions/logic-trace.js +199 -70
- package/dist/extensions/video-generation/index.d.ts +3 -0
- package/dist/extensions/video-generation/index.d.ts.map +1 -0
- package/dist/extensions/video-generation/index.js +299 -0
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +30 -0
- package/dist/generated/zeus/index.d.ts +140 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/generated/zeus/index.js +5 -0
- package/dist/mcp/server-manager.d.ts.map +1 -1
- package/dist/mcp/server-manager.js +6 -0
- package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -1
- package/dist/memory/tools/receive-agent-observations.js +12 -2
- package/dist/relay/dispatcher.d.ts +13 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +41 -2
- package/dist/sdk/agent-core/agent-loop.js +13 -3
- package/dist/sdk/agent-core/agent.d.ts +11 -4
- package/dist/sdk/agent-core/agent.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent.js +16 -4
- package/dist/sdk/agent-core/tool-call-registry.d.ts +75 -0
- package/dist/sdk/agent-core/tool-call-registry.d.ts.map +1 -0
- package/dist/sdk/agent-core/tool-call-registry.js +131 -0
- package/dist/sdk/agent-core/tool-execution.d.ts.map +1 -1
- package/dist/sdk/agent-core/tool-execution.js +103 -4
- package/dist/sdk/agent-core/types.d.ts +15 -4
- package/dist/sdk/agent-core/types.d.ts.map +1 -1
- package/dist/sdk/ai/index.d.ts +3 -0
- package/dist/sdk/ai/index.d.ts.map +1 -1
- package/dist/sdk/ai/index.js +3 -0
- package/dist/sdk/ai/providers/faux.d.ts.map +1 -1
- package/dist/sdk/ai/providers/faux.js +3 -0
- package/dist/sdk/ai/providers/leaked-tool-call-parser.d.ts +39 -6
- package/dist/sdk/ai/providers/leaked-tool-call-parser.d.ts.map +1 -1
- package/dist/sdk/ai/providers/leaked-tool-call-parser.js +159 -18
- package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.js +22 -8
- package/dist/sdk/ai/providers/openrouter-videos.d.ts +3 -0
- package/dist/sdk/ai/providers/openrouter-videos.d.ts.map +1 -0
- package/dist/sdk/ai/providers/openrouter-videos.js +412 -0
- package/dist/sdk/ai/providers/register-builtins.d.ts.map +1 -1
- package/dist/sdk/ai/providers/register-builtins.js +6 -0
- package/dist/sdk/ai/providers/simple-options.d.ts +1 -6
- package/dist/sdk/ai/providers/simple-options.d.ts.map +1 -1
- package/dist/sdk/ai/providers/simple-options.js +0 -22
- package/dist/sdk/ai/providers/transform-messages.d.ts.map +1 -1
- package/dist/sdk/ai/providers/transform-messages.js +52 -1
- package/dist/sdk/ai/types.d.ts +91 -10
- package/dist/sdk/ai/types.d.ts.map +1 -1
- package/dist/sdk/ai/video-models.d.ts +10 -0
- package/dist/sdk/ai/video-models.d.ts.map +1 -0
- package/dist/sdk/ai/video-models.generated.d.ts +440 -0
- package/dist/sdk/ai/video-models.generated.d.ts.map +1 -0
- package/dist/sdk/ai/video-models.generated.js +441 -0
- package/dist/sdk/ai/video-models.js +22 -0
- package/dist/sdk/ai/videos-api-registry.d.ts +14 -0
- package/dist/sdk/ai/videos-api-registry.d.ts.map +1 -0
- package/dist/sdk/ai/videos-api-registry.js +21 -0
- package/dist/sdk/ai/videos.d.ts +4 -0
- package/dist/sdk/ai/videos.d.ts.map +1 -0
- package/dist/sdk/ai/videos.js +13 -0
- package/dist/sdk/coding-agent/core/agent-session.d.ts +8 -0
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +11 -0
- package/dist/sdk/coding-agent/core/extensions/loader.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/loader.js +14 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts +26 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +23 -0
- package/dist/sdk/coding-agent/core/extensions/session-events.d.ts +2 -2
- package/dist/sdk/coding-agent/core/extensions/session-events.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +12 -2
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/messages.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/messages.js +42 -2
- package/dist/sdk/coding-agent/core/resource-loader.d.ts +11 -0
- package/dist/sdk/coding-agent/core/resource-loader.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/resource-loader.js +23 -6
- package/dist/sdk/coding-agent/core/sdk.d.ts +24 -2
- package/dist/sdk/coding-agent/core/sdk.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/sdk.js +15 -3
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +0 -8
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +3 -3
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +5 -0
- package/dist/sdk/coding-agent/index.d.ts +2 -2
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/index.js +1 -1
- package/dist/sdk-public.d.ts +96 -0
- package/dist/sdk-public.d.ts.map +1 -0
- package/dist/sdk-public.js +109 -0
- package/dist/server/agent-bridge/rehydrate.d.ts +2 -2
- package/dist/server/agent-bridge/rehydrate.d.ts.map +1 -1
- package/dist/server/agent-bridge/rehydrate.js +14 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts +34 -0
- package/dist/server/agent-bridge/tps-metrics.d.ts.map +1 -1
- package/dist/server/agent-bridge/tps-metrics.js +22 -0
- package/dist/server/agent-bridge.d.ts +17 -2
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +108 -5
- package/dist/server/handlers/paths-list.d.ts +1 -1
- package/dist/server/handlers/paths-list.js +1 -1
- package/dist/server/inter-agent-broker.d.ts +12 -0
- package/dist/server/inter-agent-broker.d.ts.map +1 -1
- package/dist/server/inter-agent-broker.js +1 -0
- package/dist/server/preset-chain.d.ts +151 -0
- package/dist/server/preset-chain.d.ts.map +1 -0
- package/dist/server/preset-chain.js +250 -0
- package/dist/server/session-stream.d.ts +74 -1
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +238 -24
- package/dist/server/storage/rows.d.ts +2 -0
- package/dist/server/storage/rows.d.ts.map +1 -1
- package/dist/server/storage/schema.d.ts +2 -2
- package/dist/server/storage/schema.d.ts.map +1 -1
- package/dist/server/storage/schema.js +2 -1
- package/dist/server/storage.d.ts +7 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +142 -9
- package/dist/server/wire.d.ts +84 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/dist/utils/augment-path.d.ts +51 -0
- package/dist/utils/augment-path.d.ts.map +1 -0
- package/dist/utils/augment-path.js +234 -0
- package/dist/utils/open-browser.d.ts +55 -0
- package/dist/utils/open-browser.d.ts.map +1 -0
- package/dist/utils/open-browser.js +163 -0
- package/dist/utils/serve-state.d.ts +264 -0
- package/dist/utils/serve-state.d.ts.map +1 -0
- package/dist/utils/serve-state.js +563 -0
- package/dist/utils/standalone.d.ts +14 -0
- package/dist/utils/standalone.d.ts.map +1 -0
- package/dist/utils/standalone.js +23 -0
- package/package.json +7 -3
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* generic helpers from `../memory/` and never writes any memory state.
|
|
27
27
|
*/
|
|
28
28
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
29
|
-
import { join } from "node:path";
|
|
29
|
+
import { isAbsolute, join } from "node:path";
|
|
30
30
|
import { Type } from "../sdk/ai/index.js";
|
|
31
31
|
import { agentLoop, uuidv7, } from "../sdk/agent-core/index.js";
|
|
32
32
|
import { getAgentDir, } from "../sdk/coding-agent/index.js";
|
|
@@ -776,6 +776,7 @@ async function attemptLogicTraceSync(deps) {
|
|
|
776
776
|
// ---------------------------------------------------------------------------
|
|
777
777
|
const LOGIC_TRACE_READ_TOOL_NAME = "remote_logic_trace_read";
|
|
778
778
|
const LOGIC_TRACE_WRITE_TOOL_NAME = "remote_logic_trace_write";
|
|
779
|
+
const LOGIC_TRACE_WRITE_FILE_TOOL_NAME = "remote_logic_trace_write_file";
|
|
779
780
|
const UNBOUND_TEXT = "No Aexol Studio project binding found in this directory.\n" +
|
|
780
781
|
"Use local_bind_project to link this directory to a Studio project (then read/write logic.aexol), " +
|
|
781
782
|
"or `spectral bind <project-id>` from the terminal.";
|
|
@@ -841,7 +842,9 @@ function buildWriteTool(ensureBackend) {
|
|
|
841
842
|
name: LOGIC_TRACE_WRITE_TOOL_NAME,
|
|
842
843
|
label: "Write Logic Trace",
|
|
843
844
|
description: `Manually upsert the ONE ${LOGIC_TRACE_REMOTE_PATH} file in the bound Aexol Studio project with a COMPLETE .aexol spec. ` +
|
|
844
|
-
`Merge, never append-only: read it first, then write the full updated file. Refresh it after meaningful business-logic changes
|
|
845
|
+
`Merge, never append-only: read it first, then write the full updated file. Refresh it after meaningful business-logic changes. ` +
|
|
846
|
+
`For large specs (roughly >150 lines) the inline \`content\` argument risks provider gateway timeouts while the whole merged spec streams as a single tool call — ` +
|
|
847
|
+
`instead write the merged spec to a local file (e.g. \`.aexol/logic.draft.aexol\`) and call ${LOGIC_TRACE_WRITE_FILE_TOOL_NAME}.`,
|
|
845
848
|
promptSnippet: `Use ${LOGIC_TRACE_WRITE_TOOL_NAME}(content, reason) to fully regenerate ${LOGIC_TRACE_REMOTE_PATH} (the ONE business-logic spec in the bound Studio project) after meaningful business-logic changes.`,
|
|
846
849
|
promptGuidelines: [
|
|
847
850
|
`There is EXACTLY ONE logic file per project: ${LOGIC_TRACE_REMOTE_PATH}. Never create extra spec files; never append changelogs into it.`,
|
|
@@ -849,87 +852,207 @@ function buildWriteTool(ensureBackend) {
|
|
|
849
852
|
"Only full-spec writes; this tool upserts (updates the existing file or creates it when missing) and never deletes it.",
|
|
850
853
|
"Use it after meaningful business-logic changes (new/changed types, enums, roles, workflows, agents); automatic refresh handles the rest.",
|
|
851
854
|
"The tool returns ✓ when the complete spec parses, and an Error with parser diagnostics when it does not.",
|
|
855
|
+
`For large specs (roughly >150 lines) the inline \`content\` argument risks provider timeouts — write the merged spec to a local file (e.g. \`.aexol/logic.draft.aexol\`) and use ${LOGIC_TRACE_WRITE_FILE_TOOL_NAME} instead.`,
|
|
852
856
|
],
|
|
853
857
|
parameters: WriteSchema,
|
|
854
858
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
859
|
+
return upsertLogicTraceFromAgent(ensureBackend, {
|
|
860
|
+
cwd: ctx.cwd,
|
|
861
|
+
content: params.content ?? "",
|
|
862
|
+
reason: params.reason ?? "",
|
|
863
|
+
hasUI: ctx.hasUI,
|
|
864
|
+
ui: ctx.ui,
|
|
865
|
+
});
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Full manual-write flow shared by `remote_logic_trace_write`: resolve the
|
|
871
|
+
* bound backend, then run the shared upsert. Behavior and result messages are
|
|
872
|
+
* byte-identical to the pre-refactor single-tool implementation.
|
|
873
|
+
*/
|
|
874
|
+
async function upsertLogicTraceFromAgent(ensureBackend, input) {
|
|
875
|
+
const bound = await resolveBoundBackend(ensureBackend, input.cwd);
|
|
876
|
+
if (!bound.ok)
|
|
877
|
+
return bound.result;
|
|
878
|
+
return upsertLogicTrace({
|
|
879
|
+
backend: bound.backend,
|
|
880
|
+
binding: bound.binding,
|
|
881
|
+
cwd: input.cwd,
|
|
882
|
+
content: input.content,
|
|
883
|
+
reason: input.reason,
|
|
884
|
+
hasUI: input.hasUI,
|
|
885
|
+
ui: input.ui,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Shared prelude for the manual write tools: same explicit retry semantics as
|
|
890
|
+
* remote_logic_trace_read — this is the agent deliberately acting, so a
|
|
891
|
+
* memoized bootstrap failure may be re-attempted (and is unlocked on success).
|
|
892
|
+
*/
|
|
893
|
+
async function resolveBoundBackend(ensureBackend, cwd) {
|
|
894
|
+
const backend = await ensureBackend(cwd, { retry: true });
|
|
895
|
+
if (!backend.ok) {
|
|
896
|
+
return { ok: false, result: textResult(`Aexol backend unavailable: ${backend.message}`, true) };
|
|
897
|
+
}
|
|
898
|
+
const binding = await refreshBindingForCwd(cwd);
|
|
899
|
+
if (!binding)
|
|
900
|
+
return { ok: false, result: textResult(UNBOUND_TEXT, true) };
|
|
901
|
+
return { ok: true, backend, binding };
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Shared tail for the manual write tools: trim → parser gate → mandatory
|
|
905
|
+
* list → update-or-create upsert → ack. Both `remote_logic_trace_write` and
|
|
906
|
+
* `remote_logic_trace_write_file` funnel through here so their remote
|
|
907
|
+
* behavior and result messages stay byte-identical.
|
|
908
|
+
*/
|
|
909
|
+
async function upsertLogicTrace(input) {
|
|
910
|
+
// EXPLICIT agent action: the content the agent deliberately wrote is
|
|
911
|
+
// stored verbatim (trim only) — the redaction pass is reserved for
|
|
912
|
+
// automatically drafted sync content, not for the agent's own
|
|
913
|
+
// deliberate full-spec write.
|
|
914
|
+
const content = input.content.trim();
|
|
915
|
+
if (!content) {
|
|
916
|
+
return textResult("Error: content (the complete .aexol spec) is required.", true);
|
|
917
|
+
}
|
|
918
|
+
// Parser-backed gate BEFORE any remote call: invalid content must
|
|
919
|
+
// neither mutate the cloud file nor even probe the project-file
|
|
920
|
+
// listing, so writes fail fast with parser diagnostics instead of
|
|
921
|
+
// storing a spec the Aexol toolchain cannot read.
|
|
922
|
+
const validation = validateLogicTraceSpec(content);
|
|
923
|
+
if (!validation.ok) {
|
|
924
|
+
return textResult("Error: content is not a valid .aexol spec.\n" + formatSpecErrors(validation), true);
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
// Manual upsert: list (mandatory — create silently no-ops when the
|
|
928
|
+
// path already exists) then update-or-create. No LLM, no dedupe.
|
|
929
|
+
const remote = await fetchRemoteLogicFile(input.backend.client, input.binding.projectId, input.binding.teamId);
|
|
930
|
+
const state = stateFor(input.cwd);
|
|
931
|
+
state.remote = {
|
|
932
|
+
id: remote.id,
|
|
933
|
+
contentHash: remote.content !== undefined ? hashId(remote.content) : undefined,
|
|
934
|
+
};
|
|
935
|
+
// Every remote_* call carries the binding ids (projectId + teamId
|
|
936
|
+
// when present) so team access resolves to the BOUND project, not
|
|
937
|
+
// to the first team membership of the user token.
|
|
938
|
+
const teamArg = input.binding.teamId ? { teamId: input.binding.teamId } : {};
|
|
939
|
+
let id;
|
|
940
|
+
if (remote.id) {
|
|
941
|
+
const res = await input.backend.client.callTool("remote_update_project_file", {
|
|
942
|
+
id: remote.id,
|
|
943
|
+
content,
|
|
944
|
+
projectId: input.binding.projectId,
|
|
945
|
+
...teamArg,
|
|
946
|
+
});
|
|
947
|
+
if (res.isError)
|
|
948
|
+
return textResult(`Failed to update ${LOGIC_TRACE_REMOTE_PATH}: ${toolResultText(res)}`, true);
|
|
949
|
+
id = remote.id;
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
const res = await input.backend.client.callTool("remote_create_project_file", {
|
|
953
|
+
path: LOGIC_TRACE_REMOTE_PATH,
|
|
954
|
+
content,
|
|
955
|
+
projectId: input.binding.projectId,
|
|
956
|
+
...teamArg,
|
|
957
|
+
});
|
|
958
|
+
if (res.isError)
|
|
959
|
+
return textResult(`Failed to create ${LOGIC_TRACE_REMOTE_PATH}: ${toolResultText(res)}`, true);
|
|
960
|
+
id = extractCreatedFileId(res.content);
|
|
961
|
+
}
|
|
962
|
+
const contentHash = hashId(content);
|
|
963
|
+
state.remote = { id: id ?? null, contentHash };
|
|
964
|
+
notifyWith({ hasUI: input.hasUI, ui: input.ui }, `logic.aexol updated in project ${input.binding.name ?? input.binding.projectId}`);
|
|
965
|
+
return textResult(`✓ ${LOGIC_TRACE_REMOTE_PATH} ${remote.id ? "updated" : "created"} ` +
|
|
966
|
+
`(project ${input.binding.name ?? input.binding.projectId}, reason: ${input.reason.trim()}).` +
|
|
967
|
+
(input.ackSuffix ?? ""));
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
971
|
+
return textResult(`Failed to write ${LOGIC_TRACE_REMOTE_PATH}: ${msg}`, true);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* The `-file` variant of the manual write: reads the COMPLETE merged spec
|
|
976
|
+
* from a LOCAL file instead of a giant inline `content` argument, so agents
|
|
977
|
+
* pushing large specs avoid the provider gateway timeouts that streaming one
|
|
978
|
+
* huge tool-call argument can trigger. Validation + upsert are identical to
|
|
979
|
+
* `remote_logic_trace_write` (same helpers, same messages).
|
|
980
|
+
*/
|
|
981
|
+
function buildWriteFileTool(ensureBackend) {
|
|
982
|
+
const WriteFileSchema = Type.Object({
|
|
983
|
+
path: Type.String({
|
|
984
|
+
minLength: 1,
|
|
985
|
+
description: "Path to a LOCAL file holding the COMPLETE merged .aexol spec (e.g. `.aexol/logic.draft.aexol`). Relative to the working directory or absolute.",
|
|
986
|
+
}),
|
|
987
|
+
reason: Type.String({
|
|
988
|
+
minLength: 1,
|
|
989
|
+
description: "Why this full-spec update is needed (echoed in the ack).",
|
|
990
|
+
}),
|
|
991
|
+
});
|
|
992
|
+
return {
|
|
993
|
+
name: LOGIC_TRACE_WRITE_FILE_TOOL_NAME,
|
|
994
|
+
label: "Write Logic Trace From File",
|
|
995
|
+
description: `Manually upsert the ONE ${LOGIC_TRACE_REMOTE_PATH} file in the bound Aexol Studio project from a LOCAL file on disk. ` +
|
|
996
|
+
`Reads the file, validates it with the same parser gate as ${LOGIC_TRACE_WRITE_TOOL_NAME}, then performs the identical list-first create/update upsert. ` +
|
|
997
|
+
`Merge, never append-only: read ${LOGIC_TRACE_REMOTE_PATH} first, write the FULL merged spec to a local file (e.g. \`.aexol/logic.draft.aexol\`), then call this tool. ` +
|
|
998
|
+
`PREFERRED for large specs (roughly >150 lines): passing the spec as a local file avoids the provider gateway timeouts a giant inline \`content\` argument risks while streaming a single tool call.`,
|
|
999
|
+
promptSnippet: `Use ${LOGIC_TRACE_WRITE_FILE_TOOL_NAME}(path, reason) to upsert ${LOGIC_TRACE_REMOTE_PATH} (the ONE business-logic spec in the bound Studio project) from a local .aexol file — the preferred path for large specs (>150 lines).`,
|
|
1000
|
+
promptGuidelines: [
|
|
1001
|
+
`There is EXACTLY ONE logic file per project: ${LOGIC_TRACE_REMOTE_PATH}. Never create extra spec files; never append changelogs into it.`,
|
|
1002
|
+
`Always ${LOGIC_TRACE_READ_TOOL_NAME} before writing, and write the FULL merged spec to the local file — not a diff, not an appendix.`,
|
|
1003
|
+
"Only full-spec writes; this tool upserts (updates the existing file or creates it when missing) and never deletes it.",
|
|
1004
|
+
`The tool returns ✓ (with the source file and its line count) when the complete spec parses, and an Error with parser diagnostics when it does not.`,
|
|
1005
|
+
`For large specs (roughly >150 lines) prefer this file-based write: giant inline \`content\` arguments on ${LOGIC_TRACE_WRITE_TOOL_NAME} risk provider timeouts.`,
|
|
1006
|
+
],
|
|
1007
|
+
parameters: WriteFileSchema,
|
|
1008
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1009
|
+
const rawPath = (params.path ?? "").trim();
|
|
1010
|
+
if (!rawPath) {
|
|
1011
|
+
return textResult("Error: path (the local .aexol file holding the complete spec) is required.", true);
|
|
880
1012
|
}
|
|
1013
|
+
const filePath = isAbsolute(rawPath) ? rawPath : join(ctx.cwd, rawPath);
|
|
1014
|
+
// Same explicit retry semantics as the other manual tools (see
|
|
1015
|
+
// resolveBoundBackend).
|
|
1016
|
+
const bound = await resolveBoundBackend(ensureBackend, ctx.cwd);
|
|
1017
|
+
if (!bound.ok)
|
|
1018
|
+
return bound.result;
|
|
1019
|
+
let content;
|
|
881
1020
|
try {
|
|
882
|
-
|
|
883
|
-
// path already exists) then update-or-create. No LLM, no dedupe.
|
|
884
|
-
const remote = await fetchRemoteLogicFile(backend.client, binding.projectId, binding.teamId);
|
|
885
|
-
const state = stateFor(ctx.cwd);
|
|
886
|
-
state.remote = {
|
|
887
|
-
id: remote.id,
|
|
888
|
-
contentHash: remote.content !== undefined ? hashId(remote.content) : undefined,
|
|
889
|
-
};
|
|
890
|
-
// Every remote_* call carries the binding ids (projectId + teamId
|
|
891
|
-
// when present) so team access resolves to the BOUND project, not
|
|
892
|
-
// to the first team membership of the user token.
|
|
893
|
-
const teamArg = binding.teamId ? { teamId: binding.teamId } : {};
|
|
894
|
-
let id;
|
|
895
|
-
if (remote.id) {
|
|
896
|
-
const res = await backend.client.callTool("remote_update_project_file", {
|
|
897
|
-
id: remote.id,
|
|
898
|
-
content,
|
|
899
|
-
projectId: binding.projectId,
|
|
900
|
-
...teamArg,
|
|
901
|
-
});
|
|
902
|
-
if (res.isError)
|
|
903
|
-
return textResult(`Failed to update ${LOGIC_TRACE_REMOTE_PATH}: ${toolResultText(res)}`, true);
|
|
904
|
-
id = remote.id;
|
|
905
|
-
}
|
|
906
|
-
else {
|
|
907
|
-
const res = await backend.client.callTool("remote_create_project_file", {
|
|
908
|
-
path: LOGIC_TRACE_REMOTE_PATH,
|
|
909
|
-
content,
|
|
910
|
-
projectId: binding.projectId,
|
|
911
|
-
...teamArg,
|
|
912
|
-
});
|
|
913
|
-
if (res.isError)
|
|
914
|
-
return textResult(`Failed to create ${LOGIC_TRACE_REMOTE_PATH}: ${toolResultText(res)}`, true);
|
|
915
|
-
id = extractCreatedFileId(res.content);
|
|
916
|
-
}
|
|
917
|
-
const contentHash = hashId(content);
|
|
918
|
-
state.remote = { id: id ?? null, contentHash };
|
|
919
|
-
notifyWith({ hasUI: ctx.hasUI, ui: ctx.ui }, `logic.aexol updated in project ${binding.name ?? binding.projectId}`);
|
|
920
|
-
return textResult(`✓ ${LOGIC_TRACE_REMOTE_PATH} ${remote.id ? "updated" : "created"} ` +
|
|
921
|
-
`(project ${binding.name ?? binding.projectId}, reason: ${params.reason.trim()}).`);
|
|
1021
|
+
content = readFileSync(filePath, "utf-8");
|
|
922
1022
|
}
|
|
923
1023
|
catch (err) {
|
|
924
|
-
const
|
|
925
|
-
|
|
1024
|
+
const code = err.code;
|
|
1025
|
+
const detail = code === "ENOENT"
|
|
1026
|
+
? "file does not exist"
|
|
1027
|
+
: code === "EISDIR"
|
|
1028
|
+
? "path is a directory, not a file"
|
|
1029
|
+
: code === "EACCES" || code === "EPERM"
|
|
1030
|
+
? "file is not readable"
|
|
1031
|
+
: `failed to read (${code ?? "unknown error"})`;
|
|
1032
|
+
return textResult(`Error: cannot read "${rawPath}": ${detail}.`, true);
|
|
1033
|
+
}
|
|
1034
|
+
if (content.charCodeAt(0) === 0xfeff)
|
|
1035
|
+
content = content.slice(1); // strip BOM
|
|
1036
|
+
if (!content.trim()) {
|
|
1037
|
+
return textResult(`Error: "${rawPath}" is empty — no .aexol spec content to write.`, true);
|
|
926
1038
|
}
|
|
1039
|
+
const lineCount = content.split("\n").length;
|
|
1040
|
+
return upsertLogicTrace({
|
|
1041
|
+
backend: bound.backend,
|
|
1042
|
+
binding: bound.binding,
|
|
1043
|
+
cwd: ctx.cwd,
|
|
1044
|
+
content,
|
|
1045
|
+
reason: params.reason ?? "",
|
|
1046
|
+
hasUI: ctx.hasUI,
|
|
1047
|
+
ui: ctx.ui,
|
|
1048
|
+
ackSuffix: ` Source: ${filePath} (${lineCount} lines).`,
|
|
1049
|
+
});
|
|
927
1050
|
},
|
|
928
1051
|
};
|
|
929
1052
|
}
|
|
930
1053
|
/**
|
|
931
1054
|
* Logic trace extension entry point: automatic refresh hooks
|
|
932
|
-
* (`turn_end`, `session_before_compact`) plus the
|
|
1055
|
+
* (`turn_end`, `session_before_compact`) plus the three agent tools.
|
|
933
1056
|
*/
|
|
934
1057
|
export function logicTraceExtension(ext, options = {}) {
|
|
935
1058
|
const draftRunner = options.draftRunner ?? runLogicTraceDraft;
|
|
@@ -992,6 +1115,12 @@ export function logicTraceExtension(ext, options = {}) {
|
|
|
992
1115
|
catch (err) {
|
|
993
1116
|
process.stderr.write(`[logic-trace] Failed to register ${LOGIC_TRACE_WRITE_TOOL_NAME}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
994
1117
|
}
|
|
1118
|
+
try {
|
|
1119
|
+
ext.registerTool(buildWriteFileTool(ensureBackend));
|
|
1120
|
+
}
|
|
1121
|
+
catch (err) {
|
|
1122
|
+
process.stderr.write(`[logic-trace] Failed to register ${LOGIC_TRACE_WRITE_FILE_TOOL_NAME}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1123
|
+
}
|
|
995
1124
|
ext.on("session_start", async (_event, ctx) => {
|
|
996
1125
|
const config = loadLogicTraceConfig(ctx.cwd);
|
|
997
1126
|
if (config.passive)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/video-generation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,iCAAiC,CAAC;AA+NrG,wBAA8B,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAsGvF"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { generateVideos, getVideoModel, getVideoModels } from "../../sdk/ai/index.js";
|
|
5
|
+
import { base64FromImageData } from "../../sdk/ai/utils/image-source.js";
|
|
6
|
+
import { uploadImage } from "../../backend/generated-images.js";
|
|
7
|
+
// Backend routing (backendUrl + machineJwt) is resolved the same way the
|
|
8
|
+
// image tool gets it, so video generation routes through the backend proxy
|
|
9
|
+
// (POST /video/generation) using the backend's own OPENROUTER_API_KEY and
|
|
10
|
+
// falls back to direct OpenRouter when the proxy is unavailable or the CLI
|
|
11
|
+
// is not logged in.
|
|
12
|
+
import { resolveBackendRouting } from "../../backend/machine-routing.js";
|
|
13
|
+
const frameImageSchema = Type.Union([
|
|
14
|
+
Type.String({
|
|
15
|
+
description: "Path to a frame image file (relative to the project working directory or absolute).",
|
|
16
|
+
}),
|
|
17
|
+
Type.Object({
|
|
18
|
+
data: Type.String({ description: "Frame image as raw base64 or data URL." }),
|
|
19
|
+
mimeType: Type.Optional(Type.String({ default: "image/png", description: "Frame image MIME type." })),
|
|
20
|
+
}, { description: "Frame image provided inline as base64 data." }),
|
|
21
|
+
Type.Object({
|
|
22
|
+
path: Type.String({ description: "Path to a frame image file (relative to the project working directory or absolute)." }),
|
|
23
|
+
mimeType: Type.Optional(Type.String({ description: "Frame image MIME type. Defaults to the type detected from the file extension." })),
|
|
24
|
+
}, { description: "Frame image provided as a file path." }),
|
|
25
|
+
]);
|
|
26
|
+
const videoGenerateSchema = Type.Object({
|
|
27
|
+
prompt: Type.String({ description: "Text prompt describing the video to generate." }),
|
|
28
|
+
model: Type.Optional(Type.String({ default: "google/veo-3.1", description: "OpenRouter video model id." })),
|
|
29
|
+
duration: Type.Optional(Type.Number({ description: "Clip duration in seconds (model-dependent, e.g. 4-10 for Veo)." })),
|
|
30
|
+
resolution: Type.Optional(Type.String({ description: "Resolution tier, e.g. 480p, 720p, 768p, 1080p." })),
|
|
31
|
+
aspect_ratio: Type.Optional(Type.String({ description: "Aspect ratio, e.g. 16:9, 9:16, 1:1." })),
|
|
32
|
+
size: Type.Optional(Type.String({ description: "OpenRouter size shorthand, e.g. 1280x720." })),
|
|
33
|
+
generate_audio: Type.Optional(Type.Boolean({ default: true, description: "Whether to generate audio with the video (model-dependent)." })),
|
|
34
|
+
seed: Type.Optional(Type.Number()),
|
|
35
|
+
frame_images: Type.Optional(Type.Array(frameImageSchema, { description: "Optional frame images for image-to-video generation. Each element may be a file path string, an object { data: <base64 or data URL>, mimeType?: string }, or an object { path: <file path>, mimeType?: string }." })),
|
|
36
|
+
});
|
|
37
|
+
function videoModel(modelId) {
|
|
38
|
+
const exact = getVideoModel("openrouter", modelId);
|
|
39
|
+
if (exact)
|
|
40
|
+
return exact;
|
|
41
|
+
return getVideoModels("openrouter").find((model) => model.id === modelId);
|
|
42
|
+
}
|
|
43
|
+
const GENERATED_VIDEOS_DIR = ".generated_videos";
|
|
44
|
+
const VIDEO_MIME_TYPE = "video/mp4";
|
|
45
|
+
function mimeTypeForPath(filePath) {
|
|
46
|
+
switch (extname(filePath).toLowerCase()) {
|
|
47
|
+
case ".png":
|
|
48
|
+
return "image/png";
|
|
49
|
+
case ".jpg":
|
|
50
|
+
case ".jpeg":
|
|
51
|
+
return "image/jpeg";
|
|
52
|
+
case ".webp":
|
|
53
|
+
return "image/webp";
|
|
54
|
+
case ".gif":
|
|
55
|
+
return "image/gif";
|
|
56
|
+
default:
|
|
57
|
+
return "image/png";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readFrameImageFromPath(filePath, mimeType) {
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = readFileSync(filePath).toString("base64");
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const code = error?.code;
|
|
67
|
+
if (code === "ENOENT") {
|
|
68
|
+
throw new Error(`Frame image file does not exist: ${filePath}`);
|
|
69
|
+
}
|
|
70
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
71
|
+
throw new Error(`Permission denied while reading frame image: ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
74
|
+
throw new Error(`Failed to read frame image: ${filePath} (${detail})`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: "image",
|
|
78
|
+
data,
|
|
79
|
+
mimeType: mimeType ?? mimeTypeForPath(filePath),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeFrameImages(frames, cwd) {
|
|
83
|
+
if (!frames)
|
|
84
|
+
return [];
|
|
85
|
+
return frames.map((frame, index) => {
|
|
86
|
+
if (typeof frame === "string") {
|
|
87
|
+
return readFrameImageFromPath(resolve(cwd, frame), undefined);
|
|
88
|
+
}
|
|
89
|
+
if (typeof frame !== "object" || frame === null) {
|
|
90
|
+
throw new Error(`Invalid frame image at index ${index}: expected a file path string or an object with "data" or "path".`);
|
|
91
|
+
}
|
|
92
|
+
const reference = frame;
|
|
93
|
+
if (typeof reference.data === "string" && typeof reference.path === "string") {
|
|
94
|
+
throw new Error(`Invalid frame image at index ${index}: provide only one of "data" (inline base64) or "path" (file path), not both.`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof reference.data === "string") {
|
|
97
|
+
return {
|
|
98
|
+
type: "image",
|
|
99
|
+
data: reference.data,
|
|
100
|
+
mimeType: reference.mimeType ?? "image/png",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (typeof reference.path === "string") {
|
|
104
|
+
return readFrameImageFromPath(resolve(cwd, reference.path), reference.mimeType);
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Invalid frame image at index ${index}: expected a file path string or an object with "data" or "path".`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function slugFromPrompt(prompt) {
|
|
110
|
+
const slug = prompt
|
|
111
|
+
.normalize("NFKD")
|
|
112
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
115
|
+
.replace(/^-+|-+$/g, "")
|
|
116
|
+
.slice(0, 40);
|
|
117
|
+
return slug || "video";
|
|
118
|
+
}
|
|
119
|
+
function timestamp() {
|
|
120
|
+
return new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Save a locally-downloaded video (direct OpenRouter path) under
|
|
124
|
+
* `.generated_videos/`. Proxy-path videos have no inline bytes here — the
|
|
125
|
+
* backend stores them and reports the hosted url instead.
|
|
126
|
+
*/
|
|
127
|
+
function saveGeneratedVideo(video, prompt, cwd) {
|
|
128
|
+
if (!video.data)
|
|
129
|
+
return undefined;
|
|
130
|
+
const dir = join(cwd, GENERATED_VIDEOS_DIR);
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
const filename = `${slugFromPrompt(prompt)}-${timestamp()}.mp4`;
|
|
133
|
+
const filepath = join(dir, filename);
|
|
134
|
+
writeFileSync(filepath, Buffer.from(base64FromImageData(video.data), "base64"));
|
|
135
|
+
return filepath;
|
|
136
|
+
}
|
|
137
|
+
function bytesFromVideoData(data) {
|
|
138
|
+
return new Uint8Array(Buffer.from(base64FromImageData(data), "base64"));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Make the generated video reachable through a backend URL so the session can
|
|
142
|
+
* carry a url instead of megabytes of base64. Only applies to videos that
|
|
143
|
+
* came back as inline bytes (direct OpenRouter fallback path) — proxy-path
|
|
144
|
+
* videos are already stored server-side. Best effort by design: an upload
|
|
145
|
+
* failure must not fail video generation; the caller falls back to the local
|
|
146
|
+
* file + inline bytes and annotates the result text.
|
|
147
|
+
*/
|
|
148
|
+
async function hostGeneratedVideo(video, routing, signal) {
|
|
149
|
+
if (video.url && video.videoId)
|
|
150
|
+
return { hosted: { url: video.url, videoId: video.videoId } };
|
|
151
|
+
if (!routing)
|
|
152
|
+
return { error: "not routed through the backend (video kept local)" };
|
|
153
|
+
if (!video.data)
|
|
154
|
+
return { error: "video has no inline bytes (kept local)" };
|
|
155
|
+
try {
|
|
156
|
+
const result = await uploadImage({
|
|
157
|
+
backendUrl: routing.backendUrl,
|
|
158
|
+
token: routing.machineJwt,
|
|
159
|
+
bytes: bytesFromVideoData(video.data),
|
|
160
|
+
contentType: video.mimeType || VIDEO_MIME_TYPE,
|
|
161
|
+
kind: "video",
|
|
162
|
+
signal,
|
|
163
|
+
});
|
|
164
|
+
return { result };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function isHosted(outcome) {
|
|
171
|
+
return Boolean(outcome.hosted ?? outcome.result);
|
|
172
|
+
}
|
|
173
|
+
function hostedUrl(outcome) {
|
|
174
|
+
return outcome.hosted?.url ?? outcome.result.url;
|
|
175
|
+
}
|
|
176
|
+
function hostedId(outcome) {
|
|
177
|
+
return outcome.hosted?.videoId ?? outcome.result.imageId;
|
|
178
|
+
}
|
|
179
|
+
function resultText(model, credits, savedPath, outcome) {
|
|
180
|
+
const lines = ["✓ Generated 1 video", `Model: ${model}`];
|
|
181
|
+
lines.push(`Cost: ${credits ?? 0} credits`);
|
|
182
|
+
if (savedPath)
|
|
183
|
+
lines.push(`Saved: ${savedPath}`);
|
|
184
|
+
else if (!isHosted(outcome))
|
|
185
|
+
lines.push("Saved: (no local copy — see errors below)");
|
|
186
|
+
if (isHosted(outcome)) {
|
|
187
|
+
lines.push(`Hosted: ${hostedUrl(outcome)}`);
|
|
188
|
+
}
|
|
189
|
+
if (!isHosted(outcome) && outcome.error) {
|
|
190
|
+
// Explicit annotation: the video bytes were NOT uploaded, so the video
|
|
191
|
+
// exists only as the local file (path above); remote URL unavailable.
|
|
192
|
+
lines.push(`⚠️ The video could not be uploaded to cloud storage and exists only locally; remote URL unavailable.`);
|
|
193
|
+
lines.push(`- ${outcome.error}`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
|
197
|
+
export default async function videoGenerationExtension(ext) {
|
|
198
|
+
const tool = {
|
|
199
|
+
name: "video_generate",
|
|
200
|
+
label: "Generate Video",
|
|
201
|
+
description: "Generate videos through OpenRouter's Videos API and save them to .generated_videos/ in the project. " +
|
|
202
|
+
"Returns the saved file path and hosted URL so the user can view the generated video. " +
|
|
203
|
+
"Supports text-to-video and image-to-video via optional frame images. " +
|
|
204
|
+
"Models cannot read video — the result carries a text summary; the video block is for the user's UI.",
|
|
205
|
+
promptSnippet: "`video_generate { prompt, model?, duration?, aspect_ratio?, resolution?, frame_images? }` — generate video content (mp4, saved to .generated_videos/)",
|
|
206
|
+
parameters: videoGenerateSchema,
|
|
207
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
208
|
+
const input = params;
|
|
209
|
+
const modelId = input.model ?? "google/veo-3.1";
|
|
210
|
+
const model = videoModel(modelId);
|
|
211
|
+
if (!model) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `❌ Unknown OpenRouter video model: ${modelId}` }],
|
|
214
|
+
details: { isError: true, model: modelId },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const context = {
|
|
218
|
+
input: [
|
|
219
|
+
{ type: "text", text: input.prompt },
|
|
220
|
+
...normalizeFrameImages(input.frame_images, ctx.cwd),
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
// Route through the backend video proxy (POST /video/generation)
|
|
224
|
+
// using the machine JWT, mirroring image generation. Falls back to
|
|
225
|
+
// direct OpenRouter (submit + poll + download with the local
|
|
226
|
+
// OPENROUTER_API_KEY) when routing cannot be resolved or the proxy
|
|
227
|
+
// is unavailable.
|
|
228
|
+
const routing = await resolveBackendRouting();
|
|
229
|
+
const response = await generateVideos(model, context, {
|
|
230
|
+
...input,
|
|
231
|
+
signal,
|
|
232
|
+
...(routing ? { backendUrl: routing.backendUrl, apiKey: routing.machineJwt } : {}),
|
|
233
|
+
// Surface long-poll progress to the UI as partial tool results.
|
|
234
|
+
onProgress: (message) => {
|
|
235
|
+
try {
|
|
236
|
+
onUpdate?.({
|
|
237
|
+
content: [{ type: "text", text: message }],
|
|
238
|
+
details: { progress: message, model: model.id },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Progress callbacks must never break generation.
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
if (response.stopReason !== "stop") {
|
|
247
|
+
const message = response.errorMessage ?? "Video generation failed";
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: `❌ ${message}` }],
|
|
250
|
+
details: { isError: true, model: model.id, stopReason: response.stopReason, errorMessage: message },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const video = response.output.find((item) => item.type === "video");
|
|
254
|
+
if (!video) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text", text: "❌ Video generation returned no video" }],
|
|
257
|
+
details: { isError: true, model: model.id },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// Local copy first — for videos that actually came back as bytes
|
|
261
|
+
// (direct OpenRouter fallback). Proxy-path videos have no `data`
|
|
262
|
+
// here, so nothing is written and the hosted url is reported.
|
|
263
|
+
const savedPath = saveGeneratedVideo(video, input.prompt, ctx.cwd);
|
|
264
|
+
const outcome = await hostGeneratedVideo(video, routing, signal);
|
|
265
|
+
// Swap base64 for the hosted URL when possible: the session keeps
|
|
266
|
+
// `{ url, videoId }` and avoids megabytes of base64. Videos that
|
|
267
|
+
// failed to upload keep their inline bytes (fallback, annotated in
|
|
268
|
+
// the result text).
|
|
269
|
+
const block = isHosted(outcome)
|
|
270
|
+
? {
|
|
271
|
+
type: "video",
|
|
272
|
+
url: hostedUrl(outcome),
|
|
273
|
+
videoId: hostedId(outcome),
|
|
274
|
+
mimeType: video.mimeType,
|
|
275
|
+
}
|
|
276
|
+
: video;
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: resultText(response.model, response.usage?.credits, savedPath, outcome),
|
|
282
|
+
},
|
|
283
|
+
block,
|
|
284
|
+
],
|
|
285
|
+
details: {
|
|
286
|
+
isError: false,
|
|
287
|
+
model: response.model,
|
|
288
|
+
count: 1,
|
|
289
|
+
usage: response.usage,
|
|
290
|
+
savedPath,
|
|
291
|
+
videoId: isHosted(outcome) ? hostedId(outcome) : undefined,
|
|
292
|
+
url: isHosted(outcome) ? hostedUrl(outcome) : undefined,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
ext.registerTool(tool);
|
|
298
|
+
process.stderr.write("[video-generation] Registered video_generate tool.\n");
|
|
299
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../../src/generated/zeus/const.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../../../src/generated/zeus/const.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CAm0B5C,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAC,GAAG,CA4nC1C,CAAA;AAED,eAAO,MAAM,GAAG;;;;CAIf,CAAA"}
|