openrune 1.1.2 → 2.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.
Files changed (38) hide show
  1. package/README.ko.md +26 -77
  2. package/README.md +28 -81
  3. package/bin/rune.js +43 -702
  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/bin/rune.js CHANGED
@@ -1,17 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawn } = require('child_process')
3
+ const { spawn } = require('child_process')
4
4
  const path = require('path')
5
5
  const fs = require('fs')
6
- const os = require('os')
7
-
8
- // Platform check
9
- const IS_MAC = process.platform === 'darwin'
10
- const IS_WIN = process.platform === 'win32'
11
-
12
- const RUNE_HOME = path.join(os.homedir(), '.rune')
13
- const APP_DIR = path.join(RUNE_HOME, 'app')
14
- const QUICK_ACTION_DIR = IS_MAC ? path.join(os.homedir(), 'Library', 'Services') : null
15
6
 
16
7
  const [,, command, ...args] = process.argv
17
8
 
@@ -22,564 +13,27 @@ function ensureDir(dir) {
22
13
  // ── Commands ────────────────────────────────────
23
14
 
24
15
  switch (command) {
25
- case 'install': return install()
26
16
  case 'new': return createRune(args[0], args)
27
- case 'open': return openRune(args[0])
28
17
  case 'run': return runRune(args[0], args.slice(1))
29
18
  case 'pipe': return pipeRunes(args)
30
19
  case 'loop': return loopRunes(args)
31
20
  case 'watch': return watchRune(args[0], args.slice(1))
32
21
  case 'list': return listRunes()
33
- case 'uninstall': return uninstall()
22
+ case 'open':
23
+ console.log(' ℹ️ `rune open` has been removed from the CLI.')
24
+ console.log(' For a GUI, use RuneChat: https://github.com/gilhyun/runechat')
25
+ process.exit(0)
26
+ case 'install':
27
+ case 'uninstall':
28
+ console.log(' ℹ️ `rune install` / `rune uninstall` are no longer needed.')
29
+ console.log(' Rune is now a pure CLI — install with: npm install -g openrune')
30
+ process.exit(0)
34
31
  case 'help':
35
32
  case '--help':
36
33
  case '-h':
37
34
  default: return showHelp()
38
35
  }
39
36
 
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. Rebuild native modules for Electron (node-pty)
58
- console.log(' Rebuilding native modules for Electron...')
59
- try {
60
- execSync('npx electron-rebuild -m .', { cwd: projectRoot, stdio: 'inherit' })
61
- console.log(' ✅ Native modules rebuilt')
62
- } catch (e) {
63
- console.error(' ⚠️ electron-rebuild failed (terminal may not work)')
64
- }
65
-
66
- // 3. Store project path for later use
67
- fs.writeFileSync(path.join(RUNE_HOME, 'project-path'), projectRoot)
68
- console.log(` ✅ App built at ${projectRoot}`)
69
-
70
- // 3. Install macOS Quick Action for right-click menu
71
- if (IS_MAC) {
72
- installQuickAction(projectRoot)
73
- }
74
-
75
- // 4. Register .rune file association (macOS)
76
- if (IS_MAC) {
77
- registerFileAssociation(projectRoot)
78
- }
79
-
80
- // 5. Install Claude Code channel plugin
81
- installChannelPlugin()
82
-
83
- console.log('\n🔮 Rune installed successfully!\n')
84
- console.log(' Usage:')
85
- console.log(' Right-click any folder → Quick Actions → New Rune')
86
- console.log(' Double-click any .rune file to open')
87
- console.log(' rune new <name> Create .rune file in current directory')
88
- console.log(' rune open <file> Open a .rune file')
89
- console.log(' rune list List .rune files in current directory')
90
- console.log('')
91
- }
92
-
93
- function installQuickAction(projectRoot) {
94
- console.log(' Installing macOS Quick Action...')
95
-
96
- ensureDir(QUICK_ACTION_DIR)
97
-
98
- // Remove old workflow if exists
99
- const workflowDir = path.join(QUICK_ACTION_DIR, 'New Rune.workflow')
100
- if (fs.existsSync(workflowDir)) {
101
- fs.rmSync(workflowDir, { recursive: true })
102
- }
103
-
104
- // Create helper shell script that the Quick Action will call
105
- const helperScript = path.join(RUNE_HOME, 'create-rune.sh')
106
- const nodebin = process.execPath
107
- const runeBin = path.resolve(__dirname, 'rune.js')
108
-
109
- // Write the helper shell script
110
- const scriptContent = `#!/bin/bash
111
- # Get the folder path — works for both file and folder right-clicks
112
- INPUT="$1"
113
- if [ -z "$INPUT" ]; then
114
- INPUT=$(osascript -e 'tell application "Finder" to get POSIX path of (target of front Finder window as alias)' 2>/dev/null)
115
- fi
116
- if [ -z "$INPUT" ]; then
117
- INPUT="$HOME"
118
- fi
119
- # If input is a file, use its parent directory
120
- if [ -f "$INPUT" ]; then
121
- FOLDER=$(dirname "$INPUT")
122
- else
123
- FOLDER="$INPUT"
124
- fi
125
- # Remove trailing slash
126
- FOLDER="\${FOLDER%/}"
127
-
128
- # Create .rune file with folder name as agent name
129
- NAME=$(basename "$FOLDER")
130
- FILEPATH="$FOLDER/$NAME.rune"
131
-
132
- cat > "$FILEPATH" << RUNEEOF
133
- {
134
- "name": "$NAME",
135
- "role": "General assistant",
136
- "icon": "bot",
137
- "createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
138
- "history": []
139
- }
140
- RUNEEOF
141
- `
142
- fs.writeFileSync(helperScript, scriptContent, { mode: 0o755 })
143
-
144
- // Create workflow directory structure
145
- const contentsDir = path.join(workflowDir, 'Contents')
146
- ensureDir(contentsDir)
147
-
148
- const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
149
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
150
- <plist version="1.0">
151
- <dict>
152
- <key>NSServices</key>
153
- <array>
154
- <dict>
155
- <key>NSMenuItem</key>
156
- <dict>
157
- <key>default</key>
158
- <string>New Rune</string>
159
- </dict>
160
- <key>NSMessage</key>
161
- <string>runWorkflowAsService</string>
162
- <key>NSRequiredContext</key>
163
- <dict/>
164
- <key>NSSendFileTypes</key>
165
- <array>
166
- <string>public.item</string>
167
- </array>
168
- </dict>
169
- </array>
170
- </dict>
171
- </plist>`
172
-
173
- const wflow = `<?xml version="1.0" encoding="UTF-8"?>
174
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
175
- <plist version="1.0">
176
- <dict>
177
- <key>AMApplicationBuild</key>
178
- <string>523</string>
179
- <key>AMApplicationVersion</key>
180
- <string>2.10</string>
181
- <key>AMDocumentVersion</key>
182
- <string>2</string>
183
- <key>actions</key>
184
- <array>
185
- <dict>
186
- <key>action</key>
187
- <dict>
188
- <key>AMAccepts</key>
189
- <dict>
190
- <key>Container</key>
191
- <string>List</string>
192
- <key>Optional</key>
193
- <true/>
194
- <key>Types</key>
195
- <array>
196
- <string>com.apple.cocoa.path</string>
197
- </array>
198
- </dict>
199
- <key>AMActionVersion</key>
200
- <string>2.0.3</string>
201
- <key>AMApplication</key>
202
- <array>
203
- <string>Automator</string>
204
- </array>
205
- <key>AMParameterProperties</key>
206
- <dict>
207
- <key>COMMAND_STRING</key>
208
- <dict/>
209
- <key>CheckedForUserDefaultShell</key>
210
- <dict/>
211
- <key>inputMethod</key>
212
- <dict/>
213
- <key>shell</key>
214
- <dict/>
215
- <key>source</key>
216
- <dict/>
217
- </dict>
218
- <key>AMProvides</key>
219
- <dict>
220
- <key>Container</key>
221
- <string>List</string>
222
- <key>Types</key>
223
- <array>
224
- <string>com.apple.cocoa.path</string>
225
- </array>
226
- </dict>
227
- <key>ActionBundlePath</key>
228
- <string>/System/Library/Automator/Run Shell Script.action</string>
229
- <key>ActionName</key>
230
- <string>Run Shell Script</string>
231
- <key>ActionParameters</key>
232
- <dict>
233
- <key>COMMAND_STRING</key>
234
- <string>"${helperScript}" "$@"</string>
235
- <key>CheckedForUserDefaultShell</key>
236
- <true/>
237
- <key>inputMethod</key>
238
- <integer>1</integer>
239
- <key>shell</key>
240
- <string>/bin/bash</string>
241
- <key>source</key>
242
- <string></string>
243
- </dict>
244
- <key>BundleIdentifier</key>
245
- <string>com.apple.RunShellScript</string>
246
- <key>CFBundleVersion</key>
247
- <string>2.0.3</string>
248
- <key>CanShowSelectedItemsWhenRun</key>
249
- <false/>
250
- <key>CanShowWhenRun</key>
251
- <true/>
252
- <key>Category</key>
253
- <array>
254
- <string>AMCategoryUtilities</string>
255
- </array>
256
- <key>Class Name</key>
257
- <string>RunShellScriptAction</string>
258
- <key>InputUUID</key>
259
- <string>A5C0F22C-6B6A-4E8E-8B6A-1F6C4E5D3A2B</string>
260
- <key>Keywords</key>
261
- <array>
262
- <string>Shell</string>
263
- <string>Script</string>
264
- </array>
265
- <key>OutputUUID</key>
266
- <string>B7D1F33D-7C7B-5F9F-9C7B-2A7D5F6E4B3C</string>
267
- <key>UUID</key>
268
- <string>C8E2A44E-8D8C-6A0A-0D8C-3A8E6A7F5C4D</string>
269
- <key>UnlocalizedApplications</key>
270
- <array>
271
- <string>Automator</string>
272
- </array>
273
- <key>arguments</key>
274
- <dict>
275
- <key>0</key>
276
- <dict>
277
- <key>default value</key>
278
- <string>/bin/bash</string>
279
- <key>name</key>
280
- <string>shell</string>
281
- <key>required</key>
282
- <string>0</string>
283
- <key>type</key>
284
- <string>0</string>
285
- </dict>
286
- <key>1</key>
287
- <dict>
288
- <key>default value</key>
289
- <string></string>
290
- <key>name</key>
291
- <string>COMMAND_STRING</string>
292
- <key>required</key>
293
- <string>0</string>
294
- <key>type</key>
295
- <string>0</string>
296
- </dict>
297
- <key>2</key>
298
- <dict>
299
- <key>default value</key>
300
- <integer>1</integer>
301
- <key>name</key>
302
- <string>inputMethod</string>
303
- <key>required</key>
304
- <string>0</string>
305
- <key>type</key>
306
- <string>0</string>
307
- </dict>
308
- <key>3</key>
309
- <dict>
310
- <key>default value</key>
311
- <string></string>
312
- <key>name</key>
313
- <string>source</string>
314
- <key>required</key>
315
- <string>0</string>
316
- <key>type</key>
317
- <string>0</string>
318
- </dict>
319
- </dict>
320
- <key>isViewVisible</key>
321
- <true/>
322
- <key>location</key>
323
- <string>309.000000:627.000000</string>
324
- <key>nibPath</key>
325
- <string>/System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib</string>
326
- </dict>
327
- </dict>
328
- </array>
329
- <key>connectors</key>
330
- <dict/>
331
- <key>workflowMetaData</key>
332
- <dict>
333
- <key>serviceInputTypeIdentifier</key>
334
- <string>com.apple.Automator.fileSystemObject</string>
335
- <key>serviceApplicationBundleID</key>
336
- <string>com.apple.Finder</string>
337
- <key>workflowTypeIdentifier</key>
338
- <string>com.apple.Automator.servicesMenu</string>
339
- </dict>
340
- </dict>
341
- </plist>`
342
-
343
- fs.writeFileSync(path.join(contentsDir, 'Info.plist'), infoPlist)
344
- fs.writeFileSync(path.join(contentsDir, 'document.wflow'), wflow)
345
-
346
- // Flush services cache
347
- try {
348
- execSync('/System/Library/CoreServices/pbs -flush', { stdio: 'ignore' })
349
- } catch {}
350
- try {
351
- execSync('/System/Library/CoreServices/pbs -update', { stdio: 'ignore' })
352
- } catch {}
353
-
354
- console.log(' ✅ Quick Action installed: Right-click folder → Quick Actions → New Rune')
355
- console.log(' 💡 If not visible, go to System Settings → Extensions → Finder → enable "New Rune"')
356
- }
357
-
358
- function registerFileAssociation(projectRoot) {
359
- console.log(' Registering .rune file association...')
360
-
361
- // Find the actual Electron binary (not the Node wrapper script)
362
- const electronBinary = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
363
-
364
- // Create a minimal .app wrapper for macOS file association
365
- const appDir = path.join(APP_DIR, 'Rune.app')
366
- // Remove old app if exists
367
- if (fs.existsSync(appDir)) fs.rmSync(appDir, { recursive: true })
368
- const appContents = path.join(appDir, 'Contents')
369
- const appMacOS = path.join(appContents, 'MacOS')
370
- ensureDir(appMacOS)
371
-
372
- // Info.plist with file association
373
- const appPlist = `<?xml version="1.0" encoding="UTF-8"?>
374
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
375
- <plist version="1.0">
376
- <dict>
377
- <key>CFBundleIdentifier</key>
378
- <string>com.studio-h.rune</string>
379
- <key>CFBundleName</key>
380
- <string>Rune</string>
381
- <key>CFBundleDisplayName</key>
382
- <string>Rune</string>
383
- <key>CFBundleVersion</key>
384
- <string>0.1.0</string>
385
- <key>CFBundleShortVersionString</key>
386
- <string>0.1.0</string>
387
- <key>CFBundleExecutable</key>
388
- <string>rune-launcher</string>
389
- <key>CFBundlePackageType</key>
390
- <string>APPL</string>
391
- <key>LSMinimumSystemVersion</key>
392
- <string>10.15</string>
393
- <key>CFBundleDocumentTypes</key>
394
- <array>
395
- <dict>
396
- <key>CFBundleTypeExtensions</key>
397
- <array>
398
- <string>rune</string>
399
- </array>
400
- <key>CFBundleTypeName</key>
401
- <string>Rune Agent File</string>
402
- <key>CFBundleTypeRole</key>
403
- <string>Editor</string>
404
- <key>LSHandlerRank</key>
405
- <string>Owner</string>
406
- <key>LSItemContentTypes</key>
407
- <array>
408
- <string>com.studio-h.rune</string>
409
- </array>
410
- </dict>
411
- </array>
412
- <key>UTExportedTypeDeclarations</key>
413
- <array>
414
- <dict>
415
- <key>UTTypeConformsTo</key>
416
- <array>
417
- <string>public.json</string>
418
- </array>
419
- <key>UTTypeDescription</key>
420
- <string>Rune Agent File</string>
421
- <key>UTTypeIdentifier</key>
422
- <string>com.studio-h.rune</string>
423
- <key>UTTypeTagSpecification</key>
424
- <dict>
425
- <key>public.filename-extension</key>
426
- <array>
427
- <string>rune</string>
428
- </array>
429
- </dict>
430
- </dict>
431
- </array>
432
- <key>LSUIElement</key>
433
- <true/>
434
- </dict>
435
- </plist>`
436
-
437
- fs.writeFileSync(path.join(appContents, 'Info.plist'), appPlist)
438
-
439
- // Launcher script — dynamically finds the rune binary and uses it to resolve Electron
440
- const runeBin = path.resolve(__dirname, 'rune.js')
441
- const nodebin = process.execPath
442
- const launcherScript = `#!/bin/bash
443
- # Dynamically resolve Rune's install location via the CLI binary
444
- RUNE_BIN="${runeBin}"
445
- RUNE_PROJECT="$(dirname "$(dirname "$RUNE_BIN")")"
446
-
447
- # Find Electron binary
448
- ELECTRON="$RUNE_PROJECT/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"
449
-
450
- if [ ! -f "$ELECTRON" ]; then
451
- # Fallback: try the path saved by rune install
452
- if [ -f "$HOME/.rune/project-path" ]; then
453
- RUNE_PROJECT="$(cat "$HOME/.rune/project-path")"
454
- ELECTRON="$RUNE_PROJECT/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"
455
- fi
456
- fi
457
-
458
- if [ ! -f "$ELECTRON" ]; then
459
- osascript -e 'display dialog "Electron not found. Run: rune install" buttons {"OK"}'
460
- exit 1
461
- fi
462
-
463
- # CRITICAL: unset this so Electron runs as an app, not plain Node.js
464
- unset ELECTRON_RUN_AS_NODE
465
-
466
- # macOS passes the file path as the last argument when double-clicking
467
- FILE=""
468
- for arg in "$@"; do
469
- if [[ "$arg" == *.rune ]]; then
470
- FILE="$arg"
471
- break
472
- fi
473
- done
474
-
475
- cd "$RUNE_PROJECT"
476
- if [ -n "$FILE" ]; then
477
- exec "$ELECTRON" . "$FILE"
478
- else
479
- exec "$ELECTRON" .
480
- fi
481
- `
482
- fs.writeFileSync(path.join(appMacOS, 'rune-launcher'), launcherScript, { mode: 0o755 })
483
-
484
- // Register the app with Launch Services
485
- try {
486
- execSync(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -R -f "${appDir}"`, { stdio: 'ignore' })
487
- console.log(' ✅ .rune file association registered')
488
- } catch {
489
- console.log(' ⚠️ Could not auto-register. Double-click a .rune file and choose "Open With" → Rune')
490
- }
491
-
492
- // Set as default handler for .rune files
493
- try {
494
- 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' })
495
- } catch {}
496
- }
497
-
498
- function installChannelPlugin() {
499
- console.log(' Installing Claude Code channel plugin...')
500
-
501
- const projectRoot = path.resolve(__dirname, '..')
502
- const claudePluginsDir = path.join(os.homedir(), '.claude', 'plugins')
503
- const installedFile = path.join(claudePluginsDir, 'installed_plugins.json')
504
- const marketplacesFile = path.join(claudePluginsDir, 'known_marketplaces.json')
505
-
506
- // Check if Claude Code plugins dir exists
507
- if (!fs.existsSync(claudePluginsDir)) {
508
- console.log(' ⚠️ Claude Code not found (~/.claude/plugins missing). Install Claude Code first.')
509
- return
510
- }
511
-
512
- try {
513
- // 1. Register marketplace
514
- const marketplaceName = 'rune'
515
- let marketplaces = {}
516
- if (fs.existsSync(marketplacesFile)) {
517
- try { marketplaces = JSON.parse(fs.readFileSync(marketplacesFile, 'utf-8')) } catch {}
518
- }
519
- if (!marketplaces[marketplaceName]) {
520
- marketplaces[marketplaceName] = {
521
- source: { source: 'github', repo: 'gilhyun/Rune' },
522
- installLocation: path.join(claudePluginsDir, 'marketplaces', marketplaceName),
523
- lastUpdated: new Date().toISOString(),
524
- }
525
- fs.writeFileSync(marketplacesFile, JSON.stringify(marketplaces, null, 2))
526
- }
527
-
528
- // 2. Copy plugin to cache
529
- const pluginJson = JSON.parse(fs.readFileSync(path.join(projectRoot, '.claude-plugin', 'plugin.json'), 'utf-8'))
530
- const version = pluginJson.version || '0.1.0'
531
- const cacheDir = path.join(claudePluginsDir, 'cache', marketplaceName, 'rune-channel', version)
532
- ensureDir(cacheDir)
533
-
534
- // Copy essential files
535
- const filesToCopy = ['.claude-plugin', 'dist/rune-channel.js', 'package.json', 'LICENSE']
536
- for (const f of filesToCopy) {
537
- const src = path.join(projectRoot, f)
538
- const dst = path.join(cacheDir, f)
539
- if (!fs.existsSync(src)) continue
540
- const stat = fs.statSync(src)
541
- if (stat.isDirectory()) {
542
- ensureDir(dst)
543
- for (const child of fs.readdirSync(src)) {
544
- fs.copyFileSync(path.join(src, child), path.join(dst, child))
545
- }
546
- } else {
547
- ensureDir(path.dirname(dst))
548
- fs.copyFileSync(src, dst)
549
- }
550
- }
551
-
552
- // 3. Register in installed_plugins.json
553
- let installed = { version: 2, plugins: {} }
554
- if (fs.existsSync(installedFile)) {
555
- try { installed = JSON.parse(fs.readFileSync(installedFile, 'utf-8')) } catch {}
556
- }
557
-
558
- const pluginKey = `rune-channel@${marketplaceName}`
559
- const entries = installed.plugins[pluginKey] || []
560
- // Add/update user-scope entry
561
- const userEntry = entries.find(e => e.scope === 'user')
562
- const newEntry = {
563
- scope: 'user',
564
- installPath: cacheDir,
565
- version,
566
- installedAt: userEntry?.installedAt || new Date().toISOString(),
567
- lastUpdated: new Date().toISOString(),
568
- }
569
- if (userEntry) {
570
- Object.assign(userEntry, newEntry)
571
- } else {
572
- entries.push(newEntry)
573
- }
574
- installed.plugins[pluginKey] = entries
575
- fs.writeFileSync(installedFile, JSON.stringify(installed, null, 2))
576
-
577
- console.log(' ✅ Channel plugin installed')
578
- } catch (e) {
579
- console.log(` ⚠️ Plugin install failed: ${e.message}`)
580
- console.log(' Run manually inside Claude Code: /plugin install rune-channel@rune')
581
- }
582
- }
583
37
 
