openrune 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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/bin/rune.js +718 -0
- package/bootstrap.js +4 -0
- package/channel/rune-channel.ts +467 -0
- package/electron-builder.yml +61 -0
- package/finder-extension/FinderSync.swift +47 -0
- package/finder-extension/RuneFinderSync.appex/Contents/Info.plist +27 -0
- package/finder-extension/RuneFinderSync.appex/Contents/MacOS/RuneFinderSync +0 -0
- package/finder-extension/main.swift +5 -0
- package/package.json +53 -0
- package/renderer/index.html +12 -0
- package/renderer/src/App.tsx +43 -0
- package/renderer/src/features/chat/activity-block.tsx +152 -0
- package/renderer/src/features/chat/chat-header.tsx +58 -0
- package/renderer/src/features/chat/chat-input.tsx +190 -0
- package/renderer/src/features/chat/chat-panel.tsx +150 -0
- package/renderer/src/features/chat/markdown-renderer.tsx +26 -0
- package/renderer/src/features/chat/message-bubble.tsx +79 -0
- package/renderer/src/features/chat/message-list.tsx +178 -0
- package/renderer/src/features/chat/types.ts +32 -0
- package/renderer/src/features/chat/use-chat.ts +251 -0
- package/renderer/src/features/terminal/terminal-panel.tsx +132 -0
- package/renderer/src/global.d.ts +29 -0
- package/renderer/src/globals.css +92 -0
- package/renderer/src/hooks/use-ipc.ts +24 -0
- package/renderer/src/lib/markdown.ts +83 -0
- package/renderer/src/lib/utils.ts +6 -0
- package/renderer/src/main.tsx +10 -0
- package/renderer/tsconfig.json +16 -0
- package/renderer/vite.config.ts +23 -0
- package/src/main.ts +782 -0
- package/src/preload.ts +58 -0
- package/tsconfig.json +14 -0
package/bin/rune.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
|
|
8
|
+
// Platform check — macOS only for now
|
|
9
|
+
if (process.platform !== 'darwin') {
|
|
10
|
+
console.error('\n ❌ Rune currently supports macOS only.')
|
|
11
|
+
console.error(' Windows and Linux support is coming soon.')
|
|
12
|
+
console.error(' Follow https://github.com/gilhyun/Rune for updates.\n')
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RUNE_HOME = path.join(os.homedir(), '.rune')
|
|
17
|
+
const APP_DIR = path.join(RUNE_HOME, 'app')
|
|
18
|
+
const QUICK_ACTION_DIR = path.join(os.homedir(), 'Library', 'Services')
|
|
19
|
+
|
|
20
|
+
const [,, command, ...args] = process.argv
|
|
21
|
+
|
|
22
|
+
function ensureDir(dir) {
|
|
23
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Commands ────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
switch (command) {
|
|
29
|
+
case 'install': return install()
|
|
30
|
+
case 'new': return createRune(args[0], args)
|
|
31
|
+
case 'open': return openRune(args[0])
|
|
32
|
+
case 'list': return listRunes()
|
|
33
|
+
case 'uninstall': return uninstall()
|
|
34
|
+
case 'help':
|
|
35
|
+
case '--help':
|
|
36
|
+
case '-h':
|
|
37
|
+
default: return showHelp()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── install ──────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function install() {
|
|
43
|
+
console.log('🔮 Installing Rune...\n')
|
|
44
|
+
ensureDir(RUNE_HOME)
|
|
45
|
+
ensureDir(APP_DIR)
|
|
46
|
+
|
|
47
|
+
// 1. Build the app (dev mode: use local source)
|
|
48
|
+
const projectRoot = path.resolve(__dirname, '..')
|
|
49
|
+
console.log(' Building Rune app...')
|
|
50
|
+
try {
|
|
51
|
+
execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' })
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(' ❌ Build failed')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Store project path for later use
|
|
58
|
+
fs.writeFileSync(path.join(RUNE_HOME, 'project-path'), projectRoot)
|
|
59
|
+
console.log(` ✅ App built at ${projectRoot}`)
|
|
60
|
+
|
|
61
|
+
// 3. Install macOS Quick Action for right-click menu
|
|
62
|
+
if (process.platform === 'darwin') {
|
|
63
|
+
installQuickAction(projectRoot)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Register .rune file association (macOS)
|
|
67
|
+
if (process.platform === 'darwin') {
|
|
68
|
+
registerFileAssociation(projectRoot)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Install Claude Code channel plugin
|
|
72
|
+
installChannelPlugin()
|
|
73
|
+
|
|
74
|
+
console.log('\n🔮 Rune installed successfully!\n')
|
|
75
|
+
console.log(' Usage:')
|
|
76
|
+
console.log(' Right-click any folder → Quick Actions → New Rune')
|
|
77
|
+
console.log(' Double-click any .rune file to open')
|
|
78
|
+
console.log(' rune new <name> Create .rune file in current directory')
|
|
79
|
+
console.log(' rune open <file> Open a .rune file')
|
|
80
|
+
console.log(' rune list List .rune files in current directory')
|
|
81
|
+
console.log('')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function installQuickAction(projectRoot) {
|
|
85
|
+
console.log(' Installing macOS Quick Action...')
|
|
86
|
+
|
|
87
|
+
ensureDir(QUICK_ACTION_DIR)
|
|
88
|
+
|
|
89
|
+
// Remove old workflow if exists
|
|
90
|
+
const workflowDir = path.join(QUICK_ACTION_DIR, 'New Rune.workflow')
|
|
91
|
+
if (fs.existsSync(workflowDir)) {
|
|
92
|
+
fs.rmSync(workflowDir, { recursive: true })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create helper shell script that the Quick Action will call
|
|
96
|
+
const helperScript = path.join(RUNE_HOME, 'create-rune.sh')
|
|
97
|
+
const nodebin = process.execPath
|
|
98
|
+
const runeBin = path.resolve(__dirname, 'rune.js')
|
|
99
|
+
|
|
100
|
+
// Write the helper shell script
|
|
101
|
+
const scriptContent = `#!/bin/bash
|
|
102
|
+
# Get the folder path — works for both file and folder right-clicks
|
|
103
|
+
INPUT="$1"
|
|
104
|
+
if [ -z "$INPUT" ]; then
|
|
105
|
+
INPUT=$(osascript -e 'tell application "Finder" to get POSIX path of (target of front Finder window as alias)' 2>/dev/null)
|
|
106
|
+
fi
|
|
107
|
+
if [ -z "$INPUT" ]; then
|
|
108
|
+
INPUT="$HOME"
|
|
109
|
+
fi
|
|
110
|
+
# If input is a file, use its parent directory
|
|
111
|
+
if [ -f "$INPUT" ]; then
|
|
112
|
+
FOLDER=$(dirname "$INPUT")
|
|
113
|
+
else
|
|
114
|
+
FOLDER="$INPUT"
|
|
115
|
+
fi
|
|
116
|
+
# Remove trailing slash
|
|
117
|
+
FOLDER="\${FOLDER%/}"
|
|
118
|
+
|
|
119
|
+
# Create .rune file with folder name as agent name
|
|
120
|
+
NAME=$(basename "$FOLDER")
|
|
121
|
+
FILEPATH="$FOLDER/$NAME.rune"
|
|
122
|
+
|
|
123
|
+
cat > "$FILEPATH" << RUNEEOF
|
|
124
|
+
{
|
|
125
|
+
"name": "$NAME",
|
|
126
|
+
"role": "General assistant",
|
|
127
|
+
"icon": "bot",
|
|
128
|
+
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
129
|
+
"history": []
|
|
130
|
+
}
|
|
131
|
+
RUNEEOF
|
|
132
|
+
`
|
|
133
|
+
fs.writeFileSync(helperScript, scriptContent, { mode: 0o755 })
|
|
134
|
+
|
|
135
|
+
// Create workflow directory structure
|
|
136
|
+
const contentsDir = path.join(workflowDir, 'Contents')
|
|
137
|
+
ensureDir(contentsDir)
|
|
138
|
+
|
|
139
|
+
const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
140
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
141
|
+
<plist version="1.0">
|
|
142
|
+
<dict>
|
|
143
|
+
<key>NSServices</key>
|
|
144
|
+
<array>
|
|
145
|
+
<dict>
|
|
146
|
+
<key>NSMenuItem</key>
|
|
147
|
+
<dict>
|
|
148
|
+
<key>default</key>
|
|
149
|
+
<string>New Rune</string>
|
|
150
|
+
</dict>
|
|
151
|
+
<key>NSMessage</key>
|
|
152
|
+
<string>runWorkflowAsService</string>
|
|
153
|
+
<key>NSRequiredContext</key>
|
|
154
|
+
<dict/>
|
|
155
|
+
<key>NSSendFileTypes</key>
|
|
156
|
+
<array>
|
|
157
|
+
<string>public.item</string>
|
|
158
|
+
</array>
|
|
159
|
+
</dict>
|
|
160
|
+
</array>
|
|
161
|
+
</dict>
|
|
162
|
+
</plist>`
|
|
163
|
+
|
|
164
|
+
const wflow = `<?xml version="1.0" encoding="UTF-8"?>
|
|
165
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
166
|
+
<plist version="1.0">
|
|
167
|
+
<dict>
|
|
168
|
+
<key>AMApplicationBuild</key>
|
|
169
|
+
<string>523</string>
|
|
170
|
+
<key>AMApplicationVersion</key>
|
|
171
|
+
<string>2.10</string>
|
|
172
|
+
<key>AMDocumentVersion</key>
|
|
173
|
+
<string>2</string>
|
|
174
|
+
<key>actions</key>
|
|
175
|
+
<array>
|
|
176
|
+
<dict>
|
|
177
|
+
<key>action</key>
|
|
178
|
+
<dict>
|
|
179
|
+
<key>AMAccepts</key>
|
|
180
|
+
<dict>
|
|
181
|
+
<key>Container</key>
|
|
182
|
+
<string>List</string>
|
|
183
|
+
<key>Optional</key>
|
|
184
|
+
<true/>
|
|
185
|
+
<key>Types</key>
|
|
186
|
+
<array>
|
|
187
|
+
<string>com.apple.cocoa.path</string>
|
|
188
|
+
</array>
|
|
189
|
+
</dict>
|
|
190
|
+
<key>AMActionVersion</key>
|
|
191
|
+
<string>2.0.3</string>
|
|
192
|
+
<key>AMApplication</key>
|
|
193
|
+
<array>
|
|
194
|
+
<string>Automator</string>
|
|
195
|
+
</array>
|
|
196
|
+
<key>AMParameterProperties</key>
|
|
197
|
+
<dict>
|
|
198
|
+
<key>COMMAND_STRING</key>
|
|
199
|
+
<dict/>
|
|
200
|
+
<key>CheckedForUserDefaultShell</key>
|
|
201
|
+
<dict/>
|
|
202
|
+
<key>inputMethod</key>
|
|
203
|
+
<dict/>
|
|
204
|
+
<key>shell</key>
|
|
205
|
+
<dict/>
|
|
206
|
+
<key>source</key>
|
|
207
|
+
<dict/>
|
|
208
|
+
</dict>
|
|
209
|
+
<key>AMProvides</key>
|
|
210
|
+
<dict>
|
|
211
|
+
<key>Container</key>
|
|
212
|
+
<string>List</string>
|
|
213
|
+
<key>Types</key>
|
|
214
|
+
<array>
|
|
215
|
+
<string>com.apple.cocoa.path</string>
|
|
216
|
+
</array>
|
|
217
|
+
</dict>
|
|
218
|
+
<key>ActionBundlePath</key>
|
|
219
|
+
<string>/System/Library/Automator/Run Shell Script.action</string>
|
|
220
|
+
<key>ActionName</key>
|
|
221
|
+
<string>Run Shell Script</string>
|
|
222
|
+
<key>ActionParameters</key>
|
|
223
|
+
<dict>
|
|
224
|
+
<key>COMMAND_STRING</key>
|
|
225
|
+
<string>"${helperScript}" "$@"</string>
|
|
226
|
+
<key>CheckedForUserDefaultShell</key>
|
|
227
|
+
<true/>
|
|
228
|
+
<key>inputMethod</key>
|
|
229
|
+
<integer>1</integer>
|
|
230
|
+
<key>shell</key>
|
|
231
|
+
<string>/bin/bash</string>
|
|
232
|
+
<key>source</key>
|
|
233
|
+
<string></string>
|
|
234
|
+
</dict>
|
|
235
|
+
<key>BundleIdentifier</key>
|
|
236
|
+
<string>com.apple.RunShellScript</string>
|
|
237
|
+
<key>CFBundleVersion</key>
|
|
238
|
+
<string>2.0.3</string>
|
|
239
|
+
<key>CanShowSelectedItemsWhenRun</key>
|
|
240
|
+
<false/>
|
|
241
|
+
<key>CanShowWhenRun</key>
|
|
242
|
+
<true/>
|
|
243
|
+
<key>Category</key>
|
|
244
|
+
<array>
|
|
245
|
+
<string>AMCategoryUtilities</string>
|
|
246
|
+
</array>
|
|
247
|
+
<key>Class Name</key>
|
|
248
|
+
<string>RunShellScriptAction</string>
|
|
249
|
+
<key>InputUUID</key>
|
|
250
|
+
<string>A5C0F22C-6B6A-4E8E-8B6A-1F6C4E5D3A2B</string>
|
|
251
|
+
<key>Keywords</key>
|
|
252
|
+
<array>
|
|
253
|
+
<string>Shell</string>
|
|
254
|
+
<string>Script</string>
|
|
255
|
+
</array>
|
|
256
|
+
<key>OutputUUID</key>
|
|
257
|
+
<string>B7D1F33D-7C7B-5F9F-9C7B-2A7D5F6E4B3C</string>
|
|
258
|
+
<key>UUID</key>
|
|
259
|
+
<string>C8E2A44E-8D8C-6A0A-0D8C-3A8E6A7F5C4D</string>
|
|
260
|
+
<key>UnlocalizedApplications</key>
|
|
261
|
+
<array>
|
|
262
|
+
<string>Automator</string>
|
|
263
|
+
</array>
|
|
264
|
+
<key>arguments</key>
|
|
265
|
+
<dict>
|
|
266
|
+
<key>0</key>
|
|
267
|
+
<dict>
|
|
268
|
+
<key>default value</key>
|
|
269
|
+
<string>/bin/bash</string>
|
|
270
|
+
<key>name</key>
|
|
271
|
+
<string>shell</string>
|
|
272
|
+
<key>required</key>
|
|
273
|
+
<string>0</string>
|
|
274
|
+
<key>type</key>
|
|
275
|
+
<string>0</string>
|
|
276
|
+
</dict>
|
|
277
|
+
<key>1</key>
|
|
278
|
+
<dict>
|
|
279
|
+
<key>default value</key>
|
|
280
|
+
<string></string>
|
|
281
|
+
<key>name</key>
|
|
282
|
+
<string>COMMAND_STRING</string>
|
|
283
|
+
<key>required</key>
|
|
284
|
+
<string>0</string>
|
|
285
|
+
<key>type</key>
|
|
286
|
+
<string>0</string>
|
|
287
|
+
</dict>
|
|
288
|
+
<key>2</key>
|
|
289
|
+
<dict>
|
|
290
|
+
<key>default value</key>
|
|
291
|
+
<integer>1</integer>
|
|
292
|
+
<key>name</key>
|
|
293
|
+
<string>inputMethod</string>
|
|
294
|
+
<key>required</key>
|
|
295
|
+
<string>0</string>
|
|
296
|
+
<key>type</key>
|
|
297
|
+
<string>0</string>
|
|
298
|
+
</dict>
|
|
299
|
+
<key>3</key>
|
|
300
|
+
<dict>
|
|
301
|
+
<key>default value</key>
|
|
302
|
+
<string></string>
|
|
303
|
+
<key>name</key>
|
|
304
|
+
<string>source</string>
|
|
305
|
+
<key>required</key>
|
|
306
|
+
<string>0</string>
|
|
307
|
+
<key>type</key>
|
|
308
|
+
<string>0</string>
|
|
309
|
+
</dict>
|
|
310
|
+
</dict>
|
|
311
|
+
<key>isViewVisible</key>
|
|
312
|
+
<true/>
|
|
313
|
+
<key>location</key>
|
|
314
|
+
<string>309.000000:627.000000</string>
|
|
315
|
+
<key>nibPath</key>
|
|
316
|
+
<string>/System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib</string>
|
|
317
|
+
</dict>
|
|
318
|
+
</dict>
|
|
319
|
+
</array>
|
|
320
|
+
<key>connectors</key>
|
|
321
|
+
<dict/>
|
|
322
|
+
<key>workflowMetaData</key>
|
|
323
|
+
<dict>
|
|
324
|
+
<key>serviceInputTypeIdentifier</key>
|
|
325
|
+
<string>com.apple.Automator.fileSystemObject</string>
|
|
326
|
+
<key>serviceApplicationBundleID</key>
|
|
327
|
+
<string>com.apple.Finder</string>
|
|
328
|
+
<key>workflowTypeIdentifier</key>
|
|
329
|
+
<string>com.apple.Automator.servicesMenu</string>
|
|
330
|
+
</dict>
|
|
331
|
+
</dict>
|
|
332
|
+
</plist>`
|
|
333
|
+
|
|
334
|
+
fs.writeFileSync(path.join(contentsDir, 'Info.plist'), infoPlist)
|
|
335
|
+
fs.writeFileSync(path.join(contentsDir, 'document.wflow'), wflow)
|
|
336
|
+
|
|
337
|
+
// Flush services cache
|
|
338
|
+
try {
|
|
339
|
+
execSync('/System/Library/CoreServices/pbs -flush', { stdio: 'ignore' })
|
|
340
|
+
} catch {}
|
|
341
|
+
try {
|
|
342
|
+
execSync('/System/Library/CoreServices/pbs -update', { stdio: 'ignore' })
|
|
343
|
+
} catch {}
|
|
344
|
+
|
|
345
|
+
console.log(' ✅ Quick Action installed: Right-click folder → Quick Actions → New Rune')
|
|
346
|
+
console.log(' 💡 If not visible, go to System Settings → Extensions → Finder → enable "New Rune"')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function registerFileAssociation(projectRoot) {
|
|
350
|
+
console.log(' Registering .rune file association...')
|
|
351
|
+
|
|
352
|
+
// Find the actual Electron binary (not the Node wrapper script)
|
|
353
|
+
const electronBinary = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
|
|
354
|
+
|
|
355
|
+
// Create a minimal .app wrapper for macOS file association
|
|
356
|
+
const appDir = path.join(APP_DIR, 'Rune.app')
|
|
357
|
+
// Remove old app if exists
|
|
358
|
+
if (fs.existsSync(appDir)) fs.rmSync(appDir, { recursive: true })
|
|
359
|
+
const appContents = path.join(appDir, 'Contents')
|
|
360
|
+
const appMacOS = path.join(appContents, 'MacOS')
|
|
361
|
+
ensureDir(appMacOS)
|
|
362
|
+
|
|
363
|
+
// Info.plist with file association
|
|
364
|
+
const appPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
365
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
366
|
+
<plist version="1.0">
|
|
367
|
+
<dict>
|
|
368
|
+
<key>CFBundleIdentifier</key>
|
|
369
|
+
<string>com.studio-h.rune</string>
|
|
370
|
+
<key>CFBundleName</key>
|
|
371
|
+
<string>Rune</string>
|
|
372
|
+
<key>CFBundleDisplayName</key>
|
|
373
|
+
<string>Rune</string>
|
|
374
|
+
<key>CFBundleVersion</key>
|
|
375
|
+
<string>0.1.0</string>
|
|
376
|
+
<key>CFBundleShortVersionString</key>
|
|
377
|
+
<string>0.1.0</string>
|
|
378
|
+
<key>CFBundleExecutable</key>
|
|
379
|
+
<string>rune-launcher</string>
|
|
380
|
+
<key>CFBundlePackageType</key>
|
|
381
|
+
<string>APPL</string>
|
|
382
|
+
<key>LSMinimumSystemVersion</key>
|
|
383
|
+
<string>10.15</string>
|
|
384
|
+
<key>CFBundleDocumentTypes</key>
|
|
385
|
+
<array>
|
|
386
|
+
<dict>
|
|
387
|
+
<key>CFBundleTypeExtensions</key>
|
|
388
|
+
<array>
|
|
389
|
+
<string>rune</string>
|
|
390
|
+
</array>
|
|
391
|
+
<key>CFBundleTypeName</key>
|
|
392
|
+
<string>Rune Agent File</string>
|
|
393
|
+
<key>CFBundleTypeRole</key>
|
|
394
|
+
<string>Editor</string>
|
|
395
|
+
<key>LSHandlerRank</key>
|
|
396
|
+
<string>Owner</string>
|
|
397
|
+
<key>LSItemContentTypes</key>
|
|
398
|
+
<array>
|
|
399
|
+
<string>com.studio-h.rune</string>
|
|
400
|
+
</array>
|
|
401
|
+
</dict>
|
|
402
|
+
</array>
|
|
403
|
+
<key>UTExportedTypeDeclarations</key>
|
|
404
|
+
<array>
|
|
405
|
+
<dict>
|
|
406
|
+
<key>UTTypeConformsTo</key>
|
|
407
|
+
<array>
|
|
408
|
+
<string>public.json</string>
|
|
409
|
+
</array>
|
|
410
|
+
<key>UTTypeDescription</key>
|
|
411
|
+
<string>Rune Agent File</string>
|
|
412
|
+
<key>UTTypeIdentifier</key>
|
|
413
|
+
<string>com.studio-h.rune</string>
|
|
414
|
+
<key>UTTypeTagSpecification</key>
|
|
415
|
+
<dict>
|
|
416
|
+
<key>public.filename-extension</key>
|
|
417
|
+
<array>
|
|
418
|
+
<string>rune</string>
|
|
419
|
+
</array>
|
|
420
|
+
</dict>
|
|
421
|
+
</dict>
|
|
422
|
+
</array>
|
|
423
|
+
<key>LSUIElement</key>
|
|
424
|
+
<true/>
|
|
425
|
+
</dict>
|
|
426
|
+
</plist>`
|
|
427
|
+
|
|
428
|
+
fs.writeFileSync(path.join(appContents, 'Info.plist'), appPlist)
|
|
429
|
+
|
|
430
|
+
// Launcher script — uses actual Electron binary, not Node wrapper
|
|
431
|
+
const launcherScript = `#!/bin/bash
|
|
432
|
+
RUNE_PROJECT="${projectRoot}"
|
|
433
|
+
ELECTRON="${electronBinary}"
|
|
434
|
+
|
|
435
|
+
# CRITICAL: unset this so Electron runs as an app, not plain Node.js
|
|
436
|
+
unset ELECTRON_RUN_AS_NODE
|
|
437
|
+
|
|
438
|
+
# macOS passes the file path as the last argument when double-clicking
|
|
439
|
+
FILE=""
|
|
440
|
+
for arg in "$@"; do
|
|
441
|
+
if [[ "$arg" == *.rune ]]; then
|
|
442
|
+
FILE="$arg"
|
|
443
|
+
break
|
|
444
|
+
fi
|
|
445
|
+
done
|
|
446
|
+
|
|
447
|
+
cd "$RUNE_PROJECT"
|
|
448
|
+
if [ -n "$FILE" ]; then
|
|
449
|
+
exec "$ELECTRON" . "$FILE"
|
|
450
|
+
else
|
|
451
|
+
exec "$ELECTRON" .
|
|
452
|
+
fi
|
|
453
|
+
`
|
|
454
|
+
fs.writeFileSync(path.join(appMacOS, 'rune-launcher'), launcherScript, { mode: 0o755 })
|
|
455
|
+
|
|
456
|
+
// Register the app with Launch Services
|
|
457
|
+
try {
|
|
458
|
+
execSync(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -R -f "${appDir}"`, { stdio: 'ignore' })
|
|
459
|
+
console.log(' ✅ .rune file association registered')
|
|
460
|
+
} catch {
|
|
461
|
+
console.log(' ⚠️ Could not auto-register. Double-click a .rune file and choose "Open With" → Rune')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Set as default handler for .rune files
|
|
465
|
+
try {
|
|
466
|
+
execSync(`defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers -array-add '{ LSHandlerContentType = "com.studio-h.rune"; LSHandlerRoleAll = "com.studio-h.rune"; }'`, { stdio: 'ignore' })
|
|
467
|
+
} catch {}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function installChannelPlugin() {
|
|
471
|
+
console.log(' Installing Claude Code channel plugin...')
|
|
472
|
+
|
|
473
|
+
const projectRoot = path.resolve(__dirname, '..')
|
|
474
|
+
const claudePluginsDir = path.join(os.homedir(), '.claude', 'plugins')
|
|
475
|
+
const installedFile = path.join(claudePluginsDir, 'installed_plugins.json')
|
|
476
|
+
const marketplacesFile = path.join(claudePluginsDir, 'known_marketplaces.json')
|
|
477
|
+
|
|
478
|
+
// Check if Claude Code plugins dir exists
|
|
479
|
+
if (!fs.existsSync(claudePluginsDir)) {
|
|
480
|
+
console.log(' ⚠️ Claude Code not found (~/.claude/plugins missing). Install Claude Code first.')
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// 1. Register marketplace
|
|
486
|
+
const marketplaceName = 'rune'
|
|
487
|
+
let marketplaces = {}
|
|
488
|
+
if (fs.existsSync(marketplacesFile)) {
|
|
489
|
+
try { marketplaces = JSON.parse(fs.readFileSync(marketplacesFile, 'utf-8')) } catch {}
|
|
490
|
+
}
|
|
491
|
+
if (!marketplaces[marketplaceName]) {
|
|
492
|
+
marketplaces[marketplaceName] = {
|
|
493
|
+
source: { source: 'github', repo: 'gilhyun/Rune' },
|
|
494
|
+
installLocation: path.join(claudePluginsDir, 'marketplaces', marketplaceName),
|
|
495
|
+
lastUpdated: new Date().toISOString(),
|
|
496
|
+
}
|
|
497
|
+
fs.writeFileSync(marketplacesFile, JSON.stringify(marketplaces, null, 2))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 2. Copy plugin to cache
|
|
501
|
+
const pluginJson = JSON.parse(fs.readFileSync(path.join(projectRoot, '.claude-plugin', 'plugin.json'), 'utf-8'))
|
|
502
|
+
const version = pluginJson.version || '0.1.0'
|
|
503
|
+
const cacheDir = path.join(claudePluginsDir, 'cache', marketplaceName, 'rune-channel', version)
|
|
504
|
+
ensureDir(cacheDir)
|
|
505
|
+
|
|
506
|
+
// Copy essential files
|
|
507
|
+
const filesToCopy = ['.claude-plugin', 'dist/rune-channel.js', 'package.json', 'LICENSE']
|
|
508
|
+
for (const f of filesToCopy) {
|
|
509
|
+
const src = path.join(projectRoot, f)
|
|
510
|
+
const dst = path.join(cacheDir, f)
|
|
511
|
+
if (!fs.existsSync(src)) continue
|
|
512
|
+
const stat = fs.statSync(src)
|
|
513
|
+
if (stat.isDirectory()) {
|
|
514
|
+
ensureDir(dst)
|
|
515
|
+
for (const child of fs.readdirSync(src)) {
|
|
516
|
+
fs.copyFileSync(path.join(src, child), path.join(dst, child))
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
ensureDir(path.dirname(dst))
|
|
520
|
+
fs.copyFileSync(src, dst)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 3. Register in installed_plugins.json
|
|
525
|
+
let installed = { version: 2, plugins: {} }
|
|
526
|
+
if (fs.existsSync(installedFile)) {
|
|
527
|
+
try { installed = JSON.parse(fs.readFileSync(installedFile, 'utf-8')) } catch {}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const pluginKey = `rune-channel@${marketplaceName}`
|
|
531
|
+
const entries = installed.plugins[pluginKey] || []
|
|
532
|
+
// Add/update user-scope entry
|
|
533
|
+
const userEntry = entries.find(e => e.scope === 'user')
|
|
534
|
+
const newEntry = {
|
|
535
|
+
scope: 'user',
|
|
536
|
+
installPath: cacheDir,
|
|
537
|
+
version,
|
|
538
|
+
installedAt: userEntry?.installedAt || new Date().toISOString(),
|
|
539
|
+
lastUpdated: new Date().toISOString(),
|
|
540
|
+
}
|
|
541
|
+
if (userEntry) {
|
|
542
|
+
Object.assign(userEntry, newEntry)
|
|
543
|
+
} else {
|
|
544
|
+
entries.push(newEntry)
|
|
545
|
+
}
|
|
546
|
+
installed.plugins[pluginKey] = entries
|
|
547
|
+
fs.writeFileSync(installedFile, JSON.stringify(installed, null, 2))
|
|
548
|
+
|
|
549
|
+
console.log(' ✅ Channel plugin installed')
|
|
550
|
+
} catch (e) {
|
|
551
|
+
console.log(` ⚠️ Plugin install failed: ${e.message}`)
|
|
552
|
+
console.log(' Run manually inside Claude Code: /plugin install rune-channel@rune')
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── new ──────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
function createRune(name, allArgs) {
|
|
559
|
+
if (!name) {
|
|
560
|
+
console.log('Usage: rune new <name> [--role "role description"]')
|
|
561
|
+
console.log('Example: rune new designer --role "UI/UX design expert"')
|
|
562
|
+
process.exit(1)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Parse --role flag
|
|
566
|
+
let role = 'General assistant'
|
|
567
|
+
const roleIdx = allArgs.indexOf('--role')
|
|
568
|
+
if (roleIdx !== -1 && allArgs[roleIdx + 1]) {
|
|
569
|
+
role = allArgs[roleIdx + 1]
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const fileName = name.endsWith('.rune') ? name : `${name}.rune`
|
|
573
|
+
const filePath = path.resolve(process.cwd(), fileName)
|
|
574
|
+
|
|
575
|
+
if (fs.existsSync(filePath)) {
|
|
576
|
+
console.log(` ⚠️ ${fileName} already exists. Opening it instead.`)
|
|
577
|
+
return openRune(filePath)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const runeData = {
|
|
581
|
+
name: name.replace('.rune', ''),
|
|
582
|
+
role,
|
|
583
|
+
icon: 'bot',
|
|
584
|
+
createdAt: new Date().toISOString(),
|
|
585
|
+
history: [],
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
fs.writeFileSync(filePath, JSON.stringify(runeData, null, 2))
|
|
589
|
+
console.log(`🔮 Created ${fileName}`)
|
|
590
|
+
console.log(` Name: ${runeData.name}`)
|
|
591
|
+
console.log(` Role: ${runeData.role}`)
|
|
592
|
+
console.log(` Path: ${filePath}`)
|
|
593
|
+
console.log('')
|
|
594
|
+
console.log(' Double-click the file to open, or run:')
|
|
595
|
+
console.log(` rune open ${fileName}`)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── open ─────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
function openRune(file) {
|
|
601
|
+
if (!file) {
|
|
602
|
+
console.log('Usage: rune open <file.rune>')
|
|
603
|
+
process.exit(1)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
607
|
+
if (!fs.existsSync(filePath)) {
|
|
608
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
609
|
+
process.exit(1)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Try to find the project root
|
|
613
|
+
let projectRoot
|
|
614
|
+
const savedPath = path.join(RUNE_HOME, 'project-path')
|
|
615
|
+
if (fs.existsSync(savedPath)) {
|
|
616
|
+
projectRoot = fs.readFileSync(savedPath, 'utf-8').trim()
|
|
617
|
+
} else {
|
|
618
|
+
projectRoot = path.resolve(__dirname, '..')
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const electronBinary = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
|
|
622
|
+
if (!fs.existsSync(electronBinary)) {
|
|
623
|
+
console.error(' ❌ Electron not found. Run `rune install` first.')
|
|
624
|
+
process.exit(1)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
console.log(`🔮 Opening ${path.basename(filePath)}...`)
|
|
628
|
+
|
|
629
|
+
// CRITICAL: unset ELECTRON_RUN_AS_NODE — Claude Code sets it,
|
|
630
|
+
// which makes Electron act as plain Node.js instead of an Electron app
|
|
631
|
+
const env = { ...process.env }
|
|
632
|
+
delete env.ELECTRON_RUN_AS_NODE
|
|
633
|
+
|
|
634
|
+
const child = spawn(electronBinary, ['.', filePath], {
|
|
635
|
+
cwd: projectRoot,
|
|
636
|
+
detached: true,
|
|
637
|
+
stdio: 'ignore',
|
|
638
|
+
env,
|
|
639
|
+
})
|
|
640
|
+
child.unref()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ── list ─────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
function listRunes() {
|
|
646
|
+
const cwd = process.cwd()
|
|
647
|
+
const files = fs.readdirSync(cwd).filter(f => f.endsWith('.rune'))
|
|
648
|
+
|
|
649
|
+
if (files.length === 0) {
|
|
650
|
+
console.log(' No .rune files in current directory.')
|
|
651
|
+
console.log(' Create one with: rune new <name>')
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
console.log(`🔮 Rune files in ${cwd}:\n`)
|
|
656
|
+
for (const file of files) {
|
|
657
|
+
try {
|
|
658
|
+
const data = JSON.parse(fs.readFileSync(path.join(cwd, file), 'utf-8'))
|
|
659
|
+
const msgs = (data.history || []).length
|
|
660
|
+
console.log(` ${file}`)
|
|
661
|
+
console.log(` Name: ${data.name || '?'} Role: ${data.role || '?'} Messages: ${msgs}`)
|
|
662
|
+
} catch {
|
|
663
|
+
console.log(` ${file} (invalid)`)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── uninstall ────────────────────────────────────
|
|
669
|
+
|
|
670
|
+
function uninstall() {
|
|
671
|
+
console.log('🔮 Uninstalling Rune...\n')
|
|
672
|
+
|
|
673
|
+
// Remove Quick Action
|
|
674
|
+
const workflowDir = path.join(QUICK_ACTION_DIR, 'New Rune.workflow')
|
|
675
|
+
if (fs.existsSync(workflowDir)) {
|
|
676
|
+
fs.rmSync(workflowDir, { recursive: true })
|
|
677
|
+
console.log(' ✅ Quick Action removed')
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Remove .app wrapper
|
|
681
|
+
const appDir = path.join(APP_DIR, 'Rune.app')
|
|
682
|
+
if (fs.existsSync(appDir)) {
|
|
683
|
+
fs.rmSync(appDir, { recursive: true })
|
|
684
|
+
console.log(' ✅ App wrapper removed')
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Unregister from Launch Services
|
|
688
|
+
if (process.platform === 'darwin') {
|
|
689
|
+
try {
|
|
690
|
+
execSync(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u "${appDir}"`, { stdio: 'ignore' })
|
|
691
|
+
} catch {}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
console.log('\n🔮 Rune uninstalled. Your .rune files are preserved.\n')
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ── help ─────────────────────────────────────────
|
|
698
|
+
|
|
699
|
+
function showHelp() {
|
|
700
|
+
console.log(`
|
|
701
|
+
🔮 Rune — File-based AI Agent
|
|
702
|
+
|
|
703
|
+
Usage:
|
|
704
|
+
rune install Install Rune (build app, register file association, add Quick Action)
|
|
705
|
+
rune new <name> Create a new .rune file in current directory
|
|
706
|
+
--role "description" Set the agent's role
|
|
707
|
+
rune open <file.rune> Open a .rune file
|
|
708
|
+
rune list List .rune files in current directory
|
|
709
|
+
rune uninstall Remove Rune integration (keeps .rune files)
|
|
710
|
+
rune help Show this help
|
|
711
|
+
|
|
712
|
+
Examples:
|
|
713
|
+
rune new designer --role "UI/UX design expert"
|
|
714
|
+
rune new backend --role "Backend developer, Node.js specialist"
|
|
715
|
+
rune open designer.rune
|
|
716
|
+
rune list
|
|
717
|
+
`)
|
|
718
|
+
}
|