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,2888 @@
1
+ #!/usr/bin/env bun
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import * as clack from "@clack/prompts";
4
+ import pc, { default as pc$1 } from "picocolors";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { exec, spawn } from "node:child_process";
8
+ import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
9
+ import { promisify } from "node:util";
10
+ import { createHash } from "node:crypto";
11
+
12
+ //#region rolldown:runtime
13
+ var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+
15
+ //#endregion
16
+ //#region src/ai/models-dev.ts
17
+ const MODELS_DEV_API = "https://models.dev/api.json";
18
+ const CACHE_FILE = join(homedir(), ".kly", "models-cache.json");
19
+ const CACHE_TTL = 1440 * 60 * 1e3;
20
+ /**
21
+ * Fetch models.dev data with caching
22
+ */
23
+ async function fetchModelsDevData(forceRefresh = false) {
24
+ if (!forceRefresh && existsSync(CACHE_FILE)) try {
25
+ const cached = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
26
+ if (Date.now() - cached.timestamp < CACHE_TTL) return cached;
27
+ } catch (_error) {}
28
+ try {
29
+ const response = await fetch(MODELS_DEV_API);
30
+ if (!response.ok) return null;
31
+ const cachedData = {
32
+ providers: await response.json(),
33
+ timestamp: Date.now()
34
+ };
35
+ try {
36
+ writeFileSync(CACHE_FILE, JSON.stringify(cachedData, null, 2), "utf-8");
37
+ } catch (_error) {}
38
+ return cachedData;
39
+ } catch (_error) {
40
+ if (existsSync(CACHE_FILE)) try {
41
+ return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
42
+ } catch {}
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Map our provider IDs to models.dev IDs
48
+ */
49
+ const PROVIDER_ID_MAP = {
50
+ openai: "openai",
51
+ anthropic: "anthropic",
52
+ google: "google",
53
+ deepseek: "deepseek",
54
+ groq: "groq",
55
+ mistral: "mistral",
56
+ cohere: "cohere",
57
+ ollama: "ollama"
58
+ };
59
+ /**
60
+ * Get provider info by our internal provider ID
61
+ */
62
+ function getProviderInfo(data, provider) {
63
+ const modelsDevId = PROVIDER_ID_MAP[provider];
64
+ if (!modelsDevId) return null;
65
+ return data.providers[modelsDevId] || null;
66
+ }
67
+ /**
68
+ * Get all models for a provider
69
+ */
70
+ function getProviderModels(data, provider) {
71
+ const providerInfo = getProviderInfo(data, provider);
72
+ if (!providerInfo) return [];
73
+ return Object.values(providerInfo.models);
74
+ }
75
+ /**
76
+ * Get model info by ID
77
+ */
78
+ function getModelInfo(data, provider, modelId) {
79
+ const providerInfo = getProviderInfo(data, provider);
80
+ if (!providerInfo) return null;
81
+ return providerInfo.models[modelId] || null;
82
+ }
83
+ /**
84
+ * Format price for display
85
+ */
86
+ function formatPrice(pricePerMillion) {
87
+ if (pricePerMillion === void 0) return "N/A";
88
+ if (pricePerMillion === 0) return "Free";
89
+ return pricePerMillion.toFixed(2).replace(/\.?0+$/, "");
90
+ }
91
+ /**
92
+ * Format capabilities for display
93
+ */
94
+ function formatCapabilities(model) {
95
+ const caps = [];
96
+ if (model.tool_call) caps.push("Tools");
97
+ if (model.reasoning) caps.push("Reasoning");
98
+ if (model.structured_output) caps.push("JSON");
99
+ if (model.attachment) caps.push("Files");
100
+ return caps;
101
+ }
102
+
103
+ //#endregion
104
+ //#region src/ai/provider-config.ts
105
+ /**
106
+ * Provider configurations
107
+ * Based on https://models.dev/api.json
108
+ */
109
+ const PROVIDER_CONFIGS = {
110
+ openai: {
111
+ docURL: "https://platform.openai.com/docs",
112
+ description: "OpenAI's GPT models"
113
+ },
114
+ anthropic: {
115
+ docURL: "https://docs.anthropic.com",
116
+ description: "Anthropic's Claude models"
117
+ },
118
+ google: {
119
+ docURL: "https://ai.google.dev/docs",
120
+ description: "Google's Gemini models"
121
+ },
122
+ deepseek: {
123
+ docURL: "https://platform.deepseek.com/docs",
124
+ description: "DeepSeek's AI models"
125
+ },
126
+ groq: {
127
+ baseURL: "https://api.groq.com/openai/v1",
128
+ docURL: "https://console.groq.com/docs",
129
+ description: "Ultra-fast LLM inference"
130
+ },
131
+ mistral: {
132
+ docURL: "https://docs.mistral.ai",
133
+ description: "Mistral AI models"
134
+ },
135
+ cohere: {
136
+ baseURL: "https://api.cohere.ai/v1",
137
+ docURL: "https://docs.cohere.com",
138
+ description: "Cohere's language models"
139
+ },
140
+ ollama: {
141
+ baseURL: "http://localhost:11434/v1",
142
+ docURL: "https://ollama.ai",
143
+ description: "Local AI models"
144
+ }
145
+ };
146
+ /**
147
+ * Get default base URL for a provider
148
+ */
149
+ function getDefaultBaseURL(provider) {
150
+ return PROVIDER_CONFIGS[provider]?.baseURL;
151
+ }
152
+ /**
153
+ * Get provider description
154
+ */
155
+ function getProviderDescription(provider) {
156
+ return PROVIDER_CONFIGS[provider]?.description;
157
+ }
158
+
159
+ //#endregion
160
+ //#region src/ai/storage.ts
161
+ const CONFIG_DIR$1 = join(homedir(), ".kly");
162
+ const CONFIG_FILE = join(CONFIG_DIR$1, "config.json");
163
+ /**
164
+ * Ensure config directory exists
165
+ */
166
+ function ensureConfigDir() {
167
+ if (!existsSync(CONFIG_DIR$1)) mkdirSync(CONFIG_DIR$1, { recursive: true });
168
+ }
169
+ /**
170
+ * Load configuration from ~/.kly/config.json
171
+ */
172
+ function loadConfig() {
173
+ ensureConfigDir();
174
+ if (!existsSync(CONFIG_FILE)) return { models: {} };
175
+ try {
176
+ const content = readFileSync(CONFIG_FILE, "utf-8");
177
+ return JSON.parse(content);
178
+ } catch (error) {
179
+ console.error("Failed to parse config file:", error);
180
+ return { models: {} };
181
+ }
182
+ }
183
+ /**
184
+ * Save configuration to ~/.kly/config.json
185
+ */
186
+ function saveConfig(config) {
187
+ ensureConfigDir();
188
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
189
+ }
190
+ /**
191
+ * Get current active model configuration
192
+ */
193
+ function getCurrentModelConfig() {
194
+ const config = loadConfig();
195
+ if (!config.currentModel) return null;
196
+ return config.models[config.currentModel] || null;
197
+ }
198
+ /**
199
+ * Set a model as current
200
+ */
201
+ function setCurrentModel(modelName) {
202
+ const config = loadConfig();
203
+ if (!config.models[modelName]) throw new Error(`Model '${modelName}' not found in config`);
204
+ config.currentModel = modelName;
205
+ saveConfig(config);
206
+ }
207
+ /**
208
+ * Add or update a model configuration
209
+ */
210
+ function saveModelConfig(modelName, modelConfig) {
211
+ const config = loadConfig();
212
+ config.models[modelName] = modelConfig;
213
+ if (!config.currentModel) config.currentModel = modelName;
214
+ saveConfig(config);
215
+ }
216
+ /**
217
+ * Remove a model configuration
218
+ */
219
+ function removeModelConfig(modelName) {
220
+ const config = loadConfig();
221
+ delete config.models[modelName];
222
+ if (config.currentModel === modelName) config.currentModel = void 0;
223
+ saveConfig(config);
224
+ }
225
+ /**
226
+ * List all configured models
227
+ */
228
+ function listModels() {
229
+ const config = loadConfig();
230
+ return Object.entries(config.models).map(([name, modelConfig]) => ({
231
+ name,
232
+ config: modelConfig,
233
+ isCurrent: name === config.currentModel
234
+ }));
235
+ }
236
+ /**
237
+ * Get provider display name
238
+ */
239
+ function getProviderDisplayName(provider) {
240
+ return {
241
+ openai: "OpenAI",
242
+ anthropic: "Anthropic",
243
+ google: "Google",
244
+ deepseek: "DeepSeek",
245
+ ollama: "Ollama",
246
+ groq: "Groq",
247
+ mistral: "Mistral",
248
+ cohere: "Cohere",
249
+ "openai-compatible": "OpenAI Compatible"
250
+ }[provider];
251
+ }
252
+
253
+ //#endregion
254
+ //#region src/ai/types.ts
255
+ /**
256
+ * Default models for each provider
257
+ * Based on https://models.dev/ (2025-12)
258
+ */
259
+ const DEFAULT_MODELS = {
260
+ openai: "gpt-4o-mini",
261
+ anthropic: "claude-3-5-sonnet-20241022",
262
+ google: "gemini-2.5-flash",
263
+ deepseek: "deepseek-v3",
264
+ ollama: "llama3.2",
265
+ groq: "llama-3.3-70b-versatile",
266
+ mistral: "mistral-large-2411",
267
+ cohere: "command-r-plus",
268
+ "openai-compatible": "gpt-4o-mini"
269
+ };
270
+
271
+ //#endregion
272
+ //#region src/ai/models-command.ts
273
+ const PROVIDER_OPTIONS = [
274
+ {
275
+ value: "openai",
276
+ label: "OpenAI",
277
+ hint: getProviderDescription("openai")
278
+ },
279
+ {
280
+ value: "anthropic",
281
+ label: "Anthropic",
282
+ hint: getProviderDescription("anthropic")
283
+ },
284
+ {
285
+ value: "google",
286
+ label: "Google",
287
+ hint: getProviderDescription("google")
288
+ },
289
+ {
290
+ value: "deepseek",
291
+ label: "DeepSeek",
292
+ hint: getProviderDescription("deepseek")
293
+ },
294
+ {
295
+ value: "groq",
296
+ label: "Groq",
297
+ hint: getProviderDescription("groq")
298
+ },
299
+ {
300
+ value: "mistral",
301
+ label: "Mistral",
302
+ hint: getProviderDescription("mistral")
303
+ },
304
+ {
305
+ value: "cohere",
306
+ label: "Cohere",
307
+ hint: getProviderDescription("cohere")
308
+ },
309
+ {
310
+ value: "ollama",
311
+ label: "Ollama",
312
+ hint: getProviderDescription("ollama")
313
+ },
314
+ {
315
+ value: "openai-compatible",
316
+ label: "OpenAI Compatible",
317
+ hint: "Custom endpoint"
318
+ }
319
+ ];
320
+ /**
321
+ * Main entry point for `kly models` command
322
+ */
323
+ async function modelsCommand() {
324
+ clack.intro(pc.bgCyan(pc.black(" kly models ")));
325
+ const models = listModels();
326
+ const action = await clack.select({
327
+ message: "What would you like to do?",
328
+ options: [
329
+ {
330
+ value: "list",
331
+ label: "List configured models"
332
+ },
333
+ {
334
+ value: "add",
335
+ label: "Add a new model"
336
+ },
337
+ {
338
+ value: "switch",
339
+ label: "Switch current model",
340
+ disabled: models.length === 0
341
+ },
342
+ {
343
+ value: "remove",
344
+ label: "Remove a model",
345
+ disabled: models.length === 0
346
+ }
347
+ ]
348
+ });
349
+ if (clack.isCancel(action)) {
350
+ clack.cancel("Operation cancelled");
351
+ process.exit(0);
352
+ }
353
+ switch (action) {
354
+ case "list":
355
+ await listAction();
356
+ break;
357
+ case "add":
358
+ await addAction();
359
+ break;
360
+ case "switch":
361
+ await switchAction();
362
+ break;
363
+ case "remove":
364
+ await removeAction();
365
+ break;
366
+ }
367
+ clack.outro(pc.green("Done!"));
368
+ }
369
+ /**
370
+ * List all configured models
371
+ */
372
+ async function listAction() {
373
+ const models = listModels();
374
+ if (models.length === 0) {
375
+ clack.note("No models configured yet.\nRun 'kly models' and select 'Add a new model'");
376
+ return;
377
+ }
378
+ const modelsData = await fetchModelsDevData();
379
+ const lines = [];
380
+ for (const model of models) {
381
+ const current = model.isCurrent ? pc.green("āœ“ ") : " ";
382
+ const provider = getProviderDisplayName(model.config.provider);
383
+ const modelName = model.config.model || DEFAULT_MODELS[model.config.provider];
384
+ let line = `${current}${pc.cyan(model.name)} - ${provider} (${modelName})`;
385
+ if (modelsData) {
386
+ const modelInfo = getModelInfo(modelsData, model.config.provider, modelName);
387
+ if (modelInfo) {
388
+ const metadata = formatModelMetadata(modelInfo);
389
+ if (metadata) line += ` ${pc.dim(metadata)}`;
390
+ }
391
+ }
392
+ lines.push(line);
393
+ }
394
+ clack.note(lines.join("\n"), "Configured models:");
395
+ }
396
+ /**
397
+ * Format model metadata (pricing and capabilities) for display
398
+ */
399
+ function formatModelMetadata(modelInfo) {
400
+ const parts = [];
401
+ if (modelInfo.cost?.input !== void 0 && modelInfo.cost?.output !== void 0) parts.push(`[$${formatPrice(modelInfo.cost.input)}/$${formatPrice(modelInfo.cost.output)} per 1M]`);
402
+ const caps = formatCapabilities(modelInfo);
403
+ if (caps.length > 0) parts.push(`[${caps.join(", ")}]`);
404
+ return parts.join(" ");
405
+ }
406
+ /**
407
+ * Add a new model configuration
408
+ */
409
+ async function addAction() {
410
+ const name = await clack.text({
411
+ message: "Enter a name for this model configuration:",
412
+ placeholder: "my-openai",
413
+ validate: (value) => {
414
+ if (!value) return "Name is required";
415
+ if (listModels().some((m) => m.name === value)) return "A model with this name already exists";
416
+ }
417
+ });
418
+ if (clack.isCancel(name)) {
419
+ clack.cancel("Operation cancelled");
420
+ process.exit(0);
421
+ }
422
+ const provider = await clack.select({
423
+ message: "Select a provider:",
424
+ options: PROVIDER_OPTIONS
425
+ });
426
+ if (clack.isCancel(provider)) {
427
+ clack.cancel("Operation cancelled");
428
+ process.exit(0);
429
+ }
430
+ saveModelConfig(name, await getProviderConfig(provider));
431
+ clack.note(`Model '${pc.cyan(name)}' configured with ${getProviderDisplayName(provider)}`, pc.green("Success!"));
432
+ }
433
+ /**
434
+ * Switch to a different model
435
+ */
436
+ async function switchAction() {
437
+ const models = listModels();
438
+ if (models.length === 0) {
439
+ clack.note("No models configured");
440
+ return;
441
+ }
442
+ const modelName = await clack.select({
443
+ message: "Select a model:",
444
+ options: models.map((m) => ({
445
+ value: m.name,
446
+ label: m.name,
447
+ hint: `${getProviderDisplayName(m.config.provider)} - ${m.config.model || DEFAULT_MODELS[m.config.provider]}`
448
+ }))
449
+ });
450
+ if (clack.isCancel(modelName)) {
451
+ clack.cancel("Operation cancelled");
452
+ process.exit(0);
453
+ }
454
+ setCurrentModel(modelName);
455
+ clack.note(`Switched to '${pc.cyan(modelName)}'`, pc.green("Success!"));
456
+ }
457
+ /**
458
+ * Remove a model configuration
459
+ */
460
+ async function removeAction() {
461
+ const models = listModels();
462
+ if (models.length === 0) {
463
+ clack.note("No models configured");
464
+ return;
465
+ }
466
+ const modelName = await clack.select({
467
+ message: "Select a model to remove:",
468
+ options: models.map((m) => ({
469
+ value: m.name,
470
+ label: m.name,
471
+ hint: `${getProviderDisplayName(m.config.provider)}`
472
+ }))
473
+ });
474
+ if (clack.isCancel(modelName)) {
475
+ clack.cancel("Operation cancelled");
476
+ process.exit(0);
477
+ }
478
+ const confirm$1 = await clack.confirm({ message: `Are you sure you want to remove '${modelName}'?` });
479
+ if (clack.isCancel(confirm$1) || !confirm$1) {
480
+ clack.cancel("Operation cancelled");
481
+ process.exit(0);
482
+ }
483
+ removeModelConfig(modelName);
484
+ clack.note(`Removed '${pc.cyan(modelName)}'`, pc.green("Success!"));
485
+ }
486
+ /**
487
+ * Get provider-specific configuration
488
+ */
489
+ async function getProviderConfig(provider) {
490
+ const defaultModel = DEFAULT_MODELS[provider];
491
+ if (provider === "ollama") {
492
+ const baseURL$1 = await clack.text({
493
+ message: "Ollama base URL:",
494
+ placeholder: "http://localhost:11434",
495
+ defaultValue: "http://localhost:11434"
496
+ });
497
+ if (clack.isCancel(baseURL$1)) {
498
+ clack.cancel("Operation cancelled");
499
+ process.exit(0);
500
+ }
501
+ const model$1 = await clack.text({
502
+ message: "Model name:",
503
+ placeholder: defaultModel,
504
+ defaultValue: defaultModel
505
+ });
506
+ if (clack.isCancel(model$1)) {
507
+ clack.cancel("Operation cancelled");
508
+ process.exit(0);
509
+ }
510
+ return {
511
+ provider,
512
+ baseURL: baseURL$1 || "http://localhost:11434",
513
+ model: model$1 || defaultModel
514
+ };
515
+ }
516
+ const apiKey = await clack.password({
517
+ message: `Enter your ${getProviderDisplayName(provider)} API key:`,
518
+ validate: (value) => {
519
+ if (!value) return "API key is required";
520
+ }
521
+ });
522
+ if (clack.isCancel(apiKey)) {
523
+ clack.cancel("Operation cancelled");
524
+ process.exit(0);
525
+ }
526
+ const customBaseURL = await clack.confirm({
527
+ message: "Do you want to specify a custom base URL?",
528
+ initialValue: false
529
+ });
530
+ if (clack.isCancel(customBaseURL)) {
531
+ clack.cancel("Operation cancelled");
532
+ process.exit(0);
533
+ }
534
+ let baseURL;
535
+ if (customBaseURL) {
536
+ const defaultURL = getDefaultBaseURL(provider);
537
+ const baseURLInput = await clack.text({
538
+ message: "Base URL:",
539
+ placeholder: defaultURL || ""
540
+ });
541
+ if (clack.isCancel(baseURLInput)) {
542
+ clack.cancel("Operation cancelled");
543
+ process.exit(0);
544
+ }
545
+ baseURL = baseURLInput || void 0;
546
+ }
547
+ const model = await selectModelForProvider(provider, defaultModel);
548
+ return {
549
+ provider,
550
+ apiKey,
551
+ baseURL,
552
+ model
553
+ };
554
+ }
555
+ /**
556
+ * Let user select a model for a provider
557
+ */
558
+ async function selectModelForProvider(provider, defaultModel) {
559
+ const modelsData = await fetchModelsDevData();
560
+ if (modelsData) {
561
+ const availableModels = getProviderModels(modelsData, provider);
562
+ if (availableModels.length > 0) return await selectFromModelList(availableModels, defaultModel);
563
+ }
564
+ return await selectModelWithInput(defaultModel);
565
+ }
566
+ /**
567
+ * Show model selection list with pricing and capabilities
568
+ */
569
+ async function selectFromModelList(availableModels, defaultModel) {
570
+ const modelOptions = availableModels.slice(0, 10).map((m) => {
571
+ const parts = [];
572
+ if (m.cost?.input !== void 0 && m.cost?.output !== void 0) parts.push(`$${formatPrice(m.cost.input)}/$${formatPrice(m.cost.output)} per 1M`);
573
+ const caps = formatCapabilities(m);
574
+ if (caps.length > 0) parts.push(caps.join(", "));
575
+ return {
576
+ value: m.id,
577
+ label: m.name || m.id,
578
+ hint: parts.length > 0 ? parts.join(" • ") : "No info available"
579
+ };
580
+ });
581
+ modelOptions.push({
582
+ value: "__default__",
583
+ label: `Use default (${defaultModel})`,
584
+ hint: "Recommended"
585
+ });
586
+ modelOptions.push({
587
+ value: "__custom__",
588
+ label: "Enter custom model name",
589
+ hint: "Advanced"
590
+ });
591
+ const selectedModel = await clack.select({
592
+ message: "Select a model:",
593
+ options: modelOptions
594
+ });
595
+ if (clack.isCancel(selectedModel)) {
596
+ clack.cancel("Operation cancelled");
597
+ process.exit(0);
598
+ }
599
+ if (selectedModel === "__default__") return;
600
+ if (selectedModel === "__custom__") return await promptForModelName(defaultModel);
601
+ return selectedModel;
602
+ }
603
+ /**
604
+ * Simple model selection via confirm + input
605
+ */
606
+ async function selectModelWithInput(defaultModel) {
607
+ const useDefault = await clack.confirm({
608
+ message: `Use default model (${defaultModel})?`,
609
+ initialValue: true
610
+ });
611
+ if (clack.isCancel(useDefault)) {
612
+ clack.cancel("Operation cancelled");
613
+ process.exit(0);
614
+ }
615
+ if (useDefault) return;
616
+ return await promptForModelName(defaultModel);
617
+ }
618
+ /**
619
+ * Prompt user to enter a custom model name
620
+ */
621
+ async function promptForModelName(defaultModel) {
622
+ const modelInput = await clack.text({
623
+ message: "Model name:",
624
+ placeholder: defaultModel,
625
+ defaultValue: defaultModel
626
+ });
627
+ if (clack.isCancel(modelInput)) {
628
+ clack.cancel("Operation cancelled");
629
+ process.exit(0);
630
+ }
631
+ return modelInput || defaultModel;
632
+ }
633
+
634
+ //#endregion
635
+ //#region src/shared/ipc-protocol.ts
636
+ /**
637
+ * Type guard for IPC messages
638
+ */
639
+ function isIPCRequest(msg) {
640
+ return typeof msg === "object" && msg !== null && "type" in msg && "id" in msg && typeof msg.id === "string";
641
+ }
642
+ function isExecutionCompleteMessage(msg) {
643
+ return typeof msg === "object" && msg !== null && "type" in msg && msg.type === "complete";
644
+ }
645
+
646
+ //#endregion
647
+ //#region src/host/resource-provider.ts
648
+ /**
649
+ * Resource Provider - Host-side IPC server
650
+ * Handles resource requests from the sandboxed child process
651
+ * Enforces permissions and provides controlled access to sensitive resources
652
+ */
653
+ var ResourceProvider = class {
654
+ constructor(options) {
655
+ this.options = options;
656
+ }
657
+ /**
658
+ * Handle an IPC request from sandbox
659
+ */
660
+ async handle(request) {
661
+ try {
662
+ switch (request.type) {
663
+ case "listModels": return this.handleListModels(request.id);
664
+ case "getModelConfig": return this.handleGetModelConfig(request.id, request.payload.name);
665
+ case "log": return this.handleLog(request.id, request.payload.level, request.payload.message);
666
+ case "prompt:input": return this.handlePromptInput(request.id, request.payload);
667
+ case "prompt:select": return this.handlePromptSelect(request.id, request.payload);
668
+ case "prompt:confirm": return this.handlePromptConfirm(request.id, request.payload);
669
+ case "prompt:multiselect": return this.handlePromptMultiselect(request.id, request.payload);
670
+ case "prompt:form": return this.handlePromptForm(request.id, request.payload);
671
+ default: {
672
+ const unknownRequest = request;
673
+ return {
674
+ type: "response",
675
+ id: unknownRequest.id,
676
+ success: false,
677
+ error: `Unknown request type: ${unknownRequest.type}`
678
+ };
679
+ }
680
+ }
681
+ } catch (error) {
682
+ return {
683
+ type: "response",
684
+ id: request.id,
685
+ success: false,
686
+ error: error instanceof Error ? error.message : String(error)
687
+ };
688
+ }
689
+ }
690
+ /**
691
+ * Handle: List available models (no permission required)
692
+ */
693
+ handleListModels(requestId) {
694
+ return {
695
+ type: "response",
696
+ id: requestId,
697
+ success: true,
698
+ data: listModels().map((m) => ({
699
+ name: m.name,
700
+ provider: m.config.provider,
701
+ model: m.config.model,
702
+ isCurrent: m.isCurrent
703
+ }))
704
+ };
705
+ }
706
+ /**
707
+ * Handle: Get model config with API key (requires permission)
708
+ */
709
+ handleGetModelConfig(requestId, name) {
710
+ if (!this.options.allowApiKey) return {
711
+ type: "response",
712
+ id: requestId,
713
+ success: false,
714
+ error: "Permission denied: API key access not allowed for this app"
715
+ };
716
+ let modelConfig = null;
717
+ if (name) {
718
+ const found = listModels().find((m) => m.name === name);
719
+ if (found) modelConfig = {
720
+ provider: found.config.provider,
721
+ model: found.config.model,
722
+ apiKey: found.config.apiKey,
723
+ baseURL: found.config.baseURL
724
+ };
725
+ } else {
726
+ const current = getCurrentModelConfig();
727
+ if (current) modelConfig = {
728
+ provider: current.provider,
729
+ model: current.model,
730
+ apiKey: current.apiKey,
731
+ baseURL: current.baseURL
732
+ };
733
+ }
734
+ return {
735
+ type: "response",
736
+ id: requestId,
737
+ success: true,
738
+ data: modelConfig
739
+ };
740
+ }
741
+ /**
742
+ * Handle: Log message (for debugging)
743
+ */
744
+ handleLog(requestId, level, message) {
745
+ const prefix = `[Sandbox:${this.options.appId}]`;
746
+ switch (level) {
747
+ case "info":
748
+ console.log(`${prefix} ${message}`);
749
+ break;
750
+ case "warn":
751
+ console.warn(`${prefix} ${message}`);
752
+ break;
753
+ case "error":
754
+ console.error(`${prefix} ${message}`);
755
+ break;
756
+ }
757
+ return {
758
+ type: "response",
759
+ id: requestId,
760
+ success: true,
761
+ data: void 0
762
+ };
763
+ }
764
+ /**
765
+ * Handle: Interactive input prompt
766
+ */
767
+ async handlePromptInput(requestId, payload) {
768
+ const result = await clack.text({
769
+ message: payload.prompt,
770
+ defaultValue: payload.defaultValue,
771
+ placeholder: payload.placeholder,
772
+ validate: payload.maxLength ? (value) => {
773
+ if (value && value.length > payload.maxLength) return `Input must be ${payload.maxLength} characters or less`;
774
+ } : void 0
775
+ });
776
+ if (clack.isCancel(result)) return {
777
+ type: "response",
778
+ id: requestId,
779
+ success: false,
780
+ error: "Operation cancelled by user"
781
+ };
782
+ return {
783
+ type: "response",
784
+ id: requestId,
785
+ success: true,
786
+ data: result
787
+ };
788
+ }
789
+ /**
790
+ * Handle: Interactive select prompt
791
+ */
792
+ async handlePromptSelect(requestId, payload) {
793
+ const mappedOptions = payload.options.map((opt) => ({
794
+ label: opt.name,
795
+ value: opt.value,
796
+ ...opt.description && { hint: opt.description }
797
+ }));
798
+ const result = await clack.select({
799
+ message: payload.prompt,
800
+ options: mappedOptions
801
+ });
802
+ if (clack.isCancel(result)) return {
803
+ type: "response",
804
+ id: requestId,
805
+ success: false,
806
+ error: "Operation cancelled by user"
807
+ };
808
+ return {
809
+ type: "response",
810
+ id: requestId,
811
+ success: true,
812
+ data: result
813
+ };
814
+ }
815
+ /**
816
+ * Handle: Interactive confirm prompt
817
+ */
818
+ async handlePromptConfirm(requestId, payload) {
819
+ const result = await clack.confirm({
820
+ message: payload.message,
821
+ initialValue: payload.defaultValue
822
+ });
823
+ if (clack.isCancel(result)) return {
824
+ type: "response",
825
+ id: requestId,
826
+ success: false,
827
+ error: "Operation cancelled by user"
828
+ };
829
+ return {
830
+ type: "response",
831
+ id: requestId,
832
+ success: true,
833
+ data: result
834
+ };
835
+ }
836
+ /**
837
+ * Handle: Interactive multiselect prompt
838
+ */
839
+ async handlePromptMultiselect(requestId, payload) {
840
+ const mappedOptions = payload.options.map((opt) => ({
841
+ label: opt.name,
842
+ value: opt.value,
843
+ ...opt.description && { hint: opt.description }
844
+ }));
845
+ const result = await clack.multiselect({
846
+ message: payload.prompt,
847
+ options: mappedOptions,
848
+ required: payload.required
849
+ });
850
+ if (clack.isCancel(result)) return {
851
+ type: "response",
852
+ id: requestId,
853
+ success: false,
854
+ error: "Operation cancelled by user"
855
+ };
856
+ return {
857
+ type: "response",
858
+ id: requestId,
859
+ success: true,
860
+ data: result
861
+ };
862
+ }
863
+ /**
864
+ * Handle: Interactive form prompt
865
+ */
866
+ async handlePromptForm(requestId, payload) {
867
+ const result = {};
868
+ if (payload.title) console.log(`\n${pc.bold(payload.title)}\n`);
869
+ for (const field of payload.fields) {
870
+ const label = field.description ? `${field.label} (${field.description})` : field.label;
871
+ if (field.type === "boolean") {
872
+ const value = await clack.confirm({
873
+ message: label,
874
+ initialValue: field.defaultValue
875
+ });
876
+ if (clack.isCancel(value)) return {
877
+ type: "response",
878
+ id: requestId,
879
+ success: false,
880
+ error: "Operation cancelled by user"
881
+ };
882
+ result[field.name] = value;
883
+ } else if (field.type === "enum" && field.enumValues?.length) {
884
+ const value = await clack.select({
885
+ message: label,
886
+ options: field.enumValues.map((v) => ({
887
+ label: v,
888
+ value: v
889
+ }))
890
+ });
891
+ if (clack.isCancel(value)) return {
892
+ type: "response",
893
+ id: requestId,
894
+ success: false,
895
+ error: "Operation cancelled by user"
896
+ };
897
+ result[field.name] = value;
898
+ } else if (field.type === "number") {
899
+ const strValue = await clack.text({
900
+ message: label,
901
+ defaultValue: field.defaultValue?.toString(),
902
+ validate: (value) => {
903
+ if (value && Number.isNaN(Number.parseFloat(value))) return "Please enter a valid number";
904
+ }
905
+ });
906
+ if (clack.isCancel(strValue)) return {
907
+ type: "response",
908
+ id: requestId,
909
+ success: false,
910
+ error: "Operation cancelled by user"
911
+ };
912
+ result[field.name] = Number.parseFloat(strValue);
913
+ } else {
914
+ const value = await clack.text({
915
+ message: label,
916
+ defaultValue: field.defaultValue
917
+ });
918
+ if (clack.isCancel(value)) return {
919
+ type: "response",
920
+ id: requestId,
921
+ success: false,
922
+ error: "Operation cancelled by user"
923
+ };
924
+ result[field.name] = value;
925
+ }
926
+ }
927
+ return {
928
+ type: "response",
929
+ id: requestId,
930
+ success: true,
931
+ data: result
932
+ };
933
+ }
934
+ };
935
+ /**
936
+ * Factory function to create a resource provider
937
+ */
938
+ function createResourceProvider(options) {
939
+ return new ResourceProvider(options);
940
+ }
941
+
942
+ //#endregion
943
+ //#region src/host/launcher.ts
944
+ /**
945
+ * Launch a user script in a sandboxed child process
946
+ * This is the Host-side launcher that:
947
+ * 1. Spawns a child process with IPC
948
+ * 2. Applies OS-level sandboxing
949
+ * 3. Handles IPC communication for resource access
950
+ * 4. Returns execution result
951
+ */
952
+ async function launchSandbox(options) {
953
+ const { scriptPath, args: args$1, appId, sandboxConfig, allowApiKey } = options;
954
+ await SandboxManager.initialize(sandboxConfig);
955
+ const absoluteScriptPath = resolve(process.cwd(), scriptPath);
956
+ const executorPath = resolve(__dirname, "../sandbox/bundled-executor.mjs");
957
+ if (!SandboxManager.isSandboxingEnabled()) {
958
+ console.warn("āš ļø Sandboxing is not supported on this platform.");
959
+ console.warn(" Running without OS-level isolation.");
960
+ } else {
961
+ console.log("šŸ”’ Sandbox Configuration:");
962
+ console.log(` Read denied: ${sandboxConfig.filesystem.denyRead.length} paths`);
963
+ console.log(` Write allowed: ${sandboxConfig.filesystem.allowWrite.length} paths`);
964
+ console.log(` Network: ${sandboxConfig.network.allowedDomains.join(", ") || "none"}`);
965
+ console.log("");
966
+ }
967
+ const command$1 = `bun run ${executorPath}`;
968
+ const child = spawn(await SandboxManager.wrapWithSandbox(command$1), {
969
+ shell: true,
970
+ stdio: [
971
+ "inherit",
972
+ "inherit",
973
+ "inherit",
974
+ "ipc"
975
+ ],
976
+ env: {
977
+ ...process.env,
978
+ KLY_SANDBOX_MODE: "true"
979
+ }
980
+ });
981
+ const resourceProvider = createResourceProvider({
982
+ appId,
983
+ allowApiKey,
984
+ sandboxConfig
985
+ });
986
+ child.on("message", async (message) => {
987
+ if (isIPCRequest(message)) {
988
+ const response = await resourceProvider.handle(message);
989
+ child.send(response);
990
+ }
991
+ });
992
+ const initMessage = {
993
+ type: "init",
994
+ scriptPath: absoluteScriptPath,
995
+ args: args$1,
996
+ appId,
997
+ permissions: {
998
+ allowApiKey,
999
+ sandboxConfig
1000
+ }
1001
+ };
1002
+ child.send(initMessage);
1003
+ return new Promise((resolve$1, reject) => {
1004
+ let executionResult = null;
1005
+ child.on("message", (message) => {
1006
+ if (isExecutionCompleteMessage(message)) executionResult = message;
1007
+ });
1008
+ child.on("error", (error) => {
1009
+ reject(/* @__PURE__ */ new Error(`Sandbox process error: ${error.message}`));
1010
+ });
1011
+ child.on("exit", (code) => {
1012
+ if (executionResult) resolve$1({
1013
+ exitCode: code ?? 0,
1014
+ result: executionResult.result,
1015
+ error: executionResult.error
1016
+ });
1017
+ else resolve$1({
1018
+ exitCode: code ?? 1,
1019
+ error: code !== 0 ? `Process exited with code ${code}` : void 0
1020
+ });
1021
+ });
1022
+ });
1023
+ }
1024
+
1025
+ //#endregion
1026
+ //#region src/shared/constants.ts
1027
+ /**
1028
+ * Centralized constants for the KLY project
1029
+ * Prevents magic strings and improves maintainability
1030
+ */
1031
+ /**
1032
+ * Environment variable names used throughout the application
1033
+ */
1034
+ const ENV_VARS = {
1035
+ SANDBOX_MODE: "KLY_SANDBOX_MODE",
1036
+ MCP_MODE: "KLY_MCP_MODE",
1037
+ PROGRAMMATIC: "KLY_PROGRAMMATIC",
1038
+ TRUST_ALL: "KLY_TRUST_ALL",
1039
+ LOCAL_REF: "KLY_LOCAL_REF",
1040
+ REMOTE_REF: "KLY_REMOTE_REF"
1041
+ };
1042
+ /**
1043
+ * File and directory paths used for configuration and caching
1044
+ */
1045
+ const PATHS = {
1046
+ CONFIG_DIR: ".kly",
1047
+ META_FILE: ".kly-meta.json",
1048
+ PERMISSIONS_FILE: "permissions.json",
1049
+ CONFIG_FILE: "config.json"
1050
+ };
1051
+ /**
1052
+ * Timeout values in milliseconds
1053
+ */
1054
+ const TIMEOUTS = {
1055
+ IPC_REQUEST: 3e4,
1056
+ IPC_LONG_REQUEST: 6e4
1057
+ };
1058
+ /**
1059
+ * LLM API domains for network permission configuration
1060
+ */
1061
+ const LLM_API_DOMAINS = [
1062
+ "api.openai.com",
1063
+ "*.anthropic.com",
1064
+ "generativelanguage.googleapis.com",
1065
+ "api.deepseek.com"
1066
+ ];
1067
+
1068
+ //#endregion
1069
+ //#region src/shared/runtime-mode.ts
1070
+ /**
1071
+ * Check if running in sandbox mode
1072
+ * Sandbox mode: Isolated child process with restricted permissions
1073
+ */
1074
+ function isSandbox() {
1075
+ return process.env[ENV_VARS.SANDBOX_MODE] === "true";
1076
+ }
1077
+ /**
1078
+ * Check if running in MCP (Model Context Protocol) mode
1079
+ * MCP mode: Running as an MCP server for Claude Desktop integration
1080
+ */
1081
+ function isMCP() {
1082
+ return process.env[ENV_VARS.MCP_MODE] === "true";
1083
+ }
1084
+ /**
1085
+ * Check if running with trust all flag
1086
+ * Trust all: Skip permission prompts (for testing/automation)
1087
+ */
1088
+ function isTrustAll() {
1089
+ return process.env[ENV_VARS.TRUST_ALL] === "true";
1090
+ }
1091
+ /**
1092
+ * Get local reference environment variable
1093
+ */
1094
+ function getLocalRef() {
1095
+ return process.env[ENV_VARS.LOCAL_REF];
1096
+ }
1097
+ /**
1098
+ * Get remote reference environment variable
1099
+ */
1100
+ function getRemoteRef() {
1101
+ return process.env[ENV_VARS.REMOTE_REF];
1102
+ }
1103
+
1104
+ //#endregion
1105
+ //#region src/ui/utils/tty.ts
1106
+ /**
1107
+ * Check if we're in a TTY environment
1108
+ * Returns false in CI or non-interactive environments
1109
+ */
1110
+ function isTTY() {
1111
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY && !process.env.CI);
1112
+ }
1113
+
1114
+ //#endregion
1115
+ //#region src/sandbox/ipc-client.ts
1116
+ /**
1117
+ * Send an IPC request to the host and wait for response
1118
+ * Used by UI components and other sandbox code to communicate with the host process
1119
+ */
1120
+ async function sendIPCRequest(type, payload) {
1121
+ if (!process.send) throw new Error("IPC not available - not running in sandbox mode");
1122
+ return new Promise((resolve$1, reject) => {
1123
+ const requestId = `${type}-${Date.now()}-${Math.random()}`;
1124
+ const request = {
1125
+ type,
1126
+ id: requestId,
1127
+ payload
1128
+ };
1129
+ const responseHandler = (message) => {
1130
+ if (typeof message === "object" && message !== null && "type" in message && message.type === "response" && "id" in message && message.id === requestId) {
1131
+ process.off("message", responseHandler);
1132
+ const response = message;
1133
+ if (response.success) resolve$1(response.data);
1134
+ else reject(new Error(response.error));
1135
+ }
1136
+ };
1137
+ process.on("message", responseHandler);
1138
+ if (!process.send(request)) {
1139
+ process.off("message", responseHandler);
1140
+ reject(/* @__PURE__ */ new Error("Failed to send IPC message"));
1141
+ return;
1142
+ }
1143
+ setTimeout(() => {
1144
+ process.off("message", responseHandler);
1145
+ reject(/* @__PURE__ */ new Error(`IPC request timeout: ${type}`));
1146
+ }, TIMEOUTS.IPC_LONG_REQUEST);
1147
+ });
1148
+ }
1149
+
1150
+ //#endregion
1151
+ //#region src/ui/components/confirm.ts
1152
+ /**
1153
+ * Simplified confirm function
1154
+ *
1155
+ * @example
1156
+ * ```typescript
1157
+ * const proceed = await confirm("Continue?", true);
1158
+ * ```
1159
+ */
1160
+ async function confirm(message, defaultValue = false) {
1161
+ if (isSandbox()) return sendIPCRequest("prompt:confirm", {
1162
+ message,
1163
+ defaultValue
1164
+ });
1165
+ if (!isTTY()) {
1166
+ if (isMCP()) console.warn(`[MCP Warning] Interactive confirmation not available. Using default value (${defaultValue}) for: ${message}`);
1167
+ return defaultValue;
1168
+ }
1169
+ const result = await clack.confirm({
1170
+ message,
1171
+ initialValue: defaultValue
1172
+ });
1173
+ if (clack.isCancel(result)) {
1174
+ clack.cancel("Operation cancelled");
1175
+ process.exit(0);
1176
+ }
1177
+ return result;
1178
+ }
1179
+
1180
+ //#endregion
1181
+ //#region src/ui/components/select.ts
1182
+ /**
1183
+ * Show a selection menu and wait for user choice
1184
+ *
1185
+ * @example
1186
+ * ```typescript
1187
+ * const color = await select({
1188
+ * options: [
1189
+ * { name: "Red", value: "red" },
1190
+ * { name: "Blue", value: "blue", description: "Ocean color" },
1191
+ * ],
1192
+ * prompt: "Pick a color"
1193
+ * });
1194
+ * ```
1195
+ */
1196
+ async function select(config) {
1197
+ if (isSandbox()) return sendIPCRequest("prompt:select", {
1198
+ prompt: config.prompt ?? "Select an option",
1199
+ options: config.options
1200
+ });
1201
+ if (!isTTY()) {
1202
+ if (isMCP()) throw new Error(`Interactive selection not available in MCP mode. All parameters must be defined in the tool's inputSchema. Selection prompt: ${config.prompt}`);
1203
+ const firstOption = config.options[0];
1204
+ if (!firstOption) throw new Error("No options provided");
1205
+ return firstOption.value;
1206
+ }
1207
+ const mappedOptions = config.options.map((opt) => ({
1208
+ label: opt.name,
1209
+ value: opt.value,
1210
+ ...opt.description && { hint: opt.description }
1211
+ }));
1212
+ const result = await clack.select({
1213
+ message: config.prompt ?? "Select an option",
1214
+ options: mappedOptions
1215
+ });
1216
+ if (clack.isCancel(result)) {
1217
+ clack.cancel("Operation cancelled");
1218
+ process.exit(0);
1219
+ }
1220
+ return result;
1221
+ }
1222
+
1223
+ //#endregion
1224
+ //#region src/ui/utils/colors.ts
1225
+ /**
1226
+ * Format text with picocolors
1227
+ */
1228
+ function formatText(text, options) {
1229
+ let result = text;
1230
+ if (options?.color) switch (options.color) {
1231
+ case "red":
1232
+ result = pc$1.red(result);
1233
+ break;
1234
+ case "green":
1235
+ result = pc$1.green(result);
1236
+ break;
1237
+ case "yellow":
1238
+ result = pc$1.yellow(result);
1239
+ break;
1240
+ case "blue":
1241
+ result = pc$1.blue(result);
1242
+ break;
1243
+ case "magenta":
1244
+ result = pc$1.magenta(result);
1245
+ break;
1246
+ case "cyan":
1247
+ result = pc$1.cyan(result);
1248
+ break;
1249
+ case "white":
1250
+ result = pc$1.white(result);
1251
+ break;
1252
+ case "gray":
1253
+ result = pc$1.gray(result);
1254
+ break;
1255
+ }
1256
+ if (options?.bold) result = pc$1.bold(result);
1257
+ if (options?.dim) result = pc$1.dim(result);
1258
+ if (options?.italic) result = pc$1.italic(result);
1259
+ if (options?.underline) result = pc$1.underline(result);
1260
+ return result;
1261
+ }
1262
+
1263
+ //#endregion
1264
+ //#region src/ui/components/table.ts
1265
+ /**
1266
+ * Align text within a given width
1267
+ */
1268
+ function alignText(text, width, align) {
1269
+ const textLength = stripAnsi(text).length;
1270
+ const padding = Math.max(0, width - textLength);
1271
+ switch (align) {
1272
+ case "right": return " ".repeat(padding) + text;
1273
+ case "center": {
1274
+ const leftPad = Math.floor(padding / 2);
1275
+ const rightPad = padding - leftPad;
1276
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad);
1277
+ }
1278
+ default: return text + " ".repeat(padding);
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Strip ANSI escape codes from string for length calculation
1283
+ */
1284
+ function stripAnsi(str) {
1285
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
1286
+ }
1287
+ /**
1288
+ * Calculate column widths based on content
1289
+ */
1290
+ function calculateColumnWidths(columns, rows, showHeader) {
1291
+ return columns.map((col) => {
1292
+ if (col.width !== void 0) return col.width;
1293
+ let maxWidth = showHeader ? col.header.length : 0;
1294
+ for (const row of rows) {
1295
+ const value = row[col.key];
1296
+ const length = stripAnsi(col.formatter ? col.formatter(value, row) : String(value ?? "")).length;
1297
+ maxWidth = Math.max(maxWidth, length);
1298
+ }
1299
+ return maxWidth;
1300
+ });
1301
+ }
1302
+ /**
1303
+ * Format a single cell value
1304
+ */
1305
+ function formatCell(value, row, column) {
1306
+ if (column.formatter) return column.formatter(value, row);
1307
+ if (value === null || value === void 0) return pc$1.dim("-");
1308
+ return String(value);
1309
+ }
1310
+ /**
1311
+ * Render table in TTY mode with borders and styling
1312
+ */
1313
+ function renderTTY(config) {
1314
+ const { columns, rows, showHeader = true, showBorders = true, title } = config;
1315
+ const lines = [];
1316
+ const widths = calculateColumnWidths(columns, rows, showHeader);
1317
+ if (title) {
1318
+ lines.push("");
1319
+ lines.push(formatText(`${title}`, { bold: true }));
1320
+ lines.push("");
1321
+ }
1322
+ if (showHeader) {
1323
+ const headerCells = columns.map((col, i) => {
1324
+ return alignText(formatText(col.header, {
1325
+ bold: true,
1326
+ color: "cyan"
1327
+ }), widths[i], col.align ?? "left");
1328
+ });
1329
+ lines.push(headerCells.join(" "));
1330
+ if (showBorders) {
1331
+ const separatorParts = widths.map((w) => "─".repeat(w));
1332
+ lines.push(pc$1.gray(separatorParts.join("─")));
1333
+ }
1334
+ }
1335
+ for (const row of rows) {
1336
+ const cells = columns.map((col, i) => {
1337
+ const value = row[col.key];
1338
+ return alignText(formatCell(value, row, col), widths[i], col.align ?? "left");
1339
+ });
1340
+ lines.push(cells.join(" "));
1341
+ }
1342
+ if (showBorders && rows.length > 0) {
1343
+ const separatorParts = widths.map((w) => "─".repeat(w));
1344
+ lines.push(pc$1.gray(separatorParts.join("─")));
1345
+ }
1346
+ return lines.join("\n");
1347
+ }
1348
+ /**
1349
+ * Render table in non-TTY mode (plain text)
1350
+ */
1351
+ function renderPlain(config) {
1352
+ const { columns, rows, showHeader = true, title } = config;
1353
+ const lines = [];
1354
+ const widths = calculateColumnWidths(columns, rows, showHeader);
1355
+ if (title) {
1356
+ lines.push("");
1357
+ lines.push(title);
1358
+ lines.push("");
1359
+ }
1360
+ if (showHeader) {
1361
+ const headerCells = columns.map((col, i) => alignText(col.header, widths[i], col.align ?? "left"));
1362
+ lines.push(headerCells.join(" "));
1363
+ const separator = columns.map((_, i) => "-".repeat(widths[i])).join(" ");
1364
+ lines.push(separator);
1365
+ }
1366
+ for (const row of rows) {
1367
+ const cells = columns.map((col, i) => {
1368
+ const value = row[col.key];
1369
+ return alignText(stripAnsi(formatCell(value, row, col)), widths[i], col.align ?? "left");
1370
+ });
1371
+ lines.push(cells.join(" "));
1372
+ }
1373
+ return lines.join("\n");
1374
+ }
1375
+ /**
1376
+ * Display a table with columns and rows
1377
+ *
1378
+ * @example
1379
+ * ```typescript
1380
+ * table({
1381
+ * title: "Users",
1382
+ * columns: [
1383
+ * { key: "name", header: "Name" },
1384
+ * { key: "age", header: "Age", align: "right" },
1385
+ * { key: "status", header: "Status", formatter: (val) =>
1386
+ * val === "active" ? pc.green("āœ“ Active") : pc.red("āœ— Inactive")
1387
+ * },
1388
+ * ],
1389
+ * rows: [
1390
+ * { name: "Alice", age: 25, status: "active" },
1391
+ * { name: "Bob", age: 30, status: "inactive" },
1392
+ * ],
1393
+ * });
1394
+ * ```
1395
+ */
1396
+ function table(config) {
1397
+ const output$1 = isTTY() ? renderTTY(config) : renderPlain(config);
1398
+ console.log(output$1);
1399
+ }
1400
+
1401
+ //#endregion
1402
+ //#region src/ui/utils/output.ts
1403
+ /**
1404
+ * Output a result to the console
1405
+ *
1406
+ * @param result - The result to display (string, object, etc.)
1407
+ */
1408
+ function output(result) {
1409
+ if (result === void 0 || result === null) return;
1410
+ if (typeof result === "string") console.log(result);
1411
+ else console.log(JSON.stringify(result, null, 2));
1412
+ }
1413
+
1414
+ //#endregion
1415
+ //#region src/permissions/index.ts
1416
+ const CONFIG_DIR = join(homedir(), PATHS.CONFIG_DIR);
1417
+ const PERMISSIONS_FILE = join(CONFIG_DIR, PATHS.PERMISSIONS_FILE);
1418
+ /**
1419
+ * Get app identifier from script path
1420
+ */
1421
+ function getAppIdentifier() {
1422
+ const localRef = getLocalRef();
1423
+ if (localRef) return localRef;
1424
+ const remoteRef = getRemoteRef();
1425
+ if (remoteRef) return remoteRef;
1426
+ const scriptPath = process.argv[1] ?? "";
1427
+ if (scriptPath.startsWith("/") || scriptPath.startsWith("C:\\")) return `local:${scriptPath}`;
1428
+ return scriptPath || "unknown";
1429
+ }
1430
+ /**
1431
+ * Get friendly app name for display
1432
+ */
1433
+ function getAppName(appId) {
1434
+ if (appId.startsWith("local:")) {
1435
+ const path = appId.slice(6);
1436
+ const parts = path.split("/");
1437
+ return parts[parts.length - 1] || path;
1438
+ }
1439
+ if (appId.startsWith("github.com/")) return appId.split("/").slice(1, 3).join("/");
1440
+ return appId;
1441
+ }
1442
+ /**
1443
+ * Ensure permissions config directory exists
1444
+ */
1445
+ function ensurePermissionsDir() {
1446
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
1447
+ }
1448
+ /**
1449
+ * Load permissions configuration
1450
+ */
1451
+ function loadPermissions() {
1452
+ ensurePermissionsDir();
1453
+ if (!existsSync(PERMISSIONS_FILE)) return { trustedApps: {} };
1454
+ try {
1455
+ const content = readFileSync(PERMISSIONS_FILE, "utf-8");
1456
+ return JSON.parse(content);
1457
+ } catch (error) {
1458
+ console.error("Failed to parse permissions file:", error);
1459
+ return { trustedApps: {} };
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Save permissions configuration
1464
+ */
1465
+ function savePermissions(config) {
1466
+ ensurePermissionsDir();
1467
+ writeFileSync(PERMISSIONS_FILE, JSON.stringify(config, null, 2), "utf-8");
1468
+ }
1469
+ /**
1470
+ * Request permission from user with interactive prompt
1471
+ */
1472
+ async function requestPermission(appId, appName) {
1473
+ if (!isTTY()) {
1474
+ console.error(`\nPermission required: App "${appName}" (${appId}) wants to access your API keys.`);
1475
+ console.error("Set KLY_TRUST_ALL=true environment variable to grant access in non-interactive mode.");
1476
+ return false;
1477
+ }
1478
+ console.log("");
1479
+ console.log(`App "${appName}" is requesting access to your API keys.`);
1480
+ console.log(`Source: ${appId}`);
1481
+ console.log("");
1482
+ console.log("This will allow the app to use your configured LLM models.");
1483
+ console.log("");
1484
+ const choice = await select({
1485
+ prompt: "Do you want to allow this?",
1486
+ options: [
1487
+ {
1488
+ name: "Allow once",
1489
+ value: "once",
1490
+ description: "Allow for this session only"
1491
+ },
1492
+ {
1493
+ name: "Always allow",
1494
+ value: "always",
1495
+ description: "Remember this choice for future runs"
1496
+ },
1497
+ {
1498
+ name: "Cancel",
1499
+ value: "cancel",
1500
+ description: "Cancel and exit"
1501
+ }
1502
+ ]
1503
+ });
1504
+ if (choice === "cancel") return false;
1505
+ if (choice === "always") {
1506
+ const config = loadPermissions();
1507
+ config.trustedApps[appId] = {
1508
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1509
+ choice: "always"
1510
+ };
1511
+ savePermissions(config);
1512
+ return true;
1513
+ }
1514
+ return true;
1515
+ }
1516
+ /**
1517
+ * Check if an app has permission to access API keys
1518
+ * If not, prompt user for permission (in interactive mode)
1519
+ */
1520
+ async function checkApiKeyPermission(appId) {
1521
+ if (isTrustAll()) return true;
1522
+ const record = loadPermissions().trustedApps[appId];
1523
+ if (record && record.choice === "always") return true;
1524
+ return await requestPermission(appId, getAppName(appId));
1525
+ }
1526
+ /**
1527
+ * Revoke permission for an app
1528
+ */
1529
+ function revokePermission(appId) {
1530
+ const config = loadPermissions();
1531
+ delete config.trustedApps[appId];
1532
+ savePermissions(config);
1533
+ }
1534
+ /**
1535
+ * List all granted permissions
1536
+ * Only "always allow" permissions are stored
1537
+ */
1538
+ function listPermissions() {
1539
+ const config = loadPermissions();
1540
+ return Object.entries(config.trustedApps).map(([appId, record]) => ({
1541
+ appId,
1542
+ appName: getAppName(appId),
1543
+ timestamp: record.timestamp,
1544
+ choice: record.choice
1545
+ }));
1546
+ }
1547
+ /**
1548
+ * Request sandbox configuration from user interactively
1549
+ * Returns SandboxRuntimeConfig directly (no conversion needed)
1550
+ */
1551
+ async function requestSandboxConfig(appId, appName) {
1552
+ if (!isTTY()) {
1553
+ console.error(`\nSandbox permission required for: "${appName}" (${appId})`);
1554
+ console.error("Set KLY_TRUST_ALL=true environment variable to run without sandboxing in non-interactive mode.");
1555
+ return null;
1556
+ }
1557
+ const homeDir = homedir();
1558
+ const currentDir = process.cwd();
1559
+ console.log("");
1560
+ console.log(`šŸ” Sandbox Permission Request from: ${appName}`);
1561
+ console.log("");
1562
+ console.log("šŸ“‚ Filesystem Read Access:");
1563
+ const fsReadChoice = await select({
1564
+ prompt: "Which files should be denied for reading?",
1565
+ options: [
1566
+ {
1567
+ name: "Sensitive only",
1568
+ value: "sensitive",
1569
+ description: "Deny access to ~/.kly, ~/.ssh, ~/.aws, etc."
1570
+ },
1571
+ {
1572
+ name: "All home directory",
1573
+ value: "all-home",
1574
+ description: "Deny access to entire home directory"
1575
+ },
1576
+ {
1577
+ name: "None (allow all)",
1578
+ value: "none",
1579
+ description: "No read restrictions (except ~/.kly)"
1580
+ }
1581
+ ]
1582
+ });
1583
+ let denyRead = [join(homeDir, ".kly")];
1584
+ if (fsReadChoice === "sensitive") denyRead = [
1585
+ join(homeDir, ".kly"),
1586
+ join(homeDir, ".ssh"),
1587
+ join(homeDir, ".aws"),
1588
+ join(homeDir, ".gnupg")
1589
+ ];
1590
+ else if (fsReadChoice === "all-home") denyRead = [homeDir];
1591
+ console.log("");
1592
+ console.log("šŸ“ Filesystem Write Access:");
1593
+ const fsWriteChoice = await select({
1594
+ prompt: "Which directories should be allowed for writing?",
1595
+ options: [
1596
+ {
1597
+ name: "None",
1598
+ value: "none",
1599
+ description: "No write access"
1600
+ },
1601
+ {
1602
+ name: "Current directory only",
1603
+ value: "current",
1604
+ description: `Allow write to ${currentDir}`
1605
+ },
1606
+ {
1607
+ name: "Temporary directory",
1608
+ value: "temp",
1609
+ description: "Allow write to system temp directory"
1610
+ }
1611
+ ]
1612
+ });
1613
+ let allowWrite = [];
1614
+ if (fsWriteChoice === "current") allowWrite = [currentDir];
1615
+ else if (fsWriteChoice === "temp") allowWrite = [process.env.TMPDIR || process.env.TEMP || "/tmp"];
1616
+ const denyWrite = [
1617
+ join(homeDir, ".kly"),
1618
+ join(homeDir, ".ssh"),
1619
+ join(homeDir, ".aws"),
1620
+ join(homeDir, ".gnupg")
1621
+ ];
1622
+ console.log("");
1623
+ console.log("🌐 Network Access:");
1624
+ const networkChoice = await select({
1625
+ prompt: "Which network access should be allowed?",
1626
+ options: [
1627
+ {
1628
+ name: "None",
1629
+ value: "none",
1630
+ description: "No network access"
1631
+ },
1632
+ {
1633
+ name: "LLM APIs only",
1634
+ value: "llm-apis",
1635
+ description: "OpenAI, Anthropic, Google AI"
1636
+ },
1637
+ {
1638
+ name: "Common APIs",
1639
+ value: "common",
1640
+ description: "LLM + GitHub, npm, etc."
1641
+ },
1642
+ {
1643
+ name: "All domains",
1644
+ value: "all",
1645
+ description: "Allow all network access"
1646
+ }
1647
+ ]
1648
+ });
1649
+ let allowedDomains = [];
1650
+ if (networkChoice === "llm-apis") allowedDomains = [
1651
+ "api.openai.com",
1652
+ "*.anthropic.com",
1653
+ "generativelanguage.googleapis.com"
1654
+ ];
1655
+ else if (networkChoice === "common") allowedDomains = [
1656
+ "api.openai.com",
1657
+ "*.anthropic.com",
1658
+ "generativelanguage.googleapis.com",
1659
+ "*.github.com",
1660
+ "registry.npmjs.org"
1661
+ ];
1662
+ else if (networkChoice === "all") allowedDomains = ["*"];
1663
+ console.log("");
1664
+ const duration = await select({
1665
+ prompt: "How long should these permissions last?",
1666
+ options: [
1667
+ {
1668
+ name: "One time only",
1669
+ value: "once",
1670
+ description: "Ask again next time"
1671
+ },
1672
+ {
1673
+ name: "Always allow",
1674
+ value: "always",
1675
+ description: "Remember for this app"
1676
+ },
1677
+ {
1678
+ name: "Cancel",
1679
+ value: "cancel",
1680
+ description: "Cancel and exit"
1681
+ }
1682
+ ]
1683
+ });
1684
+ if (duration === "cancel") return null;
1685
+ const sandboxConfig = {
1686
+ network: {
1687
+ allowedDomains,
1688
+ deniedDomains: []
1689
+ },
1690
+ filesystem: {
1691
+ denyRead,
1692
+ allowWrite,
1693
+ denyWrite
1694
+ }
1695
+ };
1696
+ if (duration === "always") {
1697
+ const config = loadPermissions();
1698
+ config.trustedApps[appId] = {
1699
+ sandboxConfig,
1700
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1701
+ choice: "always"
1702
+ };
1703
+ savePermissions(config);
1704
+ }
1705
+ console.log("");
1706
+ console.log("āœ… Sandbox permissions granted!");
1707
+ return sandboxConfig;
1708
+ }
1709
+ /**
1710
+ * Get sandbox configuration for an app
1711
+ * Returns SandboxRuntimeConfig directly (no conversion needed)
1712
+ *
1713
+ * @param appId - App identifier
1714
+ * @returns SandboxRuntimeConfig or null if denied
1715
+ */
1716
+ async function getAppSandboxConfig(appId) {
1717
+ const homeDir = homedir();
1718
+ if (isTrustAll()) return {
1719
+ network: {
1720
+ allowedDomains: ["*"],
1721
+ deniedDomains: []
1722
+ },
1723
+ filesystem: {
1724
+ denyRead: [join(homeDir, ".kly")],
1725
+ allowWrite: ["*"],
1726
+ denyWrite: [
1727
+ join(homeDir, ".kly"),
1728
+ join(homeDir, ".ssh"),
1729
+ join(homeDir, ".aws"),
1730
+ join(homeDir, ".gnupg")
1731
+ ]
1732
+ }
1733
+ };
1734
+ const record = loadPermissions().trustedApps[appId];
1735
+ if (record?.choice === "always" && record.sandboxConfig) return record.sandboxConfig;
1736
+ return await requestSandboxConfig(appId, getAppName(appId));
1737
+ }
1738
+ /**
1739
+ * Clear all permissions
1740
+ */
1741
+ function clearAllPermissions() {
1742
+ savePermissions({ trustedApps: {} });
1743
+ }
1744
+
1745
+ //#endregion
1746
+ //#region src/permissions/cli.ts
1747
+ /**
1748
+ * Permissions management CLI
1749
+ */
1750
+ async function permissionsCommand() {
1751
+ switch (await select({
1752
+ prompt: "Permissions Management",
1753
+ options: [
1754
+ {
1755
+ name: "List permissions",
1756
+ value: "list",
1757
+ description: "View all granted permissions"
1758
+ },
1759
+ {
1760
+ name: "Revoke permission",
1761
+ value: "revoke",
1762
+ description: "Remove permission for a specific app"
1763
+ },
1764
+ {
1765
+ name: "Clear all",
1766
+ value: "clear",
1767
+ description: "Remove all permissions"
1768
+ }
1769
+ ]
1770
+ })) {
1771
+ case "list":
1772
+ await listPermissionsAction();
1773
+ break;
1774
+ case "revoke":
1775
+ await revokePermissionAction();
1776
+ break;
1777
+ case "clear":
1778
+ await clearAllPermissionsAction();
1779
+ break;
1780
+ }
1781
+ }
1782
+ /**
1783
+ * List all permissions
1784
+ */
1785
+ async function listPermissionsAction() {
1786
+ const permissions = listPermissions();
1787
+ if (permissions.length === 0) {
1788
+ output("\nNo permissions granted yet.\n");
1789
+ return;
1790
+ }
1791
+ output("\nšŸ“‹ Granted Permissions:\n");
1792
+ table({
1793
+ columns: [{
1794
+ key: "app",
1795
+ header: "App"
1796
+ }, {
1797
+ key: "grantedAt",
1798
+ header: "Granted At"
1799
+ }],
1800
+ rows: permissions.map((p) => ({
1801
+ app: p.appName,
1802
+ grantedAt: new Date(p.timestamp).toLocaleString()
1803
+ }))
1804
+ });
1805
+ output("");
1806
+ }
1807
+ /**
1808
+ * Revoke permission for a specific app
1809
+ */
1810
+ async function revokePermissionAction() {
1811
+ const permissions = listPermissions();
1812
+ if (permissions.length === 0) {
1813
+ output("\nNo permissions to revoke.\n");
1814
+ return;
1815
+ }
1816
+ const appId = await select({
1817
+ prompt: "Select app to revoke permission:",
1818
+ options: permissions.map((p) => ({
1819
+ name: p.appName,
1820
+ value: p.appId,
1821
+ description: "Always allowed"
1822
+ }))
1823
+ });
1824
+ if (await confirm("Are you sure you want to revoke this permission?")) {
1825
+ revokePermission(appId);
1826
+ output("\nāœ… Permission revoked.\n");
1827
+ } else output("\nāŒ Cancelled.\n");
1828
+ }
1829
+ /**
1830
+ * Clear all permissions
1831
+ */
1832
+ async function clearAllPermissionsAction() {
1833
+ if (await confirm("Are you sure you want to clear ALL permissions?")) {
1834
+ clearAllPermissions();
1835
+ output("\nāœ… All permissions cleared.\n");
1836
+ } else output("\nāŒ Cancelled.\n");
1837
+ }
1838
+
1839
+ //#endregion
1840
+ //#region src/permissions/config-builder.ts
1841
+ /**
1842
+ * Always protected paths (never allow write, some deny read)
1843
+ */
1844
+ const PROTECTED_PATHS = {
1845
+ alwaysDenyWrite: [
1846
+ join(homedir(), ".kly"),
1847
+ join(homedir(), ".ssh"),
1848
+ join(homedir(), ".aws"),
1849
+ join(homedir(), ".gnupg")
1850
+ ],
1851
+ alwaysDenyRead: [join(homedir(), ".kly")]
1852
+ };
1853
+ /**
1854
+ * Build a complete SandboxRuntimeConfig from declared app permissions
1855
+ *
1856
+ * This merges:
1857
+ * 1. Default safe configuration
1858
+ * 2. Automatic LLM domains (if apiKeys: true)
1859
+ * 3. User-declared sandbox config
1860
+ * 4. Mandatory protections (always applied)
1861
+ *
1862
+ * @param permissions - Declared app permissions
1863
+ * @returns Complete sandbox configuration ready for SandboxManager
1864
+ */
1865
+ function buildSandboxConfig(permissions) {
1866
+ const currentDir = process.cwd();
1867
+ let allowedDomains = [];
1868
+ let allowWrite = [currentDir];
1869
+ let denyRead = [...PROTECTED_PATHS.alwaysDenyRead];
1870
+ if (permissions?.apiKeys) allowedDomains = [...LLM_API_DOMAINS];
1871
+ if (permissions?.sandbox) {
1872
+ const userSandbox = permissions.sandbox;
1873
+ if (userSandbox.network?.allowedDomains) allowedDomains = [...allowedDomains, ...userSandbox.network.allowedDomains];
1874
+ if (userSandbox.filesystem) {
1875
+ if (userSandbox.filesystem.allowWrite) allowWrite = userSandbox.filesystem.allowWrite;
1876
+ if (userSandbox.filesystem.denyRead) denyRead = [...denyRead, ...userSandbox.filesystem.denyRead];
1877
+ }
1878
+ }
1879
+ return {
1880
+ network: {
1881
+ allowedDomains,
1882
+ deniedDomains: []
1883
+ },
1884
+ filesystem: {
1885
+ denyRead,
1886
+ allowWrite,
1887
+ denyWrite: PROTECTED_PATHS.alwaysDenyWrite
1888
+ }
1889
+ };
1890
+ }
1891
+ /**
1892
+ * Get a human-readable summary of permissions for display
1893
+ * Only shows special/non-default permissions
1894
+ */
1895
+ function formatPermissionsSummary(permissions) {
1896
+ const summary = [];
1897
+ if (permissions?.apiKeys) summary.push("• API Keys access (to call LLM APIs)");
1898
+ const config = buildSandboxConfig(permissions);
1899
+ const currentDir = process.cwd();
1900
+ if (config.network.allowedDomains.length > 0) if (config.network.allowedDomains.includes("*")) summary.push("• Network: All domains");
1901
+ else {
1902
+ const domains = config.network.allowedDomains.slice(0, 3).join(", ");
1903
+ const more = config.network.allowedDomains.length > 3 ? ` +${config.network.allowedDomains.length - 3} more` : "";
1904
+ summary.push(`• Network: ${domains}${more}`);
1905
+ }
1906
+ if (config.filesystem.allowWrite.length > 1 || config.filesystem.allowWrite.length === 1 && config.filesystem.allowWrite[0] !== currentDir) {
1907
+ const dirs = config.filesystem.allowWrite.map((p) => p === currentDir ? "current directory" : p).slice(0, 2).join(", ");
1908
+ const more = config.filesystem.allowWrite.length > 2 ? ` +${config.filesystem.allowWrite.length - 2} more` : "";
1909
+ summary.push(`• Filesystem write: ${dirs}${more}`);
1910
+ }
1911
+ if (permissions?.sandbox?.filesystem?.denyRead) summary.push(`• Filesystem read denied: ${permissions.sandbox.filesystem.denyRead.length} path(s)`);
1912
+ return summary;
1913
+ }
1914
+
1915
+ //#endregion
1916
+ //#region src/permissions/extract.ts
1917
+ /**
1918
+ * Extract permissions from a user's app script
1919
+ * This runs in the host process BEFORE launching the sandbox
1920
+ *
1921
+ * @param scriptPath - Absolute path to the user's script
1922
+ * @returns Declared permissions or undefined if not specified
1923
+ */
1924
+ async function extractAppPermissions(scriptPath) {
1925
+ try {
1926
+ const prevMode = process.env[ENV_VARS.PROGRAMMATIC];
1927
+ process.env[ENV_VARS.PROGRAMMATIC] = "true";
1928
+ const module = await import(scriptPath);
1929
+ if (prevMode === void 0) delete process.env[ENV_VARS.PROGRAMMATIC];
1930
+ else process.env[ENV_VARS.PROGRAMMATIC] = prevMode;
1931
+ const app = module.default;
1932
+ if (!app || !app.definition) return;
1933
+ return app.definition.permissions;
1934
+ } catch (error) {
1935
+ console.warn(`Warning: Could not extract permissions from ${scriptPath}:`, error instanceof Error ? error.message : String(error));
1936
+ return;
1937
+ }
1938
+ }
1939
+
1940
+ //#endregion
1941
+ //#region src/permissions/unified-prompt.ts
1942
+ /**
1943
+ * Check if permissions require user prompt
1944
+ * Only prompt for special permissions (API keys, custom network/filesystem)
1945
+ * Default permissions (current directory write, no network) are auto-granted
1946
+ */
1947
+ function needsPermissionPrompt(permissions, sandboxConfig) {
1948
+ if (permissions?.apiKeys) return true;
1949
+ if (sandboxConfig.network.allowedDomains.length > 0) return true;
1950
+ if (permissions?.sandbox?.filesystem) return true;
1951
+ return false;
1952
+ }
1953
+ /**
1954
+ * Request permission with a single unified prompt
1955
+ * Shows all requested permissions at once
1956
+ *
1957
+ * @param appId - App identifier
1958
+ * @param appPermissions - Declared permissions from app
1959
+ * @param sandboxConfig - Generated sandbox configuration
1960
+ * @returns true if allowed, false if cancelled
1961
+ */
1962
+ async function requestUnifiedPermission(appId, appPermissions, sandboxConfig) {
1963
+ if (isTrustAll()) return true;
1964
+ const config = loadPermissions();
1965
+ const record = config.trustedApps[appId];
1966
+ if (record && record.choice === "always") return true;
1967
+ if (!needsPermissionPrompt(appPermissions, sandboxConfig)) return true;
1968
+ if (!isTTY()) {
1969
+ const appName$1 = getAppName(appId);
1970
+ console.error(`\nPermission required: App "${appName$1}" (${appId}) requests permissions.`);
1971
+ console.error("Set KLY_TRUST_ALL=true environment variable to grant access in non-interactive mode.");
1972
+ return false;
1973
+ }
1974
+ const appName = getAppName(appId);
1975
+ console.log("");
1976
+ console.log(`App "${appName}" requests the following permissions:`);
1977
+ console.log("");
1978
+ const summary = formatPermissionsSummary(appPermissions);
1979
+ for (const line of summary) console.log(` ${line}`);
1980
+ console.log("");
1981
+ console.log(`Source: ${appId}`);
1982
+ console.log("");
1983
+ const choice = await select({
1984
+ prompt: "Do you want to allow this?",
1985
+ options: [
1986
+ {
1987
+ name: "Allow once",
1988
+ value: "once",
1989
+ description: "Allow for this session only"
1990
+ },
1991
+ {
1992
+ name: "Always allow",
1993
+ value: "always",
1994
+ description: "Remember this choice for future runs"
1995
+ },
1996
+ {
1997
+ name: "Cancel",
1998
+ value: "cancel",
1999
+ description: "Cancel and exit"
2000
+ }
2001
+ ]
2002
+ });
2003
+ if (choice === "cancel") return false;
2004
+ if (choice === "always") {
2005
+ config.trustedApps[appId] = {
2006
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2007
+ choice: "always",
2008
+ sandboxConfig
2009
+ };
2010
+ savePermissions(config);
2011
+ }
2012
+ return true;
2013
+ }
2014
+ /**
2015
+ * Check if app needs permission check
2016
+ * Returns stored sandbox config if "always" was granted
2017
+ */
2018
+ function checkStoredPermission(appId) {
2019
+ if (isTrustAll()) return null;
2020
+ const record = loadPermissions().trustedApps[appId];
2021
+ if (record?.choice === "always" && record.sandboxConfig) return record.sandboxConfig;
2022
+ return null;
2023
+ }
2024
+
2025
+ //#endregion
2026
+ //#region src/remote/parser.ts
2027
+ /**
2028
+ * Parse various remote formats into RepoRef
2029
+ *
2030
+ * Supported formats:
2031
+ * - user/repo
2032
+ * - user/repo@v1.0.0
2033
+ * - user/repo@branch
2034
+ * - github.com/user/repo
2035
+ * - github.com/user/repo@ref
2036
+ * - https://github.com/user/repo
2037
+ */
2038
+ function parseRemoteRef(input) {
2039
+ let normalized = input.trim();
2040
+ normalized = normalized.replace(/^https?:\/\//, "");
2041
+ normalized = normalized.replace(/^github\.com\//, "");
2042
+ let ref = "main";
2043
+ const atIndex = normalized.indexOf("@");
2044
+ if (atIndex !== -1) {
2045
+ ref = normalized.slice(atIndex + 1);
2046
+ normalized = normalized.slice(0, atIndex);
2047
+ }
2048
+ normalized = normalized.replace(/\.git$/, "");
2049
+ const parts = normalized.split("/");
2050
+ if (parts.length !== 2) return null;
2051
+ const [owner, repo] = parts;
2052
+ if (!owner || !repo || !isValidGitHubName(owner) || !isValidGitHubName(repo)) return null;
2053
+ return {
2054
+ owner,
2055
+ repo,
2056
+ ref
2057
+ };
2058
+ }
2059
+ /**
2060
+ * Check if a string is a valid GitHub username or repo name
2061
+ */
2062
+ function isValidGitHubName(name) {
2063
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(name) || /^[a-zA-Z0-9]$/.test(name);
2064
+ }
2065
+ /**
2066
+ * Get the kly cache directory
2067
+ */
2068
+ function getCacheDir() {
2069
+ return join(homedir(), ".kly", "cache");
2070
+ }
2071
+ /**
2072
+ * Get the cache path for a specific repo ref
2073
+ */
2074
+ function getRepoCachePath(ref) {
2075
+ return join(getCacheDir(), "github.com", ref.owner, ref.repo, ref.ref);
2076
+ }
2077
+ /**
2078
+ * Check if an input looks like a remote reference (vs local file path)
2079
+ */
2080
+ function isRemoteRef(input) {
2081
+ if (input.startsWith("./") || input.startsWith("../") || input.startsWith("/") || input.includes("\\")) return false;
2082
+ return parseRemoteRef(input) !== null;
2083
+ }
2084
+ var init_parser = __esmMin((() => {}));
2085
+
2086
+ //#endregion
2087
+ //#region src/remote/cache.ts
2088
+ init_parser();
2089
+ const META_FILENAME = ".kly-meta.json";
2090
+ /**
2091
+ * Check if cache exists and is valid
2092
+ */
2093
+ function checkCache(ref) {
2094
+ const cachePath = getRepoCachePath(ref);
2095
+ const metaPath = join(cachePath, META_FILENAME);
2096
+ if (!existsSync(cachePath)) return {
2097
+ exists: false,
2098
+ valid: false,
2099
+ reason: "Cache directory does not exist"
2100
+ };
2101
+ if (!existsSync(metaPath)) return {
2102
+ exists: true,
2103
+ valid: false,
2104
+ reason: "Cache metadata missing"
2105
+ };
2106
+ try {
2107
+ const metadata = JSON.parse(readFileSync(metaPath, "utf-8"));
2108
+ if (!existsSync(join(cachePath, metadata.entryPoint))) return {
2109
+ exists: true,
2110
+ valid: false,
2111
+ metadata,
2112
+ reason: "Entry point file missing"
2113
+ };
2114
+ if (!metadata.dependenciesInstalled) return {
2115
+ exists: true,
2116
+ valid: false,
2117
+ metadata,
2118
+ reason: "Dependencies not installed"
2119
+ };
2120
+ return {
2121
+ exists: true,
2122
+ valid: true,
2123
+ metadata
2124
+ };
2125
+ } catch {
2126
+ return {
2127
+ exists: true,
2128
+ valid: false,
2129
+ reason: "Invalid cache metadata"
2130
+ };
2131
+ }
2132
+ }
2133
+ /**
2134
+ * Write cache metadata
2135
+ */
2136
+ function writeMetadata(ref, metadata) {
2137
+ const metaPath = join(getRepoCachePath(ref), META_FILENAME);
2138
+ mkdirSync(dirname(metaPath), { recursive: true });
2139
+ writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
2140
+ }
2141
+ /**
2142
+ * Remove cached repository
2143
+ */
2144
+ function invalidateCache(ref) {
2145
+ const cachePath = getRepoCachePath(ref);
2146
+ if (existsSync(cachePath)) rmSync(cachePath, {
2147
+ recursive: true,
2148
+ force: true
2149
+ });
2150
+ }
2151
+
2152
+ //#endregion
2153
+ //#region src/remote/fetcher.ts
2154
+ init_parser();
2155
+ const execAsync = promisify(exec);
2156
+ /**
2157
+ * Clone a repository to cache
2158
+ */
2159
+ async function cloneRepo(ref) {
2160
+ const repoUrl = `https://github.com/${ref.owner}/${ref.repo}.git`;
2161
+ const targetPath = getRepoCachePath(ref);
2162
+ if (existsSync(targetPath)) rmSync(targetPath, {
2163
+ recursive: true,
2164
+ force: true
2165
+ });
2166
+ mkdirSync(dirname(targetPath), { recursive: true });
2167
+ try {
2168
+ await execAsync(`git clone --depth 1 --branch ${ref.ref} ${repoUrl} "${targetPath}"`, { timeout: 6e4 });
2169
+ } catch (error) {
2170
+ if (ref.ref === "main") try {
2171
+ await execAsync(`git clone --depth 1 ${repoUrl} "${targetPath}"`, { timeout: 6e4 });
2172
+ return;
2173
+ } catch {}
2174
+ throw new Error(`Failed to clone ${ref.owner}/${ref.repo}@${ref.ref}: ${error instanceof Error ? error.message : String(error)}`);
2175
+ }
2176
+ }
2177
+ /**
2178
+ * Install dependencies using bun
2179
+ */
2180
+ async function installDependencies(repoPath) {
2181
+ if (!existsSync(`${repoPath}/package.json`)) return;
2182
+ try {
2183
+ await execAsync("bun install", {
2184
+ cwd: repoPath,
2185
+ timeout: 12e4
2186
+ });
2187
+ } catch (error) {
2188
+ throw new Error(`Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`);
2189
+ }
2190
+ }
2191
+ /**
2192
+ * Get the commit SHA of a cloned repo
2193
+ */
2194
+ async function getCommitSha(repoPath) {
2195
+ try {
2196
+ const { stdout } = await execAsync("git rev-parse HEAD", {
2197
+ cwd: repoPath,
2198
+ timeout: 5e3
2199
+ });
2200
+ return stdout.trim();
2201
+ } catch {
2202
+ return "unknown";
2203
+ }
2204
+ }
2205
+
2206
+ //#endregion
2207
+ //#region src/remote/integrity.ts
2208
+ /**
2209
+ * Directories and files to ignore when calculating repository hash
2210
+ */
2211
+ const IGNORE_PATTERNS = [
2212
+ ".git",
2213
+ "node_modules",
2214
+ "dist",
2215
+ "build",
2216
+ "coverage",
2217
+ ".kly",
2218
+ ".kly-meta.json",
2219
+ ".DS_Store",
2220
+ "*.log"
2221
+ ];
2222
+ /**
2223
+ * File extensions to include in hash calculation
2224
+ */
2225
+ const SOURCE_EXTENSIONS = [
2226
+ ".ts",
2227
+ ".js",
2228
+ ".tsx",
2229
+ ".jsx",
2230
+ ".json",
2231
+ ".md",
2232
+ ".txt"
2233
+ ];
2234
+ /**
2235
+ * Calculate integrity hash for a cloned repository
2236
+ * Uses SHA-384 (consistent with browser Subresource Integrity)
2237
+ *
2238
+ * Hash includes:
2239
+ * - All source code files (sorted by path)
2240
+ * - File paths (for structure verification)
2241
+ * - Lock file (bun.lockb) if present
2242
+ *
2243
+ * @param repoPath - Absolute path to the repository
2244
+ * @param algorithm - Hash algorithm (default: sha384)
2245
+ * @returns Hash in format "sha384-base64..."
2246
+ */
2247
+ function calculateRepoHash(repoPath, algorithm = "sha384") {
2248
+ const hash = createHash(algorithm);
2249
+ const files = collectSourceFiles(repoPath);
2250
+ files.sort();
2251
+ for (const file of files) {
2252
+ const relativePath = relative(repoPath, file);
2253
+ const content = readFileSync(file);
2254
+ hash.update(`FILE:${relativePath}\n`);
2255
+ hash.update(content);
2256
+ hash.update("\n");
2257
+ }
2258
+ const lockFile = join(repoPath, "bun.lockb");
2259
+ if (existsSync(lockFile)) {
2260
+ hash.update("LOCK:bun.lockb\n");
2261
+ hash.update(readFileSync(lockFile));
2262
+ hash.update("\n");
2263
+ }
2264
+ return `${algorithm}-${hash.digest("base64")}`;
2265
+ }
2266
+ /**
2267
+ * Recursively collect all source files in a directory
2268
+ *
2269
+ * @param dir - Directory to scan
2270
+ * @param results - Accumulator for file paths
2271
+ * @returns Array of absolute file paths
2272
+ */
2273
+ function collectSourceFiles(dir, results = []) {
2274
+ if (!existsSync(dir)) return results;
2275
+ try {
2276
+ const entries = readdirSync(dir);
2277
+ for (const entry of entries) {
2278
+ if (shouldIgnore(entry)) continue;
2279
+ const fullPath = join(dir, entry);
2280
+ let stat;
2281
+ try {
2282
+ stat = statSync(fullPath);
2283
+ } catch {
2284
+ continue;
2285
+ }
2286
+ if (stat.isDirectory()) collectSourceFiles(fullPath, results);
2287
+ else if (stat.isFile() && shouldIncludeFile(entry)) results.push(fullPath);
2288
+ }
2289
+ } catch {}
2290
+ return results;
2291
+ }
2292
+ /**
2293
+ * Check if a file/directory should be ignored
2294
+ */
2295
+ function shouldIgnore(name) {
2296
+ for (const pattern of IGNORE_PATTERNS) if (pattern.startsWith("*")) {
2297
+ const ext = pattern.slice(1);
2298
+ if (name.endsWith(ext)) return true;
2299
+ } else if (name === pattern) return true;
2300
+ return false;
2301
+ }
2302
+ /**
2303
+ * Check if a file should be included in hash calculation
2304
+ */
2305
+ function shouldIncludeFile(name) {
2306
+ return SOURCE_EXTENSIONS.some((ext) => name.endsWith(ext));
2307
+ }
2308
+
2309
+ //#endregion
2310
+ //#region src/remote/resolver.ts
2311
+ /**
2312
+ * Entry point candidates to search for (in order)
2313
+ */
2314
+ const ENTRY_CANDIDATES = [
2315
+ "index.ts",
2316
+ "main.ts",
2317
+ "src/index.ts",
2318
+ "src/main.ts",
2319
+ "app.ts"
2320
+ ];
2321
+ /**
2322
+ * Resolve entry point for a kly app
2323
+ * Priority: main field > convention candidates
2324
+ */
2325
+ function resolveEntryPoint(repoPath) {
2326
+ const pkgPath = join(repoPath, "package.json");
2327
+ if (existsSync(pkgPath)) try {
2328
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2329
+ if (pkg.main && (pkg.main.endsWith(".ts") || pkg.main.endsWith(".js"))) {
2330
+ if (existsSync(join(repoPath, pkg.main))) return pkg.main;
2331
+ }
2332
+ } catch {}
2333
+ for (const candidate of ENTRY_CANDIDATES) if (existsSync(join(repoPath, candidate))) return candidate;
2334
+ return null;
2335
+ }
2336
+ /**
2337
+ * Read kly configuration from package.json
2338
+ */
2339
+ function readKlyConfig(repoPath) {
2340
+ const pkgPath = join(repoPath, "package.json");
2341
+ if (!existsSync(pkgPath)) return null;
2342
+ try {
2343
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).kly ?? null;
2344
+ } catch {
2345
+ return null;
2346
+ }
2347
+ }
2348
+ /**
2349
+ * Check if current kly version satisfies the required version
2350
+ * Simple semver check (supports >=x.y.z format)
2351
+ */
2352
+ function validateVersion(required, current) {
2353
+ const reqMatch = required.match(/^>=?\s*(\d+)\.(\d+)\.(\d+)/);
2354
+ if (!reqMatch) return true;
2355
+ const curMatch = current.match(/^(\d+)\.(\d+)\.(\d+)/);
2356
+ if (!curMatch) return true;
2357
+ const reqMajor = Number(reqMatch[1]);
2358
+ const reqMinor = Number(reqMatch[2]);
2359
+ const reqPatch = Number(reqMatch[3]);
2360
+ const curMajor = Number(curMatch[1]);
2361
+ const curMinor = Number(curMatch[2]);
2362
+ const curPatch = Number(curMatch[3]);
2363
+ if (curMajor > reqMajor) return true;
2364
+ if (curMajor < reqMajor) return false;
2365
+ if (curMinor > reqMinor) return true;
2366
+ if (curMinor < reqMinor) return false;
2367
+ return curPatch >= reqPatch;
2368
+ }
2369
+ /**
2370
+ * Check required environment variables
2371
+ * Returns list of missing variables
2372
+ */
2373
+ function checkEnvVars(required) {
2374
+ return required.filter((name) => !process.env[name]);
2375
+ }
2376
+
2377
+ //#endregion
2378
+ //#region src/remote/sumfile.ts
2379
+ /**
2380
+ * Get the path to kly.sum file
2381
+ */
2382
+ function getSumFilePath() {
2383
+ return join(homedir(), ".kly", "kly.sum");
2384
+ }
2385
+ /**
2386
+ * Manager for kly.sum file (integrity verification database)
2387
+ *
2388
+ * File format (one entry per line):
2389
+ * github.com/owner/repo@ref sha384-hash timestamp trusted|untrusted
2390
+ *
2391
+ * Example:
2392
+ * github.com/jack/weather@v1.0.0 sha384-oqVuAfXRKap7fdgc... 1704067200 trusted
2393
+ */
2394
+ var SumFileManager = class {
2395
+ entries = /* @__PURE__ */ new Map();
2396
+ sumFilePath;
2397
+ dirty = false;
2398
+ constructor(sumFilePath) {
2399
+ this.sumFilePath = sumFilePath || getSumFilePath();
2400
+ this.load();
2401
+ }
2402
+ /**
2403
+ * Load entries from kly.sum file
2404
+ */
2405
+ load() {
2406
+ if (!existsSync(this.sumFilePath)) return;
2407
+ try {
2408
+ const lines = readFileSync(this.sumFilePath, "utf-8").split("\n");
2409
+ for (const line of lines) {
2410
+ const trimmed = line.trim();
2411
+ if (!trimmed || trimmed.startsWith("#")) continue;
2412
+ const entry = this.parseLine(trimmed);
2413
+ if (entry) this.entries.set(entry.url, entry);
2414
+ }
2415
+ } catch (error) {
2416
+ console.warn(`Warning: Failed to load kly.sum: ${error}`);
2417
+ }
2418
+ }
2419
+ /**
2420
+ * Parse a single line from kly.sum
2421
+ */
2422
+ parseLine(line) {
2423
+ const parts = line.split(/\s+/);
2424
+ if (parts.length < 4) return null;
2425
+ const [url, hash, timestampStr, trustedStr] = parts;
2426
+ if (!url || !hash || !timestampStr || !trustedStr) return null;
2427
+ const timestamp = Number(timestampStr);
2428
+ if (Number.isNaN(timestamp)) return null;
2429
+ return {
2430
+ url,
2431
+ hash,
2432
+ timestamp,
2433
+ trusted: trustedStr === "trusted"
2434
+ };
2435
+ }
2436
+ /**
2437
+ * Format an entry for writing to file
2438
+ */
2439
+ formatEntry(entry) {
2440
+ return `${entry.url} ${entry.hash} ${entry.timestamp} ${entry.trusted ? "trusted" : "untrusted"}`;
2441
+ }
2442
+ /**
2443
+ * Save entries to kly.sum file
2444
+ */
2445
+ save() {
2446
+ if (!this.dirty) return;
2447
+ try {
2448
+ const dir = dirname(this.sumFilePath);
2449
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
2450
+ const lines = [
2451
+ "# kly.sum - Integrity verification database",
2452
+ "# Format: url hash timestamp trusted|untrusted",
2453
+ "# DO NOT EDIT THIS FILE MANUALLY",
2454
+ "",
2455
+ ...Array.from(this.entries.values()).sort((a, b) => a.url.localeCompare(b.url)).map((entry) => this.formatEntry(entry))
2456
+ ];
2457
+ writeFileSync(this.sumFilePath, `${lines.join("\n")}\n`, "utf-8");
2458
+ this.dirty = false;
2459
+ } catch (error) {
2460
+ console.error(`Error: Failed to save kly.sum: ${error}`);
2461
+ throw error;
2462
+ }
2463
+ }
2464
+ /**
2465
+ * Verify a repository's integrity hash
2466
+ *
2467
+ * @param url - Full URL (e.g., "github.com/owner/repo@ref")
2468
+ * @param hash - Calculated hash to verify
2469
+ * @returns "ok" if matches, "mismatch" if different, "new" if first time
2470
+ */
2471
+ verify(url, hash) {
2472
+ const entry = this.entries.get(url);
2473
+ if (!entry) return "new";
2474
+ if (entry.hash === hash) return "ok";
2475
+ return "mismatch";
2476
+ }
2477
+ /**
2478
+ * Add or update an entry in kly.sum
2479
+ *
2480
+ * @param url - Full URL
2481
+ * @param hash - Integrity hash
2482
+ * @param trusted - Whether user explicitly trusted this code
2483
+ */
2484
+ add(url, hash, trusted = false) {
2485
+ this.entries.set(url, {
2486
+ url,
2487
+ hash,
2488
+ timestamp: Math.floor(Date.now() / 1e3),
2489
+ trusted
2490
+ });
2491
+ this.dirty = true;
2492
+ this.save();
2493
+ }
2494
+ /**
2495
+ * Update an existing entry's hash (e.g., user approved new version)
2496
+ *
2497
+ * @param url - Full URL
2498
+ * @param hash - New hash
2499
+ * @param trusted - Whether user explicitly trusted this update
2500
+ */
2501
+ update(url, hash, trusted = false) {
2502
+ const existing = this.entries.get(url);
2503
+ this.entries.set(url, {
2504
+ url,
2505
+ hash,
2506
+ timestamp: existing?.timestamp ?? Math.floor(Date.now() / 1e3),
2507
+ trusted
2508
+ });
2509
+ this.dirty = true;
2510
+ this.save();
2511
+ }
2512
+ /**
2513
+ * Remove an entry from kly.sum
2514
+ *
2515
+ * @param url - Full URL to remove
2516
+ * @returns true if removed, false if not found
2517
+ */
2518
+ remove(url) {
2519
+ const existed = this.entries.delete(url);
2520
+ if (existed) {
2521
+ this.dirty = true;
2522
+ this.save();
2523
+ }
2524
+ return existed;
2525
+ }
2526
+ /**
2527
+ * Get an entry by URL
2528
+ *
2529
+ * @param url - Full URL
2530
+ * @returns Entry if found, undefined otherwise
2531
+ */
2532
+ get(url) {
2533
+ return this.entries.get(url);
2534
+ }
2535
+ /**
2536
+ * Get all entries
2537
+ *
2538
+ * @returns Array of all sum entries
2539
+ */
2540
+ getAll() {
2541
+ return Array.from(this.entries.values());
2542
+ }
2543
+ /**
2544
+ * Clear all entries (for testing or reset)
2545
+ */
2546
+ clear() {
2547
+ this.entries.clear();
2548
+ this.dirty = true;
2549
+ this.save();
2550
+ }
2551
+ /**
2552
+ * Get statistics about the sum file
2553
+ */
2554
+ getStats() {
2555
+ const entries = Array.from(this.entries.values());
2556
+ return {
2557
+ total: entries.length,
2558
+ trusted: entries.filter((e) => e.trusted).length,
2559
+ untrusted: entries.filter((e) => !e.trusted).length
2560
+ };
2561
+ }
2562
+ };
2563
+
2564
+ //#endregion
2565
+ //#region src/remote/index.ts
2566
+ init_parser();
2567
+ /** Current kly CLI version */
2568
+ const KLY_VERSION = "0.1.0";
2569
+ /**
2570
+ * Run a remote GitHub repository as a kly app
2571
+ */
2572
+ async function runRemote(input, options = {}) {
2573
+ const ref = parseRemoteRef(input);
2574
+ if (!ref) throw new Error(`Invalid remote reference: ${input}`);
2575
+ const repoPath = getRepoCachePath(ref);
2576
+ const cacheResult = checkCache(ref);
2577
+ if (!cacheResult.valid || options.force) {
2578
+ if (options.force && cacheResult.exists) {
2579
+ console.log(`Refreshing ${ref.owner}/${ref.repo}@${ref.ref}...`);
2580
+ invalidateCache(ref);
2581
+ } else console.log(`Fetching ${ref.owner}/${ref.repo}@${ref.ref}...`);
2582
+ await cloneRepo(ref);
2583
+ const entryPoint = resolveEntryPoint(repoPath);
2584
+ if (!entryPoint) throw new Error(`No entry point found in ${ref.owner}/${ref.repo}. Set "main" in package.json or create index.ts`);
2585
+ if (!options.skipInstall) {
2586
+ console.log("Installing dependencies...");
2587
+ await installDependencies(repoPath);
2588
+ }
2589
+ writeMetadata(ref, {
2590
+ commitSha: await getCommitSha(repoPath),
2591
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2592
+ entryPoint,
2593
+ dependenciesInstalled: !options.skipInstall
2594
+ });
2595
+ console.log("Ready!\n");
2596
+ }
2597
+ if (!options.skipIntegrityCheck) {
2598
+ if (!(await verifyIntegrity(ref, repoPath)).proceedWithExecution) {
2599
+ console.error("\nāŒ Execution cancelled due to integrity verification failure");
2600
+ process.exit(1);
2601
+ }
2602
+ }
2603
+ await executeApp(ref, repoPath, options.args ?? [], options.mcp ?? false);
2604
+ }
2605
+ /**
2606
+ * Verify repository integrity using kly.sum
2607
+ *
2608
+ * @param ref - Repository reference
2609
+ * @param repoPath - Local path to repository
2610
+ * @returns Object with integrity check result and whether to proceed with execution
2611
+ */
2612
+ async function verifyIntegrity(ref, repoPath) {
2613
+ const url = `github.com/${ref.owner}/${ref.repo}@${ref.ref}`;
2614
+ console.log("\nšŸ” Verifying code integrity...");
2615
+ const hash = calculateRepoHash(repoPath);
2616
+ console.log(` Hash: ${hash.slice(0, 20)}...`);
2617
+ const sumManager = new SumFileManager();
2618
+ const verifyResult = sumManager.verify(url, hash);
2619
+ const result = {
2620
+ status: verifyResult,
2621
+ hash,
2622
+ requiresTrust: verifyResult !== "ok"
2623
+ };
2624
+ switch (verifyResult) {
2625
+ case "ok":
2626
+ console.log(" āœ“ Integrity verified\n");
2627
+ return {
2628
+ proceedWithExecution: true,
2629
+ result
2630
+ };
2631
+ case "new":
2632
+ console.log("\nāš ļø SECURITY NOTICE: First time running this tool\n");
2633
+ console.log(" This code has not been verified before.");
2634
+ console.log(" Please review the source code before proceeding:");
2635
+ console.log(` https://github.com/${ref.owner}/${ref.repo}/tree/${ref.ref}\n`);
2636
+ if (await confirm("Do you trust this code and want to proceed?")) {
2637
+ sumManager.add(url, hash, true);
2638
+ console.log(" āœ“ Code trusted and added to kly.sum\n");
2639
+ return {
2640
+ proceedWithExecution: true,
2641
+ result
2642
+ };
2643
+ }
2644
+ console.log("\n User declined to trust the code");
2645
+ return {
2646
+ proceedWithExecution: false,
2647
+ result
2648
+ };
2649
+ case "mismatch":
2650
+ result.expectedHash = sumManager.get(url)?.hash;
2651
+ console.log("\n🚨 SECURITY WARNING: Code has been modified!\n");
2652
+ console.log(" The code for this tool has changed since you last ran it.");
2653
+ console.log(" This could indicate:");
2654
+ console.log(" - A supply chain attack (code tampering)");
2655
+ console.log(" - Maintainer account compromise");
2656
+ console.log(" - Git history rewrite\n");
2657
+ console.log(" Expected hash:", `${result.expectedHash?.slice(0, 40)}...`);
2658
+ console.log(" Current hash: ", `${hash.slice(0, 40)}...\n`);
2659
+ console.log(" Recommended actions:");
2660
+ console.log(" 1. Check GitHub for official announcements");
2661
+ console.log(" 2. Contact the maintainer");
2662
+ console.log(" 3. Review code changes carefully");
2663
+ console.log(` 4. Visit: https://github.com/${ref.owner}/${ref.repo}/commits/${ref.ref}\n`);
2664
+ if (await confirm("āš ļø Proceed anyway? (NOT RECOMMENDED)", false)) {
2665
+ if (await confirm("Update kly.sum with new hash?", false)) {
2666
+ sumManager.update(url, hash, true);
2667
+ console.log(" āœ“ kly.sum updated with new hash\n");
2668
+ }
2669
+ return {
2670
+ proceedWithExecution: true,
2671
+ result
2672
+ };
2673
+ }
2674
+ console.log("\n Execution cancelled for safety");
2675
+ return {
2676
+ proceedWithExecution: false,
2677
+ result
2678
+ };
2679
+ default:
2680
+ console.error("Unknown verification result");
2681
+ return {
2682
+ proceedWithExecution: false,
2683
+ result
2684
+ };
2685
+ }
2686
+ }
2687
+ /**
2688
+ * Execute the kly app
2689
+ */
2690
+ async function executeApp(ref, repoPath, args$1, mcp) {
2691
+ const config = readKlyConfig(repoPath);
2692
+ if (config?.version) {
2693
+ if (!validateVersion(config.version, KLY_VERSION)) throw new Error(`This app requires kly ${config.version}, but you have ${KLY_VERSION}`);
2694
+ }
2695
+ if (config?.env && config.env.length > 0) {
2696
+ const missing = checkEnvVars(config.env);
2697
+ if (missing.length > 0) console.warn(`Warning: Required environment variables not set: ${missing.join(", ")}`);
2698
+ }
2699
+ const entryPoint = resolveEntryPoint(repoPath);
2700
+ if (!entryPoint) throw new Error(`Cannot resolve entry point for ${ref.owner}/${ref.repo}`);
2701
+ const absoluteEntryPath = join(repoPath, entryPoint);
2702
+ const remoteRef = `github.com/${ref.owner}/${ref.repo}`;
2703
+ const prevRemoteRef = process.env[ENV_VARS.REMOTE_REF];
2704
+ process.env[ENV_VARS.REMOTE_REF] = remoteRef;
2705
+ try {
2706
+ const { getAppIdentifier: getAppIdentifier$1, checkApiKeyPermission: checkApiKeyPermission$1, getAppSandboxConfig: getAppSandboxConfig$1 } = await import("./permissions-2r_7ZqaH.mjs");
2707
+ const { launchSandbox: launchSandbox$1 } = await import("./launcher-vTpgdO9n.mjs");
2708
+ const appId = getAppIdentifier$1();
2709
+ console.log("šŸ” Checking permissions...");
2710
+ const allowApiKey = await checkApiKeyPermission$1(appId);
2711
+ if (!allowApiKey) {
2712
+ console.error("āŒ Permission denied: API key access rejected");
2713
+ process.exit(1);
2714
+ }
2715
+ const sandboxConfig = await getAppSandboxConfig$1(appId);
2716
+ if (!sandboxConfig) {
2717
+ console.error("āŒ Permission denied: Sandbox configuration rejected");
2718
+ process.exit(1);
2719
+ }
2720
+ if (mcp) {
2721
+ console.warn("āš ļø MCP mode with remote repos not yet fully supported in new architecture");
2722
+ process.env[ENV_VARS.MCP_MODE] = "true";
2723
+ process.argv = ["bun", absoluteEntryPath];
2724
+ await import(absoluteEntryPath);
2725
+ return;
2726
+ }
2727
+ const result = await launchSandbox$1({
2728
+ scriptPath: absoluteEntryPath,
2729
+ args: args$1,
2730
+ appId,
2731
+ sandboxConfig,
2732
+ allowApiKey
2733
+ });
2734
+ if (result.error) console.error(`\nāŒ Error: ${result.error}`);
2735
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2736
+ } finally {
2737
+ if (prevRemoteRef === void 0) delete process.env[ENV_VARS.REMOTE_REF];
2738
+ else process.env[ENV_VARS.REMOTE_REF] = prevRemoteRef;
2739
+ }
2740
+ }
2741
+
2742
+ //#endregion
2743
+ //#region bin/kly.ts
2744
+ const args = process.argv.slice(2);
2745
+ const command = args[0];
2746
+ async function main() {
2747
+ if (!command || command === "--help" || command === "-h") {
2748
+ showHelp();
2749
+ return;
2750
+ }
2751
+ if (command === "--version" || command === "-v") {
2752
+ showVersion();
2753
+ return;
2754
+ }
2755
+ if (command === "models") {
2756
+ await modelsCommand();
2757
+ return;
2758
+ }
2759
+ if (command === "permissions") {
2760
+ await permissionsCommand();
2761
+ return;
2762
+ }
2763
+ if (command === "run") {
2764
+ const target = args[1];
2765
+ if (!target) {
2766
+ console.error("Error: Missing file path or remote reference");
2767
+ console.error("Usage: kly run <file|user/repo[@ref]>");
2768
+ process.exit(1);
2769
+ }
2770
+ const force = args.indexOf("--force") !== -1;
2771
+ const dashDashIndex = args.indexOf("--");
2772
+ const appArgs = dashDashIndex !== -1 ? args.slice(dashDashIndex + 1) : args.slice(2).filter((arg) => arg !== "--force");
2773
+ if (isRemoteRef(target)) await runRemote(target, {
2774
+ args: appArgs,
2775
+ force
2776
+ });
2777
+ else await runFile(target, appArgs);
2778
+ return;
2779
+ }
2780
+ if (command === "mcp") {
2781
+ const target = args[1];
2782
+ if (!target) {
2783
+ console.error("Error: Missing file path or remote reference");
2784
+ console.error("Usage: kly mcp <file|user/repo[@ref]>");
2785
+ process.exit(1);
2786
+ }
2787
+ const force = args.indexOf("--force") !== -1;
2788
+ if (isRemoteRef(target)) await runRemote(target, {
2789
+ args: [],
2790
+ force,
2791
+ mcp: true
2792
+ });
2793
+ else await runFileAsMcp(target);
2794
+ return;
2795
+ }
2796
+ console.error(`Unknown command: ${command}`);
2797
+ console.error("Run \"kly --help\" for usage");
2798
+ process.exit(1);
2799
+ }
2800
+ async function runFile(filePath, appArgs) {
2801
+ const absolutePath = resolve(process.cwd(), filePath);
2802
+ const prevLocalRef = process.env.KLY_LOCAL_REF;
2803
+ process.env.KLY_LOCAL_REF = `local:${absolutePath}`;
2804
+ try {
2805
+ const appId = getAppIdentifier();
2806
+ const storedConfig = checkStoredPermission(appId);
2807
+ let sandboxConfig;
2808
+ let allowApiKey = false;
2809
+ if (!storedConfig) {
2810
+ const appPermissions = await extractAppPermissions(absolutePath);
2811
+ sandboxConfig = buildSandboxConfig(appPermissions);
2812
+ console.log("šŸ” Checking permissions...");
2813
+ if (!await requestUnifiedPermission(appId, appPermissions, sandboxConfig)) {
2814
+ console.error("āŒ Permission denied");
2815
+ process.exit(1);
2816
+ }
2817
+ allowApiKey = appPermissions?.apiKeys ?? false;
2818
+ } else {
2819
+ sandboxConfig = storedConfig;
2820
+ allowApiKey = (await extractAppPermissions(absolutePath))?.apiKeys ?? false;
2821
+ }
2822
+ const result = await launchSandbox({
2823
+ scriptPath: absolutePath,
2824
+ args: appArgs,
2825
+ appId,
2826
+ sandboxConfig,
2827
+ allowApiKey
2828
+ });
2829
+ if (result.error) console.error(`\nāŒ Error: ${result.error}`);
2830
+ if (result.exitCode !== 0) process.exit(result.exitCode);
2831
+ } finally {
2832
+ if (prevLocalRef === void 0) delete process.env.KLY_LOCAL_REF;
2833
+ else process.env.KLY_LOCAL_REF = prevLocalRef;
2834
+ }
2835
+ }
2836
+ async function runFileAsMcp(filePath) {
2837
+ const absolutePath = resolve(process.cwd(), filePath);
2838
+ process.env.KLY_MCP_MODE = "true";
2839
+ process.argv = ["bun", absolutePath];
2840
+ await import(absolutePath);
2841
+ }
2842
+ function showHelp() {
2843
+ console.log(`
2844
+ kly - Command Line AI
2845
+
2846
+ Usage:
2847
+ kly <command> [options]
2848
+
2849
+ Commands:
2850
+ models Manage LLM model configurations
2851
+ permissions Manage app permissions
2852
+ run <target> Run a Kly app
2853
+ mcp <target> Start an MCP server for a Kly app
2854
+
2855
+ Target can be:
2856
+ ./file.ts Local file
2857
+ user/repo GitHub repo (main branch)
2858
+ user/repo@v1.0.0 GitHub repo at specific tag
2859
+ user/repo@branch GitHub repo at specific branch
2860
+
2861
+ Options:
2862
+ --force Force re-fetch remote repo (ignore cache)
2863
+ --help, -h Show help
2864
+ --version, -v Show version
2865
+
2866
+ Examples:
2867
+ kly models
2868
+ kly permissions
2869
+ kly run ./my-tool.ts
2870
+ kly run ./my-tool.ts --name=World
2871
+ kly run user/weather-app
2872
+ kly run user/weather-app@v1.0.0
2873
+ kly run user/weather-app -- --city=Beijing
2874
+ kly mcp ./my-tool.ts
2875
+ kly mcp user/weather-app
2876
+ `);
2877
+ }
2878
+ function showVersion() {
2879
+ console.log("0.1.0");
2880
+ }
2881
+ main().catch((err) => {
2882
+ console.error(err.message || err);
2883
+ process.exit(1);
2884
+ });
2885
+
2886
+ //#endregion
2887
+ export { getAppSandboxConfig as a, revokePermission as c, getAppName as i, savePermissions as l, clearAllPermissions as n, listPermissions as o, getAppIdentifier as r, loadPermissions as s, checkApiKeyPermission as t, launchSandbox as u };
2888
+ //# sourceMappingURL=kly.mjs.map