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 +114 -33
- package/package.json +1 -1
- package/renderer/src/App.tsx +2 -6
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
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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.
|
|
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": {
|
package/renderer/src/App.tsx
CHANGED
|
@@ -7,20 +7,16 @@ export function App() {
|
|
|
7
7
|
const chat = useChat()
|
|
8
8
|
const [showTerminal, setShowTerminal] = useState(true)
|
|
9
9
|
|
|
10
|
-
//
|
|
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) {
|