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.
Files changed (4) hide show
  1. package/README.md +151 -43
  2. package/bin/rune.js +435 -6
  3. package/lib/index.js +116 -0
  4. 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 Desktop App for Claude Code</strong><br/>
9
- Drop a <code>.rune</code> file in any folder. Double-click to open. Chat with your AI agent.
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 turns any folder into an AI workspace. Each `.rune` file is an independent AI agent with its own chat history, role, and context all powered by [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
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
- - **Folder-aware** — The agent knows your project. It can read files, run commands, and write code.
26
- - **Real-time activity** — See every tool call, permission request, and agent action as it happens via Claude Code hooks.
27
- - **Desktop-native** — Lightweight Electron app with built-in terminal. No browser needed.
28
- - **Right-click to create** — macOS Quick Action lets you create agents from Finder.
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
- Most AI tools lose context when you close the window. Rune doesn'tbecause your agent lives in your folder.
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
- **Persistent context** — Your agent remembers everything. Close the app, reopen it next week the conversation and context are right where you left off.
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
- **Portable** — The `.rune` file is just a file. Copy it to another machine, share it with a teammate, or check it into git. Your agent goes wherever the file goes.
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
- **Multiple agents per folder** — Need a code reviewer AND a backend developer in the same project? Create two `.rune` files. Each agent has its own role, history, and expertise — working side by side in the same folder.
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
- **No setup per project** — No config files, no extensions, no workspace settings. Drop a `.rune` file and you're ready.
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. Open and chat
79
+ ### 3. Use it
77
80
 
78
- **Double-click** the `.rune` file, or:
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
- ## Features
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
- ### Chat UI
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
- - **Markdown rendering** — Code blocks, tables, lists with syntax highlighting.
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
- ### Real-time Activity Monitoring
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
- Rune uses [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) to capture all agent activity in real-time:
121
+ ---
122
+
123
+ ## Features
98
124
 
99
- - **Tool calls** — See when the agent reads files, edits code, runs commands.
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
- No more guessing what the agent is doing everything is visible in the chat panel.
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
- ### Built-in Terminal
133
+ ### Desktop UI
107
134
 
108
- Toggle the terminal panel to see raw Claude Code output or run your own commands alongside the agent.
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
- ## Architecture
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
- User ↔ Chat UI (React)
157
- IPC
158
- Electron Main Process
159
- HTTP + SSE
160
- MCP Channel (rune-channel) Claude Code Hooks
161
- ↕ MCP ↕ HTTP POST
162
- Claude Code CLI ──────────────→ rune-channel /hook
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
- **Two paths for data:**
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
- 1. **Chat input** MCP channel → Claude Code (user messages)
168
- 2. **Claude Code hooks** → rune-channel → SSE → Chat UI (activity monitoring)
274
+ **Two modes of operation:**
169
275
 
170
- The hooks approach ensures Rune sees everything Claude does, without relying on the agent to self-report.
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 tool (install, new, open, list)
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 designer --role "UI/UX design expert"
723
- rune new backend --role "Backend developer, Node.js specialist"
724
- rune open designer.rune
725
- rune list
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.3",
4
- "description": "Rune — File-based AI Agent Desktop App",
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
  },