openrune 0.3.13 → 0.3.15

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/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.13",
3
+ "version": "0.3.15",
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": {
@@ -7,20 +7,16 @@ export function App() {
7
7
  const chat = useChat()
8
8
  const [showTerminal, setShowTerminal] = useState(true)
9
9
 
10
- // Auto-switch to chat when MCP channel connects OR when history is loaded
10
+ // Show terminal until connected, then switch to chat
11
11
  useEffect(() => {
12
12
  const handler = (data: { port: number; connected: boolean }) => {
13
13
  if (data.connected) setShowTerminal(false)
14
+ else setShowTerminal(true)
14
15
  }
15
16
  window.rune.on('rune:channelStatus', handler)
16
17
  return () => window.rune.off('rune:channelStatus', handler)
17
18
  }, [])
18
19
 
19
- // If there's existing history, show chat immediately
20
- useEffect(() => {
21
- if (chat.messages.length > 0) setShowTerminal(false)
22
- }, [chat.messages.length > 0])
23
-
24
20
  const toggleTerminal = useCallback(() => setShowTerminal(prev => !prev), [])
25
21
 
26
22
  if (!chat.runeInfo) {