openrune 0.3.12 → 0.3.14

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 (3) hide show
  1. package/bin/rune.js +114 -33
  2. package/package.json +1 -1
  3. package/src/main.ts +13 -3
package/bin/rune.js CHANGED
@@ -907,15 +907,18 @@ function runRune(file, restArgs) {
907
907
  // ── pipe (agent chaining) ───────────────────────
908
908
 
909
909
  async function pipeRunes(args) {
910
- // Parse: rune pipe agent1.rune agent2.rune ... "initial prompt" [--output json]
910
+ // Parse: rune pipe agent1.rune agent2.rune ... "initial prompt" [--output json] [--auto]
911
911
  const runeFiles = []
912
912
  let prompt = ''
913
913
  let outputFormat = 'text'
914
+ let autoMode = false
914
915
 
915
916
  for (let i = 0; i < args.length; i++) {
916
917
  if (args[i] === '--output' && args[i + 1]) {
917
918
  outputFormat = args[i + 1]
918
919
  i++
920
+ } else if (args[i] === '--auto') {
921
+ autoMode = true
919
922
  } else if (args[i].endsWith('.rune')) {
920
923
  runeFiles.push(args[i])
921
924
  } else if (!prompt) {
@@ -924,9 +927,10 @@ async function pipeRunes(args) {
924
927
  }
925
928
 
926
929
  if (runeFiles.length < 2 || !prompt) {
927
- console.log('Usage: rune pipe <agent1.rune> <agent2.rune> [...] "initial prompt"')
928
- console.log('Example: rune pipe coder.rune reviewer.rune "Implement a login page"')
930
+ console.log('Usage: rune pipe <agent1.rune> <agent2.rune> [...] "initial prompt" [--auto]')
931
+ console.log('Example: rune pipe architect.rune coder.rune "Build a REST API" --auto')
929
932
  console.log('\nThe output of each agent becomes the input for the next.')
933
+ console.log('With --auto, the last agent can write files and run commands.')
930
934
  process.exit(1)
931
935
  }
932
936
 
@@ -967,42 +971,119 @@ async function pipeRunes(args) {
967
971
  rune.memory.forEach((m, j) => systemParts.push(`${j + 1}. ${m}`))
968
972
  }
969
973
 
970
- const claudeArgs = ['-p', '--print', '--bare']
971
- if (systemParts.length > 0) {
972
- claudeArgs.push('--system-prompt', systemParts.join('\n'))
973
- }
974
- claudeArgs.push(pipeContext)
975
-
976
- const output = await new Promise((resolve, reject) => {
977
- const child = spawn('claude', claudeArgs, {
978
- cwd: folderPath,
979
- stdio: ['ignore', 'pipe', 'pipe'],
980
- env: { ...process.env },
981
- })
974
+ // Last agent in auto mode: can write files and run commands
975
+ const useAuto = autoMode && isLast
976
+
977
+ if (useAuto) {
978
+ // Temporarily hide .mcp.json
979
+ const mcpPath = path.join(folderPath, '.mcp.json')
980
+ const mcpBackup = path.join(folderPath, '.mcp.json.pipe.bak')
981
+ let mcpHidden = false
982
+ if (fs.existsSync(mcpPath)) {
983
+ fs.renameSync(mcpPath, mcpBackup)
984
+ mcpHidden = true
985
+ }
982
986
 
983
- let stdout = ''
984
- let stderr = ''
985
- child.stdout.on('data', (d) => { stdout += d.toString() })
986
- child.stderr.on('data', (d) => { stderr += d.toString() })
987
- child.on('close', (code) => {
988
- if (code !== 0) reject(new Error(stderr || `Agent ${rune.name} exited with code ${code}`))
989
- else resolve(stdout.trim())
987
+ const claudeArgs = ['-p', '--print',
988
+ '--dangerously-skip-permissions',
989
+ '--verbose',
990
+ '--output-format', 'stream-json',
991
+ ]
992
+ if (systemParts.length > 0) {
993
+ claudeArgs.push('--system-prompt', systemParts.join('\n'))
994
+ }
995
+ claudeArgs.push(pipeContext)
996
+
997
+ const output = await new Promise((resolve, reject) => {
998
+ const child = spawn('claude', claudeArgs, {
999
+ cwd: folderPath,
1000
+ stdio: ['ignore', 'pipe', 'pipe'],
1001
+ env: { ...process.env },
1002
+ })
1003
+
1004
+ let fullOutput = ''
1005
+ let buffer = ''
1006
+ child.stdout.on('data', (data) => {
1007
+ buffer += data.toString()
1008
+ const lines = buffer.split('\n')
1009
+ buffer = lines.pop()
1010
+ for (const line of lines) {
1011
+ if (!line.trim()) continue
1012
+ try {
1013
+ const event = JSON.parse(line)
1014
+ if (event.type === 'assistant' && event.message && event.message.content) {
1015
+ for (const block of event.message.content) {
1016
+ if (block.type === 'tool_use') {
1017
+ const tool = block.name || 'unknown'
1018
+ const input = block.input || {}
1019
+ if (tool === 'Bash') console.log(` ▶ Bash: ${(input.command || '').slice(0, 120)}`)
1020
+ else if (tool === 'Write') console.log(` ▶ Write: ${input.file_path || ''}`)
1021
+ else if (tool === 'Edit') console.log(` ▶ Edit: ${input.file_path || ''}`)
1022
+ else if (tool === 'Read') console.log(` ▶ Read: ${input.file_path || ''}`)
1023
+ else console.log(` ▶ ${tool}`)
1024
+ } else if (block.type === 'text' && block.text && block.text.trim()) {
1025
+ console.log(` 💬 ${block.text.trim().slice(0, 200)}`)
1026
+ }
1027
+ }
1028
+ }
1029
+ if (event.type === 'result') {
1030
+ fullOutput = event.result || ''
1031
+ if (fullOutput) console.log(`\n${fullOutput}`)
1032
+ }
1033
+ } catch {}
1034
+ }
1035
+ })
1036
+ child.stderr.on('data', (d) => { process.stderr.write(d) })
1037
+ child.on('close', (code) => {
1038
+ if (mcpHidden && fs.existsSync(mcpBackup)) fs.renameSync(mcpBackup, mcpPath)
1039
+ if (code !== 0) reject(new Error(`Agent ${rune.name} exited with code ${code}`))
1040
+ else resolve(fullOutput.trim())
1041
+ })
990
1042
  })
991
- })
992
1043
 
993
- results.push({ agent: rune.name, role: rune.role, output })
1044
+ results.push({ agent: rune.name, role: rune.role, output })
1045
+ rune.history = rune.history || []
1046
+ rune.history.push({ role: 'user', text: pipeContext, ts: Date.now() })
1047
+ rune.history.push({ role: 'assistant', text: output, ts: Date.now() })
1048
+ fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
1049
+ currentInput = output
994
1050
 
995
- // Save to .rune history
996
- rune.history = rune.history || []
997
- rune.history.push({ role: 'user', text: pipeContext, ts: Date.now() })
998
- rune.history.push({ role: 'assistant', text: output, ts: Date.now() })
999
- fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
1051
+ } else {
1052
+ // Normal pipe step: text output only, run from tmpdir to avoid .mcp.json
1053
+ const os = require('os')
1054
+ const claudeArgs = ['-p', '--print', '--add-dir', folderPath]
1055
+ if (systemParts.length > 0) {
1056
+ claudeArgs.push('--system-prompt', systemParts.join('\n') + `\nWorking folder: ${folderPath}`)
1057
+ }
1058
+ claudeArgs.push('--', pipeContext)
1059
+
1060
+ const output = await new Promise((resolve, reject) => {
1061
+ const child = spawn('claude', claudeArgs, {
1062
+ cwd: os.tmpdir(),
1063
+ stdio: ['ignore', 'pipe', 'pipe'],
1064
+ env: { ...process.env },
1065
+ })
1066
+
1067
+ let stdout = ''
1068
+ let stderr = ''
1069
+ child.stdout.on('data', (d) => { stdout += d.toString() })
1070
+ child.stderr.on('data', (d) => { stderr += d.toString() })
1071
+ child.on('close', (code) => {
1072
+ if (code !== 0) reject(new Error(stderr || `Agent ${rune.name} exited with code ${code}`))
1073
+ else resolve(stdout.trim())
1074
+ })
1075
+ })
1000
1076
 
1001
- currentInput = output
1077
+ results.push({ agent: rune.name, role: rune.role, output })
1078
+ rune.history = rune.history || []
1079
+ rune.history.push({ role: 'user', text: pipeContext, ts: Date.now() })
1080
+ rune.history.push({ role: 'assistant', text: output, ts: Date.now() })
1081
+ fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
1082
+ currentInput = output
1002
1083
 
1003
- // Print intermediate output
1004
- if (outputFormat !== 'json' && !isLast) {
1005
- console.error(` ✓ Done\n`)
1084
+ if (outputFormat !== 'json' && !isLast) {
1085
+ console.error(` ✓ Done\n`)
1086
+ }
1006
1087
  }
1007
1088
  }
1008
1089
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrune",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Rune — File-based AI Agent Harness for Claude Code",
5
5
  "keywords": ["ai", "agent", "claude", "desktop", "electron", "mcp", "claude-code", "harness", "automation"],
6
6
  "repository": {
package/src/main.ts CHANGED
@@ -344,9 +344,8 @@ async function createRuneWindow(filePath: string) {
344
344
 
345
345
  const rune = readRuneFile(filePath)
346
346
  const folderPath = path.dirname(filePath)
347
- let port = rune.port
348
- if (port && await isPortInUse(port)) port = 0
349
- if (!port) port = await allocatePort()
347
+ // Always allocate a fresh port to avoid conflicts with stale ports
348
+ const port = await allocatePort()
350
349
 
351
350
  // Sync name with filename & save port
352
351
  const fileBaseName = path.basename(filePath, '.rune')
@@ -476,6 +475,17 @@ async function createRuneWindow(filePath: string) {
476
475
  ptyProcesses.delete(id)
477
476
  ptyOwnerWindows.delete(id)
478
477
  }
478
+ // Clear port from .rune file so next session gets a fresh port
479
+ try {
480
+ const closeRune = readRuneFile(currentFilePath)
481
+ delete closeRune.port
482
+ writeRuneFile(currentFilePath, closeRune)
483
+ } catch {}
484
+ // Clean up .mcp.json if no other windows use this folder
485
+ const folderStillUsed = [...windowRegistry.values()].some(r => r.folderPath === folderPath && !r.window.isDestroyed())
486
+ if (!folderStillUsed) {
487
+ try { fs.unlinkSync(path.join(folderPath, '.mcp.json')) } catch {}
488
+ }
479
489
  updateDockVisibility()
480
490
  })
481
491
  }