openrune 0.4.2 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +51 -0
  2. package/bin/rune.js +67 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,11 +62,62 @@ Each `.rune` file is just JSON — portable, shareable, version-controllable:
62
62
  {
63
63
  "name": "reviewer",
64
64
  "role": "Code reviewer, security focused",
65
+ "permissions": {
66
+ "fileWrite": false,
67
+ "bash": false,
68
+ "allowPaths": ["src/**"],
69
+ "denyPaths": [".env", "secrets/**"]
70
+ },
65
71
  "history": [],
66
72
  "memory": []
67
73
  }
68
74
  ```
69
75
 
76
+ ### Permissions
77
+
78
+ Control what each agent can do. No permissions = full access (backward compatible).
79
+
80
+ ```json
81
+ {
82
+ "permissions": {
83
+ "fileWrite": false,
84
+ "bash": false,
85
+ "network": false,
86
+ "allowPaths": ["src/**", "tests/**"],
87
+ "denyPaths": [".env", "secrets/**", "node_modules/**"]
88
+ }
89
+ }
90
+ ```
91
+
92
+ - `fileWrite: false` — agent can read but not write/edit files
93
+ - `bash: false` — agent cannot run shell commands
94
+ - `network: false` — agent cannot make web requests
95
+ - `allowPaths` / `denyPaths` — restrict file access to specific patterns
96
+
97
+ A reviewer that can only read `src/`: safe. A coder that can write anywhere: powerful. You decide per agent.
98
+
99
+ ### Structured logging
100
+
101
+ Track what agents do, how long it takes, and how much it costs:
102
+
103
+ ```bash
104
+ rune run reviewer.rune "Review this project" --auto --log review.json
105
+ ```
106
+
107
+ ```json
108
+ {
109
+ "agent": "reviewer",
110
+ "prompt": "Review this project",
111
+ "duration_ms": 12340,
112
+ "cost_usd": 0.045,
113
+ "tool_calls": [
114
+ { "tool": "Read", "input": { "file_path": "src/index.ts" } },
115
+ { "tool": "Grep", "input": { "pattern": "TODO" } }
116
+ ],
117
+ "result": "Found 3 issues..."
118
+ }
119
+ ```
120
+
70
121
  ### Headless execution
71
122
 
72
123
  Run agents from the terminal. No GUI needed:
package/bin/rune.js CHANGED
@@ -701,12 +701,16 @@ function runRune(file, restArgs) {
701
701
  let prompt = ''
702
702
  let outputFormat = 'text'
703
703
  let autoMode = false
704
+ let logFile = ''
704
705
  for (let i = 0; i < restArgs.length; i++) {
705
706
  if (restArgs[i] === '--output' && restArgs[i + 1]) {
706
707
  outputFormat = restArgs[i + 1]
707
708
  i++
708
709
  } else if (restArgs[i] === '--auto') {
709
710
  autoMode = true
711
+ } else if (restArgs[i] === '--log' && restArgs[i + 1]) {
712
+ logFile = restArgs[i + 1]
713
+ i++
710
714
  } else if (!prompt) {
711
715
  prompt = restArgs[i]
712
716
  }
@@ -743,6 +747,40 @@ function runRune(file, restArgs) {
743
747
 
744
748
  const systemPrompt = systemParts.join('\n')
745
749
 
750
+ // Build permissions args from .rune file
751
+ function buildPermissionArgs(runeData) {
752
+ const args = []
753
+ if (runeData.permissions) {
754
+ const p = runeData.permissions
755
+ const allowed = []
756
+ const disallowed = []
757
+
758
+ if (p.fileWrite === false) disallowed.push('Write', 'Edit')
759
+ if (p.bash === false) disallowed.push('Bash')
760
+ if (p.network === false) disallowed.push('WebFetch', 'WebSearch')
761
+
762
+ if (p.allowPaths && p.allowPaths.length > 0) {
763
+ for (const ap of p.allowPaths) {
764
+ allowed.push(`Read(${ap})`, `Glob(${ap})`, `Grep(${ap})`)
765
+ if (p.fileWrite !== false) allowed.push(`Write(${ap})`, `Edit(${ap})`)
766
+ }
767
+ }
768
+ if (p.denyPaths && p.denyPaths.length > 0) {
769
+ for (const dp of p.denyPaths) {
770
+ disallowed.push(`Read(${dp})`, `Write(${dp})`, `Edit(${dp})`, `Glob(${dp})`, `Grep(${dp})`)
771
+ }
772
+ }
773
+
774
+ if (allowed.length > 0) args.push('--allowedTools', allowed.join(' '))
775
+ if (disallowed.length > 0) args.push('--disallowedTools', disallowed.join(' '))
776
+ }
777
+ return args
778
+ }
779
+
780
+ // Logging
781
+ const logEntries = []
782
+ const logStart = Date.now()
783
+
746
784
  // Auto mode: agent can read/write files, run commands, fix errors autonomously
747
785
  if (autoMode) {
748
786
  console.log(`🔮 [auto] ${rune.name} is working on: ${prompt}\n`)
@@ -761,6 +799,10 @@ function runRune(file, restArgs) {
761
799
  '--verbose',
762
800
  '--output-format', 'stream-json',
763
801
  ]
802
+ // Apply permissions (override dangerously-skip if permissions are set)
803
+ const permArgs = buildPermissionArgs(rune)
804
+ claudeArgs.push(...permArgs)
805
+
764
806
  if (systemPrompt) {
765
807
  claudeArgs.push('--system-prompt', systemPrompt)
766
808
  }
@@ -796,6 +838,7 @@ function runRune(file, restArgs) {
796
838
  if (block.type === 'tool_use') {
797
839
  const tool = block.name || 'unknown'
798
840
  const input = block.input || {}
841
+ logEntries.push({ type: 'tool_use', tool, input, ts: Date.now() })
799
842
  if (tool === 'Bash') {
800
843
  console.log(` ▶ Bash: ${(input.command || '').slice(0, 120)}`)
801
844
  } else if (tool === 'Write') {
@@ -819,12 +862,13 @@ function runRune(file, restArgs) {
819
862
 
820
863
  // Tool results
821
864
  if (event.type === 'user' && event.tool_use_result) {
822
- // silently track tool results
865
+ logEntries.push({ type: 'tool_result', result: event.tool_use_result, ts: Date.now() })
823
866
  }
824
867
 
825
868
  // Final result
826
869
  if (event.type === 'result') {
827
870
  fullOutput = event.result || ''
871
+ logEntries.push({ type: 'result', cost_usd: event.total_cost_usd, usage: event.usage, duration_ms: event.duration_ms, ts: Date.now() })
828
872
  if (fullOutput) console.log(`\n${fullOutput}`)
829
873
  }
830
874
  } catch {}
@@ -841,6 +885,27 @@ function runRune(file, restArgs) {
841
885
  rune.history.push({ role: 'assistant', text: fullOutput.trim(), ts: Date.now() })
842
886
  fs.writeFileSync(filePath, JSON.stringify(rune, null, 2))
843
887
 
888
+ // Write structured log
889
+ if (logFile) {
890
+ const costEntry = logEntries.find(e => e.type === 'result')
891
+ const log = {
892
+ agent: rune.name,
893
+ role: rune.role,
894
+ prompt,
895
+ mode: 'auto',
896
+ permissions: rune.permissions || null,
897
+ duration_ms: Date.now() - logStart,
898
+ cost_usd: costEntry?.cost_usd || null,
899
+ usage: costEntry?.usage || null,
900
+ tool_calls: logEntries.filter(e => e.type === 'tool_use').map(e => ({ tool: e.tool, input: e.input, ts: e.ts })),
901
+ result: fullOutput.trim(),
902
+ exit_code: code,
903
+ ts: new Date().toISOString(),
904
+ }
905
+ fs.writeFileSync(path.resolve(logFile), JSON.stringify(log, null, 2))
906
+ console.log(` 📋 Log saved: ${logFile}`)
907
+ }
908
+
844
909
  if (code !== 0) console.error(`\n ⚠️ Agent exited with code ${code}`)
845
910
  else console.log(`\n✓ ${rune.name} finished`)
846
911
  process.exit(code || 0)
@@ -1361,6 +1426,7 @@ Usage:
1361
1426
  rune run <file.rune> "prompt" Run agent headlessly (no GUI)
1362
1427
  --auto Auto mode: agent writes files, runs commands, fixes errors
1363
1428
  --output json|text Output format (default: text)
1429
+ --log <file.json> Save structured log (tool calls, cost, duration)
1364
1430
  rune pipe <a.rune> <b.rune> ... "prompt" Chain agents in a pipeline
1365
1431
  --output json|text Output format (default: text)
1366
1432
  rune watch <file.rune> Set up automated triggers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrune",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Rune — File-based AI Agent Harness for Claude Code",
5
5
  "keywords": ["ai", "agent", "claude", "desktop", "electron", "mcp", "claude-code", "harness", "automation"],
6
6
  "repository": {