openrune 2.0.4 → 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/README.ko.md CHANGED
@@ -34,6 +34,25 @@ Claude Code는 이미 서브에이전트, 훅, 스킬, 헤드리스 모드를
34
34
 
35
35
  ---
36
36
 
37
+ ## Agent Teams와 뭐가 다른가요?
38
+
39
+ Claude Code의 Agent Teams는 런타임에 팀원을 생성합니다 — 강력하지만, 세션이 끝나면 사라집니다.
40
+
41
+ Rune은 다른 접근을 합니다: **에이전트가 파일입니다.**
42
+
43
+ | | Agent Teams | Rune |
44
+ |---|---|---|
45
+ | **영구 저장** | 세션 한정 — 완료되면 에이전트 소멸 | `.rune` 파일로 히스토리와 메모리가 영구 보존 |
46
+ | **이동성** | 하나의 Claude Code 세션에 종속 | `.rune` 파일을 어디서든 공유, 버전 관리, 재사용 |
47
+ | **스케줄링** | 수동 실행만 가능 | Cron, 파일 변경, git-commit 트리거 |
48
+ | **권한** | 세션에서 상속 | 에이전트별 제어 (`fileWrite`, `bash`, `allowPaths`) |
49
+ | **실행** | 대화형 | 헤드리스, 파이프라인, CI/CD 지원 |
50
+ | **자기 수정** | 기본 제공 없음 | `rune loop` — 자동 리뷰-수정 반복 |
51
+
52
+ Rune 에이전트는 세션, 머신, 팀을 넘어 살아남습니다. 한 번 만들면 영원히 실행.
53
+
54
+ ---
55
+
37
56
  ## Rune의 작동 방식
38
57
 
39
58
  Rune은 Claude API를 호출하거나, 인증 정보를 다루거나, Claude Code 내부를 래핑하지 않습니다. 모든 에이전트 호출은 공식 `claude` CLI에 대한 단순한 서브프로세스 호출입니다:
@@ -89,25 +108,6 @@ rune run reviewer.rune "Review the latest commit"
89
108
 
90
109
  ---
91
110
 
92
- ## Agent Teams와 뭐가 다른가요?
93
-
94
- Claude Code의 Agent Teams는 런타임에 팀원을 생성합니다 — 강력하지만, 세션이 끝나면 사라집니다.
95
-
96
- Rune은 다른 접근을 합니다: **에이전트가 파일입니다.**
97
-
98
- | | Agent Teams | Rune |
99
- |---|---|---|
100
- | **영구 저장** | 세션 한정 — 완료되면 에이전트 소멸 | `.rune` 파일로 히스토리와 메모리가 영구 보존 |
101
- | **이동성** | 하나의 Claude Code 세션에 종속 | `.rune` 파일을 어디서든 공유, 버전 관리, 재사용 |
102
- | **스케줄링** | 수동 실행만 가능 | Cron, 파일 변경, git-commit 트리거 |
103
- | **권한** | 세션에서 상속 | 에이전트별 제어 (`fileWrite`, `bash`, `allowPaths`) |
104
- | **실행** | 대화형 | 헤드리스, 파이프라인, CI/CD 지원 |
105
- | **자기 수정** | 기본 제공 없음 | `rune loop` — 자동 리뷰-수정 반복 |
106
-
107
- Rune 에이전트는 세션, 머신, 팀을 넘어 살아남습니다. 한 번 만들면 영원히 실행.
108
-
109
- ---
110
-
111
111
  ## 핵심 개념
112
112
 
113
113
  ### 파일 하나 = 에이전트 하나
package/README.md CHANGED
@@ -34,6 +34,25 @@ If you just want a one-off specialized agent inside a single session, Claude Cod
34
34
 
35
35
  ---
36
36
 
37
+ ## How is Rune different from Agent Teams?
38
+
39
+ Claude Code's Agent Teams spawn teammates at runtime — powerful, but ephemeral. When the session ends, the agents are gone.
40
+
41
+ Rune takes a different approach: **agents are files.**
42
+
43
+ | | Agent Teams | Rune |
44
+ |---|---|---|
45
+ | **Persistence** | Session-only — agents disappear when done | `.rune` files persist forever with history and memory |
46
+ | **Portability** | Tied to a single Claude Code session | Share, version-control, and reuse `.rune` files anywhere |
47
+ | **Scheduling** | Manual execution only | Cron, file-change, and git-commit triggers |
48
+ | **Permissions** | Inherited from session | Per-agent controls (`fileWrite`, `bash`, `allowPaths`) |
49
+ | **Execution** | Interactive | Headless, pipelines, CI/CD-ready |
50
+ | **Self-correction** | Not built-in | `rune loop` — automatic review-fix cycles |
51
+
52
+ Rune agents survive across sessions, machines, and teams. Build once, run forever.
53
+
54
+ ---
55
+
37
56
  ## How Rune works
38
57
 
39
58
  Rune does not call the Claude API, handle any credentials, or wrap Claude Code's internals. Every agent invocation is a plain subprocess call to the official `claude` CLI:
@@ -89,25 +108,6 @@ That's it. You just built an agent.
89
108
 
90
109
  ---
91
110
 
92
- ## How is Rune different from Agent Teams?
93
-
94
- Claude Code's Agent Teams spawn teammates at runtime — powerful, but ephemeral. When the session ends, the agents are gone.
95
-
96
- Rune takes a different approach: **agents are files.**
97
-
98
- | | Agent Teams | Rune |
99
- |---|---|---|
100
- | **Persistence** | Session-only — agents disappear when done | `.rune` files persist forever with history and memory |
101
- | **Portability** | Tied to a single Claude Code session | Share, version-control, and reuse `.rune` files anywhere |
102
- | **Scheduling** | Manual execution only | Cron, file-change, and git-commit triggers |
103
- | **Permissions** | Inherited from session | Per-agent controls (`fileWrite`, `bash`, `allowPaths`) |
104
- | **Execution** | Interactive | Headless, pipelines, CI/CD-ready |
105
- | **Self-correction** | Not built-in | `rune loop` — automatic review-fix cycles |
106
-
107
- Rune agents survive across sessions, machines, and teams. Build once, run forever.
108
-
109
- ---
110
-
111
111
  ## Core Concepts
112
112
 
113
113
  ### One file = one agent
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
- module.exports = { load, pipe }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrune",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Persistent AI agents for Claude Code — build once, run forever.",
5
5
  "keywords": ["ai", "agent", "claude", "claude-code", "cli", "toolkit", "automation"],
6
6
  "repository": {