narraeon 0.1.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 (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +164 -0
  3. package/README.zh-CN.md +164 -0
  4. package/dist/node/cli/NarraeonCli.js +163 -0
  5. package/dist/node/cli/main.js +38 -0
  6. package/dist/node/protocol/appPreferences.js +5 -0
  7. package/dist/node/protocol/contentTree.js +14 -0
  8. package/dist/node/protocol/modelConnections.js +110 -0
  9. package/dist/node/protocol/playerViews.js +1 -0
  10. package/dist/node/protocol/v1.js +278 -0
  11. package/dist/node/runtime/V1Runtime.js +625 -0
  12. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
  13. package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
  14. package/dist/node/runtime/config/appPaths.js +19 -0
  15. package/dist/node/runtime/content/ContentLimits.js +1 -0
  16. package/dist/node/runtime/content/ContentTreeFile.js +1 -0
  17. package/dist/node/runtime/content/ContentWorkspace.js +601 -0
  18. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
  19. package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
  20. package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
  21. package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
  22. package/dist/node/runtime/model/AiFailureLog.js +332 -0
  23. package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
  24. package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
  25. package/dist/node/runtime/model/ModelHost.js +89 -0
  26. package/dist/node/runtime/model/ModelHostStream.js +367 -0
  27. package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
  28. package/dist/node/runtime/model/ProviderStream.js +90 -0
  29. package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
  30. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
  31. package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
  32. package/dist/node/runtime/play/PlayCallChain.js +1280 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
  36. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
  37. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
  38. package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
  39. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
  40. package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
  41. package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
  42. package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
  43. package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
  44. package/dist/node/runtime/world/WorldStateRevision.js +75 -0
  45. package/dist/node/server/createRuntime.js +6 -0
  46. package/dist/node/server/createServer.js +218 -0
  47. package/dist/node/server/main.js +25 -0
  48. package/dist/node/server/startWebServer.js +28 -0
  49. package/dist/node/shared/default-play-prompts.js +85 -0
  50. package/dist/node/shared/default-preset-host.js +538 -0
  51. package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
  52. package/dist/node/shared/first-party-action-choices.js +164 -0
  53. package/dist/node/shared/first-party-generic-panels.js +105 -0
  54. package/dist/node/shared/first-party-play-preset-templates.js +41 -0
  55. package/dist/node/shared/first-party-player-view.js +179 -0
  56. package/dist/node/shared/play-preset-prompt-roles.js +4 -0
  57. package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
  58. package/dist/web/assets/index-DDx1itzT.js +1192 -0
  59. package/dist/web/index.html +14 -0
  60. package/package.json +86 -0
@@ -0,0 +1,163 @@
1
+ import { createConnection } from "node:net";
2
+ import { fileURLToPath } from "node:url";
3
+ import { parseArgs } from "node:util";
4
+ import { resolveAppPaths } from "../runtime/config/appPaths.js";
5
+ import { parseWebPort, startWebServer, } from "../server/startWebServer.js";
6
+ export class NarraeonCliUsageError extends Error {
7
+ }
8
+ export async function runNarraeonCli(input) {
9
+ const output = input.writeOutput ?? ((message) => process.stdout.write(message));
10
+ const errorOutput = input.writeError ?? ((message) => process.stderr.write(message));
11
+ const command = parseCliCommand(input.args, input.environment);
12
+ if (command === null) {
13
+ output(cliHelp);
14
+ return { kind: "help" };
15
+ }
16
+ const url = `http://127.0.0.1:${command.port}`;
17
+ const inspect = input.inspectServer ?? inspectLocalWebServer;
18
+ const inspection = await inspect(url);
19
+ if (inspection === "narraeon") {
20
+ output(`Narraeon is already running: ${url}\n`);
21
+ await openBrowser(command, url, input.openUrl, errorOutput);
22
+ return { kind: "reused", url };
23
+ }
24
+ if (inspection === "occupied")
25
+ throw new Error(`Port ${String(command.port)} is already in use`);
26
+ const start = input.startServer ?? startWebServer;
27
+ let server;
28
+ try {
29
+ server = await start({
30
+ paths: resolveAppPaths(input.environment),
31
+ staticRoot: input.staticRoot,
32
+ port: command.port,
33
+ });
34
+ }
35
+ catch (error) {
36
+ if (hasErrorCode(error, "EADDRINUSE"))
37
+ throw new Error(`Port ${String(command.port)} is already in use`, {
38
+ cause: error,
39
+ });
40
+ throw error;
41
+ }
42
+ output(`Narraeon started: ${url}\n`);
43
+ await openBrowser(command, url, input.openUrl, errorOutput);
44
+ return { kind: "started", url, server };
45
+ }
46
+ export function resolvePackagedWebRoot(moduleUrl) {
47
+ return fileURLToPath(new URL("../../web/", moduleUrl));
48
+ }
49
+ export async function inspectLocalWebServer(url) {
50
+ let response;
51
+ try {
52
+ response = await fetch(`${url}/health`, {
53
+ signal: AbortSignal.timeout(750),
54
+ });
55
+ }
56
+ catch {
57
+ return (await acceptsTcpConnections(url)) ? "occupied" : "available";
58
+ }
59
+ let payload;
60
+ try {
61
+ payload = await response.json();
62
+ }
63
+ catch {
64
+ return "occupied";
65
+ }
66
+ return response.ok && isNarraeonHealth(payload) ? "narraeon" : "occupied";
67
+ }
68
+ function acceptsTcpConnections(url) {
69
+ const target = new URL(url);
70
+ return new Promise((resolveInspection) => {
71
+ const socket = createConnection({
72
+ host: target.hostname,
73
+ port: Number(target.port),
74
+ });
75
+ let settled = false;
76
+ const settle = (occupied) => {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ socket.destroy();
81
+ resolveInspection(occupied);
82
+ };
83
+ socket.setTimeout(750);
84
+ socket.once("connect", () => settle(true));
85
+ socket.once("timeout", () => settle(true));
86
+ socket.once("error", (error) => settle(error.code !== "ECONNREFUSED"));
87
+ });
88
+ }
89
+ function parseCliCommand(args, environment) {
90
+ const parsed = parseRawArguments(args);
91
+ if (parsed.values.help === true || parsed.positionals.length === 0)
92
+ return null;
93
+ if (parsed.positionals.length !== 1 || parsed.positionals[0] !== "web")
94
+ throw new NarraeonCliUsageError(`Unknown command: ${parsed.positionals.join(" ") || "(empty)"}`);
95
+ let port;
96
+ try {
97
+ port = parseWebPort(parsed.values.port ?? environment.NARRAEON_PORT, parsed.values.port === undefined ? "NARRAEON_PORT" : "--port");
98
+ }
99
+ catch (error) {
100
+ throw new NarraeonCliUsageError(errorMessage(error));
101
+ }
102
+ return {
103
+ kind: "web",
104
+ port,
105
+ openBrowser: parsed.values.open !== false,
106
+ };
107
+ }
108
+ function parseRawArguments(args) {
109
+ try {
110
+ return parseArgs({
111
+ args,
112
+ allowNegative: true,
113
+ allowPositionals: true,
114
+ strict: true,
115
+ options: {
116
+ help: { type: "boolean", short: "h" },
117
+ open: { type: "boolean" },
118
+ port: { type: "string", short: "p" },
119
+ },
120
+ });
121
+ }
122
+ catch (error) {
123
+ throw new NarraeonCliUsageError(errorMessage(error));
124
+ }
125
+ }
126
+ async function openBrowser(command, url, openUrl, writeError) {
127
+ if (!command.openBrowser)
128
+ return;
129
+ try {
130
+ await openUrl(url);
131
+ }
132
+ catch (error) {
133
+ writeError(`Could not open a browser automatically. Open ${url} manually: ${errorMessage(error)}\n`);
134
+ }
135
+ }
136
+ function isNarraeonHealth(value) {
137
+ return (typeof value === "object" &&
138
+ value !== null &&
139
+ "status" in value &&
140
+ value.status === "ok" &&
141
+ "protocol" in value &&
142
+ value.protocol === "narraeon.runtime/v1");
143
+ }
144
+ function hasErrorCode(value, code) {
145
+ if (typeof value !== "object" || value === null)
146
+ return false;
147
+ if ("code" in value && value.code === code)
148
+ return true;
149
+ return "cause" in value && hasErrorCode(value.cause, code);
150
+ }
151
+ function errorMessage(value) {
152
+ return value instanceof Error ? value.message : String(value);
153
+ }
154
+ export const cliHelp = `Narraeon
155
+
156
+ Usage:
157
+ narraeon web [--port <port>] [--no-open]
158
+
159
+ Options:
160
+ -p, --port <port> Local web port (default: 4317)
161
+ --no-open Do not open a browser after startup
162
+ -h, --help Show help
163
+ `;
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import open from "open";
3
+ import { cliHelp, NarraeonCliUsageError, resolvePackagedWebRoot, runNarraeonCli, } from "./NarraeonCli.js";
4
+ try {
5
+ const result = await runNarraeonCli({
6
+ args: process.argv.slice(2),
7
+ environment: process.env,
8
+ staticRoot: resolvePackagedWebRoot(import.meta.url),
9
+ openUrl: async (url) => {
10
+ await open(url);
11
+ },
12
+ });
13
+ if (result.kind === "started")
14
+ installGracefulShutdown(result.server);
15
+ }
16
+ catch (error) {
17
+ process.stderr.write(`Narraeon failed to start: ${errorMessage(error)}\n`);
18
+ if (error instanceof NarraeonCliUsageError)
19
+ process.stderr.write(`\n${cliHelp}`);
20
+ process.exitCode = 1;
21
+ }
22
+ function installGracefulShutdown(server) {
23
+ let closing = false;
24
+ const close = () => {
25
+ if (closing)
26
+ return;
27
+ closing = true;
28
+ void server.close().catch((error) => {
29
+ process.stderr.write(`Narraeon failed to stop: ${errorMessage(error)}\n`);
30
+ process.exitCode = 1;
31
+ });
32
+ };
33
+ process.once("SIGINT", close);
34
+ process.once("SIGTERM", close);
35
+ }
36
+ function errorMessage(value) {
37
+ return value instanceof Error ? value.message : String(value);
38
+ }
@@ -0,0 +1,5 @@
1
+ export const appLocales = ["en", "zh-CN"];
2
+ export const defaultAppLocale = "en";
3
+ export function isAppLocale(value) {
4
+ return appLocales.includes(value);
5
+ }
@@ -0,0 +1,14 @@
1
+ /** Case/Unicode folding used when comparing portable current-tree paths. */
2
+ export function portableContentTreePathKey(path) {
3
+ return path.normalize("NFKC").toLocaleLowerCase("en-US");
4
+ }
5
+ export const defaultPortableContentTreeLimits = {
6
+ maxFiles: 1_024,
7
+ maxFileBytes: 4 * 1024 * 1024,
8
+ maxTotalBytes: 32 * 1024 * 1024,
9
+ };
10
+ export function maximumPortableContentArchiveBytes(limits) {
11
+ return limits.maxTotalBytes + limits.maxFiles * 1_024 + 65_557;
12
+ }
13
+ export const maxPortableContentArchiveBytes = maximumPortableContentArchiveBytes(defaultPortableContentTreeLimits);
14
+ export const maxPortableContentArchiveBase64Characters = Math.ceil(maxPortableContentArchiveBytes / 3) * 4;
@@ -0,0 +1,110 @@
1
+ export const modelProviderPresets = [
2
+ {
3
+ id: "custom",
4
+ name: "Custom endpoint",
5
+ provider: "chat_completions",
6
+ baseUrl: "",
7
+ },
8
+ {
9
+ id: "openai",
10
+ name: "OpenAI · Responses API",
11
+ provider: "openai_responses",
12
+ baseUrl: "https://api.openai.com/v1",
13
+ },
14
+ {
15
+ id: "openai-chat",
16
+ name: "OpenAI · Chat Completions",
17
+ provider: "chat_completions",
18
+ baseUrl: "https://api.openai.com/v1",
19
+ },
20
+ {
21
+ id: "anthropic",
22
+ name: "Anthropic",
23
+ provider: "anthropic_messages",
24
+ baseUrl: "https://api.anthropic.com/v1",
25
+ },
26
+ {
27
+ id: "deepseek",
28
+ name: "DeepSeek",
29
+ provider: "chat_completions",
30
+ baseUrl: "https://api.deepseek.com",
31
+ },
32
+ {
33
+ id: "kimi",
34
+ name: "Kimi / Moonshot",
35
+ provider: "chat_completions",
36
+ baseUrl: "https://api.moonshot.cn/v1",
37
+ },
38
+ {
39
+ id: "kimi-code",
40
+ name: "Kimi Coding",
41
+ provider: "chat_completions",
42
+ baseUrl: "https://api.kimi.com/coding/v1",
43
+ },
44
+ {
45
+ id: "glm",
46
+ name: "Zhipu GLM",
47
+ provider: "chat_completions",
48
+ baseUrl: "https://open.bigmodel.cn/api/paas/v4",
49
+ },
50
+ {
51
+ id: "glm-code",
52
+ name: "Zhipu GLM Coding",
53
+ provider: "chat_completions",
54
+ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
55
+ },
56
+ {
57
+ id: "qwen",
58
+ name: "Alibaba Qwen",
59
+ provider: "chat_completions",
60
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
61
+ },
62
+ {
63
+ id: "qwen-plan",
64
+ name: "Alibaba Qwen Token Plan",
65
+ provider: "chat_completions",
66
+ baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
67
+ },
68
+ {
69
+ id: "xai",
70
+ name: "xAI",
71
+ provider: "chat_completions",
72
+ baseUrl: "https://api.x.ai/v1",
73
+ },
74
+ {
75
+ id: "mistral",
76
+ name: "Mistral",
77
+ provider: "chat_completions",
78
+ baseUrl: "https://api.mistral.ai/v1",
79
+ },
80
+ {
81
+ id: "volcengine",
82
+ name: "Volcano Engine Ark",
83
+ provider: "chat_completions",
84
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
85
+ },
86
+ {
87
+ id: "hunyuan",
88
+ name: "Tencent Hunyuan",
89
+ provider: "chat_completions",
90
+ baseUrl: "https://api.hunyuan.cloud.tencent.com/v1",
91
+ },
92
+ {
93
+ id: "siliconflow",
94
+ name: "SiliconFlow",
95
+ provider: "chat_completions",
96
+ baseUrl: "https://api.siliconflow.cn/v1",
97
+ },
98
+ {
99
+ id: "openrouter",
100
+ name: "OpenRouter",
101
+ provider: "chat_completions",
102
+ baseUrl: "https://openrouter.ai/api/v1",
103
+ },
104
+ {
105
+ id: "groq",
106
+ name: "Groq",
107
+ provider: "chat_completions",
108
+ baseUrl: "https://api.groq.com/openai/v1",
109
+ },
110
+ ];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,278 @@
1
+ import { maxPortableContentArchiveBase64Characters } from "./contentTree.js";
2
+ export const v1Protocol = "narraeon.runtime/v1";
3
+ export class V1ProtocolError extends Error {
4
+ code;
5
+ constructor(code, message, options) {
6
+ super(message, options);
7
+ this.name = "V1ProtocolError";
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function parseV1Envelope(value) {
12
+ if (!isRecord(value) || value.protocol !== v1Protocol)
13
+ throw new V1ProtocolError("incompatible_data", "Requests must use the current narraeon.runtime/v1 protocol");
14
+ if (!isRecord(value.request) || typeof value.request.type !== "string")
15
+ throw new V1ProtocolError("invalid_request", "V1 request is missing request.type");
16
+ if (!requestTypes.has(value.request.type))
17
+ throw new V1ProtocolError("invalid_request", `V1 does not support command: ${value.request.type}`);
18
+ validateRequestFields(value.request);
19
+ return value;
20
+ }
21
+ const requiredFields = {
22
+ "preferences.save": { locale: "string" },
23
+ "model.save": { connection: "object" },
24
+ "model.select": { connectionId: "string" },
25
+ "model.delete": { connectionId: "string" },
26
+ "model.models": { provider: "string", baseUrl: "string" },
27
+ "content.read": { packageId: "string" },
28
+ "content.replace": { packageId: "string", files: "array" },
29
+ "content.copy": { packageId: "string" },
30
+ "content.delete": { packageId: "string" },
31
+ "content.rename": { packageId: "string", name: "string" },
32
+ "content.import": { archiveBase64: "string" },
33
+ "content.export": { packageId: "string" },
34
+ "setting-improvement.start": {
35
+ improvementId: "string",
36
+ packageId: "string",
37
+ goal: "string",
38
+ mode: "string",
39
+ contextPaths: "array",
40
+ },
41
+ "setting-improvement.confirm": { improvementId: "string" },
42
+ "setting-improvement.progress": { improvementId: "string" },
43
+ "setting-improvement.revise-plan": {
44
+ improvementId: "string",
45
+ feedback: "string",
46
+ },
47
+ "setting-improvement.revise-candidate": {
48
+ improvementId: "string",
49
+ feedback: "string",
50
+ },
51
+ "setting-improvement.apply": { improvementId: "string" },
52
+ "setting-improvement.discard": { improvementId: "string" },
53
+ "play.create": { name: "string" },
54
+ "play.copy": { presetId: "string" },
55
+ "play.save": { presetId: "string", name: "string", files: "object" },
56
+ "play.rename": { presetId: "string", name: "string" },
57
+ "play.delete": { presetId: "string" },
58
+ "play.enable": { presetId: "string", enabled: "boolean" },
59
+ "play.scripts": { presetId: "string", enabled: "boolean" },
60
+ "play.select": { presetId: "string" },
61
+ "play.export": { presetId: "string" },
62
+ "play.import": { name: "string", files: "array" },
63
+ "play.workbench.read": {},
64
+ "prompt.preview": {
65
+ packageId: "string",
66
+ playerInput: "string",
67
+ model: "object",
68
+ },
69
+ "world.create": {
70
+ operationId: "string",
71
+ packageId: "string",
72
+ model: "object",
73
+ },
74
+ "world.read": { worldId: "string" },
75
+ "world.rename": { worldId: "string", name: "string" },
76
+ "world.delete": { worldId: "string" },
77
+ "artifacts.read": { worldId: "string" },
78
+ "artifacts.debug": { worldId: "string" },
79
+ "world.creation-outcome": { operationId: "string" },
80
+ "world.repair-materialization": { worldId: "string" },
81
+ "world.control-draft.save": { worldId: "string", files: "array" },
82
+ "world.control-draft.preview": { worldId: "string" },
83
+ "world.control-draft.apply": { worldId: "string" },
84
+ "world.derive": {
85
+ operationId: "string",
86
+ sourceWorldId: "string",
87
+ sourceHead: "string",
88
+ },
89
+ "play.chain.revise-player": {
90
+ operationId: "string",
91
+ worldId: "string",
92
+ chainId: "string",
93
+ eventId: "number",
94
+ replacementExchangeId: "string",
95
+ replacementText: "string",
96
+ },
97
+ "play.chain.start": {
98
+ worldId: "string",
99
+ chainId: "string",
100
+ exchangeId: "string",
101
+ playerText: "string",
102
+ },
103
+ "play.chain.append": {
104
+ worldId: "string",
105
+ chainId: "string",
106
+ exchangeId: "string",
107
+ playerText: "string",
108
+ },
109
+ "play.chain.inspect": { worldId: "string" },
110
+ "correction.begin": { worldId: "string", operationId: "string" },
111
+ "correction.read": { candidateId: "string", document: "string" },
112
+ "correction.patch": {
113
+ candidateId: "string",
114
+ expectedVersion: "number",
115
+ target: "string",
116
+ expectedHash: "string",
117
+ edits: "array",
118
+ },
119
+ "correction.replace": {
120
+ candidateId: "string",
121
+ expectedVersion: "number",
122
+ target: "string",
123
+ expectedHash: "string",
124
+ contents: "string",
125
+ },
126
+ "correction.preview": { candidateId: "string", expectedVersion: "number" },
127
+ "correction.apply": { candidateId: "string", expectedVersion: "number" },
128
+ "correction.cancel": { candidateId: "string", expectedVersion: "number" },
129
+ };
130
+ function validateRequestFields(request) {
131
+ for (const [field, kind] of Object.entries(requiredFields[request.type] ?? {})) {
132
+ const value = request[field];
133
+ const valid = kind === "array"
134
+ ? Array.isArray(value)
135
+ : kind === "object"
136
+ ? isRecord(value)
137
+ : typeof value === kind;
138
+ const allowsEmptyString = request.type === "play.chain.append" && field === "playerText";
139
+ if (!valid || (kind === "string" && value === "" && !allowsEmptyString))
140
+ throw new V1ProtocolError("invalid_request", `${String(request.type)}.${field} is invalid`);
141
+ }
142
+ if (request.type === "preferences.save" &&
143
+ request.locale !== "en" &&
144
+ request.locale !== "zh-CN")
145
+ throw new V1ProtocolError("invalid_request", "preferences.save.locale must be en or zh-CN");
146
+ if (request.type === "content.import") {
147
+ const archiveBase64 = request.archiveBase64;
148
+ if (archiveBase64.length > maxPortableContentArchiveBase64Characters ||
149
+ archiveBase64.length % 4 !== 0 ||
150
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(archiveBase64))
151
+ throw new V1ProtocolError("invalid_request", "content.import.archiveBase64 is not a supported content-package ZIP");
152
+ const unexpectedFields = Object.keys(request).filter((field) => field !== "type" && field !== "archiveBase64");
153
+ if (unexpectedFields.length > 0)
154
+ throw new V1ProtocolError("invalid_request", `content.import contains unsupported fields: ${unexpectedFields.join(", ")}`);
155
+ return;
156
+ }
157
+ if (request.type === "prompt.preview" && request.playPresetId !== undefined) {
158
+ if (typeof request.playPresetId !== "string" || request.playPresetId === "")
159
+ throw new V1ProtocolError("invalid_request", "prompt.preview.playPresetId is invalid");
160
+ }
161
+ if (request.type === "prompt.preview" &&
162
+ request.playPresetRevision !== undefined &&
163
+ (typeof request.playPresetRevision !== "string" ||
164
+ request.playPresetRevision === ""))
165
+ throw new V1ProtocolError("invalid_request", "prompt.preview.playPresetRevision is invalid");
166
+ if (request.type === "prompt.preview" &&
167
+ request.playPresetRevision !== undefined &&
168
+ request.playPresetId === undefined)
169
+ throw new V1ProtocolError("invalid_request", "prompt.preview.playPresetRevision must be provided with playPresetId");
170
+ if (request.type === "play.create" && request.files !== undefined) {
171
+ if (!isRecord(request.files) ||
172
+ !Object.values(request.files).every((contents) => typeof contents === "string"))
173
+ throw new V1ProtocolError("invalid_request", "play.create.files must be Record<string,string>");
174
+ }
175
+ if (request.type === "play.save" &&
176
+ !Object.values(request.files).every((contents) => typeof contents === "string"))
177
+ throw new V1ProtocolError("invalid_request", "play.save.files must be Record<string,string>");
178
+ if (request.type === "play.save" &&
179
+ request.structure !== undefined &&
180
+ !isRecord(request.structure))
181
+ throw new V1ProtocolError("invalid_request", "play.save.structure must be a structured map");
182
+ if (request.type === "play.workbench.read") {
183
+ if (request.presetId !== undefined &&
184
+ (typeof request.presetId !== "string" || request.presetId === ""))
185
+ throw new V1ProtocolError("invalid_request", "play.workbench.read.presetId is invalid");
186
+ if (request.revision !== undefined &&
187
+ (typeof request.revision !== "string" || request.revision === ""))
188
+ throw new V1ProtocolError("invalid_request", "play.workbench.read.revision is invalid");
189
+ if (request.revision !== undefined && request.presetId === undefined)
190
+ throw new V1ProtocolError("invalid_request", "play.workbench.read.revision must be provided with presetId");
191
+ }
192
+ if (request.type === "play.import") {
193
+ if (!request.files.every((file) => isRecord(file) &&
194
+ typeof file.path === "string" &&
195
+ typeof file.contents === "string" &&
196
+ file.encoding === undefined))
197
+ throw new V1ProtocolError("invalid_request", "play.import.files is invalid");
198
+ }
199
+ if ((request.type === "artifacts.read" || request.type === "artifacts.debug") &&
200
+ "channel" in request &&
201
+ request.channel !== undefined &&
202
+ (typeof request.channel !== "string" || request.channel.length === 0))
203
+ throw new V1ProtocolError("invalid_request", `${request.type}.channel is invalid`);
204
+ if (request.type === "artifacts.debug" &&
205
+ request.operationId !== undefined &&
206
+ (typeof request.operationId !== "string" || request.operationId === ""))
207
+ throw new V1ProtocolError("invalid_request", "artifacts.debug.operationId is invalid");
208
+ if (request.type !== "setting-improvement.start")
209
+ return;
210
+ if (request.mode !== "plan_first" && request.mode !== "direct_candidate")
211
+ throw new V1ProtocolError("invalid_request", "setting-improvement.start.mode is invalid");
212
+ if (!request.contextPaths.every((path) => typeof path === "string" && path.length > 0))
213
+ throw new V1ProtocolError("invalid_request", "setting-improvement.start.contextPaths is invalid");
214
+ }
215
+ const requestTypes = new Set([
216
+ "workspace.read",
217
+ "preferences.read",
218
+ "preferences.save",
219
+ "model.read",
220
+ "model.save",
221
+ "model.select",
222
+ "model.delete",
223
+ "model.models",
224
+ "content.create",
225
+ "content.read",
226
+ "content.replace",
227
+ "content.copy",
228
+ "content.delete",
229
+ "content.rename",
230
+ "content.import",
231
+ "content.export",
232
+ "setting-improvement.start",
233
+ "setting-improvement.confirm",
234
+ "setting-improvement.progress",
235
+ "setting-improvement.revise-plan",
236
+ "setting-improvement.revise-candidate",
237
+ "setting-improvement.apply",
238
+ "setting-improvement.discard",
239
+ "play.read",
240
+ "play.create",
241
+ "play.copy",
242
+ "play.save",
243
+ "play.rename",
244
+ "play.delete",
245
+ "play.enable",
246
+ "play.scripts",
247
+ "play.select",
248
+ "play.export",
249
+ "play.import",
250
+ "play.workbench.read",
251
+ "prompt.preview",
252
+ "world.create",
253
+ "world.read",
254
+ "world.rename",
255
+ "world.delete",
256
+ "artifacts.read",
257
+ "artifacts.debug",
258
+ "world.creation-outcome",
259
+ "world.repair-materialization",
260
+ "world.control-draft.save",
261
+ "world.control-draft.preview",
262
+ "world.control-draft.apply",
263
+ "world.derive",
264
+ "play.chain.revise-player",
265
+ "play.chain.start",
266
+ "play.chain.append",
267
+ "play.chain.inspect",
268
+ "correction.begin",
269
+ "correction.read",
270
+ "correction.patch",
271
+ "correction.replace",
272
+ "correction.preview",
273
+ "correction.apply",
274
+ "correction.cancel",
275
+ ]);
276
+ function isRecord(value) {
277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
278
+ }