584
38
  // ── new ──────────────────────────────────────────
585
39
 
@@ -601,8 +55,8 @@ function createRune(name, allArgs) {
601
55
  const filePath = path.resolve(process.cwd(), fileName)
602
56
 
603
57
  if (fs.existsSync(filePath)) {
604
- console.log(` ⚠️ ${fileName} already exists. Opening it instead.`)
605
- return openRune(filePath)
58
+ console.log(` ⚠️ ${fileName} already exists.`)
59
+ process.exit(1)
606
60
  }
607
61
 
608
62
  const runeData = {
@@ -619,66 +73,9 @@ function createRune(name, allArgs) {
619
73
  console.log(` Role: ${runeData.role}`)
620
74
  console.log(` Path: ${filePath}`)
621
75
  console.log('')
622
- console.log(' Double-click the file to open, or run:')
623
- console.log(` rune open ${fileName}`)
624
- }
625
-
626
- // ── open ─────────────────────────────────────────
627
-
628
- function findProjectRoot() {
629
- // Always resolve from the actual package location (works for both global and local)
630
- const fromBin = path.resolve(__dirname, '..')
631
- const electronCheck = path.join(fromBin, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
632
- if (fs.existsSync(electronCheck)) return fromBin
633
-
634
- // Fallback: check saved path
635
- const savedPath = path.join(RUNE_HOME, 'project-path')
636
- if (fs.existsSync(savedPath)) {
637
- const saved = fs.readFileSync(savedPath, 'utf-8').trim()
638
- const savedElectron = path.join(saved, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
639
- if (fs.existsSync(savedElectron)) return saved
640
- }
641
-
642
- return null
76
+ console.log(` Run: rune run ${fileName} "your prompt"`)
643
77
  }
644
78
 
645
- function openRune(file) {
646
- if (!file) {
647
- console.log('Usage: rune open <file.rune>')
648
- process.exit(1)
649
- }
650
-
651
- const filePath = path.resolve(process.cwd(), file)
652
- if (!fs.existsSync(filePath)) {
653
- console.error(` ❌ File not found: ${filePath}`)
654
- process.exit(1)
655
- }
656
-
657
- const projectRoot = findProjectRoot()
658
- if (!projectRoot) {
659
- console.error(' ❌ Electron not found. Run `rune install` first.')
660
- process.exit(1)
661
- }
662
-
663
- const electronBinary = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron')
664
-
665
- console.log(`🔮 Opening ${path.basename(filePath)}...`)
666
-
667
- // CRITICAL: unset ELECTRON_RUN_AS_NODE — Claude Code sets it,
668
- // which makes Electron act as plain Node.js instead of an Electron app
669
- const env = { ...process.env }
670
- delete env.ELECTRON_RUN_AS_NODE
671
-
672
- const child = spawn(electronBinary, ['.', filePath], {
673
- cwd: projectRoot,
674
- detached: true,
675
- stdio: 'ignore',
676
- env,
677
- })
678
- child.unref()
679
- }
680
-
681
- // ── run (headless) ──────────────────────────────
682
79
 
683
80
  function runRune(file, restArgs) {
684
81
  if (!file) {
@@ -782,16 +179,9 @@ function runRune(file, restArgs) {
782
179
  if (autoMode) {
783
180
  console.log(`🔮 [auto] ${rune.name} is working on: ${prompt}\n`)
784
181
 
785
- // Temporarily hide .mcp.json to prevent MCP interference
786
- const mcpPath = path.join(folderPath, '.mcp.json')
787
- const mcpBackup = path.join(folderPath, '.mcp.json.bak')
788
- let mcpHidden = false
789
- if (fs.existsSync(mcpPath)) {
790
- fs.renameSync(mcpPath, mcpBackup)
791
- mcpHidden = true
792
- }
793
-
794
182
  const claudeArgs = ['-p', '--print',
183
+ '--mcp-config', '{"mcpServers":{}}',
184
+ '--strict-mcp-config',
795
185
  '--dangerously-skip-permissions',
796
186
  '--verbose',
797
187
  '--output-format', 'stream-json',
@@ -805,12 +195,6 @@ function runRune(file, restArgs) {
805
195
  }
806
196
  claudeArgs.push(prompt)
807
197
 
808
- const restoreMcp = () => {
809
- if (mcpHidden && fs.existsSync(mcpBackup)) {
810
- fs.renameSync(mcpBackup, mcpPath)
811
- }
812
- }
813
-
814
198
  const child = spawn('claude', claudeArgs, {
815
199
  cwd: folderPath,
816
200
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -875,7 +259,6 @@ function runRune(file, restArgs) {
875
259
  child.stderr.on('data', (d) => { process.stderr.write(d) })
876
260
 
877
261
  child.on('close', (code) => {
878
- restoreMcp()
879
262
  // Save to history
880
263
  rune.history = rune.history || []
881
264
  rune.history.push({ role: 'user', text: prompt, ts: Date.now() })
@@ -908,16 +291,15 @@ function runRune(file, restArgs) {
908
291
  process.exit(code || 0)
909
292
  })
910
293
 
911
- // Restore .mcp.json if process is killed
912
- process.on('SIGINT', restoreMcp)
913
- process.on('SIGTERM', restoreMcp)
914
-
915
294
  return
916
295
  }
917
296
 
918
297
  // Normal mode: print-only, no tool execution
919
- // Run from tmpdir to avoid .mcp.json interference, add project folder via --add-dir
920
- const claudeArgs = ['-p', '--print', '--add-dir', folderPath]
298
+ const claudeArgs = [
299
+ '-p', '--print',
300
+ '--mcp-config', '{"mcpServers":{}}',
301
+ '--strict-mcp-config',
302
+ ]
921
303
  if (systemPrompt) {
922
304
  claudeArgs.push('--system-prompt', systemPrompt + `\nWorking folder: ${folderPath}`)
923
305
  }
@@ -926,9 +308,8 @@ function runRune(file, restArgs) {
926
308
  }
927
309
  claudeArgs.push('--', prompt)
928
310
 
929
- const os = require('os')
930
311
  const child = spawn('claude', claudeArgs, {
931
- cwd: os.tmpdir(),
312
+ cwd: folderPath,
932
313
  stdio: ['ignore', 'pipe', 'pipe'],
933
314
  env: { ...process.env },
934
315
  })
@@ -1037,16 +418,9 @@ async function pipeRunes(args) {
1037
418
  const useAuto = autoMode && isLast
1038
419
 
1039
420
  if (useAuto) {
1040
- // Temporarily hide .mcp.json
1041
- const mcpPath = path.join(folderPath, '.mcp.json')
1042
- const mcpBackup = path.join(folderPath, '.mcp.json.pipe.bak')
1043
- let mcpHidden = false
1044
- if (fs.existsSync(mcpPath)) {
1045
- fs.renameSync(mcpPath, mcpBackup)
1046
- mcpHidden = true
1047
- }
1048
-
1049
421
  const claudeArgs = ['-p', '--print',
422
+ '--mcp-config', '{"mcpServers":{}}',
423
+ '--strict-mcp-config',
1050
424
  '--dangerously-skip-permissions',
1051
425
  '--verbose',
1052
426
  '--output-format', 'stream-json',
@@ -1097,7 +471,6 @@ async function pipeRunes(args) {
1097
471
  })
1098
472
  child.stderr.on('data', (d) => { process.stderr.write(d) })
1099
473
  child.on('close', (code) => {
1100
- if (mcpHidden && fs.existsSync(mcpBackup)) fs.renameSync(mcpBackup, mcpPath)
1101
474
  if (code !== 0) reject(new Error(`Agent ${rune.name} exited with code ${code}`))
1102
475
  else resolve(fullOutput.trim())
1103
476
  })
@@ -1111,9 +484,12 @@ async function pipeRunes(args) {
1111
484
  currentInput = output
1112
485
 
1113
486
  } else {
1114
- // Normal pipe step: text output only, run from tmpdir to avoid .mcp.json
1115
- const os = require('os')
1116
- const claudeArgs = ['-p', '--print', '--add-dir', folderPath]
487
+ // Normal pipe step: text output only
488
+ const claudeArgs = [
489
+ '-p', '--print',
490
+ '--mcp-config', '{"mcpServers":{}}',
491
+ '--strict-mcp-config',
492
+ ]
1117
493
  if (systemParts.length > 0) {
1118
494
  claudeArgs.push('--system-prompt', systemParts.join('\n') + `\nWorking folder: ${folderPath}`)
1119
495
  }
@@ -1121,7 +497,7 @@ async function pipeRunes(args) {
1121
497
 
1122
498
  const output = await new Promise((resolve, reject) => {
1123
499
  const child = spawn('claude', claudeArgs, {
1124
- cwd: os.tmpdir(),
500
+ cwd: folderPath,
1125
501
  stdio: ['ignore', 'pipe', 'pipe'],
1126
502
  env: { ...process.env },
1127
503
  })
@@ -1294,15 +670,9 @@ async function loopRunes(args) {
1294
670
 
1295
671
  async function runAgent(name, folderPath, systemParts, prompt, autoMode) {
1296
672
  if (autoMode) {
1297
- const mcpPath = path.join(folderPath, '.mcp.json')
1298
- const mcpBackup = path.join(folderPath, '.mcp.json.loop.bak')
1299
- let mcpHidden = false
1300
- if (fs.existsSync(mcpPath)) {
1301
- fs.renameSync(mcpPath, mcpBackup)
1302
- mcpHidden = true
1303
- }
1304
-
1305
673
  const claudeArgs = ['-p', '--print',
674
+ '--mcp-config', '{"mcpServers":{}}',
675
+ '--strict-mcp-config',
1306
676
  '--dangerously-skip-permissions',
1307
677
  '--verbose',
1308
678
  '--output-format', 'stream-json',
@@ -1352,7 +722,6 @@ async function runAgent(name, folderPath, systemParts, prompt, autoMode) {
1352
722
  })
1353
723
  child.stderr.on('data', (d) => { process.stderr.write(d) })
1354
724
  child.on('close', (code) => {
1355
- if (mcpHidden && fs.existsSync(mcpBackup)) fs.renameSync(mcpBackup, mcpPath)
1356
725
  if (code !== 0) reject(new Error(`Agent ${name} exited with code ${code}`))
1357
726
  else resolve(fullOutput.trim())
1358
727
  })
@@ -1360,8 +729,11 @@ async function runAgent(name, folderPath, systemParts, prompt, autoMode) {
1360
729
 
1361
730
  return output
1362
731
  } else {
1363
- const tmpdir = require('os').tmpdir()
1364
- const claudeArgs = ['-p', '--print', '--add-dir', folderPath]
732
+ const claudeArgs = [
733
+ '-p', '--print',
734
+ '--mcp-config', '{"mcpServers":{}}',
735
+ '--strict-mcp-config',
736
+ ]
1365
737
  if (systemParts.length > 0) {
1366
738
  claudeArgs.push('--system-prompt', systemParts.join('\n') + `\nWorking folder: ${folderPath}`)
1367
739
  }
@@ -1369,7 +741,7 @@ async function runAgent(name, folderPath, systemParts, prompt, autoMode) {
1369
741
 
1370
742
  const output = await new Promise((resolve, reject) => {
1371
743
  const child = spawn('claude', claudeArgs, {
1372
- cwd: tmpdir,
744
+ cwd: folderPath,
1373
745
  stdio: ['ignore', 'pipe', 'pipe'],
1374
746
  env: { ...process.env },
1375
747
  })
@@ -1610,52 +982,21 @@ function listRunes() {
1610
982
  }
1611
983
  }
1612
984
 
1613
- // ── uninstall ────────────────────────────────────
1614
-
1615
- function uninstall() {
1616
- console.log('🔮 Uninstalling Rune...\n')
1617
-
1618
- // Remove Quick Action
1619
- const workflowDir = path.join(QUICK_ACTION_DIR, 'New Rune.workflow')
1620
- if (fs.existsSync(workflowDir)) {
1621
- fs.rmSync(workflowDir, { recursive: true })
1622
- console.log(' ✅ Quick Action removed')
1623
- }
1624
-
1625
- // Remove .app wrapper
1626
- const appDir = path.join(APP_DIR, 'Rune.app')
1627
- if (fs.existsSync(appDir)) {
1628
- fs.rmSync(appDir, { recursive: true })
1629
- console.log(' ✅ App wrapper removed')
1630
- }
1631
-
1632
- // Unregister from Launch Services
1633
- if (IS_MAC) {
1634
- try {
1635
- execSync(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u "${appDir}"`, { stdio: 'ignore' })
1636
- } catch {}
1637
- }
1638
-
1639
- console.log('\n🔮 Rune uninstalled. Your .rune files are preserved.\n')
1640
- }
1641
-
1642
985
  // ── help ─────────────────────────────────────────
1643
986
 
1644
987
  function showHelp() {
1645
988
  console.log(`
1646
- 🔮 Rune — File-based AI Agent Toolkit
989
+ 🔮 Rune — File-based AI Agent Toolkit (CLI)
1647
990
 
1648
991
  Usage:
1649
- rune install Install Rune (build app, register file association, add Quick Action)
1650
992
  rune new <name> Create a new .rune file in current directory
1651
993
  --role "description" Set the agent's role
1652
- rune open <file.rune> Open a .rune file (desktop GUI)
1653
- rune run <file.rune> "prompt" Run agent headlessly (no GUI)
994
+ rune run <file.rune> "prompt" Run agent headlessly
1654
995
  --auto Auto mode: agent writes files, runs commands, fixes errors
1655
996
  --output json|text Output format (default: text)
1656
997
  --log <file.json> Save structured log (tool calls, cost, duration)
1657
998
  rune pipe <a.rune> <b.rune> ... "prompt" Chain agents in a pipeline
1658
- --output json|text Output format (default: text)
999
+ --auto Last agent can write files and run commands
1659
1000
  rune loop <doer.rune> <reviewer.rune> "prompt" Self-correction loop
1660
1001
  --until "condition" Stop when reviewer output contains this text
1661
1002
  --max-iterations N Max iterations (default: 5)
@@ -1666,7 +1007,6 @@ Usage:
1666
1007
  --glob "*.ts" File pattern (for file-change)
1667
1008
  --interval 5m Schedule interval (for cron: 30s, 5m, 1h)
1668
1009
  rune list List .rune files in current directory
1669
- rune uninstall Remove Rune integration (keeps .rune files)
1670
1010
  rune help Show this help
1671
1011
 
1672
1012
  Examples:
@@ -1675,7 +1015,8 @@ Examples:
1675
1015
  rune pipe coder.rune reviewer.rune "Implement a login page"
1676
1016
  rune loop coder.rune reviewer.rune "Build a REST API" --until "no critical issues" --max-iterations 3 --auto
1677
1017
  rune watch reviewer.rune --on git-commit --prompt "Review this commit"
1678
- rune watch monitor.rune --on cron --interval 5m --prompt "Check server health"
1679
1018
  echo "Fix the bug in auth.ts" | rune run backend.rune
1019
+
1020
+ Looking for a GUI? Check out RuneChat: https://github.com/gilhyun/runechat
1680
1021
  `)
1681
1022
  }