@kai-chat/desktop 0.0.1

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.
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseRequest = parseRequest;
7
+ exports.workingFolders = workingFolders;
8
+ exports.buildArgs = buildArgs;
9
+ exports.promptInput = promptInput;
10
+ exports.mcpConfig = mcpConfig;
11
+ exports.eventsOf = eventsOf;
12
+ exports.findClaude = findClaude;
13
+ exports.runClaude = runClaude;
14
+ exports.cancelClaude = cancelClaude;
15
+ exports.cancelAllClaude = cancelAllClaude;
16
+ exports.claudeStatus = claudeStatus;
17
+ const child_process_1 = require("child_process");
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const TOOLS = ['Read', 'Glob', 'Grep', 'Edit', 'Write'];
22
+ const WRITE_TOOLS = ['Edit', 'Write'];
23
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:[\]-]{0,99}$/;
24
+ const MCP_PATH = /^\/api\/claude-code\/mcp\/[A-Za-z0-9_-]{1,100}$/;
25
+ const TOKEN = /^[A-Za-z0-9_-]{16,200}$/;
26
+ const MAX_TEXT = 4 * 1024 * 1024;
27
+ const ATTACHMENT_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'application/pdf'];
28
+ const MAX_ATTACHMENTS = 20;
29
+ const MAX_ATTACHMENT_CHARS = 12 * 1024 * 1024; // base64 characters: about 9 MB of file
30
+ const MAX_ATTACHMENTS_TOTAL_CHARS = 30 * 1024 * 1024;
31
+ const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
32
+ const MAX_TOOL_RESULT = 20_000;
33
+ const KILL_GRACE_MS = 5_000;
34
+ const NOT_FOUND = 'The Claude Code command was not found on this computer. Install Claude Code, sign in with it once, and try again.';
35
+ const TURNED_OFF = 'Claude Code is turned off on this computer. Turn it on in the desktop app under Settings > Claude Code.';
36
+ // ------------------------------------------------------------------ the request
37
+ /** IPC arguments come from the renderer, so check their shape before use. */
38
+ function parseRequest(value) {
39
+ const v = value;
40
+ const fail = (what) => {
41
+ throw new Error(`Invalid Claude Code request: ${what}`);
42
+ };
43
+ if (!v || typeof v !== 'object')
44
+ return fail('not an object');
45
+ const text = (key, max = MAX_TEXT) => {
46
+ const x = v[key];
47
+ return typeof x === 'string' && x.length <= max ? x : fail(key);
48
+ };
49
+ const runId = text('runId', 200);
50
+ if (runId === '')
51
+ fail('runId');
52
+ const model = text('model', 100);
53
+ if (model !== '' && !MODEL.test(model))
54
+ fail('model');
55
+ const tools = v['tools'];
56
+ if (!Array.isArray(tools) || !tools.every((t) => TOOLS.includes(t)))
57
+ fail('tools');
58
+ const rawAttachments = v['attachments'] === undefined ? [] : v['attachments'];
59
+ if (!Array.isArray(rawAttachments) || rawAttachments.length > MAX_ATTACHMENTS)
60
+ fail('attachments');
61
+ let total = 0;
62
+ const attachments = rawAttachments.map((item) => {
63
+ const a = item;
64
+ if (!a ||
65
+ typeof a['mediaType'] !== 'string' ||
66
+ !ATTACHMENT_TYPES.includes(a['mediaType']) ||
67
+ typeof a['data'] !== 'string' ||
68
+ a['data'].length === 0 ||
69
+ a['data'].length > MAX_ATTACHMENT_CHARS ||
70
+ a['data'].length % 4 !== 0 ||
71
+ !BASE64.test(a['data'])) {
72
+ return fail('attachments');
73
+ }
74
+ total += a['data'].length;
75
+ return { mediaType: a['mediaType'], data: a['data'] };
76
+ });
77
+ if (total > MAX_ATTACHMENTS_TOTAL_CHARS)
78
+ fail('attachments');
79
+ let mcp = null;
80
+ if (v['mcp'] !== null && v['mcp'] !== undefined) {
81
+ const m = v['mcp'];
82
+ if (typeof m['path'] !== 'string' || !MCP_PATH.test(m['path']) || typeof m['token'] !== 'string' || !TOKEN.test(m['token'])) {
83
+ fail('mcp');
84
+ }
85
+ mcp = { path: m['path'], token: m['token'] };
86
+ }
87
+ const serverUrl = text('serverUrl', 2_000);
88
+ try {
89
+ const url = new URL(serverUrl);
90
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
91
+ fail('serverUrl');
92
+ }
93
+ catch {
94
+ fail('serverUrl');
95
+ }
96
+ return {
97
+ runId,
98
+ model,
99
+ systemPrompt: text('systemPrompt'),
100
+ prompt: text('prompt'),
101
+ attachments,
102
+ tools: [...new Set(tools)],
103
+ mcp,
104
+ serverUrl,
105
+ };
106
+ }
107
+ // ---------------------------------------------------------------------- folders
108
+ function realDir(p) {
109
+ try {
110
+ const real = fs_1.default.realpathSync(p);
111
+ return fs_1.default.statSync(real).isDirectory() ? real : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * The folders the CLI may work in: the ones the user allowed in Settings, and nothing else. A run
119
+ * with a write tool only gets folders the user opened for writing: the CLI's tools cannot be held
120
+ * to write in one folder and read in another.
121
+ */
122
+ function workingFolders(tools, granted) {
123
+ if (tools.length === 0)
124
+ return [];
125
+ const writes = tools.some(t => WRITE_TOOLS.includes(t));
126
+ const found = new Set();
127
+ for (const folder of granted) {
128
+ if (writes && folder.access !== 'readwrite')
129
+ continue;
130
+ const root = realDir(folder.path);
131
+ if (root)
132
+ found.add(root);
133
+ }
134
+ return [...found];
135
+ }
136
+ // -------------------------------------------------------------------- the command
137
+ /**
138
+ * The CLI's arguments. The tool list is the ceiling (nothing else exists for the model), and the
139
+ * permission mode makes anything not pre-approved a refusal rather than a question nobody can answer:
140
+ * reads stay inside the working folders, and writes are only accepted there and only when the agent may.
141
+ */
142
+ function buildArgs(params) {
143
+ const { request, folders } = params;
144
+ const writes = request.tools.some(t => WRITE_TOOLS.includes(t));
145
+ return [
146
+ '-p',
147
+ '--output-format',
148
+ 'stream-json',
149
+ // A prompt with pictures or PDFs is a message of content blocks, not plain text.
150
+ ...(request.attachments.length > 0 ? ['--input-format', 'stream-json'] : []),
151
+ '--verbose',
152
+ '--include-partial-messages',
153
+ '--no-session-persistence',
154
+ '--permission-mode',
155
+ writes ? 'acceptEdits' : 'dontAsk',
156
+ '--tools',
157
+ request.tools.join(','),
158
+ // Nothing but the agent's own MCP endpoint: not this person's other servers or account connectors.
159
+ '--strict-mcp-config',
160
+ '--system-prompt-file',
161
+ params.systemPromptFile,
162
+ ...(request.model ? ['--model', request.model] : []),
163
+ ...(params.mcpConfigFile ? ['--mcp-config', params.mcpConfigFile, '--allowedTools', 'mcp__kai'] : []),
164
+ ...folders.slice(1).flatMap(f => ['--add-dir', f]),
165
+ ];
166
+ }
167
+ /**
168
+ * What is written to the CLI's stdin: the prompt as it is, or, with pictures or PDFs, one line of
169
+ * JSON holding a single user message (the text first, then each file as a base64 content block, in
170
+ * the order the prompt refers to them). It never goes on the command line or to disk.
171
+ */
172
+ function promptInput(request) {
173
+ if (request.attachments.length === 0)
174
+ return request.prompt;
175
+ const blocks = request.attachments.map(a => ({
176
+ type: a.mediaType === 'application/pdf' ? 'document' : 'image',
177
+ source: { type: 'base64', media_type: a.mediaType, data: a.data },
178
+ }));
179
+ return `${JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text: request.prompt }, ...blocks] } })}\n`;
180
+ }
181
+ function mcpConfig(request) {
182
+ const base = new URL(request.serverUrl);
183
+ const url = new URL(request.mcp.path, base);
184
+ // The token is only ever sent to the server this app is logged in to.
185
+ if (url.origin !== base.origin)
186
+ throw new Error('Invalid Claude Code request: mcp');
187
+ return JSON.stringify({
188
+ mcpServers: { kai: { type: 'http', url: url.toString(), headers: { Authorization: `Bearer ${request.mcp.token}` } } },
189
+ });
190
+ }
191
+ // ------------------------------------------------------------------- the output
192
+ function contentText(content) {
193
+ const text = typeof content === 'string'
194
+ ? content
195
+ : Array.isArray(content)
196
+ ? content.map(b => (b && typeof b === 'object' && typeof b.text === 'string' ? b.text : '')).join('')
197
+ : '';
198
+ return text.length > MAX_TOOL_RESULT ? `${text.slice(0, MAX_TOOL_RESULT)}\n(cut: ${text.length - MAX_TOOL_RESULT} more characters)` : text;
199
+ }
200
+ /** What one line of the CLI's `stream-json` output means for the server (nothing, for most lines). */
201
+ function eventsOf(line, runId, seenTools) {
202
+ let msg;
203
+ try {
204
+ msg = JSON.parse(line);
205
+ }
206
+ catch {
207
+ return [];
208
+ }
209
+ if (!msg || typeof msg !== 'object')
210
+ return [];
211
+ // What a sub-agent says is not the agent's answer.
212
+ const inner = msg.parent_tool_use_id != null;
213
+ if (msg.type === 'stream_event' && !inner) {
214
+ const delta = msg.event?.type === 'content_block_delta' ? msg.event.delta : null;
215
+ if (delta?.type === 'text_delta' && typeof delta.text === 'string' && delta.text !== '') {
216
+ return [{ runId, kind: 'text', delta: delta.text }];
217
+ }
218
+ return [];
219
+ }
220
+ if (msg.type === 'assistant' && !inner && Array.isArray(msg.message?.content)) {
221
+ const events = [];
222
+ for (const block of msg.message.content) {
223
+ if (block?.type === 'tool_use' && typeof block.id === 'string' && !seenTools.has(block.id)) {
224
+ seenTools.add(block.id);
225
+ events.push({
226
+ runId,
227
+ kind: 'tool_use',
228
+ toolId: block.id,
229
+ name: String(block.name),
230
+ input: block.input && typeof block.input === 'object' ? block.input : {},
231
+ });
232
+ }
233
+ }
234
+ return events;
235
+ }
236
+ if (msg.type === 'user' && !inner && Array.isArray(msg.message?.content)) {
237
+ return msg.message.content
238
+ .filter((b) => b?.type === 'tool_result' && typeof b.tool_use_id === 'string')
239
+ .map((b) => ({
240
+ runId,
241
+ kind: 'tool_result',
242
+ toolId: b.tool_use_id,
243
+ isError: b.is_error === true,
244
+ content: contentText(b.content),
245
+ }));
246
+ }
247
+ if (msg.type === 'result') {
248
+ const text = typeof msg.result === 'string' ? msg.result : '';
249
+ return msg.is_error === true || (typeof msg.subtype === 'string' && msg.subtype !== 'success')
250
+ ? [{ runId, kind: 'error', message: text || `Claude Code stopped: ${msg.subtype ?? 'unknown reason'}` }]
251
+ : [{ runId, kind: 'done', text }];
252
+ }
253
+ return [];
254
+ }
255
+ // ------------------------------------------------------------------ the process
256
+ /** Finds the CLI: on PATH first, then where its installers put it (a desktop app has a thin PATH). */
257
+ function findClaude(env = process.env, exists = p => {
258
+ try {
259
+ return fs_1.default.statSync(p).isFile();
260
+ }
261
+ catch {
262
+ return false;
263
+ }
264
+ }, home = os_1.default.homedir()) {
265
+ const names = process.platform === 'win32' ? ['claude.exe'] : ['claude'];
266
+ const dirs = [
267
+ ...(env['PATH'] ?? '').split(path_1.default.delimiter).filter(Boolean),
268
+ path_1.default.join(home, '.local', 'bin'),
269
+ path_1.default.join(home, '.claude', 'local'),
270
+ path_1.default.join(home, '.npm-global', 'bin'),
271
+ '/usr/local/bin',
272
+ '/opt/homebrew/bin',
273
+ ];
274
+ for (const dir of dirs) {
275
+ for (const name of names) {
276
+ const candidate = path_1.default.join(dir, name);
277
+ if (exists(candidate))
278
+ return candidate;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+ const running = new Map();
284
+ /**
285
+ * Runs the request and reports what happens through `deps.emit`. Every outcome ends in exactly one
286
+ * `done` or `error` event (a cancelled run ends in neither: whoever cancelled it already knows).
287
+ */
288
+ async function runClaude(input, deps) {
289
+ let runId = '';
290
+ let ended = false;
291
+ const emit = (event) => {
292
+ if (event.kind === 'done' || event.kind === 'error') {
293
+ if (ended)
294
+ return;
295
+ ended = true;
296
+ }
297
+ deps.emit(event);
298
+ };
299
+ const fail = (message) => emit({ runId, kind: 'error', message });
300
+ let request;
301
+ try {
302
+ request = parseRequest(input);
303
+ }
304
+ catch (err) {
305
+ // Without a valid run id nobody is waiting for an answer here, but the server still is: say what we can.
306
+ const id = input?.runId;
307
+ runId = typeof id === 'string' ? id : '';
308
+ if (runId)
309
+ fail(err instanceof Error ? err.message : String(err));
310
+ return;
311
+ }
312
+ runId = request.runId;
313
+ if (!deps.enabled())
314
+ return fail(TURNED_OFF);
315
+ if (running.has(runId))
316
+ return fail('Claude Code is already running for this request.');
317
+ const command = (deps.command ?? (() => findClaude()))();
318
+ if (!command)
319
+ return fail(NOT_FOUND);
320
+ const workDir = fs_1.default.mkdtempSync(path_1.default.join(deps.tmpDir ?? os_1.default.tmpdir(), 'kai-claude-'));
321
+ try {
322
+ fs_1.default.chmodSync(workDir, 0o700);
323
+ const folders = workingFolders(request.tools, deps.folders());
324
+ if (request.tools.length > 0 && folders.length === 0) {
325
+ return fail(request.tools.some(t => WRITE_TOOLS.includes(t))
326
+ ? 'No folder is open for reading and writing here. Add one under Settings > File access with "Read & write".'
327
+ : 'No folder is allowed here. Add one under Settings > File access.');
328
+ }
329
+ // Private files instead of the command line, which other programs on this computer can read.
330
+ const systemPromptFile = path_1.default.join(workDir, 'system-prompt.txt');
331
+ fs_1.default.writeFileSync(systemPromptFile, request.systemPrompt, { mode: 0o600 });
332
+ let mcpConfigFile = null;
333
+ if (request.mcp) {
334
+ mcpConfigFile = path_1.default.join(workDir, 'mcp.json');
335
+ fs_1.default.writeFileSync(mcpConfigFile, mcpConfig(request), { mode: 0o600 });
336
+ }
337
+ // With no folder granted the CLI starts in an empty one of its own.
338
+ const cwd = folders[0] ?? path_1.default.join(workDir, 'empty');
339
+ if (folders.length === 0)
340
+ fs_1.default.mkdirSync(cwd);
341
+ const args = buildArgs({ request, folders, systemPromptFile, mcpConfigFile });
342
+ const child = (deps.spawn ?? child_process_1.spawn)(command, args, { cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
343
+ const entry = { child, cancelled: false };
344
+ running.set(runId, entry);
345
+ await new Promise(resolve => {
346
+ const seenTools = new Set();
347
+ let stdout = '';
348
+ let stderr = '';
349
+ child.on('error', (err) => {
350
+ fail(err.code === 'ENOENT' ? NOT_FOUND : `Could not start Claude Code: ${err.message}`);
351
+ resolve();
352
+ });
353
+ child.on('spawn', () => {
354
+ emit({ runId, kind: 'started' });
355
+ });
356
+ child.stdout?.on('data', (chunk) => {
357
+ stdout += chunk.toString('utf8');
358
+ let newline;
359
+ while ((newline = stdout.indexOf('\n')) >= 0) {
360
+ const line = stdout.slice(0, newline).trim();
361
+ stdout = stdout.slice(newline + 1);
362
+ if (line)
363
+ for (const event of eventsOf(line, runId, seenTools))
364
+ emit(event);
365
+ }
366
+ });
367
+ child.stderr?.on('data', (chunk) => {
368
+ stderr = (stderr + chunk.toString('utf8')).slice(-2_000);
369
+ });
370
+ child.on('close', code => {
371
+ if (stdout.trim())
372
+ for (const event of eventsOf(stdout.trim(), runId, seenTools))
373
+ emit(event);
374
+ if (!ended && !entry.cancelled) {
375
+ fail(`Claude Code stopped without an answer${code !== null ? ` (exit code ${code})` : ''}.${stderr.trim() ? ` ${stderr.trim()}` : ''}`);
376
+ }
377
+ resolve();
378
+ });
379
+ child.stdin?.on('error', () => undefined); // the CLI may exit before it reads everything
380
+ child.stdin?.end(promptInput(request));
381
+ });
382
+ }
383
+ catch (err) {
384
+ fail(`Could not run Claude Code: ${err instanceof Error ? err.message : String(err)}`);
385
+ }
386
+ finally {
387
+ running.delete(runId);
388
+ fs_1.default.rmSync(workDir, { recursive: true, force: true });
389
+ }
390
+ }
391
+ /** Stops a run (the server cancelled it, or the connection to the server was lost). */
392
+ function cancelClaude(runId) {
393
+ const entry = running.get(runId);
394
+ if (!entry || entry.cancelled)
395
+ return;
396
+ entry.cancelled = true;
397
+ entry.child.kill('SIGTERM');
398
+ // A CLI that ignores SIGTERM must not keep running an agent nobody is waiting for.
399
+ const timer = setTimeout(() => entry.child.kill('SIGKILL'), KILL_GRACE_MS);
400
+ timer.unref?.();
401
+ entry.child.once('close', () => clearTimeout(timer));
402
+ }
403
+ function cancelAllClaude() {
404
+ for (const runId of [...running.keys()])
405
+ cancelClaude(runId);
406
+ }
407
+ /** For the Settings screen: is the command there, and does it run? */
408
+ async function claudeStatus(enabled, find = () => findClaude(), run = command => new Promise((resolve, reject) => (0, child_process_1.execFile)(command, ['--version'], { timeout: 10_000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))))) {
409
+ const command = find();
410
+ if (!command)
411
+ return { enabled, command: null, version: null, problem: NOT_FOUND };
412
+ try {
413
+ return { enabled, command, version: (await run(command)).trim() || null, problem: null };
414
+ }
415
+ catch (err) {
416
+ return {
417
+ enabled,
418
+ command,
419
+ version: null,
420
+ problem: `The command was found but did not run: ${err instanceof Error ? err.message : String(err)}`,
421
+ };
422
+ }
423
+ }
424
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_SIDE = void 0;
4
+ exports.fitToBounds = fitToBounds;
5
+ exports.toImageResult = toImageResult;
6
+ const electron_1 = require("electron");
7
+ // Pictures an agent is shown (a screenshot of the screen or of a page). A 4K screen is several
8
+ // megabytes as PNG and far more than a model can use, so they are scaled down before they leave.
9
+ /** The longest side, in pixels: what Anthropic recommends, larger images are scaled down by the API anyway. */
10
+ exports.MAX_SIDE = 1568;
11
+ /** The PNG, scaled down (never up) so that neither side is longer than `maxSide`. */
12
+ function fitToBounds(png, maxSide = exports.MAX_SIDE) {
13
+ const image = electron_1.nativeImage.createFromBuffer(png);
14
+ if (image.isEmpty())
15
+ throw new Error('The picture could not be read');
16
+ const { width, height } = image.getSize();
17
+ const scale = Math.min(1, maxSide / Math.max(width, height));
18
+ if (scale === 1)
19
+ return png;
20
+ return image
21
+ .resize({ width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)), quality: 'best' })
22
+ .toPNG();
23
+ }
24
+ function toImageResult(png) {
25
+ return { kind: 'image', mediaType: 'image/png', data: fitToBounds(png).toString('base64') };
26
+ }
27
+ //# sourceMappingURL=image.js.map
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const electron_1 = require("electron");
7
+ const path_1 = __importDefault(require("path"));
8
+ const filesystem_js_1 = require("./capabilities/filesystem.js");
9
+ const screen_js_1 = require("./capabilities/screen.js");
10
+ const browser_window_js_1 = require("./browser-window.js");
11
+ const claude_js_1 = require("./claude.js");
12
+ const settings_js_1 = require("./settings.js");
13
+ const isDev = process.env['NODE_ENV'] !== 'production';
14
+ function createWindow() {
15
+ const win = new electron_1.BrowserWindow({
16
+ width: 1180,
17
+ height: 780,
18
+ minWidth: 860,
19
+ minHeight: 560,
20
+ resizable: true,
21
+ webPreferences: {
22
+ nodeIntegration: false,
23
+ contextIsolation: true,
24
+ preload: path_1.default.join(__dirname, 'preload.js'),
25
+ },
26
+ title: 'KAI',
27
+ });
28
+ if (isDev) {
29
+ win.loadURL('http://localhost:5173');
30
+ }
31
+ else {
32
+ win.loadFile(path_1.default.join(__dirname, '../renderer/index.html'));
33
+ }
34
+ return win;
35
+ }
36
+ electron_1.app.whenReady().then(() => {
37
+ createWindow();
38
+ electron_1.app.on('activate', () => {
39
+ if (electron_1.BrowserWindow.getAllWindows().length === 0)
40
+ createWindow();
41
+ });
42
+ });
43
+ electron_1.app.on('window-all-closed', () => {
44
+ if (process.platform !== 'darwin')
45
+ electron_1.app.quit();
46
+ });
47
+ // IPC arguments come from the renderer, so check their shape before use.
48
+ // Folder access is read from the user's saved settings here; the renderer never supplies it.
49
+ function asString(v, name) {
50
+ if (typeof v !== 'string')
51
+ throw new Error(`${name} must be a string`);
52
+ return v;
53
+ }
54
+ // Filesystem IPC handlers
55
+ electron_1.ipcMain.handle('filesystem:list', async (_e, p) => (0, filesystem_js_1.listFiles)(asString(p, 'path'), (0, settings_js_1.getFolders)()));
56
+ electron_1.ipcMain.handle('filesystem:read', async (_e, p) => (0, filesystem_js_1.readFile)(asString(p, 'path'), (0, settings_js_1.getFolders)()));
57
+ electron_1.ipcMain.handle('filesystem:write', async (_e, p, content) => (0, filesystem_js_1.writeFile)(asString(p, 'path'), asString(content, 'content'), (0, settings_js_1.getFolders)()));
58
+ // Settings IPC handlers (file permissions)
59
+ electron_1.ipcMain.handle('settings:getFolders', async () => (0, settings_js_1.getFolders)());
60
+ electron_1.ipcMain.handle('settings:addFolder', async (e) => (0, settings_js_1.addFolder)(electron_1.BrowserWindow.fromWebContents(e.sender)));
61
+ electron_1.ipcMain.handle('settings:removeFolder', async (_e, p) => (0, settings_js_1.removeFolder)(asString(p, 'path')));
62
+ electron_1.ipcMain.handle('settings:setAccess', async (_e, p, access) => {
63
+ if (access !== 'read' && access !== 'readwrite')
64
+ throw new Error('invalid access level');
65
+ return (0, settings_js_1.setAccess)(asString(p, 'path'), access);
66
+ });
67
+ // Claude Code: the server asks this app to run the CLI for an agent (see claude.ts for the checks)
68
+ electron_1.ipcMain.handle('claude:status', async () => (0, claude_js_1.claudeStatus)((0, settings_js_1.getClaudeCodeEnabled)()));
69
+ electron_1.ipcMain.handle('claude:setEnabled', async (_e, enabled) => {
70
+ if (typeof enabled !== 'boolean')
71
+ throw new Error('enabled must be true or false');
72
+ return (0, settings_js_1.setClaudeCodeEnabled)(enabled);
73
+ });
74
+ electron_1.ipcMain.handle('claude:run', async (e, request) => {
75
+ const sender = e.sender;
76
+ // Nobody is left to read what the CLI does once the window is gone.
77
+ const onGone = () => (0, claude_js_1.cancelAllClaude)();
78
+ sender.once('destroyed', onGone);
79
+ try {
80
+ await (0, claude_js_1.runClaude)(request, {
81
+ enabled: settings_js_1.getClaudeCodeEnabled,
82
+ folders: settings_js_1.getFolders,
83
+ emit: event => {
84
+ if (!sender.isDestroyed())
85
+ sender.send('claude:event', event);
86
+ },
87
+ });
88
+ }
89
+ finally {
90
+ sender.removeListener('destroyed', onGone);
91
+ }
92
+ });
93
+ electron_1.ipcMain.handle('claude:cancel', async (_e, runId) => {
94
+ if (typeof runId === 'string')
95
+ (0, claude_js_1.cancelClaude)(runId);
96
+ });
97
+ electron_1.ipcMain.handle('claude:cancelAll', async () => (0, claude_js_1.cancelAllClaude)());
98
+ // Screen IPC handler
99
+ electron_1.ipcMain.handle('screen:capture', async () => (0, screen_js_1.captureScreen)());
100
+ // Browser IPC handlers: the controller checks the tool and its arguments itself, and refuses
101
+ // everything while the person has not turned the browser on in Settings.
102
+ electron_1.ipcMain.handle('browser:run', async (_e, tool, args) => browser_window_js_1.browser.run(tool, args));
103
+ electron_1.ipcMain.handle('settings:getBrowserEnabled', async () => (0, settings_js_1.getBrowserEnabled)());
104
+ electron_1.ipcMain.handle('settings:setBrowserEnabled', async (_e, enabled) => {
105
+ const on = enabled === true;
106
+ if (!on)
107
+ browser_window_js_1.browser.shutdown();
108
+ return (0, settings_js_1.setBrowserEnabled)(on);
109
+ });
110
+ electron_1.app.on('before-quit', () => browser_window_js_1.browser.shutdown());
111
+ // Secure storage IPC handlers
112
+ electron_1.ipcMain.handle('storage:encrypt', async (_e, val) => {
113
+ if (electron_1.safeStorage.isEncryptionAvailable()) {
114
+ return electron_1.safeStorage.encryptString(val).toString('base64');
115
+ }
116
+ return val;
117
+ });
118
+ electron_1.ipcMain.handle('storage:decrypt', async (_e, val) => {
119
+ if (electron_1.safeStorage.isEncryptionAvailable()) {
120
+ return electron_1.safeStorage.decryptString(Buffer.from(val, 'base64'));
121
+ }
122
+ return val;
123
+ });
124
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
4
+ electron_1.contextBridge.exposeInMainWorld('kai', {
5
+ filesystem: {
6
+ list: (p) => electron_1.ipcRenderer.invoke('filesystem:list', p),
7
+ read: (p) => electron_1.ipcRenderer.invoke('filesystem:read', p),
8
+ write: (p, content) => electron_1.ipcRenderer.invoke('filesystem:write', p, content),
9
+ },
10
+ settings: {
11
+ getFolders: () => electron_1.ipcRenderer.invoke('settings:getFolders'),
12
+ addFolder: () => electron_1.ipcRenderer.invoke('settings:addFolder'),
13
+ removeFolder: (p) => electron_1.ipcRenderer.invoke('settings:removeFolder', p),
14
+ setAccess: (p, access) => electron_1.ipcRenderer.invoke('settings:setAccess', p, access),
15
+ },
16
+ screen: {
17
+ capture: () => electron_1.ipcRenderer.invoke('screen:capture'),
18
+ },
19
+ browser: {
20
+ run: (tool, args) => electron_1.ipcRenderer.invoke('browser:run', tool, args),
21
+ getEnabled: () => electron_1.ipcRenderer.invoke('settings:getBrowserEnabled'),
22
+ setEnabled: (enabled) => electron_1.ipcRenderer.invoke('settings:setBrowserEnabled', enabled),
23
+ },
24
+ claude: {
25
+ status: () => electron_1.ipcRenderer.invoke('claude:status'),
26
+ setEnabled: (enabled) => electron_1.ipcRenderer.invoke('claude:setEnabled', enabled),
27
+ run: (request) => electron_1.ipcRenderer.invoke('claude:run', request),
28
+ cancel: (runId) => electron_1.ipcRenderer.invoke('claude:cancel', runId),
29
+ cancelAll: () => electron_1.ipcRenderer.invoke('claude:cancelAll'),
30
+ onEvent: (callback) => {
31
+ const listener = (_e, event) => callback(event);
32
+ electron_1.ipcRenderer.on('claude:event', listener);
33
+ return () => {
34
+ electron_1.ipcRenderer.removeListener('claude:event', listener);
35
+ };
36
+ },
37
+ },
38
+ storage: {
39
+ encrypt: (val) => electron_1.ipcRenderer.invoke('storage:encrypt', val),
40
+ decrypt: (val) => electron_1.ipcRenderer.invoke('storage:decrypt', val),
41
+ },
42
+ });
43
+ //# sourceMappingURL=preload.js.map