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 +19 -19
- package/README.md +19 -19
- package/bin/rune.js +221 -0
- package/lib/index.js +87 -1
- package/package.json +1 -1
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
|
-
|
|
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