kly 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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/ai/context.mjs +79 -0
  4. package/dist/ai/context.mjs.map +1 -0
  5. package/dist/ai/storage.mjs +50 -0
  6. package/dist/ai/storage.mjs.map +1 -0
  7. package/dist/bin/kly.d.mts +1 -0
  8. package/dist/bin/kly.mjs +2888 -0
  9. package/dist/bin/kly.mjs.map +1 -0
  10. package/dist/bin/launcher-vTpgdO9n.mjs +3 -0
  11. package/dist/bin/permissions-2r_7ZqaH.mjs +3 -0
  12. package/dist/cli.mjs +229 -0
  13. package/dist/cli.mjs.map +1 -0
  14. package/dist/define-app.d.mts +33 -0
  15. package/dist/define-app.d.mts.map +1 -0
  16. package/dist/define-app.mjs +183 -0
  17. package/dist/define-app.mjs.map +1 -0
  18. package/dist/index.d.mts +16 -0
  19. package/dist/index.mjs +15 -0
  20. package/dist/mcp/index.mjs +4 -0
  21. package/dist/mcp/schema-converter.d.mts +13 -0
  22. package/dist/mcp/schema-converter.d.mts.map +1 -0
  23. package/dist/mcp/schema-converter.mjs +30 -0
  24. package/dist/mcp/schema-converter.mjs.map +1 -0
  25. package/dist/mcp/server.d.mts +33 -0
  26. package/dist/mcp/server.d.mts.map +1 -0
  27. package/dist/mcp/server.mjs +92 -0
  28. package/dist/mcp/server.mjs.map +1 -0
  29. package/dist/permissions/index.mjs +123 -0
  30. package/dist/permissions/index.mjs.map +1 -0
  31. package/dist/sandbox/bundled-executor.d.mts +17 -0
  32. package/dist/sandbox/bundled-executor.d.mts.map +1 -0
  33. package/dist/sandbox/bundled-executor.mjs +175 -0
  34. package/dist/sandbox/bundled-executor.mjs.map +1 -0
  35. package/dist/sandbox/ipc-client.mjs +40 -0
  36. package/dist/sandbox/ipc-client.mjs.map +1 -0
  37. package/dist/sandbox/sandboxed-context.mjs +14 -0
  38. package/dist/sandbox/sandboxed-context.mjs.map +1 -0
  39. package/dist/shared/constants.mjs +36 -0
  40. package/dist/shared/constants.mjs.map +1 -0
  41. package/dist/shared/runtime-mode.mjs +59 -0
  42. package/dist/shared/runtime-mode.mjs.map +1 -0
  43. package/dist/tool.d.mts +42 -0
  44. package/dist/tool.d.mts.map +1 -0
  45. package/dist/tool.mjs +38 -0
  46. package/dist/tool.mjs.map +1 -0
  47. package/dist/types.d.mts +282 -0
  48. package/dist/types.d.mts.map +1 -0
  49. package/dist/types.mjs +19 -0
  50. package/dist/types.mjs.map +1 -0
  51. package/dist/ui/components/confirm.d.mts +13 -0
  52. package/dist/ui/components/confirm.d.mts.map +1 -0
  53. package/dist/ui/components/confirm.mjs +37 -0
  54. package/dist/ui/components/confirm.mjs.map +1 -0
  55. package/dist/ui/components/form.d.mts +50 -0
  56. package/dist/ui/components/form.d.mts.map +1 -0
  57. package/dist/ui/components/form.mjs +92 -0
  58. package/dist/ui/components/form.mjs.map +1 -0
  59. package/dist/ui/components/input.d.mts +29 -0
  60. package/dist/ui/components/input.d.mts.map +1 -0
  61. package/dist/ui/components/input.mjs +42 -0
  62. package/dist/ui/components/input.mjs.map +1 -0
  63. package/dist/ui/components/select.d.mts +41 -0
  64. package/dist/ui/components/select.d.mts.map +1 -0
  65. package/dist/ui/components/select.mjs +50 -0
  66. package/dist/ui/components/select.mjs.map +1 -0
  67. package/dist/ui/components/spinner.d.mts +28 -0
  68. package/dist/ui/components/spinner.d.mts.map +1 -0
  69. package/dist/ui/components/spinner.mjs +35 -0
  70. package/dist/ui/components/spinner.mjs.map +1 -0
  71. package/dist/ui/components/table.d.mts +60 -0
  72. package/dist/ui/components/table.d.mts.map +1 -0
  73. package/dist/ui/components/table.mjs +143 -0
  74. package/dist/ui/components/table.mjs.map +1 -0
  75. package/dist/ui/index.d.mts +9 -0
  76. package/dist/ui/utils/colors.d.mts +38 -0
  77. package/dist/ui/utils/colors.d.mts.map +1 -0
  78. package/dist/ui/utils/colors.mjs +64 -0
  79. package/dist/ui/utils/colors.mjs.map +1 -0
  80. package/dist/ui/utils/output.d.mts +23 -0
  81. package/dist/ui/utils/output.d.mts.map +1 -0
  82. package/dist/ui/utils/output.mjs +42 -0
  83. package/dist/ui/utils/output.mjs.map +1 -0
  84. package/dist/ui/utils/tty.d.mts +9 -0
  85. package/dist/ui/utils/tty.d.mts.map +1 -0
  86. package/dist/ui/utils/tty.mjs +12 -0
  87. package/dist/ui/utils/tty.mjs.map +1 -0
  88. package/package.json +81 -0
