@jgengine/editor 0.10.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/dist/AssetBrowser.d.ts +20 -0
  3. package/dist/AssetBrowser.js +49 -0
  4. package/dist/CatalogsPanel.d.ts +10 -0
  5. package/dist/CatalogsPanel.js +41 -0
  6. package/dist/CollectionsPanel.d.ts +10 -0
  7. package/dist/CollectionsPanel.js +35 -0
  8. package/dist/DebugDraw.d.ts +17 -0
  9. package/dist/DebugDraw.js +233 -0
  10. package/dist/EditorApp.d.ts +26 -0
  11. package/dist/EditorApp.js +333 -0
  12. package/dist/EditorCameraDriver.d.ts +5 -0
  13. package/dist/EditorCameraDriver.js +18 -0
  14. package/dist/EditorChrome.d.ts +22 -0
  15. package/dist/EditorChrome.js +559 -0
  16. package/dist/EditorContextMenu.d.ts +11 -0
  17. package/dist/EditorContextMenu.js +12 -0
  18. package/dist/InspectorPanel.d.ts +14 -0
  19. package/dist/InspectorPanel.js +110 -0
  20. package/dist/MaterialDropZone.d.ts +10 -0
  21. package/dist/MaterialDropZone.js +77 -0
  22. package/dist/OutlinerPanel.d.ts +14 -0
  23. package/dist/OutlinerPanel.js +84 -0
  24. package/dist/PerfProbe.d.ts +5 -0
  25. package/dist/PerfProbe.js +34 -0
  26. package/dist/PrefabsPanel.d.ts +13 -0
  27. package/dist/PrefabsPanel.js +42 -0
  28. package/dist/RuntimePlayBridge.d.ts +18 -0
  29. package/dist/RuntimePlayBridge.js +140 -0
  30. package/dist/ScatterPreview.d.ts +11 -0
  31. package/dist/ScatterPreview.js +30 -0
  32. package/dist/SchemaInspector.d.ts +19 -0
  33. package/dist/SchemaInspector.js +78 -0
  34. package/dist/SelectionGizmo.d.ts +25 -0
  35. package/dist/SelectionGizmo.js +455 -0
  36. package/dist/StandaloneEditor.d.ts +44 -0
  37. package/dist/StandaloneEditor.js +92 -0
  38. package/dist/TerrainPanel.d.ts +7 -0
  39. package/dist/TerrainPanel.js +59 -0
  40. package/dist/TerrainSculpt.d.ts +14 -0
  41. package/dist/TerrainSculpt.js +415 -0
  42. package/dist/TriggerInspector.d.ts +13 -0
  43. package/dist/TriggerInspector.js +60 -0
  44. package/dist/agent/AgentPanel.d.ts +13 -0
  45. package/dist/agent/AgentPanel.js +156 -0
  46. package/dist/agent/context.d.ts +27 -0
  47. package/dist/agent/context.js +39 -0
  48. package/dist/agent/endpoint.d.ts +68 -0
  49. package/dist/agent/endpoint.js +186 -0
  50. package/dist/agent/toolBridge.d.ts +35 -0
  51. package/dist/agent/toolBridge.js +93 -0
  52. package/dist/agent/turn.d.ts +71 -0
  53. package/dist/agent/turn.js +113 -0
  54. package/dist/chromeFields.d.ts +15 -0
  55. package/dist/chromeFields.js +13 -0
  56. package/dist/chromeStyles.d.ts +3 -0
  57. package/dist/chromeStyles.js +3 -0
  58. package/dist/index.d.ts +20 -0
  59. package/dist/index.js +19 -0
  60. package/dist/mcp/bridgeServer.d.ts +13 -0
  61. package/dist/mcp/bridgeServer.js +1 -0
  62. package/dist/mcp/loadGameCatalogs.d.ts +24 -0
  63. package/dist/mcp/loadGameCatalogs.js +87 -0
  64. package/dist/mcp/rpcRequest.d.ts +22 -0
  65. package/dist/mcp/rpcRequest.js +97 -0
  66. package/dist/mcp/tools.d.ts +8 -0
  67. package/dist/mcp/tools.js +764 -0
  68. package/dist/outlinerModel.d.ts +76 -0
  69. package/dist/outlinerModel.js +95 -0
  70. package/dist/perfMarks.d.ts +25 -0
  71. package/dist/perfMarks.js +36 -0
  72. package/dist/session.d.ts +400 -0
  73. package/dist/session.js +1032 -0
  74. package/dist/uiStore.d.ts +108 -0
  75. package/dist/uiStore.js +128 -0
  76. package/dist/useF2Chord.d.ts +2 -0
  77. package/dist/useF2Chord.js +32 -0
  78. package/dist/useStoreSelector.d.ts +31 -0
  79. package/dist/useStoreSelector.js +51 -0
  80. package/dist/viewportContextMenu.d.ts +24 -0
  81. package/dist/viewportContextMenu.js +32 -0
  82. package/package.json +58 -0
  83. package/skills/jgengine/SKILL.md +823 -0
  84. package/skills/jgengine/api.md +416 -0
  85. package/skills/jgengine/capabilities.md +21 -0
  86. package/skills/jgengine/reference-cartridge.md +72 -0
  87. package/skills/jgengine/reference-orphans.md +9 -0
  88. package/skills/jgengine/reference-systems.md +60 -0
  89. package/skills/jgengine-editor/SKILL.md +382 -0
  90. package/skills/jgengine-editor/api.md +416 -0
  91. package/skills/jgengine-editor/capabilities.md +29 -0
  92. package/skills/jgengine-editor/reference.md +64 -0
