openrune 1.1.2 → 2.0.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 (38) hide show
  1. package/README.ko.md +5 -76
  2. package/README.md +7 -80
  3. package/bin/rune.js +18 -653
  4. package/package.json +10 -40
  5. package/.claude-plugin/marketplace.json +0 -17
  6. package/.claude-plugin/plugin.json +0 -24
  7. package/.mcp.json +0 -16
  8. package/bootstrap.js +0 -8
  9. package/channel/rune-channel.ts +0 -486
  10. package/electron-builder.yml +0 -61
  11. package/finder-extension/FinderSync.swift +0 -47
  12. package/finder-extension/RuneFinderSync.appex/Contents/Info.plist +0 -27
  13. package/finder-extension/RuneFinderSync.appex/Contents/MacOS/RuneFinderSync +0 -0
  14. package/finder-extension/main.swift +0 -5
  15. package/renderer/index.html +0 -12
  16. package/renderer/src/App.tsx +0 -44
  17. package/renderer/src/features/chat/activity-block.tsx +0 -152
  18. package/renderer/src/features/chat/chat-header.tsx +0 -58
  19. package/renderer/src/features/chat/chat-input.tsx +0 -190
  20. package/renderer/src/features/chat/chat-panel.tsx +0 -151
  21. package/renderer/src/features/chat/markdown-renderer.tsx +0 -26
  22. package/renderer/src/features/chat/message-bubble.tsx +0 -79
  23. package/renderer/src/features/chat/message-list.tsx +0 -178
  24. package/renderer/src/features/chat/types.ts +0 -32
  25. package/renderer/src/features/chat/use-chat.ts +0 -260
  26. package/renderer/src/features/terminal/terminal-panel.tsx +0 -155
  27. package/renderer/src/global.d.ts +0 -29
  28. package/renderer/src/globals.css +0 -92
  29. package/renderer/src/hooks/use-ipc.ts +0 -24
  30. package/renderer/src/lib/markdown.ts +0 -83
  31. package/renderer/src/lib/utils.ts +0 -6
  32. package/renderer/src/main.tsx +0 -10
  33. package/renderer/tsconfig.json +0 -16
  34. package/renderer/vite.config.ts +0 -23
  35. package/screenshot-chatting-ui.png +0 -0
  36. package/src/main.ts +0 -796
  37. package/src/preload.ts +0 -58
  38. package/tsconfig.json +0 -14
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "openrune",
3
- "version": "1.1.2",
3
+ "version": "2.0.0",
4
4
  "description": "Persistent AI agents for Claude Code — build once, run forever.",
5
- "keywords": ["ai", "agent", "claude", "desktop", "electron", "mcp", "claude-code", "toolkit", "automation"],
5
+ "keywords": ["ai", "agent", "claude", "claude-code", "cli", "toolkit", "automation"],
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/gilhyun/Rune.git"
@@ -10,48 +10,18 @@
10
10
  "homepage": "https://github.com/gilhyun/Rune",
11
11
  "license": "MIT",
12
12
  "author": "gilhyun",
13
- "main": "bootstrap.js",
13
+ "main": "lib/index.js",
14
14
  "exports": {
15
15
  ".": "./lib/index.js"
16
16
  },
17
17
  "bin": {
18
18
  "rune": "bin/rune.js"
19
19
  },