@@ -0,0 +1,92 @@
1
+ import { convertToJsonSchema } from "./schema-converter.mjs";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+
6
+ //#region src/mcp/server.ts
7
+ /**
8
+ * Start an MCP server for a Kly app
9
+ *
10
+ * This makes all tools from the app available to Claude Desktop/Code via the MCP protocol.
11
+ *
12
+ * @param app - The Kly app instance returned by defineApp()
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { defineApp, tool, startMcpServer } from "kly"
17
+ * import { z } from "zod"
18
+ *
19
+ * const app = defineApp({
20
+ * name: "my-app",
21
+ * version: "1.0.0",
22
+ * description: "My app",
23
+ * tools: [myTool],
24
+ * })
25
+ *
26
+ * // In MCP mode, start the server
27
+ * if (process.env.KLY_MCP_MODE === "true") {
28
+ * await startMcpServer(app)
29
+ * }
30
+ * ```
31
+ */
32
+ async function startMcpServer(app) {
33
+ const { definition } = app;
34
+ const server = new Server({
35
+ name: definition.name,
36
+ version: definition.version
37
+ }, { capabilities: { tools: {} } });
38
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
39
+ return { tools: definition.tools.map((tool) => ({
40
+ name: tool.name,
41
+ description: tool.description ?? `Execute ${tool.name}`,
42
+ inputSchema: convertToJsonSchema(tool.inputSchema)
43
+ })) };
44
+ });
45
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
46
+ const { name, arguments: args } = request.params;
47
+ try {
48
+ return {
49
+ content: formatToolResult(await app.execute(name, args)),
50
+ isError: false
51
+ };
52
+ } catch (error) {
53
+ return {
54
+ content: [{
55
+ type: "text",
56
+ text: `Error executing tool '${name}': ${error instanceof Error ? error.message : String(error)}`
57
+ }],
58
+ isError: true
59
+ };
60
+ }
61
+ });
62
+ server.onerror = (error) => {
63
+ console.error("[MCP Error]", error);
64
+ };
65
+ process.on("SIGINT", async () => {
66
+ await server.close();
67
+ process.exit(0);
68
+ });
69
+ const transport = new StdioServerTransport();
70
+ await server.connect(transport);
71
+ }
72
+ /**
73
+ * Format tool result for MCP response
74
+ */
75
+ function formatToolResult(result) {
76
+ if (result === void 0 || result === null) return [{
77
+ type: "text",
78
+ text: "Success"
79
+ }];
80
+ if (typeof result === "string") return [{
81
+ type: "text",
82
+ text: result
83
+ }];
84
+ return [{
85
+ type: "text",
86
+ text: JSON.stringify(result, null, 2)
87
+ }];
88
+ }
89
+
90
+ //#endregion
91
+ export { startMcpServer };
92
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.mjs","names":[],"sources":["../../src/mcp/server.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { KlyApp } from \"../types\";\nimport { convertToJsonSchema } from \"./schema-converter\";\n\n/**\n * Start an MCP server for a Kly app\n *\n * This makes all tools from the app available to Claude Desktop/Code via the MCP protocol.\n *\n * @param app - The Kly app instance returned by defineApp()\n *\n * @example\n * ```typescript\n * import { defineApp, tool, startMcpServer } from \"kly\"\n * import { z } from \"zod\"\n *\n * const app = defineApp({\n * name: \"my-app\",\n * version: \"1.0.0\",\n * description: \"My app\",\n * tools: [myTool],\n * })\n *\n * // In MCP mode, start the server\n * if (process.env.KLY_MCP_MODE === \"true\") {\n * await startMcpServer(app)\n * }\n * ```\n */\nexport async function startMcpServer(app: KlyApp): Promise<void> {\n const { definition } = app;\n\n // Create MCP server instance\n const server = new Server(\n {\n name: definition.name,\n version: definition.version,\n },\n {\n capabilities: {\n tools: {},\n },\n },\n );\n\n // Handle tools/list - enumerate all tools from the app\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const tools = definition.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? `Execute ${tool.name}`,\n inputSchema: convertToJsonSchema(tool.inputSchema),\n }));\n\n return { tools };\n });\n\n // Handle tools/call - execute a specific tool\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n // Execute the tool through the app's execute method\n const result = await app.execute(name, args as Record<string, unknown>);\n\n // Convert result to MCP content format\n const content = formatToolResult(result);\n\n return {\n content,\n isError: false,\n };\n } catch (error) {\n // Return error in MCP format\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n\n return {\n content: [\n {\n type: \"text\",\n text: `Error executing tool '${name}': ${errorMessage}`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Error handling\n server.onerror = (error) => {\n console.error(\"[MCP Error]\", error);\n };\n\n process.on(\"SIGINT\", async () => {\n await server.close();\n process.exit(0);\n });\n\n // Start the server with stdio transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Note: In MCP mode, we should not log to stdout as it interferes with JSON-RPC\n // The SDK handles logging internally via stderr\n}\n\n/**\n * Format tool result for MCP response\n */\nfunction formatToolResult(\n result: unknown,\n): Array<{ type: string; text: string }> {\n if (result === undefined || result === null) {\n return [\n {\n type: \"text\",\n text: \"Success\",\n },\n ];\n }\n\n // If result is already a string, use it directly\n if (typeof result === \"string\") {\n return [\n {\n type: \"text\",\n text: result,\n },\n ];\n }\n\n // Otherwise, stringify the result as JSON\n return [\n {\n type: \"text\",\n text: JSON.stringify(result, null, 2),\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,eAAe,KAA4B;CAC/D,MAAM,EAAE,eAAe;CAGvB,MAAM,SAAS,IAAI,OACjB;EACE,MAAM,WAAW;EACjB,SAAS,WAAW;EACrB,EACD,EACE,cAAc,EACZ,OAAO,EAAE,EACV,EACF,CACF;AAGD,QAAO,kBAAkB,wBAAwB,YAAY;AAO3D,SAAO,EAAE,OANK,WAAW,MAAM,KAAK,UAAU;GAC5C,MAAM,KAAK;GACX,aAAa,KAAK,eAAe,WAAW,KAAK;GACjD,aAAa,oBAAoB,KAAK,YAAY;GACnD,EAAE,EAEa;GAChB;AAGF,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,EAAE,MAAM,WAAW,SAAS,QAAQ;AAE1C,MAAI;AAOF,UAAO;IACL,SAHc,iBAHD,MAAM,IAAI,QAAQ,MAAM,KAAgC,CAG/B;IAItC,SAAS;IACV;WACM,OAAO;AAKd,UAAO;IACL,SAAS,CACP;KACE,MAAM;KACN,MAAM,yBAAyB,KAAK,KANxC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;KAOnD,CACF;IACD,SAAS;IACV;;GAEH;AAGF,QAAO,WAAW,UAAU;AAC1B,UAAQ,MAAM,eAAe,MAAM;;AAGrC,SAAQ,GAAG,UAAU,YAAY;AAC/B,QAAM,OAAO,OAAO;AACpB,UAAQ,KAAK,EAAE;GACf;CAGF,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;;;;;AASjC,SAAS,iBACP,QACuC;AACvC,KAAI,WAAW,UAAa,WAAW,KACrC,QAAO,CACL;EACE,MAAM;EACN,MAAM;EACP,CACF;AAIH,KAAI,OAAO,WAAW,SACpB,QAAO,CACL;EACE,MAAM;EACN,MAAM;EACP,CACF;AAIH,QAAO,CACL;EACE,MAAM;EACN,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;EACtC,CACF"}
@@ -0,0 +1,123 @@
1
+ import { PATHS } from "../shared/constants.mjs";
2
+ import { getLocalRef, getRemoteRef, isTrustAll } from "../shared/runtime-mode.mjs";
3
+ import { isTTY } from "../ui/utils/tty.mjs";
4
+ import { select } from "../ui/components/select.mjs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ //#region src/permissions/index.ts
10
+ const CONFIG_DIR = join(homedir(), PATHS.CONFIG_DIR);
11
+ const PERMISSIONS_FILE = join(CONFIG_DIR, PATHS.PERMISSIONS_FILE);
12
+ /**
13
+ * Get app identifier from script path
14
+ */
15
+ function getAppIdentifier() {
16
+ const localRef = getLocalRef();
17
+ if (localRef) return localRef;
18
+ const remoteRef = getRemoteRef();
19
+ if (remoteRef) return remoteRef;
20
+ const scriptPath = process.argv[1] ?? "";
21
+ if (scriptPath.startsWith("/") || scriptPath.startsWith("C:\\")) return `local:${scriptPath}`;
22
+ return scriptPath || "unknown";
23
+ }
24
+ /**
25
+ * Get friendly app name for display
26
+ */
27
+ function getAppName(appId) {
28
+ if (appId.startsWith("local:")) {
29
+ const path = appId.slice(6);
30
+ const parts = path.split("/");
31
+ return parts[parts.length - 1] || path;
32
+ }
33
+ if (appId.startsWith("github.com/")) return appId.split("/").slice(1, 3).join("/");
34
+ return appId;
35
+ }
36
+ /**
37
+ * Ensure permissions config directory exists
38
+ */
39
+ function ensurePermissionsDir() {
40
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
41
+ }
42
+ /**
43
+ * Load permissions configuration
44
+ */
45
+ function loadPermissions() {
46
+ ensurePermissionsDir();
47
+ if (!existsSync(PERMISSIONS_FILE)) return { trustedApps: {} };
48
+ try {
49
+ const content = readFileSync(PERMISSIONS_FILE, "utf-8");
50
+ return JSON.parse(content);
51
+ } catch (error) {
52
+ console.error("Failed to parse permissions file:", error);
53
+ return { trustedApps: {} };
54
+ }
55
+ }
56
+ /**
57
+ * Save permissions configuration
58
+ */
59
+ function savePermissions(config) {
60
+ ensurePermissionsDir();
61
+ writeFileSync(PERMISSIONS_FILE, JSON.stringify(config, null, 2), "utf-8");
62
+ }
63
+ /**
64
+ * Request permission from user with interactive prompt
65
+ */
66
+ async function requestPermission(appId, appName) {
67
+ if (!isTTY()) {
68
+ console.error(`\nPermission required: App "${appName}" (${appId}) wants to access your API keys.`);
69
+ console.error("Set KLY_TRUST_ALL=true environment variable to grant access in non-interactive mode.");
70
+ return false;
71
+ }
72
+ console.log("");
73
+ console.log(`App "${appName}" is requesting access to your API keys.`);
74
+ console.log(`Source: ${appId}`);
75
+ console.log("");
76
+ console.log("This will allow the app to use your configured LLM models.");
77
+ console.log("");
78
+ const choice = await select({
79
+ prompt: "Do you want to allow this?",
80
+ options: [
81
+ {
82
+ name: "Allow once",
83
+ value: "once",
84
+ description: "Allow for this session only"
85
+ },
86
+ {
87
+ name: "Always allow",
88
+ value: "always",
89
+ description: "Remember this choice for future runs"
90
+ },
91
+ {
92
+ name: "Cancel",
93
+ value: "cancel",
94
+ description: "Cancel and exit"
95
+ }
96
+ ]
97
+ });
98
+ if (choice === "cancel") return false;
99
+ if (choice === "always") {
100
+ const config = loadPermissions();
101
+ config.trustedApps[appId] = {
102
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
103
+ choice: "always"
104
+ };
105
+ savePermissions(config);
106
+ return true;
107
+ }
108
+ return true;
109
+ }
110
+ /**
111
+ * Check if an app has permission to access API keys
112
+ * If not, prompt user for permission (in interactive mode)
113
+ */
114
+ async function checkApiKeyPermission(appId) {
115
+ if (isTrustAll()) return true;
116
+ const record = loadPermissions().trustedApps[appId];
117
+ if (record && record.choice === "always") return true;
118
+ return await requestPermission(appId, getAppName(appId));
119
+ }
120
+
121
+ //#endregion
122
+ export { checkApiKeyPermission, getAppIdentifier };
123
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/permissions/index.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { SandboxRuntimeConfig } from \"@anthropic-ai/sandbox-runtime\";\nimport { PATHS } from \"../shared/constants\";\nimport { getLocalRef, getRemoteRef, isTrustAll } from \"../shared/runtime-mode\";\nimport { select } from \"../ui\";\nimport { isTTY } from \"../ui/utils/tty\";\n\nconst CONFIG_DIR = join(homedir(), PATHS.CONFIG_DIR);\nconst PERMISSIONS_FILE = join(CONFIG_DIR, PATHS.PERMISSIONS_FILE);\n\n/**\n * Permission record for an app\n * Only \"always\" choices are stored\n */\ninterface PermissionRecord {\n /** When the permission was granted */\n timestamp: string;\n /** User's choice: always \"always\" (only stored choice) */\n choice: \"always\";\n /** Sandbox configuration (optional, for sandboxed execution) */\n sandboxConfig?: SandboxRuntimeConfig;\n}\n\n/**\n * Permissions configuration\n */\ninterface PermissionsConfig {\n /** Trusted apps with their permissions */\n trustedApps: Record<string, PermissionRecord>;\n}\n\n/**\n * Get app identifier from script path\n */\nexport function getAppIdentifier(): string {\n // Check for explicit local file reference (set by kly run command)\n const localRef = getLocalRef();\n if (localRef) {\n return localRef;\n }\n\n // Remote app (from environment variable set by remote loader)\n const remoteRef = getRemoteRef();\n if (remoteRef) {\n return remoteRef;\n }\n\n // Fallback to script path for direct execution\n const scriptPath = process.argv[1] ?? \"\";\n if (scriptPath.startsWith(\"/\") || scriptPath.startsWith(\"C:\\\\\")) {\n return `local:${scriptPath}`;\n }\n\n // Default to script path\n return scriptPath || \"unknown\";\n}\n\n/**\n * Get friendly app name for display\n */\nexport function getAppName(appId: string): string {\n if (appId.startsWith(\"local:\")) {\n const path = appId.slice(6);\n const parts = path.split(\"/\");\n return parts[parts.length - 1] || path;\n }\n\n if (appId.startsWith(\"github.com/\")) {\n const parts = appId.split(\"/\");\n return parts.slice(1, 3).join(\"/\");\n }\n\n return appId;\n}\n\n/**\n * Ensure permissions config directory exists\n */\nfunction ensurePermissionsDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\n/**\n * Load permissions configuration\n */\nexport function loadPermissions(): PermissionsConfig {\n ensurePermissionsDir();\n\n if (!existsSync(PERMISSIONS_FILE)) {\n return { trustedApps: {} };\n }\n\n try {\n const content = readFileSync(PERMISSIONS_FILE, \"utf-8\");\n return JSON.parse(content);\n } catch (error) {\n console.error(\"Failed to parse permissions file:\", error);\n return { trustedApps: {} };\n }\n}\n\n/**\n * Save permissions configuration\n */\nexport function savePermissions(config: PermissionsConfig): void {\n ensurePermissionsDir();\n writeFileSync(PERMISSIONS_FILE, JSON.stringify(config, null, 2), \"utf-8\");\n}\n\n/**\n * Request permission from user with interactive prompt\n */\nasync function requestPermission(\n appId: string,\n appName: string,\n): Promise<boolean> {\n // Check if running in TTY mode\n if (!isTTY()) {\n console.error(\n `\\nPermission required: App \"${appName}\" (${appId}) wants to access your API keys.`,\n );\n console.error(\n \"Set KLY_TRUST_ALL=true environment variable to grant access in non-interactive mode.\",\n );\n return false;\n }\n\n console.log(\"\");\n console.log(`App \"${appName}\" is requesting access to your API keys.`);\n console.log(`Source: ${appId}`);\n console.log(\"\");\n console.log(\"This will allow the app to use your configured LLM models.\");\n console.log(\"\");\n\n const choice = await select({\n prompt: \"Do you want to allow this?\",\n options: [\n {\n name: \"Allow once\",\n value: \"once\",\n description: \"Allow for this session only\",\n },\n {\n name: \"Always allow\",\n value: \"always\",\n description: \"Remember this choice for future runs\",\n },\n { name: \"Cancel\", value: \"cancel\", description: \"Cancel and exit\" },\n ],\n });\n\n // Cancel - don't save, just reject\n if (choice === \"cancel\") {\n return false;\n }\n\n // Always - save to config\n if (choice === \"always\") {\n const config = loadPermissions();\n config.trustedApps[appId] = {\n timestamp: new Date().toISOString(),\n choice: \"always\",\n };\n savePermissions(config);\n return true;\n }\n\n // Once - don't save, just allow for this run\n return true;\n}\n\n/**\n * Check if an app has permission to access API keys\n * If not, prompt user for permission (in interactive mode)\n */\nexport async function checkApiKeyPermission(appId: string): Promise<boolean> {\n // Allow bypass via environment variable (for CI/automation)\n if (isTrustAll()) {\n return true;\n }\n\n // Check stored permissions\n const config = loadPermissions();\n const record = config.trustedApps[appId];\n\n // If record exists, it's always \"always allow\" (we only store grants)\n if (record && record.choice === \"always\") {\n return true;\n }\n\n // No stored permission - need to request\n const appName = getAppName(appId);\n return await requestPermission(appId, appName);\n}\n\n/**\n * Revoke permission for an app\n */\nexport function revokePermission(appId: string): void {\n const config = loadPermissions();\n delete config.trustedApps[appId];\n savePermissions(config);\n}\n\n/**\n * List all granted permissions\n * Only \"always allow\" permissions are stored\n */\nexport function listPermissions(): Array<{\n appId: string;\n appName: string;\n timestamp: string;\n choice: string;\n}> {\n const config = loadPermissions();\n\n return Object.entries(config.trustedApps).map(([appId, record]) => ({\n appId,\n appName: getAppName(appId),\n timestamp: record.timestamp,\n choice: record.choice,\n }));\n}\n\n/**\n * Request sandbox configuration from user interactively\n * Returns SandboxRuntimeConfig directly (no conversion needed)\n */\nasync function requestSandboxConfig(\n appId: string,\n appName: string,\n): Promise<SandboxRuntimeConfig | null> {\n if (!isTTY()) {\n console.error(`\\nSandbox permission required for: \"${appName}\" (${appId})`);\n console.error(\n \"Set KLY_TRUST_ALL=true environment variable to run without sandboxing in non-interactive mode.\",\n );\n return null;\n }\n\n const homeDir = homedir();\n const currentDir = process.cwd();\n\n console.log(\"\");\n console.log(`🔐 Sandbox Permission Request from: ${appName}`);\n console.log(\"\");\n\n // Ask for filesystem read permissions\n console.log(\"📂 Filesystem Read Access:\");\n const fsReadChoice = await select({\n prompt: \"Which files should be denied for reading?\",\n options: [\n {\n name: \"Sensitive only\",\n value: \"sensitive\",\n description: \"Deny access to ~/.kly, ~/.ssh, ~/.aws, etc.\",\n },\n {\n name: \"All home directory\",\n value: \"all-home\",\n description: \"Deny access to entire home directory\",\n },\n {\n name: \"None (allow all)\",\n value: \"none\",\n description: \"No read restrictions (except ~/.kly)\",\n },\n ],\n });\n\n // Always deny reading sensitive directories (hardcoded for security)\n let denyRead: string[] = [join(homeDir, \".kly\")];\n\n if (fsReadChoice === \"sensitive\") {\n denyRead = [\n join(homeDir, \".kly\"),\n join(homeDir, \".ssh\"),\n join(homeDir, \".aws\"),\n join(homeDir, \".gnupg\"),\n ];\n } else if (fsReadChoice === \"all-home\") {\n denyRead = [homeDir];\n }\n // Note: Even if user chooses \"none\", .kly is still protected\n\n // Ask for filesystem write permissions\n console.log(\"\");\n console.log(\"📝 Filesystem Write Access:\");\n const fsWriteChoice = await select({\n prompt: \"Which directories should be allowed for writing?\",\n options: [\n {\n name: \"None\",\n value: \"none\",\n description: \"No write access\",\n },\n {\n name: \"Current directory only\",\n value: \"current\",\n description: `Allow write to ${currentDir}`,\n },\n {\n name: \"Temporary directory\",\n value: \"temp\",\n description: \"Allow write to system temp directory\",\n },\n ],\n });\n\n let allowWrite: string[] = [];\n if (fsWriteChoice === \"current\") {\n allowWrite = [currentDir];\n } else if (fsWriteChoice === \"temp\") {\n const tmpdir = process.env.TMPDIR || process.env.TEMP || \"/tmp\";\n allowWrite = [tmpdir];\n }\n\n // Always deny writing to sensitive directories (hardcoded for security)\n const denyWrite = [\n join(homeDir, \".kly\"), // KLY config and permissions\n join(homeDir, \".ssh\"), // SSH keys\n join(homeDir, \".aws\"), // AWS credentials\n join(homeDir, \".gnupg\"), // GPG keys\n ];\n\n // Ask for network permissions\n console.log(\"\");\n console.log(\"🌐 Network Access:\");\n const networkChoice = await select({\n prompt: \"Which network access should be allowed?\",\n options: [\n {\n name: \"None\",\n value: \"none\",\n description: \"No network access\",\n },\n {\n name: \"LLM APIs only\",\n value: \"llm-apis\",\n description: \"OpenAI, Anthropic, Google AI\",\n },\n {\n name: \"Common APIs\",\n value: \"common\",\n description: \"LLM + GitHub, npm, etc.\",\n },\n {\n name: \"All domains\",\n value: \"all\",\n description: \"Allow all network access\",\n },\n ],\n });\n\n let allowedDomains: string[] = [];\n if (networkChoice === \"llm-apis\") {\n allowedDomains = [\n \"api.openai.com\",\n \"*.anthropic.com\",\n \"generativelanguage.googleapis.com\",\n ];\n } else if (networkChoice === \"common\") {\n allowedDomains = [\n \"api.openai.com\",\n \"*.anthropic.com\",\n \"generativelanguage.googleapis.com\",\n \"*.github.com\",\n \"registry.npmjs.org\",\n ];\n } else if (networkChoice === \"all\") {\n allowedDomains = [\"*\"];\n }\n\n // Ask how long to remember this choice\n console.log(\"\");\n const duration = await select({\n prompt: \"How long should these permissions last?\",\n options: [\n {\n name: \"One time only\",\n value: \"once\",\n description: \"Ask again next time\",\n },\n {\n name: \"Always allow\",\n value: \"always\",\n description: \"Remember for this app\",\n },\n {\n name: \"Cancel\",\n value: \"cancel\",\n description: \"Cancel and exit\",\n },\n ],\n });\n\n // Cancel - don't save, just reject\n if (duration === \"cancel\") {\n return null;\n }\n\n // Construct SandboxRuntimeConfig directly\n const sandboxConfig: SandboxRuntimeConfig = {\n network: {\n allowedDomains,\n deniedDomains: [],\n },\n filesystem: {\n denyRead,\n allowWrite,\n denyWrite,\n },\n };\n\n // Save permission record only if \"always\"\n if (duration === \"always\") {\n const config = loadPermissions();\n config.trustedApps[appId] = {\n sandboxConfig,\n timestamp: new Date().toISOString(),\n choice: \"always\",\n };\n savePermissions(config);\n }\n\n console.log(\"\");\n console.log(\"✅ Sandbox permissions granted!\");\n return sandboxConfig;\n}\n\n/**\n * Get sandbox configuration for an app\n * Returns SandboxRuntimeConfig directly (no conversion needed)\n *\n * @param appId - App identifier\n * @returns SandboxRuntimeConfig or null if denied\n */\nexport async function getAppSandboxConfig(\n appId: string,\n): Promise<SandboxRuntimeConfig | null> {\n const homeDir = homedir();\n\n // Check for trust-all bypass (for automation)\n if (isTrustAll()) {\n // Even in trust-all mode, protect sensitive directories\n return {\n network: { allowedDomains: [\"*\"], deniedDomains: [] },\n filesystem: {\n denyRead: [join(homeDir, \".kly\")], // ALWAYS deny reading KLY config\n allowWrite: [\"*\"],\n denyWrite: [\n join(homeDir, \".kly\"), // KLY config and permissions\n join(homeDir, \".ssh\"), // SSH keys\n join(homeDir, \".aws\"), // AWS credentials\n join(homeDir, \".gnupg\"), // GPG keys\n ],\n },\n };\n }\n\n const config = loadPermissions();\n const record = config.trustedApps[appId];\n\n // If permission already granted as \"always\", return cached config\n if (record?.choice === \"always\" && record.sandboxConfig) {\n return record.sandboxConfig;\n }\n\n // Request new sandbox permissions\n const appName = getAppName(appId);\n return await requestSandboxConfig(appId, appName);\n}\n\n/**\n * Clear all permissions\n */\nexport function clearAllPermissions(): void {\n savePermissions({ trustedApps: {} });\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,aAAa,KAAK,SAAS,EAAE,MAAM,WAAW;AACpD,MAAM,mBAAmB,KAAK,YAAY,MAAM,iBAAiB;;;;AA0BjE,SAAgB,mBAA2B;CAEzC,MAAM,WAAW,aAAa;AAC9B,KAAI,SACF,QAAO;CAIT,MAAM,YAAY,cAAc;AAChC,KAAI,UACF,QAAO;CAIT,MAAM,aAAa,QAAQ,KAAK,MAAM;AACtC,KAAI,WAAW,WAAW,IAAI,IAAI,WAAW,WAAW,OAAO,CAC7D,QAAO,SAAS;AAIlB,QAAO,cAAc;;;;;AAMvB,SAAgB,WAAW,OAAuB;AAChD,KAAI,MAAM,WAAW,SAAS,EAAE;EAC9B,MAAM,OAAO,MAAM,MAAM,EAAE;EAC3B,MAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,SAAO,MAAM,MAAM,SAAS,MAAM;;AAGpC,KAAI,MAAM,WAAW,cAAc,CAEjC,QADc,MAAM,MAAM,IAAI,CACjB,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI;AAGpC,QAAO;;;;;AAMT,SAAS,uBAA6B;AACpC,KAAI,CAAC,WAAW,WAAW,CACzB,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;;;;;AAO9C,SAAgB,kBAAqC;AACnD,uBAAsB;AAEtB,KAAI,CAAC,WAAW,iBAAiB,CAC/B,QAAO,EAAE,aAAa,EAAE,EAAE;AAG5B,KAAI;EACF,MAAM,UAAU,aAAa,kBAAkB,QAAQ;AACvD,SAAO,KAAK,MAAM,QAAQ;UACnB,OAAO;AACd,UAAQ,MAAM,qCAAqC,MAAM;AACzD,SAAO,EAAE,aAAa,EAAE,EAAE;;;;;;AAO9B,SAAgB,gBAAgB,QAAiC;AAC/D,uBAAsB;AACtB,eAAc,kBAAkB,KAAK,UAAU,QAAQ,MAAM,EAAE,EAAE,QAAQ;;;;;AAM3E,eAAe,kBACb,OACA,SACkB;AAElB,KAAI,CAAC,OAAO,EAAE;AACZ,UAAQ,MACN,+BAA+B,QAAQ,KAAK,MAAM,kCACnD;AACD,UAAQ,MACN,uFACD;AACD,SAAO;;AAGT,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,QAAQ,QAAQ,0CAA0C;AACtE,SAAQ,IAAI,WAAW,QAAQ;AAC/B,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,6DAA6D;AACzE,SAAQ,IAAI,GAAG;CAEf,MAAM,SAAS,MAAM,OAAO;EAC1B,QAAQ;EACR,SAAS;GACP;IACE,MAAM;IACN,OAAO;IACP,aAAa;IACd;GACD;IACE,MAAM;IACN,OAAO;IACP,aAAa;IACd;GACD;IAAE,MAAM;IAAU,OAAO;IAAU,aAAa;IAAmB;GACpE;EACF,CAAC;AAGF,KAAI,WAAW,SACb,QAAO;AAIT,KAAI,WAAW,UAAU;EACvB,MAAM,SAAS,iBAAiB;AAChC,SAAO,YAAY,SAAS;GAC1B,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,QAAQ;GACT;AACD,kBAAgB,OAAO;AACvB,SAAO;;AAIT,QAAO;;;;;;AAOT,eAAsB,sBAAsB,OAAiC;AAE3E,KAAI,YAAY,CACd,QAAO;CAKT,MAAM,SADS,iBAAiB,CACV,YAAY;AAGlC,KAAI,UAAU,OAAO,WAAW,SAC9B,QAAO;AAKT,QAAO,MAAM,kBAAkB,OADf,WAAW,MAAM,CACa"}
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ //#region src/sandbox/executor.d.ts
3
+ /**
4
+ * Sandbox Executor - Entry point for sandboxed child process
5
+ * This file runs inside the sandbox and:
6
+ * 1. Receives initialization message from host via IPC
7
+ * 2. Loads and executes the user's script
8
+ * 3. Provides sandboxed context to the script
9
+ * 4. Sends execution results back to host
10
+ */
11
+ /**
12
+ * Send IPC request to host and wait for response
13
+ */
14
+ declare function sendIPCRequest<T>(type: string, payload: unknown): Promise<T>;
15
+ //#endregion
16
+ export { sendIPCRequest };
17
+ //# sourceMappingURL=bundled-executor.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-executor.d.mts","names":[],"sources":["../../src/sandbox/executor.ts"],"sourcesContent":[],"mappings":";;;AAmEA;;;;;;;;;;iBAAgB,mDAAmD,QAAQ"}
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bun
2
+ //#region src/shared/constants.ts
3
+ /**
4
+ * Timeout values in milliseconds
5
+ */
6
+ const TIMEOUTS = {
7
+ IPC_REQUEST: 3e4,
8
+ IPC_LONG_REQUEST: 6e4
9
+ };
10
+
11
+ //#endregion
12
+ //#region src/shared/ipc-protocol.ts
13
+ function isIPCResponse(msg) {
14
+ return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "response" && "id" in msg && typeof msg.id === "string";
15
+ }
16
+ function isSandboxInitMessage(msg) {
17
+ return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "init";
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/sandbox/sandboxed-context.ts
22
+ /**
23
+ * Create sandboxed models context that communicates with host via IPC
24
+ * This context is injected into user tools running in the sandbox
25
+ * All API key access is controlled by the host process
26
+ */
27
+ function createSandboxedModelsContext() {
28
+ return {
29
+ list() {
30
+ throw new Error("Synchronous list() not supported in sandbox. Use async methods or move this logic to host.");
31
+ },
32
+ getCurrent() {
33
+ throw new Error("Synchronous getCurrent() not supported in sandbox. Use async methods or move this logic to host.");
34
+ },
35
+ get(_name) {
36
+ throw new Error("Synchronous get() not supported in sandbox. Use async methods or move this logic to host.");
37
+ },
38
+ async getConfigAsync(name) {
39
+ try {
40
+ const response = await sendIPCRequest("getModelConfig", { name });
41
+ if (!response) return null;
42
+ return {
43
+ provider: response.provider,
44
+ model: response.model,
45
+ apiKey: response.apiKey,
46
+ baseURL: response.baseURL
47
+ };
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : "Failed to get model config";
50
+ throw new Error(`Permission denied: ${message}`);
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/sandbox/executor.ts
58
+ /**
59
+ * Sandbox Executor - Entry point for sandboxed child process
60
+ * This file runs inside the sandbox and:
61
+ * 1. Receives initialization message from host via IPC
62
+ * 2. Loads and executes the user's script
63
+ * 3. Provides sandboxed context to the script
64
+ * 4. Sends execution results back to host
65
+ */
66
+ /**
67
+ * Global state for the sandbox
68
+ */
69
+ let initMessage = null;
70
+ const pendingIPCResponses = /* @__PURE__ */ new Map();
71
+ /**
72
+ * Set up IPC communication
73
+ */
74
+ function setupIPC() {
75
+ if (!process.send) throw new Error("IPC channel not available");
76
+ process.on("message", (message) => {
77
+ if (isSandboxInitMessage(message)) {
78
+ initMessage = message;
79
+ executeUserScript().catch((error) => {
80
+ sendExecutionComplete(false, void 0, error.message);
81
+ process.exit(1);
82
+ });
83
+ return;
84
+ }
85
+ if (isIPCResponse(message)) {
86
+ const pending = pendingIPCResponses.get(message.id);
87
+ if (pending) {
88
+ pendingIPCResponses.delete(message.id);
89
+ pending.resolve(message);
90
+ }
91
+ return;
92
+ }
93
+ });
94
+ }
95
+ /**
96
+ * Send IPC request to host and wait for response
97
+ */
98
+ function sendIPCRequest(type, payload) {
99
+ return new Promise((resolve, reject) => {
100
+ if (!process.send) {
101
+ reject(/* @__PURE__ */ new Error("IPC channel not available"));
102
+ return;
103
+ }
104
+ const id = `${type}-${Date.now()}-${Math.random()}`;
105
+ const request = {
106
+ type,
107
+ id,
108
+ payload
109
+ };
110
+ pendingIPCResponses.set(id, {
111
+ resolve: (response) => {
112
+ if (response.success) resolve(response.data);
113
+ else reject(new Error(response.error));
114
+ },
115
+ reject
116
+ });
117
+ process.send(request);
118
+ setTimeout(() => {
119
+ const pending = pendingIPCResponses.get(id);
120
+ if (pending) {
121
+ pendingIPCResponses.delete(id);
122
+ pending.reject(/* @__PURE__ */ new Error("IPC request timeout"));
123
+ }
124
+ }, TIMEOUTS.IPC_REQUEST);
125
+ });
126
+ }
127
+ /**
128
+ * Execute the user's script
129
+ */
130
+ async function executeUserScript() {
131
+ if (!initMessage) throw new Error("No initialization message received");
132
+ const { scriptPath, args } = initMessage;
133
+ try {
134
+ process.argv = [
135
+ "bun",
136
+ scriptPath,
137
+ ...args
138
+ ];
139
+ process.env.KLY_SANDBOX_MODE = "true";
140
+ global.__KLY_SANDBOXED_CONTEXT__ = { modelsContext: createSandboxedModelsContext() };
141
+ await import(scriptPath);
142
+ sendExecutionComplete(true);
143
+ } catch (error) {
144
+ sendExecutionComplete(false, void 0, error instanceof Error ? error.message : String(error));
145
+ throw error;
146
+ }
147
+ }
148
+ /**
149
+ * Send execution complete message to host
150
+ */
151
+ function sendExecutionComplete(success, result, error) {
152
+ if (!process.send) return;
153
+ const message = {
154
+ type: "complete",
155
+ success,
156
+ result,
157
+ error
158
+ };
159
+ process.send(message);
160
+ }
161
+ /**
162
+ * Main entry point
163
+ */
164
+ function main() {
165
+ if (process.env.KLY_SANDBOX_MODE !== "true") {
166
+ console.error("Error: This script must be run in sandbox mode");
167
+ process.exit(1);
168
+ }
169
+ setupIPC();
170
+ }
171
+ main();
172
+
173
+ //#endregion
174
+ export { sendIPCRequest };
175
+ //# sourceMappingURL=bundled-executor.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-executor.mjs","names":["initMessage: SandboxInitMessage | null","message: ExecutionCompleteMessage"],"sources":["../../src/shared/constants.ts","../../src/shared/ipc-protocol.ts","../../src/sandbox/sandboxed-context.ts","../../src/sandbox/executor.ts"],"sourcesContent":["/**\n * Centralized constants for the KLY project\n * Prevents magic strings and improves maintainability\n */\n\n/**\n * Environment variable names used throughout the application\n */\nexport const ENV_VARS = {\n SANDBOX_MODE: \"KLY_SANDBOX_MODE\",\n MCP_MODE: \"KLY_MCP_MODE\",\n PROGRAMMATIC: \"KLY_PROGRAMMATIC\",\n TRUST_ALL: \"KLY_TRUST_ALL\",\n LOCAL_REF: \"KLY_LOCAL_REF\",\n REMOTE_REF: \"KLY_REMOTE_REF\",\n} as const;\n\n/**\n * File and directory paths used for configuration and caching\n */\nexport const PATHS = {\n CONFIG_DIR: \".kly\",\n META_FILE: \".kly-meta.json\",\n PERMISSIONS_FILE: \"permissions.json\",\n CONFIG_FILE: \"config.json\",\n} as const;\n\n/**\n * Timeout values in milliseconds\n */\nexport const TIMEOUTS = {\n /** Standard IPC request timeout (30 seconds) */\n IPC_REQUEST: 30_000,\n /** Long-running IPC request timeout (60 seconds) */\n IPC_LONG_REQUEST: 60_000,\n} as const;\n\n/**\n * LLM API domains for network permission configuration\n */\nexport const LLM_API_DOMAINS = [\n \"api.openai.com\",\n \"*.anthropic.com\",\n \"generativelanguage.googleapis.com\",\n \"api.deepseek.com\",\n] as const;\n","import type { SandboxRuntimeConfig } from \"@anthropic-ai/sandbox-runtime\";\nimport type { ModelConfig } from \"../types\";\n\n/**\n * Message sent from Host to Sandbox on initialization\n */\nexport interface SandboxInitMessage {\n type: \"init\";\n scriptPath: string;\n args: string[];\n appId: string;\n permissions: {\n allowApiKey: boolean;\n sandboxConfig: SandboxRuntimeConfig;\n };\n}\n\n/**\n * Request types sent from Sandbox to Host\n */\nexport type IPCRequest =\n | {\n type: \"getModelConfig\";\n id: string;\n payload: { name?: string };\n }\n | {\n type: \"listModels\";\n id: string;\n payload: Record<string, never>;\n }\n | {\n type: \"log\";\n id: string;\n payload: { level: \"info\" | \"warn\" | \"error\"; message: string };\n }\n | {\n type: \"prompt:input\";\n id: string;\n payload: {\n prompt: string;\n defaultValue?: string;\n placeholder?: string;\n maxLength?: number;\n };\n }\n | {\n type: \"prompt:select\";\n id: string;\n payload: {\n prompt: string;\n options: Array<{\n name: string;\n description?: string;\n value: string;\n }>;\n };\n }\n | {\n type: \"prompt:confirm\";\n id: string;\n payload: {\n message: string;\n defaultValue?: boolean;\n };\n }\n | {\n type: \"prompt:multiselect\";\n id: string;\n payload: {\n prompt: string;\n options: Array<{\n name: string;\n description?: string;\n value: string;\n }>;\n required?: boolean;\n };\n }\n | {\n type: \"prompt:form\";\n id: string;\n payload: {\n title?: string;\n fields: Array<{\n name: string;\n label: string;\n type: \"string\" | \"number\" | \"boolean\" | \"enum\";\n required?: boolean;\n defaultValue?: unknown;\n description?: string;\n enumValues?: string[];\n }>;\n };\n };\n\n/**\n * Response types sent from Host to Sandbox\n */\nexport type IPCResponse<T = unknown> =\n | {\n type: \"response\";\n id: string;\n success: true;\n data: T;\n }\n | {\n type: \"response\";\n id: string;\n success: false;\n error: string;\n };\n\n/**\n * Model info response (without sensitive data)\n */\nexport interface ModelInfoResponse {\n name: string;\n provider: string;\n model?: string;\n isCurrent: boolean;\n}\n\n/**\n * Model config response (with sensitive data like API keys)\n */\nexport interface ModelConfigResponse extends ModelConfig {\n provider: string;\n model?: string;\n apiKey?: string;\n baseURL?: string;\n}\n\n/**\n * Message sent from Sandbox to Host when execution completes\n */\nexport interface ExecutionCompleteMessage {\n type: \"complete\";\n success: boolean;\n result?: unknown;\n error?: string;\n}\n\n/**\n * Type guard for IPC messages\n */\nexport function isIPCRequest(msg: unknown): msg is IPCRequest {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n \"id\" in msg &&\n typeof msg.id === \"string\"\n );\n}\n\nexport function isIPCResponse(msg: unknown): msg is IPCResponse {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"response\" &&\n \"id\" in msg &&\n typeof msg.id === \"string\"\n );\n}\n\nexport function isSandboxInitMessage(msg: unknown): msg is SandboxInitMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"init\"\n );\n}\n\nexport function isExecutionCompleteMessage(\n msg: unknown,\n): msg is ExecutionCompleteMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"complete\"\n );\n}\n","import type { ModelConfigResponse } from \"../shared/ipc-protocol\";\nimport type { ModelConfig, ModelInfo, ModelsContext } from \"../types\";\nimport { sendIPCRequest } from \"./executor\";\n\n/**\n * Create sandboxed models context that communicates with host via IPC\n * This context is injected into user tools running in the sandbox\n * All API key access is controlled by the host process\n */\nexport function createSandboxedModelsContext(): ModelsContext {\n return {\n /**\n * List available models (no permission required)\n */\n list(): ModelInfo[] {\n throw new Error(\n \"Synchronous list() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get current model info (no permission required)\n */\n getCurrent(): ModelInfo | null {\n throw new Error(\n \"Synchronous getCurrent() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get model info by name (no permission required)\n */\n get(_name: string): ModelInfo | null {\n throw new Error(\n \"Synchronous get() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get model config with API key (requires permission, enforced by host)\n */\n async getConfigAsync(name?: string): Promise<ModelConfig | null> {\n try {\n const response = await sendIPCRequest<ModelConfigResponse | null>(\n \"getModelConfig\",\n { name },\n );\n\n if (!response) {\n return null;\n }\n\n return {\n provider: response.provider,\n model: response.model,\n apiKey: response.apiKey,\n baseURL: response.baseURL,\n };\n } catch (error) {\n // Re-throw with clear error message\n const message =\n error instanceof Error ? error.message : \"Failed to get model config\";\n throw new Error(`Permission denied: ${message}`);\n }\n },\n };\n}\n\n/**\n * Get the sandboxed context from global scope\n * This is injected by the executor before loading user scripts\n */\nexport function getSandboxedContext(): {\n modelsContext: ModelsContext;\n} {\n const globalWithContext = global as {\n __KLY_SANDBOXED_CONTEXT__?: {\n modelsContext: ModelsContext;\n };\n };\n\n if (!globalWithContext.__KLY_SANDBOXED_CONTEXT__) {\n throw new Error(\n \"Sandboxed context not available. This should only be called from within the sandbox.\",\n );\n }\n\n return globalWithContext.__KLY_SANDBOXED_CONTEXT__;\n}\n","#!/usr/bin/env bun\n\n/**\n * Sandbox Executor - Entry point for sandboxed child process\n * This file runs inside the sandbox and:\n * 1. Receives initialization message from host via IPC\n * 2. Loads and executes the user's script\n * 3. Provides sandboxed context to the script\n * 4. Sends execution results back to host\n */\n\nimport { TIMEOUTS } from \"../shared/constants\";\nimport type {\n ExecutionCompleteMessage,\n IPCResponse,\n SandboxInitMessage,\n} from \"../shared/ipc-protocol\";\nimport { isIPCResponse, isSandboxInitMessage } from \"../shared/ipc-protocol\";\nimport { createSandboxedModelsContext } from \"./sandboxed-context\";\n\n/**\n * Global state for the sandbox\n */\nlet initMessage: SandboxInitMessage | null = null;\nconst pendingIPCResponses = new Map<\n string,\n {\n resolve: (value: IPCResponse) => void;\n reject: (error: Error) => void;\n }\n>();\n\n/**\n * Set up IPC communication\n */\nfunction setupIPC() {\n if (!process.send) {\n throw new Error(\"IPC channel not available\");\n }\n\n process.on(\"message\", (message: unknown) => {\n // Handle init message\n if (isSandboxInitMessage(message)) {\n initMessage = message;\n // Start execution once we receive init\n executeUserScript().catch((error) => {\n sendExecutionComplete(false, undefined, error.message);\n process.exit(1);\n });\n return;\n }\n\n // Handle IPC responses\n if (isIPCResponse(message)) {\n const pending = pendingIPCResponses.get(message.id);\n if (pending) {\n pendingIPCResponses.delete(message.id);\n pending.resolve(message);\n }\n return;\n }\n });\n}\n\n/**\n * Send IPC request to host and wait for response\n */\nexport function sendIPCRequest<T>(type: string, payload: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n if (!process.send) {\n reject(new Error(\"IPC channel not available\"));\n return;\n }\n\n const id = `${type}-${Date.now()}-${Math.random()}`;\n const request = { type, id, payload };\n\n // Store pending promise\n pendingIPCResponses.set(id, {\n resolve: (response: IPCResponse) => {\n if (response.success) {\n resolve(response.data as T);\n } else {\n reject(new Error(response.error));\n }\n },\n reject,\n });\n\n // Send request\n process.send(request);\n\n // Timeout for standard IPC requests\n setTimeout(() => {\n const pending = pendingIPCResponses.get(id);\n if (pending) {\n pendingIPCResponses.delete(id);\n pending.reject(new Error(\"IPC request timeout\"));\n }\n }, TIMEOUTS.IPC_REQUEST);\n });\n}\n\n/**\n * Execute the user's script\n */\nasync function executeUserScript(): Promise<void> {\n if (!initMessage) {\n throw new Error(\"No initialization message received\");\n }\n\n const { scriptPath, args } = initMessage;\n\n try {\n // Set environment for the script\n process.argv = [\"bun\", scriptPath, ...args];\n process.env.KLY_SANDBOX_MODE = \"true\";\n\n // Inject sandboxed context into global scope\n // This allows defineApp to access the sandboxed context\n (\n global as { __KLY_SANDBOXED_CONTEXT__?: unknown }\n ).__KLY_SANDBOXED_CONTEXT__ = {\n modelsContext: createSandboxedModelsContext(),\n };\n\n // Import and execute the user's script\n // The script should use defineApp which will auto-execute in CLI mode\n await import(scriptPath);\n\n // If we reach here without error, execution succeeded\n sendExecutionComplete(true);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n sendExecutionComplete(false, undefined, errorMessage);\n throw error;\n }\n}\n\n/**\n * Send execution complete message to host\n */\nfunction sendExecutionComplete(\n success: boolean,\n result?: unknown,\n error?: string,\n): void {\n if (!process.send) {\n return;\n }\n\n const message: ExecutionCompleteMessage = {\n type: \"complete\",\n success,\n result,\n error,\n };\n\n process.send(message);\n}\n\n/**\n * Main entry point\n */\nfunction main() {\n // Ensure we're in sandbox mode\n if (process.env.KLY_SANDBOX_MODE !== \"true\") {\n console.error(\"Error: This script must be run in sandbox mode\");\n process.exit(1);\n }\n\n // Setup IPC\n setupIPC();\n\n // Wait for init message (handled by IPC listener)\n}\n\n// Start the executor\nmain();\n"],"mappings":";;;;;AA8BA,MAAa,WAAW;CAEtB,aAAa;CAEb,kBAAkB;CACnB;;;;ACyHD,SAAgB,cAAc,KAAkC;AAC9D,QACE,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACV,IAAI,SAAS,cACb,QAAQ,OACR,OAAO,IAAI,OAAO;;AAItB,SAAgB,qBAAqB,KAAyC;AAC5E,QACE,OAAO,QAAQ,YACf,QAAQ,QACR,UAAU,OACV,IAAI,SAAS;;;;;;;;;;ACnKjB,SAAgB,+BAA8C;AAC5D,QAAO;EAIL,OAAoB;AAClB,SAAM,IAAI,MACR,6FACD;;EAMH,aAA+B;AAC7B,SAAM,IAAI,MACR,mGACD;;EAMH,IAAI,OAAiC;AACnC,SAAM,IAAI,MACR,4FACD;;EAMH,MAAM,eAAe,MAA4C;AAC/D,OAAI;IACF,MAAM,WAAW,MAAM,eACrB,kBACA,EAAE,MAAM,CACT;AAED,QAAI,CAAC,SACH,QAAO;AAGT,WAAO;KACL,UAAU,SAAS;KACnB,OAAO,SAAS;KAChB,QAAQ,SAAS;KACjB,SAAS,SAAS;KACnB;YACM,OAAO;IAEd,MAAM,UACJ,iBAAiB,QAAQ,MAAM,UAAU;AAC3C,UAAM,IAAI,MAAM,sBAAsB,UAAU;;;EAGrD;;;;;;;;;;;;;;;;AC1CH,IAAIA,cAAyC;AAC7C,MAAM,sCAAsB,IAAI,KAM7B;;;;AAKH,SAAS,WAAW;AAClB,KAAI,CAAC,QAAQ,KACX,OAAM,IAAI,MAAM,4BAA4B;AAG9C,SAAQ,GAAG,YAAY,YAAqB;AAE1C,MAAI,qBAAqB,QAAQ,EAAE;AACjC,iBAAc;AAEd,sBAAmB,CAAC,OAAO,UAAU;AACnC,0BAAsB,OAAO,QAAW,MAAM,QAAQ;AACtD,YAAQ,KAAK,EAAE;KACf;AACF;;AAIF,MAAI,cAAc,QAAQ,EAAE;GAC1B,MAAM,UAAU,oBAAoB,IAAI,QAAQ,GAAG;AACnD,OAAI,SAAS;AACX,wBAAoB,OAAO,QAAQ,GAAG;AACtC,YAAQ,QAAQ,QAAQ;;AAE1B;;GAEF;;;;;AAMJ,SAAgB,eAAkB,MAAc,SAA8B;AAC5E,QAAO,IAAI,SAAS,SAAS,WAAW;AACtC,MAAI,CAAC,QAAQ,MAAM;AACjB,0BAAO,IAAI,MAAM,4BAA4B,CAAC;AAC9C;;EAGF,MAAM,KAAK,GAAG,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ;EACjD,MAAM,UAAU;GAAE;GAAM;GAAI;GAAS;AAGrC,sBAAoB,IAAI,IAAI;GAC1B,UAAU,aAA0B;AAClC,QAAI,SAAS,QACX,SAAQ,SAAS,KAAU;QAE3B,QAAO,IAAI,MAAM,SAAS,MAAM,CAAC;;GAGrC;GACD,CAAC;AAGF,UAAQ,KAAK,QAAQ;AAGrB,mBAAiB;GACf,MAAM,UAAU,oBAAoB,IAAI,GAAG;AAC3C,OAAI,SAAS;AACX,wBAAoB,OAAO,GAAG;AAC9B,YAAQ,uBAAO,IAAI,MAAM,sBAAsB,CAAC;;KAEjD,SAAS,YAAY;GACxB;;;;;AAMJ,eAAe,oBAAmC;AAChD,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,qCAAqC;CAGvD,MAAM,EAAE,YAAY,SAAS;AAE7B,KAAI;AAEF,UAAQ,OAAO;GAAC;GAAO;GAAY,GAAG;GAAK;AAC3C,UAAQ,IAAI,mBAAmB;AAI/B,EACE,OACA,4BAA4B,EAC5B,eAAe,8BAA8B,EAC9C;AAID,QAAM,OAAO;AAGb,wBAAsB,KAAK;UACpB,OAAO;AAEd,wBAAsB,OAAO,QADR,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CACtB;AACrD,QAAM;;;;;;AAOV,SAAS,sBACP,SACA,QACA,OACM;AACN,KAAI,CAAC,QAAQ,KACX;CAGF,MAAMC,UAAoC;EACxC,MAAM;EACN;EACA;EACA;EACD;AAED,SAAQ,KAAK,QAAQ;;;;;AAMvB,SAAS,OAAO;AAEd,KAAI,QAAQ,IAAI,qBAAqB,QAAQ;AAC3C,UAAQ,MAAM,iDAAiD;AAC/D,UAAQ,KAAK,EAAE;;AAIjB,WAAU;;AAMZ,MAAM"}
@@ -0,0 +1,40 @@
1
+ import { TIMEOUTS } from "../shared/constants.mjs";
2
+
3
+ //#region src/sandbox/ipc-client.ts
4
+ /**
5
+ * Send an IPC request to the host and wait for response
6
+ * Used by UI components and other sandbox code to communicate with the host process
7
+ */
8
+ async function sendIPCRequest(type, payload) {
9
+ if (!process.send) throw new Error("IPC not available - not running in sandbox mode");
10
+ return new Promise((resolve, reject) => {
11
+ const requestId = `${type}-${Date.now()}-${Math.random()}`;
12
+ const request = {
13
+ type,
14
+ id: requestId,
15
+ payload
16
+ };
17
+ const responseHandler = (message) => {
18
+ if (typeof message === "object" && message !== null && "type" in message && message.type === "response" && "id" in message && message.id === requestId) {
19
+ process.off("message", responseHandler);
20
+ const response = message;
21
+ if (response.success) resolve(response.data);
22
+ else reject(new Error(response.error));
23
+ }
24
+ };
25
+ process.on("message", responseHandler);
26
+ if (!process.send(request)) {
27
+ process.off("message", responseHandler);
28
+ reject(/* @__PURE__ */ new Error("Failed to send IPC message"));
29
+ return;
30
+ }
31
+ setTimeout(() => {
32
+ process.off("message", responseHandler);
33
+ reject(/* @__PURE__ */ new Error(`IPC request timeout: ${type}`));
34
+ }, TIMEOUTS.IPC_LONG_REQUEST);
35
+ });
36
+ }
37
+
38
+ //#endregion
39
+ export { sendIPCRequest };
40
+ //# sourceMappingURL=ipc-client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipc-client.mjs","names":["request: IPCRequest"],"sources":["../../src/sandbox/ipc-client.ts"],"sourcesContent":["import { TIMEOUTS } from \"../shared/constants\";\nimport type { IPCRequest, IPCResponse } from \"../shared/ipc-protocol\";\n\n/**\n * Send an IPC request to the host and wait for response\n * Used by UI components and other sandbox code to communicate with the host process\n */\nexport async function sendIPCRequest<T>(\n type: IPCRequest[\"type\"],\n payload: unknown,\n): Promise<T> {\n if (!process.send) {\n throw new Error(\"IPC not available - not running in sandbox mode\");\n }\n\n return new Promise((resolve, reject) => {\n const requestId = `${type}-${Date.now()}-${Math.random()}`;\n\n const request: IPCRequest = {\n type,\n id: requestId,\n payload,\n } as IPCRequest;\n\n // Set up response listener\n const responseHandler = (message: unknown) => {\n if (\n typeof message === \"object\" &&\n message !== null &&\n \"type\" in message &&\n message.type === \"response\" &&\n \"id\" in message &&\n message.id === requestId\n ) {\n process.off(\"message\", responseHandler);\n\n const response = message as IPCResponse<T>;\n if (response.success) {\n resolve(response.data);\n } else {\n reject(new Error(response.error));\n }\n }\n };\n\n process.on(\"message\", responseHandler);\n\n // Send request\n if (!process.send!(request)) {\n // Send failed immediately\n process.off(\"message\", responseHandler);\n reject(new Error(\"Failed to send IPC message\"));\n return;\n }\n\n // Timeout for long-running requests (prompts, etc.)\n setTimeout(() => {\n process.off(\"message\", responseHandler);\n reject(new Error(`IPC request timeout: ${type}`));\n }, TIMEOUTS.IPC_LONG_REQUEST);\n });\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,eACpB,MACA,SACY;AACZ,KAAI,CAAC,QAAQ,KACX,OAAM,IAAI,MAAM,kDAAkD;AAGpE,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ;EAExD,MAAMA,UAAsB;GAC1B;GACA,IAAI;GACJ;GACD;EAGD,MAAM,mBAAmB,YAAqB;AAC5C,OACE,OAAO,YAAY,YACnB,YAAY,QACZ,UAAU,WACV,QAAQ,SAAS,cACjB,QAAQ,WACR,QAAQ,OAAO,WACf;AACA,YAAQ,IAAI,WAAW,gBAAgB;IAEvC,MAAM,WAAW;AACjB,QAAI,SAAS,QACX,SAAQ,SAAS,KAAK;QAEtB,QAAO,IAAI,MAAM,SAAS,MAAM,CAAC;;;AAKvC,UAAQ,GAAG,WAAW,gBAAgB;AAGtC,MAAI,CAAC,QAAQ,KAAM,QAAQ,EAAE;AAE3B,WAAQ,IAAI,WAAW,gBAAgB;AACvC,0BAAO,IAAI,MAAM,6BAA6B,CAAC;AAC/C;;AAIF,mBAAiB;AACf,WAAQ,IAAI,WAAW,gBAAgB;AACvC,0BAAO,IAAI,MAAM,wBAAwB,OAAO,CAAC;KAChD,SAAS,iBAAiB;GAC7B"}
@@ -0,0 +1,14 @@
1
+ //#region src/sandbox/sandboxed-context.ts
2
+ /**
3
+ * Get the sandboxed context from global scope
4
+ * This is injected by the executor before loading user scripts
5
+ */
6
+ function getSandboxedContext() {
7
+ const globalWithContext = global;
8
+ if (!globalWithContext.__KLY_SANDBOXED_CONTEXT__) throw new Error("Sandboxed context not available. This should only be called from within the sandbox.");
9
+ return globalWithContext.__KLY_SANDBOXED_CONTEXT__;
10
+ }
11
+
12
+ //#endregion
13
+ export { getSandboxedContext };
14
+ //# sourceMappingURL=sandboxed-context.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandboxed-context.mjs","names":[],"sources":["../../src/sandbox/sandboxed-context.ts"],"sourcesContent":["import type { ModelConfigResponse } from \"../shared/ipc-protocol\";\nimport type { ModelConfig, ModelInfo, ModelsContext } from \"../types\";\nimport { sendIPCRequest } from \"./executor\";\n\n/**\n * Create sandboxed models context that communicates with host via IPC\n * This context is injected into user tools running in the sandbox\n * All API key access is controlled by the host process\n */\nexport function createSandboxedModelsContext(): ModelsContext {\n return {\n /**\n * List available models (no permission required)\n */\n list(): ModelInfo[] {\n throw new Error(\n \"Synchronous list() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get current model info (no permission required)\n */\n getCurrent(): ModelInfo | null {\n throw new Error(\n \"Synchronous getCurrent() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get model info by name (no permission required)\n */\n get(_name: string): ModelInfo | null {\n throw new Error(\n \"Synchronous get() not supported in sandbox. Use async methods or move this logic to host.\",\n );\n },\n\n /**\n * Get model config with API key (requires permission, enforced by host)\n */\n async getConfigAsync(name?: string): Promise<ModelConfig | null> {\n try {\n const response = await sendIPCRequest<ModelConfigResponse | null>(\n \"getModelConfig\",\n { name },\n );\n\n if (!response) {\n return null;\n }\n\n return {\n provider: response.provider,\n model: response.model,\n apiKey: response.apiKey,\n baseURL: response.baseURL,\n };\n } catch (error) {\n // Re-throw with clear error message\n const message =\n error instanceof Error ? error.message : \"Failed to get model config\";\n throw new Error(`Permission denied: ${message}`);\n }\n },\n };\n}\n\n/**\n * Get the sandboxed context from global scope\n * This is injected by the executor before loading user scripts\n */\nexport function getSandboxedContext(): {\n modelsContext: ModelsContext;\n} {\n const globalWithContext = global as {\n __KLY_SANDBOXED_CONTEXT__?: {\n modelsContext: ModelsContext;\n };\n };\n\n if (!globalWithContext.__KLY_SANDBOXED_CONTEXT__) {\n throw new Error(\n \"Sandboxed context not available. This should only be called from within the sandbox.\",\n );\n }\n\n return globalWithContext.__KLY_SANDBOXED_CONTEXT__;\n}\n"],"mappings":";;;;;AAwEA,SAAgB,sBAEd;CACA,MAAM,oBAAoB;AAM1B,KAAI,CAAC,kBAAkB,0BACrB,OAAM,IAAI,MACR,uFACD;AAGH,QAAO,kBAAkB"}
@@ -0,0 +1,36 @@
1
+ //#region src/shared/constants.ts
2
+ /**
3
+ * Centralized constants for the KLY project
4
+ * Prevents magic strings and improves maintainability
5
+ */
6
+ /**
7
+ * Environment variable names used throughout the application
8
+ */
9
+ const ENV_VARS = {
10
+ SANDBOX_MODE: "KLY_SANDBOX_MODE",
11
+ MCP_MODE: "KLY_MCP_MODE",
12
+ PROGRAMMATIC: "KLY_PROGRAMMATIC",
13
+ TRUST_ALL: "KLY_TRUST_ALL",
14
+ LOCAL_REF: "KLY_LOCAL_REF",
15
+ REMOTE_REF: "KLY_REMOTE_REF"
16
+ };
17
+ /**
18
+ * File and directory paths used for configuration and caching
19
+ */
20
+ const PATHS = {
21
+ CONFIG_DIR: ".kly",
22
+ META_FILE: ".kly-meta.json",
23
+ PERMISSIONS_FILE: "permissions.json",
24
+ CONFIG_FILE: "config.json"
25
+ };
26
+ /**
27
+ * Timeout values in milliseconds
28
+ */
29
+ const TIMEOUTS = {
30
+ IPC_REQUEST: 3e4,
31
+ IPC_LONG_REQUEST: 6e4
32
+ };
33
+
34
+ //#endregion
35
+ export { ENV_VARS, PATHS, TIMEOUTS };
36
+ //# sourceMappingURL=constants.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.mjs","names":[],"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Centralized constants for the KLY project\n * Prevents magic strings and improves maintainability\n */\n\n/**\n * Environment variable names used throughout the application\n */\nexport const ENV_VARS = {\n SANDBOX_MODE: \"KLY_SANDBOX_MODE\",\n MCP_MODE: \"KLY_MCP_MODE\",\n PROGRAMMATIC: \"KLY_PROGRAMMATIC\",\n TRUST_ALL: \"KLY_TRUST_ALL\",\n LOCAL_REF: \"KLY_LOCAL_REF\",\n REMOTE_REF: \"KLY_REMOTE_REF\",\n} as const;\n\n/**\n * File and directory paths used for configuration and caching\n */\nexport const PATHS = {\n CONFIG_DIR: \".kly\",\n META_FILE: \".kly-meta.json\",\n PERMISSIONS_FILE: \"permissions.json\",\n CONFIG_FILE: \"config.json\",\n} as const;\n\n/**\n * Timeout values in milliseconds\n */\nexport const TIMEOUTS = {\n /** Standard IPC request timeout (30 seconds) */\n IPC_REQUEST: 30_000,\n /** Long-running IPC request timeout (60 seconds) */\n IPC_LONG_REQUEST: 60_000,\n} as const;\n\n/**\n * LLM API domains for network permission configuration\n */\nexport const LLM_API_DOMAINS = [\n \"api.openai.com\",\n \"*.anthropic.com\",\n \"generativelanguage.googleapis.com\",\n \"api.deepseek.com\",\n] as const;\n"],"mappings":";;;;;;;;AAQA,MAAa,WAAW;CACtB,cAAc;CACd,UAAU;CACV,cAAc;CACd,WAAW;CACX,WAAW;CACX,YAAY;CACb;;;;AAKD,MAAa,QAAQ;CACnB,YAAY;CACZ,WAAW;CACX,kBAAkB;CAClB,aAAa;CACd;;;;AAKD,MAAa,WAAW;CAEtB,aAAa;CAEb,kBAAkB;CACnB"}