openhermes 1.3.0 → 1.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.
package/autorecall.mjs CHANGED
@@ -4,6 +4,8 @@ import fs from "node:fs"
4
4
  import { atomicWriteJson, fingerprintEnvironment, isTruthy, sanitizeRecord, truncateText } from "./lib/hardening.mjs"
5
5
  import { getDataRoot, getCacheRoot, getMemoryRoot, getRecallRoot, getRuntimeRoot } from "./lib/paths.mjs"
6
6
 
7
+ const OLD_BASE = path.join(os.homedir(), ".config", "opencode", "openhermes")
8
+
7
9
  function readJson(fp, fallback) {
8
10
  try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
9
11
  }
@@ -75,24 +77,28 @@ function formatMemoryWriteGap(memory) {
75
77
  }
76
78
 
77
79
  async function loadMemoryAndWriteCache(projectKey, directory) {
78
- const OLD_MEMORY = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory")
79
- const OLD_CACHE = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory", "recall")
80
80
  const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
81
81
  if (!fs.existsSync(SENTINEL)) {
82
- if (fs.existsSync(OLD_MEMORY)) {
83
- fs.cpSync(OLD_MEMORY, getMemoryRoot(), { recursive: true })
84
- fs.rmSync(OLD_MEMORY, { recursive: true, force: true })
82
+ const oldMemory = path.join(OLD_BASE, "memory")
83
+ if (fs.existsSync(oldMemory)) {
84
+ fs.cpSync(oldMemory, getMemoryRoot(), { recursive: true })
85
+ fs.rmSync(oldMemory, { recursive: true, force: true })
85
86
  }
86
- if (fs.existsSync(OLD_CACHE)) {
87
+ const oldCache = path.join(OLD_BASE, "memory", "recall")
88
+ if (fs.existsSync(oldCache)) {
87
89
  fs.mkdirSync(getRecallRoot(), { recursive: true })
88
- const files = fs.readdirSync(OLD_CACHE).filter(f => f.endsWith(".json"))
89
- for (const f of files) fs.cpSync(path.join(OLD_CACHE, f), path.join(getRecallRoot(), f))
90
+ const files = fs.readdirSync(oldCache).filter(f => f.endsWith(".json"))
91
+ for (const f of files) fs.cpSync(path.join(oldCache, f), path.join(getRecallRoot(), f))
90
92
  }
91
- const oldRuntime = path.join(os.homedir(), ".config", "opencode", "openhermes", "runtime")
93
+ const oldRuntime = path.join(OLD_BASE, "runtime")
92
94
  if (fs.existsSync(oldRuntime)) {
93
95
  fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
94
96
  fs.rmSync(oldRuntime, { recursive: true, force: true })
95
97
  }
98
+ const oldArchive = path.join(OLD_BASE, "archive")
99
+ if (fs.existsSync(oldArchive)) {
100
+ fs.rmSync(oldArchive, { recursive: true, force: true })
101
+ }
96
102
  fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
97
103
  }
98
104
 
package/bootstrap.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import path from "node:path"
2
2
  import fs from "node:fs"
3
- import os from "node:os"
4
3
  import { fileURLToPath } from "node:url"
5
4
 
6
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -9,8 +8,7 @@ const RULES_DIR = path.join(HARNESS_DIR, "rules")
9
8
  const SKILLS_DIR = path.join(HARNESS_DIR, "skills")
10
9
  const CONSTITUTION_FILE = path.join(HARNESS_DIR, "constitution", "soul.md")
11
10
  const RUNTIME_FILE = path.join(HARNESS_DIR, "instructions", "RUNTIME.md")
12
- const TOOLS_SOURCE_DIR = path.resolve(__dirname, "lib", "tools")
13
- const USER_TOOLS_DIR = path.join(os.homedir(), ".config", "opencode", "tools")
11
+
14
12
 
15
13
  let _bootstrapCache = undefined
16
14
 
@@ -116,35 +114,7 @@ function getOwnVersion() {
116
114
  } catch { return "1.0.0" }
117
115
  }
118
116
 
119
- function installToolFiles() {
120
- try {
121
- if (!fs.existsSync(TOOLS_SOURCE_DIR)) return
122
- const files = fs.readdirSync(TOOLS_SOURCE_DIR).filter(f => f.endsWith(".mjs") && f !== "_memory.mjs")
123
- if (!files.length) return
124
- fs.mkdirSync(USER_TOOLS_DIR, { recursive: true })
125
- const pkgVersion = getOwnVersion()
126
- const markerPath = path.join(USER_TOOLS_DIR, ".openhermes-version")
127
- let installedVersion = ""
128
- try { installedVersion = fs.readFileSync(markerPath, "utf8").trim() } catch {}
129
- if (installedVersion === pkgVersion) {
130
- const existing = fs.readdirSync(USER_TOOLS_DIR).filter(f => f.endsWith(".mjs"))
131
- const needed = [...files, "_memory.mjs"]
132
- if (needed.every(f => existing.includes(f))) return
133
- }
134
- for (const f of ["_memory.mjs", ...files]) {
135
- const src = path.join(TOOLS_SOURCE_DIR, f)
136
- const dst = path.join(USER_TOOLS_DIR, f)
137
- if (fs.existsSync(src)) fs.copyFileSync(src, dst)
138
- }
139
- fs.writeFileSync(markerPath, pkgVersion, "utf8")
140
- process.stderr.write(`[openhermes-bootstrap] installed ${files.length + 1} tool files (v${pkgVersion})\n`)
141
- } catch (err) {
142
- process.stderr.write(`[openhermes-bootstrap] tool install error: ${err.message}\n`)
143
- }
144
- }
145
-
146
117
  export const BootstrapPlugin = async ({ client, directory }) => {
147
- installToolFiles()
148
118
 
149
119
  const getContent = () => {
150
120
  if (_bootstrapCache !== undefined) return _bootstrapCache
@@ -158,8 +128,6 @@ export const BootstrapPlugin = async ({ client, directory }) => {
158
128
  }
159
129
 
160
130
  return {
161
- name: "openhermes-bootstrap",
162
-
163
131
  config: async (config) => {
164
132
  config.skills = config.skills || {}
165
133
  config.skills.paths = config.skills.paths || []
@@ -167,75 +135,88 @@ export const BootstrapPlugin = async ({ client, directory }) => {
167
135
  config.skills.paths.push(SKILLS_DIR)
168
136
  }
169
137
 
170
- config.agent = config.agent || {}
171
138
  const PROMPTS_DIR = path.join(HARNESS_DIR, "prompts")
172
139
  const p = (name) => `{file:${path.join(PROMPTS_DIR, name)}}`
140
+ const COMMANDS_DIR = path.join(HARNESS_DIR, "commands")
141
+ const ct = (file) => `{file:${path.join(COMMANDS_DIR, file)}}\n\n$ARGUMENTS`
142
+
143
+ const existingCommands = config.command ?? {}
144
+ const existingAgents = { ...(config.agent ?? {}) }
173
145
 
174
- const SUBAGENTS = {
146
+ if (existingAgents.build && typeof existingAgents.build === "object") {
147
+ existingAgents.build = { ...existingAgents.build, mode: "subagent", hidden: true }
148
+ }
149
+
150
+ config.command = {
151
+ ...existingCommands,
152
+ "build-fix": { agent: "build-error-resolver", description: "Fix build and TypeScript errors", subtask: true, template: ct("build-fix.md") },
153
+ "code-review": { agent: "code-reviewer", description: "Review code for quality, security, and maintainability", subtask: true, template: ct("code-review.md") },
154
+ "plan": { agent: "planner", description: "Create a detailed implementation plan", subtask: true, template: ct("plan.md") },
155
+ "security": { agent: "security-reviewer", description: "Run comprehensive security review", subtask: true, template: ct("security.md") },
156
+ "doctor": { agent: "OpenHermes", description: "Run OpenCode OpenHermes health diagnostics", subtask: true, template: ct("doctor.md") },
157
+ "memory-search": { agent: "OpenHermes", description: "Search OpenHermes memory with LLM summarization", subtask: true, template: ct("memory-search.md") },
158
+ "learn": { agent: "OpenHermes", description: "Create a new skill from recent work patterns", subtask: true, template: ct("learn.md") },
159
+ }
160
+
161
+ config.agent = {
162
+ ...existingAgents,
163
+ "OpenHermes": {
164
+ description: "Fully autonomous primary coding agent (all tools allowed)",
165
+ mode: "primary",
166
+ color: "#F59E0B",
167
+ permission: {
168
+ bash: { "*": "allow" },
169
+ edit: "allow",
170
+ read: "allow",
171
+ task: { "*": "allow" },
172
+ },
173
+ },
175
174
  "architect": {
176
175
  description: "Software architecture specialist for system design",
177
176
  mode: "subagent",
178
177
  prompt: p("architect.txt"),
179
- permission: { read: "allow", edit: "deny", bash: "deny" }
178
+ permission: { read: "allow", edit: "deny", bash: "deny" },
180
179
  },
181
180
  "build-error-resolver": {
182
181
  description: "Build and TypeScript error resolution specialist",
183
182
  mode: "subagent",
184
183
  prompt: p("build-error-resolver.md"),
185
- permission: { read: "allow", edit: "allow" }
184
+ permission: { read: "allow", edit: "allow" },
186
185
  },
187
186
  "code-reviewer": {
188
187
  description: "Expert code review specialist",
189
188
  mode: "subagent",
190
189
  prompt: p("code-reviewer.md"),
191
- permission: { read: "allow", edit: "deny", bash: "deny", task: { explore: "allow", "*": "deny" } }
190
+ permission: { read: "allow", edit: "deny", bash: "deny", task: { explore: "allow", "*": "deny" } },
192
191
  },
193
192
  "e2e-runner": {
194
193
  description: "End-to-end testing specialist using Playwright",
195
194
  mode: "subagent",
196
195
  prompt: p("e2e-runner.txt"),
197
- permission: { read: "allow", edit: "allow" }
196
+ permission: { read: "allow", edit: "allow" },
198
197
  },
199
198
  "explore": {
200
199
  description: "Fast read-only codebase exploration agent",
201
200
  mode: "subagent",
202
201
  prompt: p("explore.md"),
203
- permission: { read: "allow", grep: "allow", glob: "allow", list: "allow", edit: "deny", bash: "deny" }
202
+ permission: { read: "allow", grep: "allow", glob: "allow", list: "allow", edit: "deny", bash: "deny" },
204
203
  },
205
204
  "planner": {
206
205
  description: "Expert planning specialist for complex features and refactoring",
207
206
  mode: "subagent",
208
207
  color: "#3B82F6",
209
208
  prompt: p("planner.md"),
210
- permission: { read: "allow", edit: "deny", bash: "deny" }
209
+ permission: { read: "allow", edit: "deny", bash: "deny" },
211
210
  },
212
211
  "security-reviewer": {
213
212
  description: "Security vulnerability detection and remediation specialist",
214
213
  mode: "subagent",
215
214
  prompt: p("security-reviewer.md"),
216
- permission: { read: "allow", edit: "deny", bash: "deny", task: { "*": "allow" } }
217
- }
218
- }
219
- for (const [name, def] of Object.entries(SUBAGENTS)) {
220
- if (!config.agent[name]) config.agent[name] = def
215
+ permission: { read: "allow", edit: "deny", bash: "deny", task: { "*": "allow" } },
216
+ },
221
217
  }
222
218
 
223
- config.command = config.command || {}
224
- const COMMANDS_DIR = path.join(HARNESS_DIR, "commands")
225
- const ct = (file) => `{file:${path.join(COMMANDS_DIR, file)}}\n\n$ARGUMENTS`
226
-
227
- const COMMANDS = {
228
- "build-fix": { agent: "build-error-resolver", description: "Fix build and TypeScript errors", subtask: true, template: ct("build-fix.md") },
229
- "code-review": { agent: "code-reviewer", description: "Review code for quality, security, and maintainability", subtask: true, template: ct("code-review.md") },
230
- "plan": { agent: "planner", description: "Create a detailed implementation plan", subtask: true, template: ct("plan.md") },
231
- "security": { agent: "security-reviewer", description: "Run comprehensive security review", subtask: true, template: ct("security.md") },
232
- "doctor": { agent: "OpenHermes", description: "Run OpenCode OpenHermes health diagnostics", subtask: true, template: ct("doctor.md") },
233
- "memory-search": { agent: "OpenHermes", description: "Search OpenHermes memory with LLM summarization", subtask: true, template: ct("memory-search.md") },
234
- "learn": { agent: "OpenHermes", description: "Create a new skill from recent work patterns", subtask: true, template: ct("learn.md") }
235
- }
236
- for (const [name, def] of Object.entries(COMMANDS)) {
237
- if (!config.command[name]) config.command[name] = def
238
- }
219
+ config.default_agent = "OpenHermes"
239
220
  },
240
221
 
241
222
  "experimental.chat.messages.transform": async (_input, output) => {
package/curator.mjs CHANGED
@@ -5,7 +5,7 @@ import { findUnsupportedSchemaKeywords, validateSchema } from "./lib/schema-vali
5
5
  import { atomicWriteJson, fingerprintEnvironment, fingerprintFile, isTruthy, redactSensitiveText, sanitizeRecord, truncateText } from "./lib/hardening.mjs"
6
6
  import { fileURLToPath } from "node:url"
7
7
  import { dirname } from "node:path"
8
- import { getConfigRoot, getDataRoot, getMemoryRoot, getRuntimeRoot, getArchiveRoot, getSchemaRoot } from "./lib/paths.mjs"
8
+ import { getDataRoot, getMemoryRoot, getRuntimeRoot, getArchiveRoot } from "./lib/paths.mjs"
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
11
11
 
@@ -78,10 +78,8 @@ function updateLoopState(root, patch) {
78
78
  }
79
79
 
80
80
  function loadSchema(classId) {
81
- const fp = path.join(getSchemaRoot(), `${classId}.schema.json`)
82
- try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch {}
83
- const bundled = path.join(__dirname, "schemas", `${classId}.schema.json`)
84
- try { return JSON.parse(fs.readFileSync(bundled, "utf8")) } catch { return null }
81
+ const fp = path.join(__dirname, "schemas", `${classId}.schema.json`)
82
+ try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return null }
85
83
  }
86
84
 
87
85
  function validateRecordAgainstSchema(record) {
@@ -377,7 +375,7 @@ async function handlePermissionReplied(directory, project, event) {
377
375
  environment_fingerprint: environmentFingerprint,
378
376
  }
379
377
  const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
380
- const auditSchema = loadSchema("audit") || readJson(path.join(__dirname, "schemas", "audit.schema.json"), null)
378
+ const auditSchema = loadSchema("audit")
381
379
  if (auditSchema) {
382
380
  const unsupported = findUnsupportedSchemaKeywords(auditSchema)
383
381
  if (!unsupported.length) {
@@ -398,18 +396,23 @@ async function handlePermissionReplied(directory, project, event) {
398
396
  export const CuratorPlugin = async ({ project, directory }) => {
399
397
  return {
400
398
  event: async ({ event }) => {
401
- const OLD_DATA = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory")
399
+ const OLD_BASE = path.join(os.homedir(), ".config", "opencode", "openhermes")
402
400
  const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
403
401
  if (!fs.existsSync(SENTINEL)) {
404
- if (fs.existsSync(OLD_DATA)) {
405
- fs.cpSync(OLD_DATA, getMemoryRoot(), { recursive: true })
406
- fs.rmSync(OLD_DATA, { recursive: true, force: true })
402
+ const oldMemory = path.join(OLD_BASE, "memory")
403
+ if (fs.existsSync(oldMemory)) {
404
+ fs.cpSync(oldMemory, getMemoryRoot(), { recursive: true })
405
+ fs.rmSync(oldMemory, { recursive: true, force: true })
407
406
  }
408
- const oldRuntime = path.join(os.homedir(), ".config", "opencode", "openhermes", "runtime")
407
+ const oldRuntime = path.join(OLD_BASE, "runtime")
409
408
  if (fs.existsSync(oldRuntime)) {
410
409
  fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
411
410
  fs.rmSync(oldRuntime, { recursive: true, force: true })
412
411
  }
412
+ const oldArchive = path.join(OLD_BASE, "archive")
413
+ if (fs.existsSync(oldArchive)) {
414
+ fs.rmSync(oldArchive, { recursive: true, force: true })
415
+ }
413
416
  fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
414
417
  }
415
418
  if (event.type === "session.idle") {
package/index.mjs CHANGED
@@ -1,5 +1,33 @@
1
- export { AutorecallPlugin } from "./autorecall.mjs"
2
- export { CuratorPlugin } from "./curator.mjs"
3
- export { SkillBuilderPlugin } from "./skill-builder.mjs"
4
- export { BootstrapPlugin } from "./bootstrap.mjs"
5
- export { MemoryToolsPlugin } from "./lib/memory-tools-plugin.mjs"
1
+ import { AutorecallPlugin } from "./autorecall.mjs"
2
+ import { CuratorPlugin } from "./curator.mjs"
3
+ import { SkillBuilderPlugin } from "./skill-builder.mjs"
4
+ import { BootstrapPlugin } from "./bootstrap.mjs"
5
+ import { MemoryToolsPlugin } from "./lib/memory-tools-plugin.mjs"
6
+
7
+ export default async (input) => {
8
+ const [bootstrap, autorecall, curator, skillBuilder, memoryTools] = await Promise.all([
9
+ BootstrapPlugin(input),
10
+ AutorecallPlugin(input),
11
+ CuratorPlugin(input),
12
+ SkillBuilderPlugin(input),
13
+ MemoryToolsPlugin(input),
14
+ ])
15
+
16
+ const merged = {}
17
+ if (bootstrap.config) merged.config = bootstrap.config
18
+ if (memoryTools.tool) merged.tool = memoryTools.tool
19
+
20
+ const eventHandlers = [autorecall.event, curator.event, skillBuilder.event].filter(Boolean)
21
+ if (eventHandlers.length) {
22
+ merged.event = async (payload) => {
23
+ await Promise.all(eventHandlers.map(fn => fn(payload)))
24
+ }
25
+ }
26
+
27
+ for (const hook of ["experimental.chat.messages.transform", "experimental.session.compacting", "tool.execute.after"]) {
28
+ const handler = bootstrap[hook] || curator[hook] || skillBuilder[hook]
29
+ if (handler) merged[hook] = handler
30
+ }
31
+
32
+ return merged
33
+ }
@@ -6,10 +6,10 @@ import os from "os"
6
6
 
7
7
  import { atomicWriteJson, fingerprintEnvironment, sanitizeRecord, truncateText } from "./hardening.mjs"
8
8
  import { findUnsupportedSchemaKeywords, validateSchema } from "./schema-validator.mjs"
9
- import { getMemoryRoot, getConfigRoot, getDataRoot, getSchemaRoot, getRuntimeRoot } from "../lib/paths.mjs"
9
+ import { getMemoryRoot, getRuntimeRoot } from "../lib/paths.mjs"
10
10
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
- const PACKAGE_SCHEMAS = path.resolve(__dirname, "..", "schemas")
12
+ const SCHEMAS_DIR = path.resolve(__dirname, "..", "schemas")
13
13
  const MEMORY_DIR = getMemoryRoot()
14
14
 
15
15
  const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
@@ -141,7 +141,7 @@ function handlePut(cls, id, dataStr) {
141
141
  const now = new Date().toISOString()
142
142
  const record = { ...parsed, id, class: cls, source: parsed.source ?? "agent", status: parsed.status ?? "active", created_at: parsed.created_at ?? now, updated_at: now }
143
143
 
144
- const schema = readJSON(path.join(getSchemaRoot(), `${cls}.schema.json`), null) || readJSON(path.join(PACKAGE_SCHEMAS, `${cls}.schema.json`), null)
144
+ const schema = readJSON(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
145
145
  if (schema) {
146
146
  const unsupported = findUnsupportedSchemaKeywords(schema)
147
147
  if (unsupported.length) return `Unsupported schema keywords: ${unsupported.join(", ")}`
package/lib/paths.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import path from "node:path"
2
2
  import os from "node:os"
3
3
  import fs from "node:fs"
4
+ import { fileURLToPath } from "node:url"
4
5
 
5
6
  const HOME = process.env.USERPROFILE || os.homedir()
6
- const CONFIG_ROOT = path.join(HOME, ".config", "opencode", "openhermes")
7
7
  const DATA_ROOT = path.join(HOME, ".local", "share", "opencode", "openhermes")
8
8
  const CACHE_ROOT = path.join(HOME, ".cache", "opencode", "openhermes")
9
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url))
9
10
 
10
11
  function resolveRoot(envVar, fallback) {
11
12
  if (!isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) return fallback
@@ -16,7 +17,7 @@ function resolveRoot(envVar, fallback) {
16
17
  }
17
18
 
18
19
  export function getConfigRoot() {
19
- return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", CONFIG_ROOT)
20
+ return null // legacy — no longer used
20
21
  }
21
22
 
22
23
  export function getDataRoot() {
@@ -40,11 +41,11 @@ export function getRecallRoot() {
40
41
  }
41
42
 
42
43
  export function getArchiveRoot() {
43
- return path.join(getConfigRoot(), "archive")
44
+ return path.join(getDataRoot(), "archive")
44
45
  }
45
46
 
46
47
  export function getSchemaRoot() {
47
- return path.join(getConfigRoot(), "schemas")
48
+ return path.resolve(PKG_DIR, "..", "schemas")
48
49
  }
49
50
 
50
51
  function isTruthy(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhermes",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "OpenHermes plugin suite for OpenCode — autonomous checkpointing, native memory tools, subagent routing, slash commands, and skill-candidate detection.",
5
5
  "type": "module",
6
6
  "license": "MIT",