20
- "scripts": {
21
- "pack": "electron-builder --dir",
22
- "dist": "electron-builder",
23
- "build:main": "esbuild src/main.ts --bundle --platform=node --outfile=dist/main.js --external:electron --external:node-pty && esbuild src/preload.ts --bundle --platform=node --outfile=dist/preload.js --external:electron && esbuild channel/rune-channel.ts --bundle --platform=node --outfile=dist/rune-channel.js",
24
- "build:renderer": "cd renderer && npx vite build",
25
- "build": "npm run build:main && npm run build:renderer",
26
- "postinstall": "node bin/rune.js install",
27
- "start": "npm run build && electron .",
28
- "dev": "npm run build && electron ."
29
- },
30
- "devDependencies": {
31
- "@types/node": "^22.0.0",
32
- "@types/react": "^19.2.14",
33
- "@types/react-dom": "^19.2.3",
34
- "@types/ws": "^8.18.1",
35
- "electron-builder": "^25.0.0",
36
- "typescript": "^5.7.0"
37
- },
38
- "dependencies": {
39
- "@electron/rebuild": "^3.7.1",
40
- "@modelcontextprotocol/sdk": "^1.27.1",
41
- "@tailwindcss/vite": "^4.2.2",
42
- "@vitejs/plugin-react": "^6.0.1",
43
- "@xterm/addon-fit": "^0.11.0",
44
- "@xterm/xterm": "^6.0.0",
45
- "clsx": "^2.1.1",
46
- "electron": "^28.3.3",
47
- "esbuild": "^0.27.4",
48
- "lucide-react": "^0.577.0",
49
- "node-pty": "^1.1.0",
50
- "react": "^19.2.4",
51
- "react-dom": "^19.2.4",
52
- "sonner": "^2.0.7",
53
- "tailwind-merge": "^3.5.0",
54
- "tailwindcss": "^4.2.2",
55
- "vite": "^8.0.1"
56
- }
20
+ "files": [
21
+ "bin",
22
+ "lib",
23
+ "LICENSE",
24
+ "README.md",
25
+ "README.ko.md"
26
+ ]
57
27
  }
