openrune 0.1.3 → 0.2.1
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.md +151 -43
- package/bin/rune.js +435 -6
- package/lib/index.js +116 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">Rune</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>File-based AI Agent
|
|
9
|
-
Drop a <code>.rune</code> file in any folder.
|
|
8
|
+
<strong>File-based AI Agent Harness for Claude Code</strong><br/>
|
|
9
|
+
Drop a <code>.rune</code> file in any folder. Run it headlessly, chain agents, or open the desktop UI.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -19,27 +19,30 @@
|
|
|
19
19
|
|
|
20
20
|
## What is Rune?
|
|
21
21
|
|
|
22
|
-
Rune
|
|
22
|
+
Rune is a file-based agent harness for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Each `.rune` file is an independent AI agent with its own role, memory, and context. Run it from the CLI, chain agents together, automate with triggers, or open the desktop UI.
|
|
23
23
|
|
|
24
24
|
- **File-based** — One `.rune` file = one agent. Move it, share it, version it with git.
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
25
|
+
- **Headless execution** — Run agents from the CLI or scripts. No GUI needed.
|
|
26
|
+
- **Agent chaining** — Pipe agents together in a pipeline. Output → input, automatically.
|
|
27
|
+
- **Automated triggers** — Run agents on file changes, git commits, or a cron schedule.
|
|
28
|
+
- **Node.js API** — Use agents programmatically with `require('openrune')`.
|
|
29
|
+
- **Desktop UI** — Chat interface with real-time activity monitoring and built-in terminal.
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
32
33
|
## Why Rune?
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
Building a Claude Code harness usually means wiring up process management, I/O parsing, state handling, and a UI from scratch. Rune lets you skip all of that — just drop a file and go.
|
|
35
36
|
|
|
36
|
-
**
|
|
37
|
+
**No harness boilerplate** — No SDK wiring, no process management, no custom I/O parsing. One `.rune` file gives you a fully working agent you can run from CLI, scripts, or the desktop UI.
|
|
37
38
|
|
|
38
|
-
**
|
|
39
|
+
**Persistent context** — Role, memory, and chat history live in the `.rune` file. Close the app, reopen it next week — the agent picks up right where you left off.
|
|
39
40
|
|
|
40
|
-
**
|
|
41
|
+
**Portable & shareable** — The `.rune` file is just JSON. Commit it to git, share it with teammates, or move it to another machine. The agent goes wherever the file goes.
|
|
41
42
|
|
|
42
|
-
**
|
|
43
|
+
**Multiple agents per project** — A reviewer, a backend dev, a designer — each with its own role and history, working side by side in the same folder.
|
|
44
|
+
|
|
45
|
+
**Scriptable** — Chain agents, set up triggers, or call agents from your own code via the Node.js API. One file format, multiple ways to use it.
|
|
43
46
|
|
|
44
47
|
<p align="center">
|
|
45
48
|
<img src="demo.gif" width="100%" alt="Rune demo" />
|
|
@@ -73,39 +76,65 @@ Or right-click any folder in Finder → Quick Actions → **New Rune**
|
|
|
73
76
|
<img src="Screenshot.png" width="500" alt="Right-click to create a Rune agent" />
|
|
74
77
|
</p>
|
|
75
78
|
|
|
76
|
-
### 3.
|
|
79
|
+
### 3. Use it
|
|
77
80
|
|
|
78
|
-
**Double-click
|
|
81
|
+
**Desktop UI** — Double-click the `.rune` file, or:
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
84
|
rune open myagent.rune
|
|
82
85
|
```
|
|
83
86
|
|
|
87
|
+
**Headless** — Run from the CLI without a GUI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
rune run myagent.rune "Explain this project's architecture"
|
|
91
|
+
```
|
|
92
|
+
|
|
84
93
|
---
|
|
85
94
|
|
|
86
|
-
##
|
|
95
|
+
## Use Cases
|
|
96
|
+
|
|
97
|
+
**Solo dev workflow** — Create `reviewer.rune` and `coder.rune` in your project. Use one to write code, the other to review it. Each agent keeps its own context and history.
|
|
98
|
+
|
|
99
|
+
**Automated code review** — Set up a trigger to review every commit automatically:
|
|
100
|
+
```bash
|
|
101
|
+
rune watch reviewer.rune --on git-commit --prompt "Review this commit for bugs and security issues"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**CI/CD integration** — Run agents headlessly in your pipeline:
|
|
105
|
+
```bash
|
|
106
|
+
rune run qa.rune "Run tests and report any failures" --output json
|
|
107
|
+
```
|
|
87
108
|
|
|
88
|
-
|
|
109
|
+
**Agent pipeline** — Chain specialized agents for complex tasks:
|
|
110
|
+
```bash
|
|
111
|
+
rune pipe architect.rune coder.rune reviewer.rune "Add OAuth2 login flow"
|
|
112
|
+
```
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
- **File attachment** — Drag and drop files or click to attach. The agent reads them from your filesystem.
|
|
92
|
-
- **Stream cancellation** — Stop a response mid-stream.
|
|
93
|
-
- **Chat history** — Persisted in the `.rune` file. Clear anytime.
|
|
114
|
+
**Team collaboration** — Commit `.rune` files to git. Your teammates get the same agent with the same role and memory — no setup needed.
|
|
94
115
|
|
|
95
|
-
|
|
116
|
+
**Monitoring** — Schedule an agent to check things periodically:
|
|
117
|
+
```bash
|
|
118
|
+
rune watch ops.rune --on cron --interval 10m --prompt "Check if the API is healthy"
|
|
119
|
+
```
|
|
96
120
|
|
|
97
|
-
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Features
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
- **Tool results** — See the output of each action.
|
|
101
|
-
- **Permission requests** — Get notified when the agent needs approval.
|
|
102
|
-
- **Session events** — Track when sessions start, stop, or encounter errors.
|
|
125
|
+
### Harness
|
|
103
126
|
|
|
104
|
-
|
|
127
|
+
- **Headless execution** — `rune run` lets you run agents from the CLI, scripts, or CI/CD. No GUI needed.
|
|
128
|
+
- **Agent chaining** — `rune pipe` connects agents in sequence. Each agent's output feeds into the next.
|
|
129
|
+
- **Automated triggers** — `rune watch` runs agents on file changes, git commits, or a cron schedule.
|
|
130
|
+
- **Node.js API** — `require('openrune')` to use agents programmatically in your own code.
|
|
131
|
+
- **Persistent context** — Role, memory, and chat history are saved in the `.rune` file across sessions.
|
|
105
132
|
|
|
106
|
-
###
|
|
133
|
+
### Desktop UI
|
|
107
134
|
|
|
108
|
-
|
|
135
|
+
- **Chat interface** — Markdown rendering, file attachment, stream cancellation.
|
|
136
|
+
- **Real-time activity** — See every tool call, result, and permission request as it happens via [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks).
|
|
137
|
+
- **Built-in terminal** — Raw Claude Code output and your own commands, side by side.
|
|
109
138
|
|
|
110
139
|
### Agent Roles
|
|
111
140
|
|
|
@@ -144,30 +173,108 @@ Edit the `role` field anytime to change the agent's behavior.
|
|
|
144
173
|
| `rune install` | Build app, register file association, install Quick Action |
|
|
145
174
|
| `rune new <name>` | Create a `.rune` file in the current directory |
|
|
146
175
|
| `rune new <name> --role "..."` | Create with a custom role |
|
|
147
|
-
| `rune open <file.rune>` | Open a `.rune` file |
|
|
176
|
+
| `rune open <file.rune>` | Open a `.rune` file (desktop GUI) |
|
|
177
|
+
| `rune run <file.rune> "prompt"` | Run agent headlessly (no GUI) |
|
|
178
|
+
| `rune pipe <a.rune> <b.rune> "prompt"` | Chain agents in a pipeline |
|
|
179
|
+
| `rune watch <file.rune> --on <event>` | Set up automated triggers |
|
|
148
180
|
| `rune list` | List `.rune` files in the current directory |
|
|
149
181
|
| `rune uninstall` | Remove Rune integration (keeps your `.rune` files) |
|
|
150
182
|
|
|
151
183
|
---
|
|
152
184
|
|
|
153
|
-
##
|
|
185
|
+
## Harness Mode
|
|
186
|
+
|
|
187
|
+
Rune isn't just a desktop app — it's a full agent harness. Use it from scripts, CI/CD, or your own tools.
|
|
188
|
+
|
|
189
|
+
### Headless execution
|
|
190
|
+
|
|
191
|
+
Run any `.rune` agent from the command line without opening the GUI:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
rune run reviewer.rune "Review the latest commit"
|
|
154
195
|
|
|
196
|
+
# Pipe input from other commands
|
|
197
|
+
git diff | rune run reviewer.rune "Review this diff"
|
|
198
|
+
|
|
199
|
+
# JSON output for scripting
|
|
200
|
+
rune run reviewer.rune "Review src/auth.ts" --output json
|
|
155
201
|
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
|
|
203
|
+
### Agent chaining
|
|
204
|
+
|
|
205
|
+
Chain multiple agents into a pipeline. The output of each agent becomes the input for the next:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
rune pipe coder.rune reviewer.rune tester.rune "Implement a login page"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
This runs: coder writes the code → reviewer reviews it → tester writes tests.
|
|
212
|
+
|
|
213
|
+
### Automated triggers
|
|
214
|
+
|
|
215
|
+
Set agents to run automatically on events:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Run on every git commit
|
|
219
|
+
rune watch reviewer.rune --on git-commit --prompt "Review this commit"
|
|
220
|
+
|
|
221
|
+
# Watch for file changes
|
|
222
|
+
rune watch linter.rune --on file-change --glob "src/**/*.ts" --prompt "Check for issues"
|
|
223
|
+
|
|
224
|
+
# Run on a schedule
|
|
225
|
+
rune watch monitor.rune --on cron --interval 5m --prompt "Check server health"
|
|
163
226
|
```
|
|
164
227
|
|
|
165
|
-
|
|
228
|
+
### Node.js API
|
|
229
|
+
|
|
230
|
+
Use Rune agents programmatically in your own code:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const rune = require('openrune')
|
|
234
|
+
|
|
235
|
+
const reviewer = rune.load('reviewer.rune')
|
|
236
|
+
const result = await reviewer.send('Review the latest commit')
|
|
237
|
+
console.log(result)
|
|
238
|
+
|
|
239
|
+
// Agent chaining via API
|
|
240
|
+
const { finalOutput } = await rune.pipe(
|
|
241
|
+
['coder.rune', 'reviewer.rune'],
|
|
242
|
+
'Implement a login page'
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Architecture
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
┌─────────────────────────┐
|
|
252
|
+
│ Desktop UI Mode │
|
|
253
|
+
│ User ↔ Chat UI (React) │
|
|
254
|
+
│ ↕ IPC │
|
|
255
|
+
│ Electron Main Process │
|
|
256
|
+
│ ↕ HTTP + SSE │
|
|
257
|
+
└────────────┬────────────┘
|
|
258
|
+
│
|
|
259
|
+
┌────────────┴────────────┐
|
|
260
|
+
│ MCP Channel │ Claude Code Hooks
|
|
261
|
+
│ (rune-channel) │ ↕ HTTP POST
|
|
262
|
+
│ ↕ MCP │←──── rune-channel /hook
|
|
263
|
+
└────────────┬────────────┘
|
|
264
|
+
│
|
|
265
|
+
┌────────────┴────────────┐
|
|
266
|
+
│ Claude Code CLI │
|
|
267
|
+
└─────────────────────────┘
|
|
268
|
+
|
|
269
|
+
Harness Mode (rune run / pipe / watch):
|
|
270
|
+
CLI → Claude Code CLI (-p) → stdout
|
|
271
|
+
No MCP channel, no Electron — direct execution
|
|
272
|
+
```
|
|
166
273
|
|
|
167
|
-
|
|
168
|
-
2. **Claude Code hooks** → rune-channel → SSE → Chat UI (activity monitoring)
|
|
274
|
+
**Two modes of operation:**
|
|
169
275
|
|
|
170
|
-
|
|
276
|
+
1. **Desktop UI** — Chat input → MCP channel → Claude Code, with hooks for real-time activity monitoring.
|
|
277
|
+
2. **Harness** — Direct CLI execution via `claude -p`. Agents run headlessly with context from the `.rune` file.
|
|
171
278
|
|
|
172
279
|
---
|
|
173
280
|
|
|
@@ -195,7 +302,8 @@ npm run build
|
|
|
195
302
|
|
|
196
303
|
```
|
|
197
304
|
Rune/
|
|
198
|
-
bin/rune.js # CLI
|
|
305
|
+
bin/rune.js # CLI (install, new, open, run, pipe, watch, list)
|
|
306
|
+
lib/index.js # Node.js API (require('openrune'))
|
|
199
307
|
src/
|
|
200
308
|
main.ts # Electron main process
|
|
201
309
|
preload.ts # Preload bridge (IPC security)
|
package/bin/rune.js
CHANGED
|
@@ -29,6 +29,9 @@ switch (command) {
|
|
|
29
29
|
case 'install': return install()
|
|
30
30
|
case 'new': return createRune(args[0], args)
|
|
31
31
|
case 'open': return openRune(args[0])
|
|
32
|
+
case 'run': return runRune(args[0], args.slice(1))
|
|
33
|
+
case 'pipe': return pipeRunes(args)
|
|
34
|
+
case 'watch': return watchRune(args[0], args.slice(1))
|
|
32
35
|
case 'list': return listRunes()
|
|
33
36
|
case 'uninstall': return uninstall()
|
|
34
37
|
case 'help':
|
|
@@ -649,6 +652,421 @@ function openRune(file) {
|
|
|
649
652
|
child.unref()
|
|
650
653
|
}
|
|
651
654
|
|
|
655
|
+
// ── run (headless) ──────────────────────────────
|
|
656
|
+
|
|
657
|
+
function runRune(file, restArgs) {
|
|
658
|
+
if (!file) {
|
|
659
|
+
console.log('Usage: rune run <file.rune> "prompt" [--output json|text]')
|
|
660
|
+
console.log('Example: rune run reviewer.rune "Review the latest commit"')
|
|
661
|
+
process.exit(1)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
665
|
+
if (!fs.existsSync(filePath)) {
|
|
666
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
667
|
+
process.exit(1)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Parse args: prompt and flags
|
|
671
|
+
let prompt = ''
|
|
672
|
+
let outputFormat = 'text'
|
|
673
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
674
|
+
if (restArgs[i] === '--output' && restArgs[i + 1]) {
|
|
675
|
+
outputFormat = restArgs[i + 1]
|
|
676
|
+
i++
|
|
677
|
+
} else if (!prompt) {
|
|
678
|
+
prompt = restArgs[i]
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Read from stdin if no prompt provided
|
|
683
|
+
if (!prompt && process.stdin.isTTY === false) {
|
|
684
|
+
prompt = fs.readFileSync('/dev/stdin', 'utf-8').trim()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!prompt) {
|
|
688
|
+
console.error(' ❌ No prompt provided. Pass a prompt string or pipe via stdin.')
|
|
689
|
+
process.exit(1)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const rune = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
693
|
+
const folderPath = path.dirname(filePath)
|
|
694
|
+
|
|
695
|
+
// Build system prompt from .rune context
|
|
696
|
+
const systemParts = []
|
|
697
|
+
if (rune.role) systemParts.push(`Your role: ${rune.role}`)
|
|
698
|
+
if (rune.memory && rune.memory.length > 0) {
|
|
699
|
+
systemParts.push('Saved memory from previous sessions:')
|
|
700
|
+
rune.memory.forEach((m, i) => systemParts.push(`${i + 1}. ${m}`))
|
|
701
|
+
}
|
|
702
|
+
if (rune.history && rune.history.length > 0) {
|
|
703
|
+
const recent = rune.history.slice(-10)
|
|
704
|
+
systemParts.push(`\nRecent conversation (${rune.history.length} total, showing last ${recent.length}):`)
|
|
705
|
+
for (const msg of recent) {
|
|
706
|
+
const who = msg.role === 'user' ? 'User' : 'Assistant'
|
|
707
|
+
systemParts.push(`${who}: ${msg.text}`)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const systemPrompt = systemParts.join('\n')
|
|
712
|
+
|
|
713
|
+
// Build claude CLI args
|
|
714
|
+
const claudeArgs = ['-p', '--print']
|
|
715
|
+
if (systemPrompt) {
|
|
716
|
+
claudeArgs.push('--system-prompt', systemPrompt)
|
|
717
|
+
}
|
|
718
|
+
if (outputFormat === 'json') {
|
|
719
|
+
claudeArgs.push('--output-format', 'json')
|
|
720
|
+
}
|
|
721
|
+
claudeArgs.push(prompt)
|
|
722
|
+
|
|
723
|
+
const child = spawn('claude', claudeArgs, {
|
|
724
|
+
cwd: folderPath,
|
|
725
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
726
|
+
env: { ...process.env },
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
let stdout = ''
|
|
730
|
+
let stderr = ''
|
|
731
|
+
|
|
732
|
+
child.stdout.on('data', (data) => {
|
|
733
|
+
const text = data.toString()
|
|
734
|
+
stdout += text
|
|
735
|
+
if (outputFormat !== 'json') process.stdout.write(text)
|
|
736
|
+
})
|
|
737
|
+
child.stderr.on('data', (data) => { stderr += data.toString() })
|
|
738
|
+
|
|
739
|
+
child.on('close', (code) => {
|
|
740
|
+
if (outputFormat === 'json') {
|
|
741
|
+
try {
|
|
742
|
+
const parsed = JSON.parse(stdout)
|
|
743
|
+
console.log(JSON.stringify({ agent: rune.name, role: rune.role, response: parsed }, null, 2))
|
|
744
|
+
} catch {
|
|
745
|
+
console.log(JSON.stringify({ agent: rune.name, role: rune.role, response: stdout.trim() }, null, 2))
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Save to history
|
|
750
|
+
rune.history = rune.history || []
|
|
751
|
+
rune.history.push({ role: 'user', text: prompt, ts: Date.now() })
|
|
752
|
+
rune.history.push({ role: 'assistant', text: stdout.trim(), ts: Date.now() })
|
|
753
|
+
fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
|
|
754
|
+
|
|
755
|
+
if (code !== 0 && stderr) {
|
|
756
|
+
console.error(stderr)
|
|
757
|
+
}
|
|
758
|
+
process.exit(code || 0)
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── pipe (agent chaining) ───────────────────────
|
|
763
|
+
|
|
764
|
+
async function pipeRunes(args) {
|
|
765
|
+
// Parse: rune pipe agent1.rune agent2.rune ... "initial prompt" [--output json]
|
|
766
|
+
const runeFiles = []
|
|
767
|
+
let prompt = ''
|
|
768
|
+
let outputFormat = 'text'
|
|
769
|
+
|
|
770
|
+
for (let i = 0; i < args.length; i++) {
|
|
771
|
+
if (args[i] === '--output' && args[i + 1]) {
|
|
772
|
+
outputFormat = args[i + 1]
|
|
773
|
+
i++
|
|
774
|
+
} else if (args[i].endsWith('.rune')) {
|
|
775
|
+
runeFiles.push(args[i])
|
|
776
|
+
} else if (!prompt) {
|
|
777
|
+
prompt = args[i]
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (runeFiles.length < 2 || !prompt) {
|
|
782
|
+
console.log('Usage: rune pipe <agent1.rune> <agent2.rune> [...] "initial prompt"')
|
|
783
|
+
console.log('Example: rune pipe coder.rune reviewer.rune "Implement a login page"')
|
|
784
|
+
console.log('\nThe output of each agent becomes the input for the next.')
|
|
785
|
+
process.exit(1)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Read from stdin if no prompt
|
|
789
|
+
if (!prompt && process.stdin.isTTY === false) {
|
|
790
|
+
prompt = fs.readFileSync('/dev/stdin', 'utf-8').trim()
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
let currentInput = prompt
|
|
794
|
+
const results = []
|
|
795
|
+
|
|
796
|
+
for (let i = 0; i < runeFiles.length; i++) {
|
|
797
|
+
const file = runeFiles[i]
|
|
798
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
799
|
+
|
|
800
|
+
if (!fs.existsSync(filePath)) {
|
|
801
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
802
|
+
process.exit(1)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const rune = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
806
|
+
const folderPath = path.dirname(filePath)
|
|
807
|
+
|
|
808
|
+
const isLast = i === runeFiles.length - 1
|
|
809
|
+
const pipeContext = i > 0
|
|
810
|
+
? `You are step ${i + 1} of ${runeFiles.length} in a pipeline. The previous agent (${results[i-1].agent}) produced the following output:\n\n${currentInput}\n\nNow do your part:`
|
|
811
|
+
: currentInput
|
|
812
|
+
|
|
813
|
+
if (outputFormat !== 'json') {
|
|
814
|
+
console.error(`\n ▶ [${i + 1}/${runeFiles.length}] ${rune.name} (${rune.role || 'assistant'})`)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Build system prompt
|
|
818
|
+
const systemParts = []
|
|
819
|
+
if (rune.role) systemParts.push(`Your role: ${rune.role}`)
|
|
820
|
+
if (rune.memory && rune.memory.length > 0) {
|
|
821
|
+
systemParts.push('Saved memory:')
|
|
822
|
+
rune.memory.forEach((m, j) => systemParts.push(`${j + 1}. ${m}`))
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const claudeArgs = ['-p', '--print']
|
|
826
|
+
if (systemParts.length > 0) {
|
|
827
|
+
claudeArgs.push('--system-prompt', systemParts.join('\n'))
|
|
828
|
+
}
|
|
829
|
+
claudeArgs.push(pipeContext)
|
|
830
|
+
|
|
831
|
+
const output = await new Promise((resolve, reject) => {
|
|
832
|
+
const child = spawn('claude', claudeArgs, {
|
|
833
|
+
cwd: folderPath,
|
|
834
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
835
|
+
env: { ...process.env },
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
let stdout = ''
|
|
839
|
+
let stderr = ''
|
|
840
|
+
child.stdout.on('data', (d) => { stdout += d.toString() })
|
|
841
|
+
child.stderr.on('data', (d) => { stderr += d.toString() })
|
|
842
|
+
child.on('close', (code) => {
|
|
843
|
+
if (code !== 0) reject(new Error(stderr || `Agent ${rune.name} exited with code ${code}`))
|
|
844
|
+
else resolve(stdout.trim())
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
results.push({ agent: rune.name, role: rune.role, output })
|
|
849
|
+
|
|
850
|
+
// Save to .rune history
|
|
851
|
+
rune.history = rune.history || []
|
|
852
|
+
rune.history.push({ role: 'user', text: pipeContext, ts: Date.now() })
|
|
853
|
+
rune.history.push({ role: 'assistant', text: output, ts: Date.now() })
|
|
854
|
+
fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
|
|
855
|
+
|
|
856
|
+
currentInput = output
|
|
857
|
+
|
|
858
|
+
// Print intermediate output
|
|
859
|
+
if (outputFormat !== 'json' && !isLast) {
|
|
860
|
+
console.error(` ✓ Done\n`)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Final output
|
|
865
|
+
if (outputFormat === 'json') {
|
|
866
|
+
console.log(JSON.stringify({ pipeline: results, finalOutput: currentInput }, null, 2))
|
|
867
|
+
} else {
|
|
868
|
+
console.log(currentInput)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ── watch (triggers) ────────────────────────────
|
|
873
|
+
|
|
874
|
+
function watchRune(file, restArgs) {
|
|
875
|
+
if (!file) {
|
|
876
|
+
console.log('Usage: rune watch <file.rune> --on <event> [options]')
|
|
877
|
+
console.log('')
|
|
878
|
+
console.log('Events:')
|
|
879
|
+
console.log(' file-change Watch for file changes in the project folder')
|
|
880
|
+
console.log(' git-push Run after git push (installs a git hook)')
|
|
881
|
+
console.log(' git-commit Run after git commit (installs a git hook)')
|
|
882
|
+
console.log(' cron Run on a schedule (e.g. --interval 5m)')
|
|
883
|
+
console.log('')
|
|
884
|
+
console.log('Options:')
|
|
885
|
+
console.log(' --prompt "..." The prompt to send when triggered')
|
|
886
|
+
console.log(' --glob "*.ts" File pattern to watch (for file-change)')
|
|
887
|
+
console.log(' --interval 5m Interval for cron (e.g. 30s, 5m, 1h)')
|
|
888
|
+
console.log('')
|
|
889
|
+
console.log('Examples:')
|
|
890
|
+
console.log(' rune watch reviewer.rune --on file-change --glob "src/**/*.ts" --prompt "Review changed files"')
|
|
891
|
+
console.log(' rune watch reviewer.rune --on git-commit --prompt "Review this commit"')
|
|
892
|
+
console.log(' rune watch monitor.rune --on cron --interval 5m --prompt "Check server status"')
|
|
893
|
+
process.exit(1)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const filePath = path.resolve(process.cwd(), file)
|
|
897
|
+
if (!fs.existsSync(filePath)) {
|
|
898
|
+
console.error(` ❌ File not found: ${filePath}`)
|
|
899
|
+
process.exit(1)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Parse args
|
|
903
|
+
let event = ''
|
|
904
|
+
let prompt = ''
|
|
905
|
+
let glob = ''
|
|
906
|
+
let interval = '5m'
|
|
907
|
+
|
|
908
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
909
|
+
if (restArgs[i] === '--on' && restArgs[i + 1]) { event = restArgs[++i] }
|
|
910
|
+
else if (restArgs[i] === '--prompt' && restArgs[i + 1]) { prompt = restArgs[++i] }
|
|
911
|
+
else if (restArgs[i] === '--glob' && restArgs[i + 1]) { glob = restArgs[++i] }
|
|
912
|
+
else if (restArgs[i] === '--interval' && restArgs[i + 1]) { interval = restArgs[++i] }
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (!event) {
|
|
916
|
+
console.error(' ❌ --on <event> is required')
|
|
917
|
+
process.exit(1)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!prompt) {
|
|
921
|
+
console.error(' ❌ --prompt is required')
|
|
922
|
+
process.exit(1)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const rune = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
926
|
+
const folderPath = path.dirname(filePath)
|
|
927
|
+
|
|
928
|
+
function triggerRun(triggerInfo) {
|
|
929
|
+
const fullPrompt = triggerInfo
|
|
930
|
+
? `[Triggered by: ${triggerInfo}]\n\n${prompt}`
|
|
931
|
+
: prompt
|
|
932
|
+
console.log(`\n🔮 [${new Date().toLocaleTimeString()}] Triggered: ${rune.name} — ${triggerInfo || event}`)
|
|
933
|
+
|
|
934
|
+
const systemParts = []
|
|
935
|
+
if (rune.role) systemParts.push(`Your role: ${rune.role}`)
|
|
936
|
+
|
|
937
|
+
const claudeArgs = ['-p', '--print']
|
|
938
|
+
if (systemParts.length > 0) {
|
|
939
|
+
claudeArgs.push('--system-prompt', systemParts.join('\n'))
|
|
940
|
+
}
|
|
941
|
+
claudeArgs.push(fullPrompt)
|
|
942
|
+
|
|
943
|
+
const child = spawn('claude', claudeArgs, {
|
|
944
|
+
cwd: folderPath,
|
|
945
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
946
|
+
env: { ...process.env },
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
let stdout = ''
|
|
950
|
+
child.stdout.on('data', (d) => {
|
|
951
|
+
const text = d.toString()
|
|
952
|
+
stdout += text
|
|
953
|
+
process.stdout.write(text)
|
|
954
|
+
})
|
|
955
|
+
child.stderr.on('data', (d) => { process.stderr.write(d) })
|
|
956
|
+
child.on('close', () => {
|
|
957
|
+
// Save to history
|
|
958
|
+
const fresh = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
959
|
+
fresh.history = fresh.history || []
|
|
960
|
+
fresh.history.push({ role: 'user', text: fullPrompt, ts: Date.now() })
|
|
961
|
+
fresh.history.push({ role: 'assistant', text: stdout.trim(), ts: Date.now() })
|
|
962
|
+
fs.writeFileSync(filePath, JSON.stringify(fresh, null, 2))
|
|
963
|
+
console.log(`\n✓ Done`)
|
|
964
|
+
})
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ── Event handlers ──
|
|
968
|
+
|
|
969
|
+
if (event === 'file-change') {
|
|
970
|
+
const watchDir = folderPath
|
|
971
|
+
console.log(`🔮 Watching ${watchDir} for file changes...`)
|
|
972
|
+
if (glob) console.log(` Pattern: ${glob}`)
|
|
973
|
+
console.log(` Agent: ${rune.name} (${rune.role || 'assistant'})`)
|
|
974
|
+
console.log(' Press Ctrl+C to stop\n')
|
|
975
|
+
|
|
976
|
+
let debounce = null
|
|
977
|
+
fs.watch(watchDir, { recursive: true }, (eventType, filename) => {
|
|
978
|
+
if (!filename) return
|
|
979
|
+
if (filename.endsWith('.rune')) return // ignore .rune file changes
|
|
980
|
+
if (filename.startsWith('.git')) return
|
|
981
|
+
if (filename.includes('node_modules')) return
|
|
982
|
+
|
|
983
|
+
// Simple glob matching
|
|
984
|
+
if (glob) {
|
|
985
|
+
const ext = glob.replace('*', '')
|
|
986
|
+
if (!filename.endsWith(ext) && !filename.includes(glob.replace('*', ''))) return
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Debounce: wait 1s after last change
|
|
990
|
+
if (debounce) clearTimeout(debounce)
|
|
991
|
+
debounce = setTimeout(() => {
|
|
992
|
+
triggerRun(`file changed: ${filename}`)
|
|
993
|
+
}, 1000)
|
|
994
|
+
})
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
else if (event === 'git-commit' || event === 'git-push') {
|
|
998
|
+
const hookName = event === 'git-commit' ? 'post-commit' : 'post-push'
|
|
999
|
+
const gitDir = path.join(folderPath, '.git', 'hooks')
|
|
1000
|
+
|
|
1001
|
+
if (!fs.existsSync(path.join(folderPath, '.git'))) {
|
|
1002
|
+
console.error(' ❌ Not a git repository')
|
|
1003
|
+
process.exit(1)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
ensureDir(gitDir)
|
|
1007
|
+
|
|
1008
|
+
// For git-push, use pre-push since post-push doesn't exist natively
|
|
1009
|
+
const actualHook = event === 'git-push' ? 'pre-push' : 'post-commit'
|
|
1010
|
+
const hookPath = path.join(gitDir, actualHook)
|
|
1011
|
+
const runeBin = path.resolve(__dirname, 'rune.js')
|
|
1012
|
+
const nodebin = process.execPath
|
|
1013
|
+
|
|
1014
|
+
const hookScript = `#!/bin/bash
|
|
1015
|
+
# Rune auto-trigger: ${rune.name}
|
|
1016
|
+
"${nodebin}" "${runeBin}" run "${filePath}" "${prompt.replace(/"/g, '\\"')}" &
|
|
1017
|
+
`
|
|
1018
|
+
|
|
1019
|
+
// Append if hook exists, create if not
|
|
1020
|
+
if (fs.existsSync(hookPath)) {
|
|
1021
|
+
const existing = fs.readFileSync(hookPath, 'utf-8')
|
|
1022
|
+
if (!existing.includes('Rune auto-trigger')) {
|
|
1023
|
+
fs.appendFileSync(hookPath, '\n' + hookScript)
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(` ⚠️ Rune hook already installed in ${actualHook}`)
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
fs.writeFileSync(hookPath, hookScript, { mode: 0o755 })
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
console.log(`🔮 Git hook installed: ${actualHook}`)
|
|
1033
|
+
console.log(` Agent: ${rune.name} will run on every ${event.replace('git-', '')}`)
|
|
1034
|
+
console.log(` Prompt: "${prompt}"`)
|
|
1035
|
+
console.log(` Hook: ${hookPath}`)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
else if (event === 'cron') {
|
|
1039
|
+
const ms = parseInterval(interval)
|
|
1040
|
+
console.log(`🔮 Running ${rune.name} every ${interval}`)
|
|
1041
|
+
console.log(` Prompt: "${prompt}"`)
|
|
1042
|
+
console.log(' Press Ctrl+C to stop\n')
|
|
1043
|
+
|
|
1044
|
+
// Run immediately, then on interval
|
|
1045
|
+
triggerRun(`cron (every ${interval})`)
|
|
1046
|
+
setInterval(() => {
|
|
1047
|
+
triggerRun(`cron (every ${interval})`)
|
|
1048
|
+
}, ms)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
else {
|
|
1052
|
+
console.error(` ❌ Unknown event: ${event}. Use: file-change, git-commit, git-push, cron`)
|
|
1053
|
+
process.exit(1)
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function parseInterval(str) {
|
|
1058
|
+
const match = str.match(/^(\d+)(s|m|h)$/)
|
|
1059
|
+
if (!match) {
|
|
1060
|
+
console.error(` ❌ Invalid interval: ${str}. Use format like 30s, 5m, 1h`)
|
|
1061
|
+
process.exit(1)
|
|
1062
|
+
}
|
|
1063
|
+
const num = parseInt(match[1])
|
|
1064
|
+
const unit = match[2]
|
|
1065
|
+
if (unit === 's') return num * 1000
|
|
1066
|
+
if (unit === 'm') return num * 60 * 1000
|
|
1067
|
+
if (unit === 'h') return num * 60 * 60 * 1000
|
|
1068
|
+
}
|
|
1069
|
+
|
|
652
1070
|
// ── list ─────────────────────────────────────────
|
|
653
1071
|
|
|
654
1072
|
function listRunes() {
|
|
@@ -707,21 +1125,32 @@ function uninstall() {
|
|
|
707
1125
|
|
|
708
1126
|
function showHelp() {
|
|
709
1127
|
console.log(`
|
|
710
|
-
🔮 Rune — File-based AI Agent
|
|
1128
|
+
🔮 Rune — File-based AI Agent Harness
|
|
711
1129
|
|
|
712
1130
|
Usage:
|
|
713
1131
|
rune install Install Rune (build app, register file association, add Quick Action)
|
|
714
1132
|
rune new <name> Create a new .rune file in current directory
|
|
715
1133
|
--role "description" Set the agent's role
|
|
716
|
-
rune open <file.rune> Open a .rune file
|
|
1134
|
+
rune open <file.rune> Open a .rune file (desktop GUI)
|
|
1135
|
+
rune run <file.rune> "prompt" Run agent headlessly (no GUI)
|
|
1136
|
+
--output json|text Output format (default: text)
|
|
1137
|
+
rune pipe <a.rune> <b.rune> ... "prompt" Chain agents in a pipeline
|
|
1138
|
+
--output json|text Output format (default: text)
|
|
1139
|
+
rune watch <file.rune> Set up automated triggers
|
|
1140
|
+
--on <event> Event: file-change, git-commit, git-push, cron
|
|
1141
|
+
--prompt "..." Prompt to send when triggered
|
|
1142
|
+
--glob "*.ts" File pattern (for file-change)
|
|
1143
|
+
--interval 5m Schedule interval (for cron: 30s, 5m, 1h)
|
|
717
1144
|
rune list List .rune files in current directory
|
|
718
1145
|
rune uninstall Remove Rune integration (keeps .rune files)
|
|
719
1146
|
rune help Show this help
|
|
720
1147
|
|
|
721
1148
|
Examples:
|
|
722
|
-
rune new
|
|
723
|
-
rune
|
|
724
|
-
rune
|
|
725
|
-
rune
|
|
1149
|
+
rune new reviewer --role "Code reviewer, security focused"
|
|
1150
|
+
rune run reviewer.rune "Review the latest commit"
|
|
1151
|
+
rune pipe coder.rune reviewer.rune "Implement a login page"
|
|
1152
|
+
rune watch reviewer.rune --on git-commit --prompt "Review this commit"
|
|
1153
|
+
rune watch monitor.rune --on cron --interval 5m --prompt "Check server health"
|
|
1154
|
+
echo "Fix the bug in auth.ts" | rune run backend.rune
|
|
726
1155
|
`)
|
|
727
1156
|
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRune — Node.js API
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const rune = require('openrune')
|
|
6
|
+
* const agent = rune.load('reviewer.rune')
|
|
7
|
+
* const result = await agent.send('Review this code')
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
|
|
14
|
+
function load(filePath) {
|
|
15
|
+
const resolved = path.resolve(filePath)
|
|
16
|
+
if (!fs.existsSync(resolved)) {
|
|
17
|
+
throw new Error(`Rune file not found: ${resolved}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rune = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
21
|
+
const folderPath = path.dirname(resolved)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: rune.name,
|
|
25
|
+
role: rune.role,
|
|
26
|
+
filePath: resolved,
|
|
27
|
+
|
|
28
|
+
async send(prompt) {
|
|
29
|
+
const fresh = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
30
|
+
|
|
31
|
+
// Build system prompt
|
|
32
|
+
const systemParts = []
|
|
33
|
+
if (fresh.role) systemParts.push(`Your role: ${fresh.role}`)
|
|
34
|
+
if (fresh.memory && fresh.memory.length > 0) {
|
|
35
|
+
systemParts.push('Saved memory:')
|
|
36
|
+
fresh.memory.forEach((m, i) => systemParts.push(`${i + 1}. ${m}`))
|
|
37
|
+
}
|
|
38
|
+
if (fresh.history && fresh.history.length > 0) {
|
|
39
|
+
const recent = fresh.history.slice(-10)
|
|
40
|
+
systemParts.push(`\nRecent conversation (last ${recent.length}):`)
|
|
41
|
+
for (const msg of recent) {
|
|
42
|
+
const who = msg.role === 'user' ? 'User' : 'Assistant'
|
|
43
|
+
systemParts.push(`${who}: ${msg.text}`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const claudeArgs = ['-p', '--print']
|
|
48
|
+
if (systemParts.length > 0) {
|
|
49
|
+
claudeArgs.push('--system-prompt', systemParts.join('\n'))
|
|
50
|
+
}
|
|
51
|
+
claudeArgs.push(prompt)
|
|
52
|
+
|
|
53
|
+
const result = await new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn('claude', claudeArgs, {
|
|
55
|
+
cwd: folderPath,
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
env: { ...process.env },
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
let stdout = ''
|
|
61
|
+
let stderr = ''
|
|
62
|
+
child.stdout.on('data', (d) => { stdout += d.toString() })
|
|
63
|
+
child.stderr.on('data', (d) => { stderr += d.toString() })
|
|
64
|
+
child.on('close', (code) => {
|
|
65
|
+
if (code !== 0) reject(new Error(stderr || `Exit code ${code}`))
|
|
66
|
+
else resolve(stdout.trim())
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Save to history
|
|
71
|
+
fresh.history = fresh.history || []
|
|
72
|
+
fresh.history.push({ role: 'user', text: prompt, ts: Date.now() })
|
|
73
|
+
fresh.history.push({ role: 'assistant', text: result, ts: Date.now() })
|
|
74
|
+
fs.writeFileSync(resolved, JSON.stringify(fresh, null, 2))
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
getHistory() {
|
|
80
|
+
const fresh = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
81
|
+
return fresh.history || []
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
getMemory() {
|
|
85
|
+
const fresh = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
86
|
+
return fresh.memory || []
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
addMemory(text) {
|
|
90
|
+
const fresh = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
91
|
+
fresh.memory = fresh.memory || []
|
|
92
|
+
fresh.memory.push(text)
|
|
93
|
+
fs.writeFileSync(resolved, JSON.stringify(fresh, null, 2))
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function pipe(runeFiles, prompt) {
|
|
99
|
+
let currentInput = prompt
|
|
100
|
+
const results = []
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < runeFiles.length; i++) {
|
|
103
|
+
const agent = load(runeFiles[i])
|
|
104
|
+
const input = i > 0
|
|
105
|
+
? `Previous agent (${results[i-1].agent}) output:\n\n${currentInput}\n\nNow do your part:`
|
|
106
|
+
: currentInput
|
|
107
|
+
|
|
108
|
+
const output = await agent.send(input)
|
|
109
|
+
results.push({ agent: agent.name, role: agent.role, output })
|
|
110
|
+
currentInput = output
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { pipeline: results, finalOutput: currentInput }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { load, pipe }
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openrune",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Rune — File-based AI Agent
|
|
5
|
-
"keywords": ["ai", "agent", "claude", "desktop", "electron", "mcp", "claude-code"],
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Rune — File-based AI Agent Harness for Claude Code",
|
|
5
|
+
"keywords": ["ai", "agent", "claude", "desktop", "electron", "mcp", "claude-code", "harness", "automation"],
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/gilhyun/Rune.git"
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"author": "gilhyun",
|
|
13
13
|
"main": "bootstrap.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./lib/index.js"
|
|
16
|
+
},
|
|
14
17
|
"bin": {
|
|
15
18
|
"rune": "bin/rune.js"
|
|
16
19
|
},
|