openrune 2.0.5 → 2.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/bin/rune.js +221 -0
- package/lib/index.js +87 -1
- package/package.json +1 -1
package/bin/rune.js
CHANGED
|
@@ -19,6 +19,8 @@ switch (command) {
|
|
|
19
19
|
case 'loop': return loopRunes(args)
|
|
20
20
|
case 'watch': return watchRune(args[0], args.slice(1))
|
|
21
21
|
case 'list': return listRunes()
|
|
22
|
+
case 'backup': return backupRune(args[0], args.slice(1))
|
|
23
|
+
case 'inbound': return inboundRune(args[0], args.slice(1))
|
|
22
24
|
case 'open':
|
|
23
25
|
console.log(' ℹ️ `rune open` has been removed from the CLI.')
|
|
24
26
|
console.log(' For a GUI, use RuneChat: https://github.com/gilhyun/runechat')
|
|
@@ -982,6 +984,215 @@ function listRunes() {
|
|
|
982
984
|
}
|
|
983
985
|
}
|
|
984
986
|
|
|
987
|
+
// ── backup ──────────────────────────────────────
|
|
988
|
+
|
|
989
|
+
function backupRune(file, restArgs) {
|
|
990
|
+
if (!file) {
|
|
991
|
+
console.log('Usage: rune backup <file.rune> [--format md|json|rune] [--output <path>]')
|
|
992
|
+
console.log('')
|
|
993
|
+
console.log('Formats:')
|
|
994
|
+
console.log(' md Markdown — readable conversation export (default)')
|
|
995
|
+
console.log(' json JSON — full data including memory and metadata')
|
|
996
|
+
console.log(' rune Clone — timestamped copy of the .rune file')
|
|
997
|
+
console.log('')
|
|
998
|
+
console.log('Examples:')
|
|
999
|
+
console.log(' rune backup reviewer.rune')
|
|
1000
|
+
console.log(' rune backup reviewer.rune --format json')
|
|
1001
|
+
console.log(' rune backup reviewer.rune --format rune --output backups/')
|
|
1002
|
+
process.exit(1)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
1006
|
+
if (!fs.existsSync(filePath)) {
|
|
1007
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
1008
|
+
process.exit(1)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Parse args
|
|
1012
|
+
let format = 'md'
|
|
1013
|
+
let outputPath = ''
|
|
1014
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
1015
|
+
if (restArgs[i] === '--format' && restArgs[i + 1]) { format = restArgs[++i] }
|
|
1016
|
+
else if (restArgs[i] === '--output' && restArgs[i + 1]) { outputPath = restArgs[++i] }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const rune = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
1020
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
1021
|
+
const baseName = rune.name || path.basename(file, '.rune')
|
|
1022
|
+
|
|
1023
|
+
let outFile, content
|
|
1024
|
+
|
|
1025
|
+
if (format === 'md') {
|
|
1026
|
+
outFile = `${baseName}-backup-${ts}.md`
|
|
1027
|
+
const lines = []
|
|
1028
|
+
lines.push(`# ${rune.name} — Conversation Backup`)
|
|
1029
|
+
lines.push(``)
|
|
1030
|
+
lines.push(`- **Role**: ${rune.role || 'N/A'}`)
|
|
1031
|
+
lines.push(`- **Created**: ${rune.createdAt || 'N/A'}`)
|
|
1032
|
+
lines.push(`- **Exported**: ${new Date().toISOString()}`)
|
|
1033
|
+
lines.push(`- **Messages**: ${(rune.history || []).length}`)
|
|
1034
|
+
lines.push(``)
|
|
1035
|
+
|
|
1036
|
+
if (rune.memory && rune.memory.length > 0) {
|
|
1037
|
+
lines.push(`## Memory`)
|
|
1038
|
+
lines.push(``)
|
|
1039
|
+
rune.memory.forEach((m, i) => lines.push(`${i + 1}. ${m}`))
|
|
1040
|
+
lines.push(``)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (rune.history && rune.history.length > 0) {
|
|
1044
|
+
lines.push(`## Conversation`)
|
|
1045
|
+
lines.push(``)
|
|
1046
|
+
for (const msg of rune.history) {
|
|
1047
|
+
const who = msg.role === 'user' ? '**User**' : '**Assistant**'
|
|
1048
|
+
const time = msg.ts ? new Date(msg.ts).toLocaleString() : ''
|
|
1049
|
+
lines.push(`### ${who} ${time ? `(${time})` : ''}`)
|
|
1050
|
+
lines.push(``)
|
|
1051
|
+
lines.push(msg.text)
|
|
1052
|
+
lines.push(``)
|
|
1053
|
+
lines.push(`---`)
|
|
1054
|
+
lines.push(``)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
content = lines.join('\n')
|
|
1059
|
+
|
|
1060
|
+
} else if (format === 'json') {
|
|
1061
|
+
outFile = `${baseName}-backup-${ts}.json`
|
|
1062
|
+
content = JSON.stringify({
|
|
1063
|
+
...rune,
|
|
1064
|
+
_backup: {
|
|
1065
|
+
exportedAt: new Date().toISOString(),
|
|
1066
|
+
sourceFile: filePath,
|
|
1067
|
+
messageCount: (rune.history || []).length,
|
|
1068
|
+
}
|
|
1069
|
+
}, null, 2)
|
|
1070
|
+
|
|
1071
|
+
} else if (format === 'rune') {
|
|
1072
|
+
outFile = `${baseName}-backup-${ts}.rune`
|
|
1073
|
+
content = JSON.stringify(rune, null, 2)
|
|
1074
|
+
|
|
1075
|
+
} else {
|
|
1076
|
+
console.error(` ❌ Unknown format: ${format}. Use: md, json, rune`)
|
|
1077
|
+
process.exit(1)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Resolve output path
|
|
1081
|
+
if (outputPath) {
|
|
1082
|
+
const resolved = path.resolve(process.cwd(), outputPath)
|
|
1083
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
1084
|
+
outFile = path.join(resolved, outFile)
|
|
1085
|
+
} else {
|
|
1086
|
+
// Treat as file path
|
|
1087
|
+
outFile = resolved
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
outFile = path.resolve(process.cwd(), outFile)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Ensure parent dir exists
|
|
1094
|
+
const outDir = path.dirname(outFile)
|
|
1095
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })
|
|
1096
|
+
|
|
1097
|
+
fs.writeFileSync(outFile, content)
|
|
1098
|
+
|
|
1099
|
+
const stats = {
|
|
1100
|
+
messages: (rune.history || []).length,
|
|
1101
|
+
memory: (rune.memory || []).length,
|
|
1102
|
+
size: fs.statSync(outFile).size,
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
console.log(`📦 Backup created`)
|
|
1106
|
+
console.log(` Agent: ${rune.name} (${rune.role || 'N/A'})`)
|
|
1107
|
+
console.log(` Format: ${format}`)
|
|
1108
|
+
console.log(` Messages: ${stats.messages}, Memory: ${stats.memory}`)
|
|
1109
|
+
console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`)
|
|
1110
|
+
console.log(` Output: ${outFile}`)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
// ── inbound ─────────────────────────────────────
|
|
1115
|
+
|
|
1116
|
+
function inboundRune(file, restArgs) {
|
|
1117
|
+
if (!file) {
|
|
1118
|
+
console.log('Usage: rune inbound <file.rune> "message" [--run] [--auto] [--source <name>]')
|
|
1119
|
+
console.log('')
|
|
1120
|
+
console.log('Options:')
|
|
1121
|
+
console.log(' --run Send the message and immediately run the agent')
|
|
1122
|
+
console.log(' --auto Run in auto mode (agent can write files, run commands)')
|
|
1123
|
+
console.log(' --source <name> Label the message source (default: "external")')
|
|
1124
|
+
console.log('')
|
|
1125
|
+
console.log('Without --run, the message is queued in history for the next rune run.')
|
|
1126
|
+
console.log('')
|
|
1127
|
+
console.log('Examples:')
|
|
1128
|
+
console.log(' rune inbound reviewer.rune "PR #42 is ready for review" --run')
|
|
1129
|
+
console.log(' rune inbound coder.rune "Build failed, fix it" --run --auto')
|
|
1130
|
+
console.log(' rune inbound monitor.rune "Deploy complete" --source deploy-bot')
|
|
1131
|
+
console.log(' echo "Error log..." | rune inbound coder.rune --run --auto')
|
|
1132
|
+
process.exit(1)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
1136
|
+
if (!fs.existsSync(filePath)) {
|
|
1137
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
1138
|
+
process.exit(1)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Parse args
|
|
1142
|
+
let message = ''
|
|
1143
|
+
let shouldRun = false
|
|
1144
|
+
let autoMode = false
|
|
1145
|
+
let source = 'external'
|
|
1146
|
+
|
|
1147
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
1148
|
+
if (restArgs[i] === '--run') { shouldRun = true }
|
|
1149
|
+
else if (restArgs[i] === '--auto') { autoMode = true; shouldRun = true }
|
|
1150
|
+
else if (restArgs[i] === '--source' && restArgs[i + 1]) { source = restArgs[++i] }
|
|
1151
|
+
else if (!message) { message = restArgs[i] }
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Read from stdin if no message
|
|
1155
|
+
if (!message && process.stdin.isTTY === false) {
|
|
1156
|
+
message = fs.readFileSync('/dev/stdin', 'utf-8').trim()
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (!message) {
|
|
1160
|
+
console.error(' ❌ No message provided. Pass a message string or pipe via stdin.')
|
|
1161
|
+
process.exit(1)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const rune = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
1165
|
+
|
|
1166
|
+
// Add message to history with source metadata
|
|
1167
|
+
rune.history = rune.history || []
|
|
1168
|
+
rune.history.push({
|
|
1169
|
+
role: 'user',
|
|
1170
|
+
text: message,
|
|
1171
|
+
ts: Date.now(),
|
|
1172
|
+
source,
|
|
1173
|
+
})
|
|
1174
|
+
fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
|
|
1175
|
+
|
|
1176
|
+
console.log(`📨 Message delivered to ${rune.name}`)
|
|
1177
|
+
console.log(` From: ${source}`)
|
|
1178
|
+
console.log(` Text: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}`)
|
|
1179
|
+
|
|
1180
|
+
if (!shouldRun) {
|
|
1181
|
+
console.log(` Status: Queued (will be seen on next rune run)`)
|
|
1182
|
+
console.log(`\n To run now: rune run ${file} "continue"`)
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Run the agent with the inbound message
|
|
1187
|
+
console.log(` Status: Running agent...\n`)
|
|
1188
|
+
|
|
1189
|
+
const runArgs = [file, message]
|
|
1190
|
+
if (autoMode) runArgs.push('--auto')
|
|
1191
|
+
|
|
1192
|
+
return runRune(file, runArgs.slice(1))
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
|
|
985
1196
|
// ── help ─────────────────────────────────────────
|
|
986
1197
|
|
|
987
1198
|
function showHelp() {
|
|
@@ -1006,6 +1217,13 @@ Usage:
|
|
|
1006
1217
|
--prompt "..." Prompt to send when triggered
|
|
1007
1218
|
--glob "*.ts" File pattern (for file-change)
|
|
1008
1219
|
--interval 5m Schedule interval (for cron: 30s, 5m, 1h)
|
|
1220
|
+
rune backup <file.rune> Export agent conversation/data
|
|
1221
|
+
--format md|json|rune Output format (default: md)
|
|
1222
|
+
--output <path> Output file or directory
|
|
1223
|
+
rune inbound <file.rune> "msg" Send external message to agent
|
|
1224
|
+
--run Immediately run the agent after delivery
|
|
1225
|
+
--auto Run in auto mode (implies --run)
|
|
1226
|
+
--source <name> Label the message source (default: "external")
|
|
1009
1227
|
rune list List .rune files in current directory
|
|
1010
1228
|
rune help Show this help
|
|
1011
1229
|
|
|
@@ -1015,6 +1233,9 @@ Examples:
|
|
|
1015
1233
|
rune pipe coder.rune reviewer.rune "Implement a login page"
|
|
1016
1234
|
rune loop coder.rune reviewer.rune "Build a REST API" --until "no critical issues" --max-iterations 3 --auto
|
|
1017
1235
|
rune watch reviewer.rune --on git-commit --prompt "Review this commit"
|
|
1236
|
+
rune backup reviewer.rune --format md
|
|
1237
|
+
rune inbound coder.rune "Build failed, fix it" --run --auto
|
|
1238
|
+
echo "Error log..." | rune inbound coder.rune --run --auto
|
|
1018
1239
|
echo "Fix the bug in auth.ts" | rune run backend.rune
|
|
1019
1240
|
|
|
1020
1241
|
Looking for a GUI? Check out RuneChat: https://github.com/gilhyun/runechat
|
package/lib/index.js
CHANGED
|
@@ -120,4 +120,90 @@ async function pipe(runeFiles, prompt) {
|
|
|
120
120
|
return { pipeline: results, finalOutput: currentInput }
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Backup a .rune file to md/json/rune format
|
|
125
|
+
* @param {string} filePath - path to .rune file
|
|
126
|
+
* @param {object} opts - { format: 'md'|'json'|'rune' }
|
|
127
|
+
* @returns {{ content: string, filename: string }}
|
|
128
|
+
*/
|
|
129
|
+
function backup(filePath, opts = {}) {
|
|
130
|
+
const resolved = path.resolve(filePath)
|
|
131
|
+
if (!fs.existsSync(resolved)) {
|
|
132
|
+
throw new Error(`Rune file not found: ${resolved}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const rune = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
136
|
+
const format = opts.format || 'md'
|
|
137
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
138
|
+
const baseName = rune.name || path.basename(filePath, '.rune')
|
|
139
|
+
|
|
140
|
+
if (format === 'md') {
|
|
141
|
+
const lines = []
|
|
142
|
+
lines.push(`# ${rune.name} — Conversation Backup\n`)
|
|
143
|
+
lines.push(`- **Role**: ${rune.role || 'N/A'}`)
|
|
144
|
+
lines.push(`- **Exported**: ${new Date().toISOString()}`)
|
|
145
|
+
lines.push(`- **Messages**: ${(rune.history || []).length}\n`)
|
|
146
|
+
|
|
147
|
+
if (rune.memory && rune.memory.length > 0) {
|
|
148
|
+
lines.push(`## Memory\n`)
|
|
149
|
+
rune.memory.forEach((m, i) => lines.push(`${i + 1}. ${m}`))
|
|
150
|
+
lines.push('')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rune.history && rune.history.length > 0) {
|
|
154
|
+
lines.push(`## Conversation\n`)
|
|
155
|
+
for (const msg of rune.history) {
|
|
156
|
+
const who = msg.role === 'user' ? '**User**' : '**Assistant**'
|
|
157
|
+
const time = msg.ts ? new Date(msg.ts).toLocaleString() : ''
|
|
158
|
+
lines.push(`### ${who} ${time ? `(${time})` : ''}\n`)
|
|
159
|
+
lines.push(msg.text)
|
|
160
|
+
lines.push('\n---\n')
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { content: lines.join('\n'), filename: `${baseName}-backup-${ts}.md` }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (format === 'json') {
|
|
168
|
+
return {
|
|
169
|
+
content: JSON.stringify({ ...rune, _backup: { exportedAt: new Date().toISOString(), sourceFile: resolved } }, null, 2),
|
|
170
|
+
filename: `${baseName}-backup-${ts}.json`,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (format === 'rune') {
|
|
175
|
+
return {
|
|
176
|
+
content: JSON.stringify(rune, null, 2),
|
|
177
|
+
filename: `${baseName}-backup-${ts}.rune`,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error(`Unknown format: ${format}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Send an inbound message to a .rune agent
|
|
186
|
+
* @param {string} filePath - path to .rune file
|
|
187
|
+
* @param {string} message - the message text
|
|
188
|
+
* @param {object} opts - { source: string }
|
|
189
|
+
*/
|
|
190
|
+
function inbound(filePath, message, opts = {}) {
|
|
191
|
+
const resolved = path.resolve(filePath)
|
|
192
|
+
if (!fs.existsSync(resolved)) {
|
|
193
|
+
throw new Error(`Rune file not found: ${resolved}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const rune = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
197
|
+
rune.history = rune.history || []
|
|
198
|
+
rune.history.push({
|
|
199
|
+
role: 'user',
|
|
200
|
+
text: message,
|
|
201
|
+
ts: Date.now(),
|
|
202
|
+
source: opts.source || 'external',
|
|
203
|
+
})
|
|
204
|
+
fs.writeFileSync(resolved, JSON.stringify(rune, null, 2))
|
|
205
|
+
|
|
206
|
+
return { agent: rune.name, queued: true, historyLength: rune.history.length }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { load, pipe, backup, inbound }
|
package/package.json
CHANGED