@@ -1,17 +0,0 @@
1
- {
2
- "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
- "name": "rune",
4
- "description": "Rune — File-based AI Agent Desktop App plugins",
5
- "owner": {
6
- "name": "gilhyun",
7
- "email": "gilhyun@github.com"
8
- },
9
- "plugins": [
10
- {
11
- "name": "rune-channel",
12
- "description": "Rune desktop app channel for Claude Code",
13
- "source": "./",
14
- "category": "development"
15
- }
16
- ]
17
- }
@@ -1,24 +0,0 @@
1
- {
2
- "name": "rune-channel",
3
- "version": "0.1.0",
4
- "description": "Rune desktop app channel for Claude Code — bridges Claude Code with the Rune AI agent UI",
5
- "author": {
6
- "name": "gilhyun",
7
- "url": "https://github.com/gilhyun"
8
- },
9
- "homepage": "https://github.com/gilhyun/Rune",
10
- "repository": "https://github.com/gilhyun/Rune",
11
- "license": "MIT",
12
- "keywords": ["rune", "channel", "mcp", "desktop", "agent"],
13
- "mcpServers": {
14
- "rune-channel": {
15
- "command": "node",
16
- "args": ["${CLAUDE_PLUGIN_ROOT}/dist/rune-channel.js"]
17
- }
18
- },
19
- "channels": [
20
- {
21
- "server": "rune-channel"
22
- }
23
- ]
24
- }
package/.mcp.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "rune-channel": {
4
- "command": "node",
5
- "args": [
6
- "/Users/gilhyun/IdeaProjects/Rune/dist/rune-channel.js"
7
- ],
8
- "env": {
9
- "RUNE_FOLDER_PATH": "/Users/gilhyun/IdeaProjects/Rune",
10
- "RUNE_CHANNEL_PORT": "51234",
11
- "RUNE_AGENT_ROLE": "General assistant",
12
- "RUNE_FILE_PATH": "/Users/gilhyun/IdeaProjects/Rune/Rune.rune"
13
- }
14
- }
15
- }
16
- }
package/bootstrap.js DELETED
@@ -1,8 +0,0 @@
1
- // When loaded via Electron, start the desktop app
2
- // When loaded via require('openrune'), export the Node.js API
3
- try {
4
- require('electron')
5
- require('./dist/main.js')
6
- } catch {
7
- module.exports = require('./lib/index.js')
8
- }
@@ -1,486 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
- import * as http from 'http'
5
-
6
- import * as fs from 'fs'
7
-
8
- const PORT = Number(process.env.RUNE_CHANNEL_PORT || 51234)
9
- const FOLDER_PATH = process.env.RUNE_FOLDER_PATH || ''
10
- const AGENT_ROLE = process.env.RUNE_AGENT_ROLE || ''
11
- const RUNE_FILE_PATH = process.env.RUNE_FILE_PATH || ''
12
-
13
- // ── .rune File I/O ──────────────────────────────
14
- interface RuneFile {
15
- name: string
16
- role: string
17
- history?: { role: 'user' | 'assistant'; text: string; ts: number }[]
18
- memory?: string[]
19
- [key: string]: unknown
20
- }
21
-
22
- function readRuneFile(): RuneFile | null {
23
- if (!RUNE_FILE_PATH) return null
24
- try {
25
- return JSON.parse(fs.readFileSync(RUNE_FILE_PATH, 'utf-8'))
26
- } catch {
27
- return null
28
- }
29
- }
30
-
31
- function writeRuneFile(data: RuneFile) {
32
- if (!RUNE_FILE_PATH) return
33
- try {
34
- fs.writeFileSync(RUNE_FILE_PATH, JSON.stringify(data, null, 2), 'utf-8')
35
- } catch (e: any) {
36
- console.error(`[rune-channel] Failed to write .rune file: ${e.message}`)
37
- }
38
- }
39
-
40
- function buildSessionContext(): string {
41
- const rune = readRuneFile()
42
- if (!rune) return ''
43
-
44
- const parts: string[] = []
45
-
46
- // Memory
47
- if (rune.memory && rune.memory.length > 0) {
48
- parts.push('## Saved Memory')
49
- parts.push('These are notes you saved from previous sessions:')
50
- rune.memory.forEach((m, i) => parts.push(`${i + 1}. ${m}`))
51
- }
52
-
53
- // History: last 50 messages, recent 10 in full detail
54
- if (rune.history && rune.history.length > 0) {
55
- const TOTAL_LIMIT = 50
56
- const FULL_DETAIL_COUNT = 10
57
- const recent = rune.history.slice(-TOTAL_LIMIT)
58
- const olderMessages = recent.slice(0, -FULL_DETAIL_COUNT)
59
- const recentMessages = recent.slice(-FULL_DETAIL_COUNT)
60
-
61
- parts.push('\n## Conversation History')
62
- parts.push(`(${rune.history.length} total messages, showing last ${recent.length})`)
63
-
64
- // Older messages: summarized (truncated to 500 chars)
65
- if (olderMessages.length > 0) {
66
- parts.push('\n### Earlier Context')
67
- for (const msg of olderMessages) {
68
- const who = msg.role === 'user' ? 'User' : 'You'
69
- const text = msg.text.length > 500 ? msg.text.slice(0, 500) + '...' : msg.text
70
- parts.push(`- **${who}**: ${text}`)
71
- }
72
- }
73
-
74
- // Recent messages: full text (no truncation)
75
- if (recentMessages.length > 0) {
76
- parts.push('\n### Recent Messages (Full Detail)')
77
- for (const msg of recentMessages) {
78
- const who = msg.role === 'user' ? 'User' : 'You'
79
- parts.push(`- **${who}**: ${msg.text}`)
80
- }
81
- }
82
- }
83
-
84
- return parts.join('\n')
85
- }
86
-
87
- // ── MCP Server ──────────────────────────────────
88
-
89
- const mcp = new Server(
90
- { name: 'rune-channel', version: '0.1.0' },
91
- {
92
- capabilities: {
93
- experimental: { 'claude/channel': {} },
94
- tools: {},
95
- },
96
- instructions: `You are an AI agent running inside the Rune desktop app.
97
-
98
- ## System Info
99
- - Current time: ${new Date().toISOString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})
100
- - Local time: ${new Date().toLocaleString('en-US', { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, hour12: false })}
101
- - Working folder: ${FOLDER_PATH || '(none)'}
102
- ${AGENT_ROLE ? `\n## Your Role\n${AGENT_ROLE}\n` : ''}
103
- Messages arrive as <channel source="rune-channel" type="..." request_id="...">.
104
-
105
- ## Message types
106
- - type="chat": User message from the Rune chat UI. Always reply using the rune_reply tool with the request_id.
107
-
108
- ## CRITICAL: ALL output MUST go through rune_reply
109
- You are connected to a desktop app UI. The user CANNOT see your regular text output — they can ONLY see messages sent via rune_reply.
110
- - NEVER output plain text without calling rune_reply. Any text not sent through rune_reply is invisible to the user.
111
- - For chat messages: call rune_reply with the request_id.
112
- - For proactive updates: call rune_reply WITHOUT request_id to push via SSE.
113
- - If you need to do multi-step work (fetch data, analyze, etc.), do all the work FIRST, then send ONE comprehensive rune_reply at the end.
114
- - Even error messages and status updates must go through rune_reply.
115
-
116
- ## CRITICAL: Report ALL activities via rune_activity
117
- The user wants to see EVERYTHING you do — thinking, tool calls, and results — in real-time, just like the Claude Code VSCode plugin.
118
- - BEFORE you start working on a request, call rune_activity with type="thinking" and share your plan/reasoning.
119
- - BEFORE each tool call (Read, Edit, Bash, Write, Grep, Glob, etc.), call rune_activity with type="tool_use", tool name, and key args.
120
- - AFTER each tool call completes, call rune_activity with type="tool_result", tool name, and a brief summary of the result.
121
- - This makes your work transparent. Without rune_activity calls, the user sees a blank screen until rune_reply.
122
- - Keep thinking text concise but informative. For tool results, summarize key findings (1-2 sentences).
123
- - You MUST call rune_activity for EVERY tool you use. Do NOT skip any.
124
-
125
- ## CRITICAL: Actions, Not Words
126
- When the user asks you to do something, ACTUALLY DO IT. Never just describe what you would do.
127
- - Read files, write code, run commands — take action.
128
- - Only explain when the user asks for an explanation.
129
-
130
- ## Memory
131
- You have a rune_memory tool to save persistent notes across sessions.
132
- - Save important context: user preferences, project decisions, key findings, recurring patterns.
133
- - Memory is stored in the .rune file and provided to you at the start of each session.
134
- - Use it proactively when you learn something worth remembering.
135
- `,
136
- }
137
- )
138
-
139
- // ── Pending Replies ──────────────────────────────
140
-
141
- const pendingReplies = new Map<string, (text: string) => void>()
142
- const sseClients = new Set<http.ServerResponse>()
143
- let mcpConnected = false
144
- let sessionStarted = false
145
-
146
- function broadcastSSE(data: Record<string, unknown>) {
147
- const msg = `data: ${JSON.stringify(data)}\n\n`
148
- for (const client of sseClients) {
149
- try { client.write(msg) } catch {}
150
- }
151
- }
152
-
153
- // ── Tools ────────────────────────────────────────
154
-
155
- mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
156
- tools: [
157
- {
158
- name: 'rune_reply',
159
- description: 'Send a reply back to the Rune UI. Use this for every chat message. If request_id is omitted, the message is pushed as a proactive notification.',
160
- inputSchema: {
161
- type: 'object' as const,
162
- properties: {
163
- request_id: { type: 'string', description: 'The request_id from the incoming channel message. Omit to send a proactive push message.' },
164
- text: { type: 'string', description: 'Your response in markdown' },
165
- },
166
- required: ['text'],
167
- },
168
- },
169
- {
170
- name: 'rune_activity',
171
- description: 'Report your current activity to the Rune chat UI in real-time. Call this BEFORE and AFTER each action so the user can see what you are doing. This makes your work visible — without it, the user sees nothing until rune_reply.',
172
- inputSchema: {
173
- type: 'object' as const,
174
- properties: {
175
- type: {
176
- type: 'string',
177
- enum: ['thinking', 'tool_use', 'tool_result'],
178
- description: 'thinking: share your reasoning/plan. tool_use: report a tool you are about to call. tool_result: report the result of a tool call.',
179
- },
180
- content: { type: 'string', description: 'For thinking: your reasoning text. For tool_result: a brief summary of what happened.' },
181
- tool: { type: 'string', description: 'Tool name (e.g. Read, Edit, Bash, Grep, Write). Required for tool_use and tool_result.' },
182
- args: {
183
- type: 'object',
184
- description: 'Key arguments for the tool call (e.g. {file_path: "/foo.ts"} or {command: "npm test"}). For tool_use only.',
185
- },
186
- },
187
- required: ['type'],
188
- },
189
- },
190
- {
191
- name: 'rune_search_history',
192
- description: 'Search past conversation history by keyword. Returns matching messages with surrounding context. Use this when the user references a past conversation or you need to recall previous discussions.',
193
- inputSchema: {
194
- type: 'object' as const,
195
- properties: {
196
- query: { type: 'string', description: 'Search keyword or phrase to find in past messages' },
197
- limit: { type: 'number', description: 'Max number of results to return (default: 10)' },
198
- },
199
- required: ['query'],
200
- },
201
- },
202
- {
203
- name: 'rune_memory',
204
- description: 'Save, list, or delete persistent memory notes in the .rune file. Use this to remember important context across sessions — user preferences, project decisions, key findings, etc. Memory persists even when the session ends.',
205
- inputSchema: {
206
- type: 'object' as const,
207
- properties: {
208
- action: { type: 'string', enum: ['save', 'list', 'delete'], description: 'save: add a new memory note. list: show all saved memories. delete: remove a memory by index (1-based).' },
209
- text: { type: 'string', description: 'The memory note to save (required for "save" action)' },
210
- index: { type: 'number', description: 'The 1-based index of the memory to delete (required for "delete" action)' },
211
- },
212
- required: ['action'],
213
- },
214
- },
215
- ],
216
- }))
217
-
218
- mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
219
- const toolName = req.params.name
220
-
221
- // Broadcast tool_start for non-internal tools
222
- if (toolName !== 'rune_reply' && toolName !== 'rune_memory' && toolName !== 'rune_activity') {
223
- const argsSummary: Record<string, unknown> = {}
224
- const rawArgs = req.params.arguments as Record<string, unknown> | undefined
225
- if (rawArgs) {
226
- for (const [k, v] of Object.entries(rawArgs)) {
227
- if (typeof v === 'string' && v.length > 80) argsSummary[k] = (v as string).slice(0, 80) + '…'
228
- else argsSummary[k] = v
229
- }
230
- }
231
- broadcastSSE({ type: 'tool_start', tool: toolName, args: argsSummary })
232
- }
233
-
234
- // rune_activity tool — real-time activity reporting
235
- if (req.params.name === 'rune_activity') {
236
- const { type: activityType, content, tool, args } = req.params.arguments as {
237
- type: string; content?: string; tool?: string; args?: Record<string, unknown>
238
- }
239
- broadcastSSE({ type: 'activity', activityType, content, tool, args })
240
- return { content: [{ type: 'text' as const, text: 'ok' }] }
241
- }
242
-
243
- if (req.params.name === 'rune_reply') {
244
- const { request_id, text } = req.params.arguments as { request_id?: string; text: string }
245
- if (request_id) {
246
- const resolve = pendingReplies.get(request_id)
247
- if (resolve) {
248
- resolve(text)
249
- pendingReplies.delete(request_id)
250
- } else {
251
- broadcastSSE({ type: 'push', text })
252
- }
253
- } else {
254
- broadcastSSE({ type: 'push', text })
255
- }
256
- return { content: [{ type: 'text' as const, text: 'sent' }] }
257
- }
258
-
259
- // rune_search_history tool
260
- if (req.params.name === 'rune_search_history') {
261
- const { query, limit: maxResults } = req.params.arguments as { query: string; limit?: number }
262
- const rune = readRuneFile()
263
- if (!rune || !rune.history || rune.history.length === 0) {
264
- return { content: [{ type: 'text' as const, text: 'No conversation history found.' }] }
265
- }
266
-
267
- const cap = maxResults || 10
268
- const keywords = query.toLowerCase().split(/\s+/)
269
- const scored: { idx: number; msg: typeof rune.history[0]; score: number }[] = []
270
-
271
- for (let i = 0; i < rune.history.length; i++) {
272
- const text = rune.history[i].text.toLowerCase()
273
- let score = 0
274
- for (const kw of keywords) {
275
- if (text.includes(kw)) score++
276
- }
277
- if (score > 0) scored.push({ idx: i, msg: rune.history[i], score })
278
- }
279
-
280
- if (scored.length === 0) {
281
- return { content: [{ type: 'text' as const, text: `No messages matching "${query}".` }] }
282
- }
283
-
284
- scored.sort((a, b) => b.score - a.score)
285
- const results = scored.slice(0, cap)
286
-
287
- const output = results.map(r => {
288
- const who = r.msg.role === 'user' ? 'User' : 'Assistant'
289
- const date = new Date(r.msg.ts).toLocaleString()
290
- const text = r.msg.text.length > 300 ? r.msg.text.slice(0, 300) + '…' : r.msg.text
291
- return `[#${r.idx + 1} | ${date} | ${who}]\n${text}`
292
- }).join('\n\n---\n\n')
293
-
294
- return { content: [{ type: 'text' as const, text: `Found ${scored.length} matches (showing top ${results.length}):\n\n${output}` }] }
295
- }
296
-
297
- // rune_memory tool
298
- if (req.params.name === 'rune_memory') {
299
- const { action, text, index } = req.params.arguments as { action: string; text?: string; index?: number }
300
- const rune = readRuneFile()
301
- if (!rune) {
302
- return { content: [{ type: 'text' as const, text: 'No .rune file available' }], isError: true }
303
- }
304
- if (!rune.memory) rune.memory = []
305
-
306
- if (action === 'save') {
307
- if (!text) return { content: [{ type: 'text' as const, text: 'text is required for save action' }], isError: true }
308
- rune.memory.push(text)
309
- writeRuneFile(rune)
310
- broadcastSSE({ type: 'memory_update' })
311
- return { content: [{ type: 'text' as const, text: `Memory saved (${rune.memory.length} total)` }] }
312
- }
313
-
314
- if (action === 'list') {
315
- if (rune.memory.length === 0) {
316
- return { content: [{ type: 'text' as const, text: 'No memories saved yet.' }] }
317
- }
318
- const list = rune.memory.map((m, i) => `${i + 1}. ${m}`).join('\n')
319
- return { content: [{ type: 'text' as const, text: `Saved memories:\n${list}` }] }
320
- }
321
-
322
- if (action === 'delete') {
323
- if (!index || index < 1 || index > rune.memory.length) {
324
- return { content: [{ type: 'text' as const, text: `Invalid index. Valid range: 1-${rune.memory.length}` }], isError: true }
325
- }
326
- const removed = rune.memory.splice(index - 1, 1)
327
- writeRuneFile(rune)
328
- broadcastSSE({ type: 'memory_update' })
329
- return { content: [{ type: 'text' as const, text: `Deleted: "${removed[0]}" (${rune.memory.length} remaining)` }] }
330
- }
331
-
332
- return { content: [{ type: 'text' as const, text: `Unknown action: ${action}` }], isError: true }
333
- }
334
-
335
- // Unknown tool
336
- return { content: [{ type: 'text' as const, text: `Unknown tool: ${toolName}` }], isError: true }
337
- })
338
-
339
- // ── Main ─────────────────────────────────────────
340
-
341
- async function main() {
342
- const transport = new StdioServerTransport()
343
- await mcp.connect(transport)
344
- mcpConnected = true
345
- console.error(`[rune-channel] MCP connected (port=${PORT}, folder=${FOLDER_PATH})`)
346
-
347
- // Notify Claude about session start with history + memory context
348
- setTimeout(() => {
349
- const rune = readRuneFile()
350
- const hasHistory = rune?.history && rune.history.length > 0
351
- const sessionContext = buildSessionContext()
352
- const contextBlock = sessionContext ? `\n\n${sessionContext}` : ''
353
- const greetInstruction = hasHistory
354
- ? ''
355
- : `\n\nThis is a new conversation with no prior history. Greet the user briefly via rune_reply (no request_id). Introduce yourself based on your role and mention the working folder. Keep it short — 1-2 sentences.`
356
- mcp.notification({
357
- method: 'notifications/claude/channel',
358
- params: {
359
- content: `[SESSION_START] Channel connected. Folder: ${FOLDER_PATH || 'none'}${AGENT_ROLE ? `. Role: ${AGENT_ROLE}` : ''}${contextBlock}\n\nUse the rune_memory tool to save important context that should persist across sessions.${greetInstruction}`,
360
- meta: { type: 'session_start' },
361
- },
362
- }).then(() => {
363
- sessionStarted = true
364
- broadcastSSE({ type: 'session_start' })
365
- }).catch((e: any) => console.error(`[rune-channel] Startup notification failed: ${e.message}`))
366
- }, 1000)
367
-
368
- // Handle disconnect
369
- function handleDisconnect(reason: string) {
370
- if (!mcpConnected) return
371
- console.error(`[rune-channel] ${reason}, shutting down`)
372
- mcpConnected = false
373
- broadcastSSE({ type: 'mcp_disconnected' })
374
- setTimeout(() => process.exit(0), 500)
375
- }
376
-
377
- mcp.onclose = () => handleDisconnect('MCP connection closed')
378
- process.stdin.on('end', () => handleDisconnect('stdin closed'))
379
- process.stdin.on('error', () => handleDisconnect('stdin error'))
380
-
381
- // ── HTTP server ─────────────────────────────────
382
-
383
- let reqId = 0
384
-
385
- const server = http.createServer(async (req, res) => {
386
- // Hook events from Claude Code hooks system
387
- if (req.method === 'POST' && req.url === '/hook') {
388
- let body = ''
389
- for await (const chunk of req) body += chunk
390
- try {
391
- const hookData = JSON.parse(body)
392
- console.error(`[rune-channel] hook: ${hookData.hook_event_name} ${hookData.tool_name || ''}`)
393
- broadcastSSE({ type: 'hook', event: hookData.hook_event_name, ...hookData })
394
- } catch (e: any) {
395
- console.error(`[rune-channel] hook parse error: ${e.message}`)
396
- }
397
- res.writeHead(200, { 'Content-Type': 'application/json' })
398
- res.end('{"status":"ok"}')
399
- return
400
- }
401
-
402
- // SSE endpoint
403
- if (req.method === 'GET' && req.url === '/sse') {
404
- res.writeHead(200, {
405
- 'Content-Type': 'text/event-stream',
406
- 'Cache-Control': 'no-cache',
407
- Connection: 'keep-alive',
408
- 'Access-Control-Allow-Origin': '*',
409
- })
410
- res.write('data: {"type":"connected"}\n\n')
411
- if (sessionStarted) {
412
- res.write('data: {"type":"session_start"}\n\n')
413
- }
414
- sseClients.add(res)
415
- console.error(`[rune-channel] SSE client connected (total: ${sseClients.size}, sessionStarted: ${sessionStarted})`)
416
- req.on('close', () => {
417
- sseClients.delete(res)
418
- console.error(`[rune-channel] SSE client disconnected (total: ${sseClients.size})`)
419
- })
420
- return
421
- }
422
-
423
- // Health check
424
- if (req.method === 'GET') {
425
- res.writeHead(200, { 'Content-Type': 'application/json' })
426
- res.end(JSON.stringify({ status: mcpConnected ? 'ok' : 'no-mcp', name: 'rune-channel', mcpConnected }))
427
- return
428
- }
429
-
430
- // POST: chat message
431
- try {
432
- let body = ''
433
- for await (const chunk of req) body += chunk
434
- const { type, content } = JSON.parse(body) as { type: string; content: string }
435
- const id = String(++reqId)
436
-
437
- console.error(`[rune-channel] received ${type} message (id=${id}): ${content.slice(0, 100)}`)
438
-
439
- // Push to Claude Code
440
- try {
441
- await mcp.notification({
442
- method: 'notifications/claude/channel',
443
- params: {
444
- content,
445
- meta: { type, request_id: id },
446
- },
447
- })
448
- } catch (notifErr: any) {
449
- console.error(`[rune-channel] MCP notification FAILED: ${notifErr.message}`)
450
- }
451
-
452
- // For chat messages, wait for reply
453
- if (type === 'chat') {
454
- const reply = await new Promise<string>((resolve) => {
455
- pendingReplies.set(id, resolve)
456
- })
457
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
458
- res.end(reply)
459
- return
460
- }
461
-
462
- res.writeHead(200)
463
- res.end('ok')
464
- } catch (e: any) {
465
- console.error(`[rune-channel] HTTP error: ${e.message}`)
466
- res.writeHead(400, { 'Content-Type': 'application/json' })
467
- res.end(JSON.stringify({ error: e.message }))
468
- }
469
- })
470
-
471
- server.listen(PORT, '127.0.0.1', () => {
472
- console.error(`[rune-channel] listening on http://127.0.0.1:${PORT}`)
473
- })
474
-
475
- // Graceful shutdown: close HTTP server to release port
476
- const shutdown = () => {
477
- console.error(`[rune-channel] shutting down, releasing port ${PORT}`)
478
- server.close()
479
- process.exit(0)
480
- }
481
- process.on('SIGTERM', shutdown)
482
- process.on('SIGINT', shutdown)
483
- process.on('SIGHUP', shutdown)
484
- }
485
-
486
- main()
@@ -1,61 +0,0 @@
1
- appId: com.studio-h.rune
2
- productName: Rune
3
- copyright: Copyright © 2026 Studio-H
4
-
5
- directories:
6
- output: release
7
- buildResources: assets
8
-
9
- files:
10
- - dist/**/*
11
- - bootstrap.js
12
- - package.json
13
- - "!node_modules/**/*"
14
- - node_modules/node-pty/**/*
15
- - node_modules/@modelcontextprotocol/**/*
16
-
17
- asar: true
18
-
19
- mac:
20
- category: public.app-category.developer-tools
21
- target:
22
- - target: dmg
23
- arch: [arm64, x64]
24
- - target: zip
25
- arch: [arm64, x64]
26
- entitlements: null
27
- entitlementsInherit: null
28
- hardenedRuntime: false
29
- icon: assets/icon.icns
30
- fileAssociations:
31
- - ext: rune
32
- name: Rune Agent File
33
- description: Rune AI Agent Configuration
34
- role: Editor
35
- icon: assets/rune-file.icns
36
-
37
- dmg:
38
- title: Rune
39
- contents:
40
- - x: 130
41
- y: 220
42
- - x: 410
43
- y: 220
44
- type: link
45
- path: /Applications
46
-
47
- win:
48
- target:
49
- - nsis
50
- fileAssociations:
51
- - ext: rune
52
- name: Rune Agent File
53
- description: Rune AI Agent Configuration
54
-
55
- linux:
56
- target:
57
- - AppImage
58
- fileAssociations:
59
- - ext: rune
60
- name: Rune Agent File
61
- mimeType: application/x-rune