@@ -0,0 +1,156 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { BTN, INPUT, MICRO } from "../chromeStyles.js";
4
+ import { createDefaultAgentEndpoint, createHttpAgentEndpoint, createLocalAgentEndpoint, EDITOR_AGENT_KEY_ENV, EDITOR_AGENT_URL_ENV, resolveAgentEndpointConfig, } from "./endpoint.js";
5
+ import { packAgentContext } from "./context.js";
6
+ import { runAgentTurn, undoAgentPatch, } from "./turn.js";
7
+ const STORAGE_URL = "jgengine.editorAgentUrl";
8
+ const STORAGE_KEY = "jgengine.editorAgentKey";
9
+ function readStoredConfig() {
10
+ if (typeof localStorage === "undefined")
11
+ return { url: "", apiKey: "" };
12
+ return {
13
+ url: localStorage.getItem(STORAGE_URL) ?? "",
14
+ apiKey: localStorage.getItem(STORAGE_KEY) ?? "",
15
+ };
16
+ }
17
+ function buildEndpoint(url, apiKey) {
18
+ const env = resolveAgentEndpointConfig();
19
+ const resolvedUrl = url.trim() || env.url || "";
20
+ const resolvedKey = apiKey.trim() || env.apiKey || "";
21
+ if (resolvedUrl.length > 0) {
22
+ return createHttpAgentEndpoint({ url: resolvedUrl, apiKey: resolvedKey || undefined });
23
+ }
24
+ return createLocalAgentEndpoint();
25
+ }
26
+ /**
27
+ * Dockable in-editor agent chat: humans and agents share the same RPC/undo surface.
28
+ * Tool calls route through `EditorHostApi.handle`; patches appear in the transcript and can be undone.
29
+ * @internal — mounted by `EditorChrome`.
30
+ */
31
+ export function AgentPanel({ api, onClose, }) {
32
+ const stored = useMemo(() => readStoredConfig(), []);
33
+ const [url, setUrl] = useState(stored.url);
34
+ const [apiKey, setApiKey] = useState(stored.apiKey);
35
+ const [showConfig, setShowConfig] = useState(false);
36
+ const [input, setInput] = useState("");
37
+ const [busy, setBusy] = useState(false);
38
+ const [history, setHistory] = useState([]);
39
+ const [transcript, setTranscript] = useState([]);
40
+ const [patches, setPatches] = useState([]);
41
+ const [contextTick, setContextTick] = useState(0);
42
+ const listRef = useRef(null);
43
+ const endpoint = useMemo(() => buildEndpoint(url, apiKey), [url, apiKey]);
44
+ const context = useMemo(() => packAgentContext(api), [api, contextTick]);
45
+ useEffect(() => {
46
+ const unsubSession = api.getSession().subscribe(() => setContextTick((v) => v + 1));
47
+ const unsubMode = api.subscribeMode(() => setContextTick((v) => v + 1));
48
+ const unsubFocus = api.subscribeFocus(() => setContextTick((v) => v + 1));
49
+ return () => {
50
+ unsubSession();
51
+ unsubMode();
52
+ unsubFocus();
53
+ };
54
+ }, [api]);
55
+ useEffect(() => {
56
+ const el = listRef.current;
57
+ if (el !== null)
58
+ el.scrollTop = el.scrollHeight;
59
+ }, [transcript, busy]);
60
+ const persistConfig = useCallback(() => {
61
+ if (typeof localStorage === "undefined")
62
+ return;
63
+ if (url.trim().length > 0)
64
+ localStorage.setItem(STORAGE_URL, url.trim());
65
+ else
66
+ localStorage.removeItem(STORAGE_URL);
67
+ if (apiKey.trim().length > 0)
68
+ localStorage.setItem(STORAGE_KEY, apiKey.trim());
69
+ else
70
+ localStorage.removeItem(STORAGE_KEY);
71
+ }, [url, apiKey]);
72
+ const send = useCallback(async () => {
73
+ const text = input.trim();
74
+ if (text.length === 0 || busy)
75
+ return;
76
+ setInput("");
77
+ setBusy(true);
78
+ try {
79
+ const turn = await runAgentTurn({
80
+ api,
81
+ endpoint,
82
+ history,
83
+ userMessage: text,
84
+ });
85
+ setHistory(turn.messages);
86
+ setTranscript((prev) => [...prev, ...turn.transcript]);
87
+ setPatches((prev) => [...prev, ...turn.patches]);
88
+ setContextTick((v) => v + 1);
89
+ }
90
+ catch (error) {
91
+ const content = error instanceof Error ? error.message : String(error);
92
+ setTranscript((prev) => [
93
+ ...prev,
94
+ { kind: "user", id: `user_${Date.now()}`, content: text, at: Date.now() },
95
+ { kind: "error", id: `err_${Date.now()}`, content, at: Date.now() },
96
+ ]);
97
+ }
98
+ finally {
99
+ setBusy(false);
100
+ }
101
+ }, [api, busy, endpoint, history, input]);
102
+ const onUndoPatch = useCallback((patchId) => {
103
+ const result = undoAgentPatch(api, patches, patchId);
104
+ if (!result.ok)
105
+ return;
106
+ setPatches(result.patches);
107
+ setTranscript((prev) => prev.map((entry) => entry.kind === "patch" && entry.patch.id === patchId
108
+ ? { ...entry, patch: { ...entry.patch, undone: true } }
109
+ : entry));
110
+ setContextTick((v) => v + 1);
111
+ }, [api, patches]);
112
+ const envHint = resolveAgentEndpointConfig();
113
+ return (_jsxs("aside", { className: "pointer-events-auto flex w-80 min-w-64 max-w-[48vw] resize-x flex-col overflow-hidden border-l border-white/[0.08] bg-[#0d0f13]/95 backdrop-blur-md", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-white/[0.08] px-2 py-2", children: [_jsx("div", { className: MICRO, children: "Agent" }), _jsx("span", { className: `rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider ${endpoint.id === "http"
114
+ ? "bg-violet-500/20 text-violet-200 ring-1 ring-inset ring-violet-400/30"
115
+ : "bg-white/[0.06] text-neutral-400 ring-1 ring-inset ring-white/10"}`, title: endpoint.id === "http" ? "Remote endpoint" : "Local offline agent", children: endpoint.id }), _jsx("button", { type: "button", className: `${BTN} ml-auto text-[10px]`, onClick: () => setShowConfig((v) => !v), children: "Config" }), _jsx("button", { type: "button", className: "rounded-md px-2 py-1 text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-200", onClick: onClose, "aria-label": "Close agent panel", children: "\u00D7" })] }), showConfig ? (_jsxs("div", { className: "space-y-2 border-b border-white/[0.08] p-2", children: [_jsx("div", { className: `${MICRO} text-neutral-500`, children: "Endpoint" }), _jsx("input", { className: `w-full text-[11px] ${INPUT}`, placeholder: `${EDITOR_AGENT_URL_ENV} or https://…`, value: url, onChange: (event) => setUrl(event.target.value), onBlur: persistConfig }), _jsx("input", { className: `w-full text-[11px] ${INPUT}`, type: "password", placeholder: `${EDITOR_AGENT_KEY_ENV} / ANTHROPIC_API_KEY`, value: apiKey, onChange: (event) => setApiKey(event.target.value), onBlur: persistConfig }), _jsxs("p", { className: "text-[10px] leading-snug text-neutral-500", children: ["Env: ", EDITOR_AGENT_URL_ENV, envHint.url !== undefined ? " ✓" : " (unset)", " \u00B7 ", EDITOR_AGENT_KEY_ENV, envHint.apiKey !== undefined ? " ✓" : " (unset)", ". Empty URL uses the local command agent (/help). Remote: POST JSON ", "{messages,context,tools}", " \u2192 ", "{message?,toolCalls?}", "."] }), _jsx("button", { type: "button", className: BTN, onClick: () => {
116
+ setUrl("");
117
+ setApiKey("");
118
+ if (typeof localStorage !== "undefined") {
119
+ localStorage.removeItem(STORAGE_URL);
120
+ localStorage.removeItem(STORAGE_KEY);
121
+ }
122
+ }, children: "Use local agent" })] })) : null, _jsxs("div", { className: "border-b border-white/[0.08] px-2 py-1.5 text-[10px] text-neutral-500", children: [_jsx("span", { className: "text-neutral-400", children: context.mode }), _jsx("span", { className: "mx-1.5 text-neutral-700", children: "\u00B7" }), _jsxs("span", { children: ["sel ", context.selection.length === 0 ? "—" : context.selection.slice(0, 3).join(", "), context.selection.length > 3 ? ` +${context.selection.length - 3}` : ""] }), _jsx("span", { className: "mx-1.5 text-neutral-700", children: "\u00B7" }), _jsxs("span", { children: [context.summary.markers, "m ", context.summary.volumes, "v ", context.summary.paths, "p"] })] }), _jsxs("div", { ref: listRef, className: "min-h-0 flex-1 space-y-2 overflow-auto p-2", children: [transcript.length === 0 ? (_jsxs("div", { className: "rounded-lg border border-dashed border-white/10 bg-black/20 p-3 text-[11px] leading-relaxed text-neutral-500", children: ["Chat drives the live scene through the same RPC verbs as the MCP bridge. Edits share undo with the toolbar. Local: try ", _jsx("code", { className: "text-cyan-300", children: "/status" }), ",", " ", _jsx("code", { className: "text-cyan-300", children: "/help" }), ", or", " ", _jsx("code", { className: "text-cyan-300", children: "move id x y z" }), "."] })) : null, transcript.map((entry) => (_jsx(TranscriptRow, { entry: entry, patches: patches, onUndo: onUndoPatch }, entry.id))), busy ? _jsx("div", { className: "text-[10px] text-violet-300", children: "Thinking\u2026" }) : null] }), patches.some((p) => !p.undone) ? (_jsxs("div", { className: "max-h-28 overflow-auto border-t border-white/[0.08] p-2", children: [_jsx("div", { className: `${MICRO} mb-1`, children: "Agent patches" }), _jsx("ul", { className: "space-y-1", children: patches
123
+ .filter((p) => !p.undone)
124
+ .map((patch, index, live) => {
125
+ const isTop = index === live.length - 1;
126
+ return (_jsxs("li", { className: "flex items-center gap-1 text-[10px] text-neutral-300", children: [_jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-cyan-200/90", children: patch.summary }), _jsx("button", { type: "button", className: `${BTN} shrink-0 px-1.5 py-0.5 text-[10px] disabled:opacity-40`, disabled: !isTop, title: isTop ? "Undo this edit" : "Undo newer patches first", onClick: () => onUndoPatch(patch.id), children: "Undo" })] }, patch.id));
127
+ }) })] })) : null, _jsxs("div", { className: "flex gap-1 border-t border-white/[0.08] p-2", children: [_jsx("input", { className: `min-w-0 flex-1 text-[12px] ${INPUT}`, placeholder: endpoint.id === "local" ? "/help · /status · move id x y z" : "Ask the agent to edit the scene…", value: input, disabled: busy, onChange: (event) => setInput(event.target.value), onKeyDown: (event) => {
128
+ if (event.key === "Enter" && !event.shiftKey) {
129
+ event.preventDefault();
130
+ void send();
131
+ }
132
+ } }), _jsx("button", { type: "button", className: `${BTN} shrink-0 disabled:opacity-40`, disabled: busy || input.trim().length === 0, onClick: () => void send(), children: "Send" })] })] }));
133
+ }
134
+ function TranscriptRow({ entry, patches, onUndo, }) {
135
+ if (entry.kind === "user") {
136
+ return (_jsx("div", { className: "rounded-lg bg-cyan-500/10 px-2.5 py-1.5 text-[11px] text-cyan-50 ring-1 ring-inset ring-cyan-400/20", children: entry.content }));
137
+ }
138
+ if (entry.kind === "assistant") {
139
+ return (_jsx("div", { className: "rounded-lg bg-white/[0.04] px-2.5 py-1.5 text-[11px] whitespace-pre-wrap text-neutral-200 ring-1 ring-inset ring-white/[0.06]", children: entry.content }));
140
+ }
141
+ if (entry.kind === "error") {
142
+ return (_jsx("div", { className: "rounded-lg bg-rose-500/10 px-2.5 py-1.5 text-[11px] text-rose-100 ring-1 ring-inset ring-rose-400/25", children: entry.content }));
143
+ }
144
+ if (entry.kind === "tool") {
145
+ const tone = entry.result.ok
146
+ ? entry.result.mutated
147
+ ? "text-emerald-200"
148
+ : "text-neutral-400"
149
+ : "text-rose-200";
150
+ return (_jsxs("div", { className: `font-mono text-[10px] ${tone}`, children: ["\u21B3 ", entry.result.patchSummary, !entry.result.ok && entry.result.error !== undefined ? ` — ${entry.result.error}` : ""] }));
151
+ }
152
+ const patch = patches.find((p) => p.id === entry.patch.id) ?? entry.patch;
153
+ return (_jsxs("div", { className: "flex items-center gap-1 rounded-md border border-violet-400/20 bg-violet-500/[0.08] px-2 py-1 text-[10px]", children: [_jsxs("span", { className: `min-w-0 flex-1 truncate font-mono ${patch.undone ? "text-neutral-600 line-through" : "text-violet-100"}`, children: ["patch \u00B7 ", patch.summary] }), !patch.undone ? (_jsx("button", { type: "button", className: `${BTN} px-1.5 py-0.5 text-[10px]`, onClick: () => onUndo(patch.id), children: "Undo" })) : (_jsx("span", { className: "text-neutral-600", children: "undone" }))] }));
154
+ }
155
+ /** Re-export for hosts that want a default endpoint without the panel. */
156
+ export { createDefaultAgentEndpoint };
@@ -0,0 +1,27 @@
1
+ import type { EditorHostApi, EditorRunMode } from "../session.js";
2
+ /** Compact editor state injected into every agent turn so the model sees selection/mode/camera. */
3
+ export interface AgentEditorContext {
4
+ gameId: string;
5
+ mode: EditorRunMode;
6
+ selection: string[];
7
+ focus: {
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ } | null;
12
+ canUndo: boolean;
13
+ canRedo: boolean;
14
+ summary: {
15
+ markers: number;
16
+ volumes: number;
17
+ paths: number;
18
+ annotations: number;
19
+ };
20
+ }
21
+ /**
22
+ * Packs the live host's selection, mode, focus, and document counts for agent prompts.
23
+ * Injected into every embedded-panel turn so the agent shares the human's current view.
24
+ */
25
+ export declare function packAgentContext(api: EditorHostApi): AgentEditorContext;
26
+ /** @internal Stable, prompt-friendly serialization of {@link AgentEditorContext}. */
27
+ export declare function formatAgentContext(context: AgentEditorContext): string;
@@ -0,0 +1,39 @@
1
+ import { summarizeEditorSession } from "@jgengine/core/editor/index";
2
+ /**
3
+ * Packs the live host's selection, mode, focus, and document counts for agent prompts.
4
+ * Injected into every embedded-panel turn so the agent shares the human's current view.
5
+ */
6
+ export function packAgentContext(api) {
7
+ const session = api.getSession();
8
+ const state = session.getState();
9
+ const summary = summarizeEditorSession(state);
10
+ return {
11
+ gameId: api.gameId,
12
+ mode: api.getMode(),
13
+ selection: [...state.selection],
14
+ focus: api.getFocusTarget(),
15
+ canUndo: session.canUndo(),
16
+ canRedo: session.canRedo(),
17
+ summary: {
18
+ markers: summary.markers,
19
+ volumes: summary.volumes,
20
+ paths: summary.paths,
21
+ annotations: summary.annotations,
22
+ },
23
+ };
24
+ }
25
+ /** @internal Stable, prompt-friendly serialization of {@link AgentEditorContext}. */
26
+ export function formatAgentContext(context) {
27
+ const focus = context.focus === null
28
+ ? "none"
29
+ : `(${context.focus.x.toFixed(1)}, ${context.focus.y.toFixed(1)}, ${context.focus.z.toFixed(1)})`;
30
+ const selection = context.selection.length === 0 ? "(none)" : context.selection.join(", ");
31
+ return [
32
+ `gameId=${context.gameId}`,
33
+ `mode=${context.mode}`,
34
+ `selection=[${selection}]`,
35
+ `focus=${focus}`,
36
+ `counts=markers:${context.summary.markers} volumes:${context.summary.volumes} paths:${context.summary.paths} notes:${context.summary.annotations}`,
37
+ `history=undo:${context.canUndo ? "yes" : "no"} redo:${context.canRedo ? "yes" : "no"}`,
38
+ ].join("\n");
39
+ }
@@ -0,0 +1,68 @@
1
+ import type { EditorMcpTool } from "../mcp/tools.js";
2
+ import type { AgentEditorContext } from "./context.js";
3
+ import type { AgentToolCall } from "./toolBridge.js";
4
+ /** One message in an agent chat transcript (user, assistant, or tool result). */
5
+ export type AgentChatRole = "user" | "assistant" | "tool" | "system";
6
+ /** One turn message exchanged with a pluggable agent endpoint (user, assistant, tool, or system). */
7
+ export interface AgentChatMessage {
8
+ role: AgentChatRole;
9
+ content: string;
10
+ /** Present on tool-result messages. */
11
+ toolCallId?: string;
12
+ name?: string;
13
+ }
14
+ /** Payload sent to a pluggable agent backend for one model turn. */
15
+ export interface AgentChatRequest {
16
+ messages: readonly AgentChatMessage[];
17
+ context: AgentEditorContext;
18
+ tools: readonly EditorMcpTool[];
19
+ }
20
+ /** Model reply: optional prose plus zero or more tool calls to run via editor RPC. */
21
+ export interface AgentChatResponse {
22
+ message?: string;
23
+ toolCalls?: AgentToolCall[];
24
+ }
25
+ /** Pluggable agent backend — HTTP, Claude SDK adapter, or a local rule engine. */
26
+ export interface AgentEndpoint {
27
+ /** Stable id for UI status (e.g. "http", "local"). */
28
+ id: string;
29
+ chat(request: AgentChatRequest): Promise<AgentChatResponse>;
30
+ }
31
+ /** Env var name for the remote agent HTTP URL (`JGENGINE_EDITOR_AGENT_URL`). */
32
+ export declare const EDITOR_AGENT_URL_ENV = "JGENGINE_EDITOR_AGENT_URL";
33
+ /** Env var name for the optional Bearer token (`JGENGINE_EDITOR_AGENT_KEY`). */
34
+ export declare const EDITOR_AGENT_KEY_ENV = "JGENGINE_EDITOR_AGENT_KEY";
35
+ /** Fallback API-key env when `JGENGINE_EDITOR_AGENT_KEY` is unset (`ANTHROPIC_API_KEY`). */
36
+ export declare const EDITOR_AGENT_KEY_FALLBACK_ENV = "ANTHROPIC_API_KEY";
37
+ /** Resolved remote agent settings: optional URL and optional Bearer API key. */
38
+ export interface AgentEndpointConfig {
39
+ url?: string;
40
+ apiKey?: string;
41
+ }
42
+ /**
43
+ * Reads agent endpoint config from an env map.
44
+ * Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`).
45
+ * Empty URL → local offline agent; set the URL for a remote model/tool-call backend.
46
+ */
47
+ export declare function resolveAgentEndpointConfig(env?: Record<string, string | undefined>): AgentEndpointConfig;
48
+ /**
49
+ * HTTP POST agent endpoint: `{ messages, context, tools }` → `{ message?, toolCalls? }`.
50
+ * Bearer auth from `apiKey` when provided (`JGENGINE_EDITOR_AGENT_KEY` / `ANTHROPIC_API_KEY`).
51
+ */
52
+ export declare function createHttpAgentEndpoint(config: {
53
+ url: string;
54
+ apiKey?: string;
55
+ fetchImpl?: typeof fetch;
56
+ }): AgentEndpoint;
57
+ /**
58
+ * Offline rule-based agent for demos and tests when no remote URL is configured.
59
+ * Understands slash-style commands that map onto editor RPC verbs.
60
+ * @internal
61
+ */
62
+ export declare function createLocalAgentEndpoint(): AgentEndpoint;
63
+ /** @internal Parses a local agent command into message + optional tool calls. */
64
+ export declare function interpretLocalCommand(text: string, context: AgentEditorContext): AgentChatResponse;
65
+ /**
66
+ * Picks HTTP endpoint when `JGENGINE_EDITOR_AGENT_URL` (or config.url) is set, otherwise the offline local agent.
67
+ */
68
+ export declare function createDefaultAgentEndpoint(config?: AgentEndpointConfig): AgentEndpoint;
@@ -0,0 +1,186 @@
1
+ /** Env var name for the remote agent HTTP URL (`JGENGINE_EDITOR_AGENT_URL`). */
2
+ export const EDITOR_AGENT_URL_ENV = "JGENGINE_EDITOR_AGENT_URL";
3
+ /** Env var name for the optional Bearer token (`JGENGINE_EDITOR_AGENT_KEY`). */
4
+ export const EDITOR_AGENT_KEY_ENV = "JGENGINE_EDITOR_AGENT_KEY";
5
+ /** Fallback API-key env when `JGENGINE_EDITOR_AGENT_KEY` is unset (`ANTHROPIC_API_KEY`). */
6
+ export const EDITOR_AGENT_KEY_FALLBACK_ENV = "ANTHROPIC_API_KEY";
7
+ function readProcessEnv() {
8
+ const root = globalThis;
9
+ return root.process?.env ?? {};
10
+ }
11
+ /**
12
+ * Reads agent endpoint config from an env map.
13
+ * Prefer `JGENGINE_EDITOR_AGENT_URL` + `JGENGINE_EDITOR_AGENT_KEY` (falls back to `ANTHROPIC_API_KEY`).
14
+ * Empty URL → local offline agent; set the URL for a remote model/tool-call backend.
15
+ */
16
+ export function resolveAgentEndpointConfig(env = readProcessEnv()) {
17
+ const url = env[EDITOR_AGENT_URL_ENV]?.trim() || undefined;
18
+ const apiKey = env[EDITOR_AGENT_KEY_ENV]?.trim() || env[EDITOR_AGENT_KEY_FALLBACK_ENV]?.trim() || undefined;
19
+ return { url, apiKey };
20
+ }
21
+ /**
22
+ * HTTP POST agent endpoint: `{ messages, context, tools }` → `{ message?, toolCalls? }`.
23
+ * Bearer auth from `apiKey` when provided (`JGENGINE_EDITOR_AGENT_KEY` / `ANTHROPIC_API_KEY`).
24
+ */
25
+ export function createHttpAgentEndpoint(config) {
26
+ const fetchImpl = config.fetchImpl ?? fetch;
27
+ return {
28
+ id: "http",
29
+ async chat(request) {
30
+ const headers = { "content-type": "application/json" };
31
+ if (config.apiKey !== undefined && config.apiKey.length > 0) {
32
+ headers.authorization = `Bearer ${config.apiKey}`;
33
+ }
34
+ const response = await fetchImpl(config.url, {
35
+ method: "POST",
36
+ headers,
37
+ body: JSON.stringify(request),
38
+ });
39
+ if (!response.ok) {
40
+ const body = await response.text().catch(() => "");
41
+ throw new Error(`agent endpoint HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
42
+ }
43
+ const raw = (await response.json());
44
+ return normalizeChatResponse(raw);
45
+ },
46
+ };
47
+ }
48
+ function normalizeChatResponse(raw) {
49
+ const toolCalls = Array.isArray(raw.toolCalls)
50
+ ? raw.toolCalls
51
+ .filter((call) => call !== null && typeof call === "object")
52
+ .map((call, index) => {
53
+ const record = call;
54
+ const name = typeof record.name === "string" ? record.name : "";
55
+ const id = typeof record.id === "string" && record.id.length > 0 ? record.id : `call_${index}`;
56
+ const args = record.arguments !== null && typeof record.arguments === "object" && !Array.isArray(record.arguments)
57
+ ? record.arguments
58
+ : {};
59
+ return { id, name, arguments: args };
60
+ })
61
+ .filter((call) => call.name.length > 0)
62
+ : undefined;
63
+ return {
64
+ ...(typeof raw.message === "string" ? { message: raw.message } : {}),
65
+ ...(toolCalls !== undefined && toolCalls.length > 0 ? { toolCalls } : {}),
66
+ };
67
+ }
68
+ /**
69
+ * Offline rule-based agent for demos and tests when no remote URL is configured.
70
+ * Understands slash-style commands that map onto editor RPC verbs.
71
+ * @internal
72
+ */
73
+ export function createLocalAgentEndpoint() {
74
+ return {
75
+ id: "local",
76
+ async chat(request) {
77
+ const lastUser = [...request.messages].reverse().find((m) => m.role === "user");
78
+ const text = lastUser?.content.trim() ?? "";
79
+ if (text.length === 0) {
80
+ return { message: "Send a command (e.g. /status, /select id, /undo, or free text)." };
81
+ }
82
+ return interpretLocalCommand(text, request.context);
83
+ },
84
+ };
85
+ }
86
+ /** @internal Parses a local agent command into message + optional tool calls. */
87
+ export function interpretLocalCommand(text, context) {
88
+ const trimmed = text.trim();
89
+ const lower = trimmed.toLowerCase();
90
+ if (lower === "/help" || lower === "help") {
91
+ return {
92
+ message: [
93
+ "Local agent commands (tool calls → editor RPC, same undo as the GUI):",
94
+ "/status /summary /selection /frame /undo /redo",
95
+ "/select <id…> /clear",
96
+ "/goto <id> /move <id> <x> <y> <z>",
97
+ "Or configure JGENGINE_EDITOR_AGENT_URL for a remote model endpoint.",
98
+ ].join("\n"),
99
+ };
100
+ }
101
+ if (lower === "/status" || lower === "status") {
102
+ return {
103
+ message: `mode=${context.mode} selection=${context.selection.join(",") || "(none)"}`,
104
+ toolCalls: [{ id: "local_status", name: "editor_status", arguments: {} }],
105
+ };
106
+ }
107
+ if (lower === "/summary" || lower === "summary") {
108
+ return {
109
+ message: "Pulling scene summary…",
110
+ toolCalls: [{ id: "local_summary", name: "scene_summary", arguments: {} }],
111
+ };
112
+ }
113
+ if (lower === "/selection" || lower === "selection") {
114
+ return {
115
+ message: context.selection.length === 0 ? "Nothing selected." : `Selected: ${context.selection.join(", ")}`,
116
+ toolCalls: [{ id: "local_sel", name: "list_selection", arguments: {} }],
117
+ };
118
+ }
119
+ if (lower === "/frame" || lower === "frame") {
120
+ return {
121
+ message: "Framing scene…",
122
+ toolCalls: [{ id: "local_frame", name: "camera_frame", arguments: {} }],
123
+ };
124
+ }
125
+ if (lower === "/undo" || lower === "undo") {
126
+ return {
127
+ message: context.canUndo ? "Undoing last edit…" : "Nothing to undo.",
128
+ toolCalls: context.canUndo ? [{ id: "local_undo", name: "undo", arguments: {} }] : undefined,
129
+ };
130
+ }
131
+ if (lower === "/redo" || lower === "redo") {
132
+ return {
133
+ message: context.canRedo ? "Redoing…" : "Nothing to redo.",
134
+ toolCalls: context.canRedo ? [{ id: "local_redo", name: "redo", arguments: {} }] : undefined,
135
+ };
136
+ }
137
+ if (lower === "/clear" || lower === "clear selection") {
138
+ return {
139
+ message: "Clearing selection…",
140
+ toolCalls: [{ id: "local_clear", name: "clear_selection", arguments: {} }],
141
+ };
142
+ }
143
+ const selectMatch = trimmed.match(/^\/?select\s+(.+)$/i);
144
+ if (selectMatch !== null) {
145
+ const ids = selectMatch[1].trim().split(/[\s,]+/).filter((id) => id.length > 0);
146
+ return {
147
+ message: `Selecting ${ids.join(", ")}…`,
148
+ toolCalls: [{ id: "local_select", name: "select", arguments: { ids } }],
149
+ };
150
+ }
151
+ const gotoMatch = trimmed.match(/^\/?goto\s+(\S+)$/i);
152
+ if (gotoMatch !== null) {
153
+ const id = gotoMatch[1];
154
+ return {
155
+ message: `Focusing ${id}…`,
156
+ toolCalls: [{ id: "local_goto", name: "camera_goto", arguments: { id } }],
157
+ };
158
+ }
159
+ const moveMatch = trimmed.match(/^\/?move\s+(\S+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*$/i);
160
+ if (moveMatch !== null) {
161
+ const id = moveMatch[1];
162
+ const x = Number(moveMatch[2]);
163
+ const y = Number(moveMatch[3]);
164
+ const z = Number(moveMatch[4]);
165
+ return {
166
+ message: `Moving ${id} → (${x}, ${y}, ${z})…`,
167
+ toolCalls: [{ id: "local_move", name: "set_transform", arguments: { id, x, y, z } }],
168
+ };
169
+ }
170
+ return {
171
+ message: [
172
+ `No local handler for “${trimmed.slice(0, 80)}”.`,
173
+ "Try /help, or set JGENGINE_EDITOR_AGENT_URL (+ optional JGENGINE_EDITOR_AGENT_KEY) for a remote agent.",
174
+ `Context: mode=${context.mode}, selection=${context.selection.join(",") || "(none)"}.`,
175
+ ].join("\n"),
176
+ };
177
+ }
178
+ /**
179
+ * Picks HTTP endpoint when `JGENGINE_EDITOR_AGENT_URL` (or config.url) is set, otherwise the offline local agent.
180
+ */
181
+ export function createDefaultAgentEndpoint(config = resolveAgentEndpointConfig()) {
182
+ if (config.url !== undefined && config.url.length > 0) {
183
+ return createHttpAgentEndpoint({ url: config.url, apiKey: config.apiKey });
184
+ }
185
+ return createLocalAgentEndpoint();
186
+ }
@@ -0,0 +1,35 @@
1
+ import type { EditorBridgeRequest, EditorHostApi } from "../session.js";
2
+ /** One tool call from an agent turn — name maps 1:1 onto an editor RPC method. */
3
+ export interface AgentToolCall {
4
+ id: string;
5
+ name: string;
6
+ arguments: Record<string, unknown>;
7
+ }
8
+ /** Result of routing one tool call through the editor host. */
9
+ export interface AgentToolResult {
10
+ id: string;
11
+ name: string;
12
+ ok: boolean;
13
+ result?: unknown;
14
+ error?: string;
15
+ /** True when the document JSON changed — lands as one undo step the human can reverse. */
16
+ mutated: boolean;
17
+ /** Short human-readable summary for the patch transcript. */
18
+ patchSummary: string;
19
+ }
20
+ /** @internal Builds an editor RPC request from a tool name + args (tool name ≡ method). */
21
+ export declare function toolCallToRequest(call: AgentToolCall): {
22
+ ok: true;
23
+ request: EditorBridgeRequest;
24
+ } | {
25
+ ok: false;
26
+ error: string;
27
+ };
28
+ /**
29
+ * Routes one agent tool call through the same editor RPC surface humans use.
30
+ * Mutating calls share the session undo stack — no parallel history.
31
+ * Tool name maps 1:1 onto `EditorBridgeRequest.method` (same verbs as MCP/CLI).
32
+ */
33
+ export declare function routeToolCall(api: EditorHostApi, call: AgentToolCall): AgentToolResult;
34
+ /** @internal Routes every tool call in order; returns one result per call. */
35
+ export declare function routeToolCalls(api: EditorHostApi, calls: readonly AgentToolCall[]): AgentToolResult[];
@@ -0,0 +1,93 @@
1
+ import { decodeEditorBridgeRequest } from "../mcp/rpcRequest.js";
2
+ /** Methods that never write the document (no undo step). */
3
+ const READ_ONLY_METHODS = new Set([
4
+ "editor_status",
5
+ "list_layers",
6
+ "list_selection",
7
+ "get_marker",
8
+ "get_volume",
9
+ "camera_goto",
10
+ "camera_frame",
11
+ "scene_summary",
12
+ "export_document",
13
+ "list_assets",
14
+ "perf_report",
15
+ "terrain_summary",
16
+ "terrain_materials",
17
+ "terrain_layers",
18
+ "scatter_summary",
19
+ "hierarchy",
20
+ "list_prefabs",
21
+ "list_collections",
22
+ ]);
23
+ /** @internal Builds an editor RPC request from a tool name + args (tool name ≡ method). */
24
+ export function toolCallToRequest(call) {
25
+ const payload = { method: call.name, ...call.arguments };
26
+ const decoded = decodeEditorBridgeRequest(payload);
27
+ if (!decoded.ok) {
28
+ const detail = decoded.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
29
+ return { ok: false, error: detail };
30
+ }
31
+ return { ok: true, request: decoded.request };
32
+ }
33
+ function summarizeCall(name, args, response, mutated) {
34
+ if (!response.ok)
35
+ return `${name} failed: ${response.error ?? "unknown error"}`;
36
+ if (mutated) {
37
+ const id = typeof args.id === "string" ? args.id : undefined;
38
+ const ids = Array.isArray(args.ids) ? args.ids.join(",") : undefined;
39
+ const target = id ?? ids;
40
+ return target !== undefined && target.length > 0 ? `${name} · ${target}` : name;
41
+ }
42
+ return `${name} (read)`;
43
+ }
44
+ /**
45
+ * Routes one agent tool call through the same editor RPC surface humans use.
46
+ * Mutating calls share the session undo stack — no parallel history.
47
+ * Tool name maps 1:1 onto `EditorBridgeRequest.method` (same verbs as MCP/CLI).
48
+ */
49
+ export function routeToolCall(api, call) {
50
+ const built = toolCallToRequest(call);
51
+ if (!built.ok) {
52
+ return {
53
+ id: call.id,
54
+ name: call.name,
55
+ ok: false,
56
+ error: built.error,
57
+ mutated: false,
58
+ patchSummary: `${call.name} rejected: ${built.error}`,
59
+ };
60
+ }
61
+ const session = api.getSession();
62
+ const before = session.exportJson(false);
63
+ let response;
64
+ try {
65
+ response = api.handle(built.request);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ return {
70
+ id: call.id,
71
+ name: call.name,
72
+ ok: false,
73
+ error: message,
74
+ mutated: false,
75
+ patchSummary: `${call.name} threw: ${message}`,
76
+ };
77
+ }
78
+ const after = session.exportJson(false);
79
+ const mutated = !READ_ONLY_METHODS.has(call.name) && before !== after && response.ok === true;
80
+ return {
81
+ id: call.id,
82
+ name: call.name,
83
+ ok: response.ok,
84
+ result: response.result,
85
+ error: response.error,
86
+ mutated,
87
+ patchSummary: summarizeCall(call.name, call.arguments, response, mutated),
88
+ };
89
+ }
90
+ /** @internal Routes every tool call in order; returns one result per call. */
91
+ export function routeToolCalls(api, calls) {
92
+ return calls.map((call) => routeToolCall(api, call));
93
+ }