openhermes 1.5.6 → 1.12.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -111
  3. package/autorecall.mjs +2 -12
  4. package/bootstrap.mjs +158 -8
  5. package/curator.mjs +1 -5
  6. package/harness/commands/checkpoint.md +68 -0
  7. package/harness/commands/eval.md +89 -0
  8. package/harness/commands/go-build.md +87 -0
  9. package/harness/commands/go-review.md +71 -0
  10. package/harness/commands/harness-audit.md +90 -0
  11. package/harness/commands/learn.md +2 -2
  12. package/harness/commands/loop-start.md +38 -0
  13. package/harness/commands/loop-status.md +30 -0
  14. package/harness/commands/memory-search.md +2 -2
  15. package/harness/commands/model-route.md +32 -0
  16. package/harness/commands/orchestrate.md +88 -0
  17. package/harness/commands/quality-gate.md +35 -0
  18. package/harness/commands/refactor-clean.md +102 -0
  19. package/harness/commands/rust-build.md +78 -0
  20. package/harness/commands/rust-review.md +65 -0
  21. package/harness/commands/setup-pm.md +65 -0
  22. package/harness/commands/skill-create.md +99 -0
  23. package/harness/commands/test-coverage.md +80 -0
  24. package/harness/commands/update-codemaps.md +81 -0
  25. package/harness/commands/update-docs.md +67 -0
  26. package/harness/commands/verify.md +68 -0
  27. package/harness/instructions/CONVENTIONS.md +206 -0
  28. package/harness/instructions/RUNTIME.md +8 -1
  29. package/harness/prompts/build-cpp.md +84 -0
  30. package/harness/prompts/build-error-resolver.md +2 -1
  31. package/harness/prompts/build-go.md +326 -0
  32. package/harness/prompts/build-java.md +126 -0
  33. package/harness/prompts/build-kotlin.md +123 -0
  34. package/harness/prompts/build-rust.md +94 -0
  35. package/harness/prompts/code-reviewer.md +2 -1
  36. package/harness/prompts/doc-updater.md +193 -0
  37. package/harness/prompts/docs-lookup.md +60 -0
  38. package/harness/prompts/explore.md +1 -0
  39. package/harness/prompts/harness-optimizer.md +30 -0
  40. package/harness/prompts/loop-operator.md +42 -0
  41. package/harness/prompts/planner.md +3 -2
  42. package/harness/prompts/refactor-cleaner.md +242 -0
  43. package/harness/prompts/review-cpp.md +68 -0
  44. package/harness/prompts/review-database.md +248 -0
  45. package/harness/prompts/review-go.md +244 -0
  46. package/harness/prompts/review-java.md +100 -0
  47. package/harness/prompts/review-kotlin.md +130 -0
  48. package/harness/prompts/review-python.md +88 -0
  49. package/harness/prompts/review-rust.md +64 -0
  50. package/harness/prompts/security-reviewer.md +3 -2
  51. package/harness/prompts/tdd-guide.md +214 -0
  52. package/harness/rules/delegation.md +28 -22
  53. package/harness/rules/memory-management.md +4 -4
  54. package/harness/rules/retrieval.md +5 -5
  55. package/harness/rules/runtime-guards.md +1 -1
  56. package/harness/rules/session-start.md +4 -4
  57. package/harness/rules/skills-management.md +2 -2
  58. package/harness/rules/state-drift.md +1 -1
  59. package/harness/rules/verification.md +4 -4
  60. package/harness/skills/coding-standards/SKILL.md +1 -1
  61. package/index.mjs +25 -4
  62. package/lib/hardening.mjs +11 -1
  63. package/lib/memory-tools-plugin.mjs +84 -71
  64. package/lib/ohc/config.mjs +30 -0
  65. package/lib/ohc/pruner.mjs +239 -0
  66. package/lib/ohc/reaper.mjs +61 -0
  67. package/lib/ohc/state.mjs +32 -0
  68. package/lib/ohc/updater.mjs +110 -0
  69. package/package.json +1 -1
  70. package/skill-builder.mjs +2 -6
@@ -130,7 +130,7 @@ The agent can create, update, and delete skills during sessions. This is the ski
130
130
 
131
131
  - Never create a skill from a single data point.
132
132
  - Minimum: 3 verified successes or 3 same-type mistakes in 7 days.
133
- - Check existing skills via `hm_search` before creating to avoid duplicates.
133
+ - Check existing skills via `search_memory` before creating to avoid duplicates.
134
134
 
135
135
  ### Skill Quality Gates
136
136
 
@@ -162,4 +162,4 @@ Skills live in three locations (discovered by OpenCode):
162
162
  After creating or updating a skill:
163
163
  1. Run the workflow defined in the SKILL.md.
164
164
  2. Verify it produces the expected outcome.
165
- 3. Write a verification receipt via `hm_put` with class `verification_receipt`.
165
+ 3. Write a verification receipt via `add_memory` with class `verification_receipt`.
@@ -15,7 +15,7 @@ This creates "phantom" compressed data that references stale environments.
15
15
  {
16
16
  "fingerprint": {
17
17
  "cwd": "C:/path/to/project",
18
- "harness_root": "C:/Users/nathan/.config/opencode/openhermes",
18
+ "harness_root": "C:/Users/nathan/.config/opencode",
19
19
  "project_root": "C:/path/to/project",
20
20
  "project": "my-project",
21
21
  "session_id": "session-123",
@@ -14,7 +14,7 @@ Verification receipts prove that an artifact was observed in a particular state.
14
14
 
15
15
  ## Verification Cache (Memory-Backed)
16
16
 
17
- Successful verifications are stored via `hm_put` so repeated checks of unchanged artifacts are skipped.
17
+ Successful verifications are stored via `add_memory` so repeated checks of unchanged artifacts are skipped.
18
18
 
19
19
  ### Cache Key
20
20
 
@@ -24,14 +24,14 @@ Each verification receipt is keyed by:
24
24
 
25
25
  ### Cache Lifecycle
26
26
 
27
- 1. **Before trusting a claim**: search memory (`hm_get` or `hm_list`) for matching receipt.
27
+ 1. **Before trusting a claim**: search memory (`fetch_memory` or `list_memory`) for matching receipt.
28
28
  2. **Receipt found + fingerprint matches**: artifact unchanged. Trust cached result. Skip re-verify.
29
29
  3. **Receipt found + fingerprint differs**: artifact changed. Re-verify. Stale receipt is invalid.
30
30
  4. **No receipt found**: verify fresh. Store receipt on success.
31
31
 
32
32
  ### Receipt Storage
33
33
 
34
- Use `hm_put` with class `verification_receipt` — a dedicated memory class (schema: `schemas\verification_receipt.schema.json`). Receipts are stored as file-per-object in `memory\verification_receipts\<id>.json`.
34
+ Use `add_memory` with class `verification_receipt` — a dedicated memory class (schema: `schemas\verification_receipt.schema.json`). Receipts are stored as file-per-object in `memory\verification_receipts\<id>.json`.
35
35
 
36
36
  Required fields:
37
37
  - **artifact**: path or logical identity of the verified artifact
@@ -69,7 +69,7 @@ Receipts stored under `decision` with `v:` prefix are deprecated. Migrate to `ve
69
69
  When verification reveals evidence contradicts a document or user claim:
70
70
 
71
71
  1. **Pause** — do not proceed on either source.
72
- 2. **Log** — `hm_put` as `constraint` or `backlog` with both claim and contradictory evidence.
72
+ 2. **Log** — `add_memory` as `constraint` or `backlog` with both claim and contradictory evidence.
73
73
  3. **Flag** — ask user about the discrepancy. Present both sides.
74
74
  4. **Resolve** — let user decide which source is authoritative. Update document if needed.
75
75
 
@@ -34,7 +34,7 @@ Activate this skill for:
34
34
  Do not use this skill as the primary source for:
35
35
  - React composition, hooks, or rendering patterns
36
36
  - backend architecture, API design, or database layering
37
- - domain-specific framework guidance when a narrower ECC skill already exists
37
+ - domain-specific framework guidance when a narrower OpenHermes skill already exists
38
38
 
39
39
  ## Code Quality Principles
40
40
 
package/index.mjs CHANGED
@@ -3,19 +3,40 @@ import { CuratorPlugin } from "./curator.mjs"
3
3
  import { SkillBuilderPlugin } from "./skill-builder.mjs"
4
4
  import { BootstrapPlugin } from "./bootstrap.mjs"
5
5
  import { MemoryToolsPlugin } from "./lib/memory-tools-plugin.mjs"
6
+ import { OhcPlugin } from "./lib/ohc/pruner.mjs"
7
+ import { UpdaterPlugin } from "./lib/ohc/updater.mjs"
8
+
9
+ function chain(...fns) {
10
+ const h = fns.filter(Boolean)
11
+ if (!h.length) return undefined
12
+ if (h.length === 1) return h[0]
13
+ return async (i, o) => { for (const fn of h) await fn(i, o) }
14
+ }
6
15
 
7
16
  export default async (input) => {
8
- const [bootstrap, autorecall, curator, skillBuilder, memoryTools] = await Promise.all([
17
+ const [bootstrap, autorecall, curator, skillBuilder, memoryTools, ohc, updater] = await Promise.all([
9
18
  BootstrapPlugin(input),
10
19
  AutorecallPlugin(input),
11
20
  CuratorPlugin(input),
12
21
  SkillBuilderPlugin(input),
13
22
  MemoryToolsPlugin(input),
23
+ OhcPlugin(input),
24
+ UpdaterPlugin(input),
14
25
  ])
15
26
 
16
27
  const merged = {}
28
+
17
29
  if (bootstrap.config) merged.config = bootstrap.config
18
- if (memoryTools.tool) merged.tool = memoryTools.tool
30
+
31
+ const toolHandlers = { ...memoryTools.tool, ...ohc.tool }
32
+ if (Object.keys(toolHandlers).length) merged.tool = toolHandlers
33
+
34
+ merged["experimental.chat.system.transform"] = chain(ohc["experimental.chat.system.transform"])
35
+ merged["experimental.chat.messages.transform"] = chain(
36
+ bootstrap["experimental.chat.messages.transform"],
37
+ ohc["experimental.chat.messages.transform"],
38
+ )
39
+ merged["command.execute.before"] = chain(updater["command.execute.before"], ohc["command.execute.before"])
19
40
 
20
41
  const eventHandlers = [autorecall.event, curator.event, skillBuilder.event].filter(Boolean)
21
42
  if (eventHandlers.length) {
@@ -24,8 +45,8 @@ export default async (input) => {
24
45
  }
25
46
  }
26
47
 
27
- for (const hook of ["experimental.chat.messages.transform", "experimental.session.compacting", "tool.execute.after"]) {
28
- const handler = bootstrap[hook] || curator[hook] || skillBuilder[hook]
48
+ for (const hook of ["experimental.session.compacting", "tool.execute.after"]) {
49
+ const handler = chain(curator[hook], skillBuilder[hook])
29
50
  if (handler) merged[hook] = handler
30
51
  }
31
52
 
package/lib/hardening.mjs CHANGED
@@ -110,4 +110,14 @@ function isTruthy(value) {
110
110
  return /^(1|true|yes|on)$/i.test(String(value || ""))
111
111
  }
112
112
 
113
- export { atomicWriteJson, fingerprintEnvironment, fingerprintFile, isTruthy, redactSensitiveText, sanitizeRecord, truncateText }
113
+ function readJson(fp, fallback) {
114
+ try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
115
+ }
116
+
117
+ function readJsonl(fp) {
118
+ try {
119
+ return fs.readFileSync(fp, "utf8").trim().split("\n").filter(Boolean).map(l => JSON.parse(l))
120
+ } catch { return [] }
121
+ }
122
+
123
+ export { atomicWriteJson, fingerprintEnvironment, fingerprintFile, isTruthy, readJson, readJsonl, redactSensitiveText, sanitizeRecord, truncateText }
@@ -1,12 +1,12 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
- import { fileURLToPath } from "url"
3
- import path from "path"
4
- import fs from "fs"
5
- import os from "os"
2
+ import { fileURLToPath } from "node:url"
3
+ import path from "node:path"
4
+ import fs from "node:fs"
5
+ import os from "node:os"
6
6
 
7
- import { atomicWriteJson, fingerprintEnvironment, sanitizeRecord, truncateText } from "./hardening.mjs"
7
+ import { atomicWriteJson, fingerprintEnvironment, readJson, readJsonl, sanitizeRecord, truncateText } from "./hardening.mjs"
8
8
  import { findUnsupportedSchemaKeywords, validateSchema } from "./schema-validator.mjs"
9
- import { getMemoryRoot, getRuntimeRoot } from "../lib/paths.mjs"
9
+ import { getMemoryRoot, getRuntimeRoot } from "./paths.mjs"
10
10
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
12
  const SCHEMAS_DIR = path.resolve(__dirname, "..", "schemas")
@@ -44,16 +44,6 @@ function hasExpired(r) {
44
44
  return false
45
45
  }
46
46
 
47
- function readJSON(fp, fallback) {
48
- try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
49
- }
50
-
51
- function readJSONL(fp) {
52
- try {
53
- return fs.readFileSync(fp, "utf8").split(/\r?\n/).map(l => l.trim()).filter(Boolean).map(l => JSON.parse(l))
54
- } catch { return [] }
55
- }
56
-
57
47
  function sortRecent(entries) {
58
48
  function ts(e) { return e?.updated_at ?? e?.created_at ?? "" }
59
49
  return [...entries].sort((a, b) => {
@@ -71,7 +61,7 @@ function writeObject(cls, record) {
71
61
  const fp = path.join(dir, `${record.id}.json`)
72
62
  atomicWriteJson(fp, record)
73
63
  const indexPath = path.join(dir, "index.json")
74
- let index = readJSON(indexPath, [])
64
+ let index = readJson(indexPath, [])
75
65
  if (!Array.isArray(index)) index = []
76
66
  const idx = index.findIndex(e => e?.id === record.id)
77
67
  const entry = buildEntry(cls, record)
@@ -83,7 +73,7 @@ function upsertMistake(record) {
83
73
  const dir = classDir("mistake")
84
74
  fs.mkdirSync(dir, { recursive: true })
85
75
  const fp = path.join(dir, "mistakes.jsonl")
86
- let entries = readJSONL(fp)
76
+ let entries = readJsonl(fp)
87
77
  const idx = entries.findIndex(e => e?.id === record.id)
88
78
  if (idx >= 0) entries[idx] = record; else entries.push(record)
89
79
  const text = entries.map(e => stableStringify(e)).join("\n")
@@ -92,18 +82,18 @@ function upsertMistake(record) {
92
82
 
93
83
  function queryList(cls, limit = 10) {
94
84
  if (cls === "mistake") {
95
- return sortRecent(filterActive(readJSONL(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
85
+ return sortRecent(filterActive(readJsonl(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
96
86
  }
97
87
  const dir = classDir(cls)
98
88
  let files = []
99
89
  try { files = fs.readdirSync(dir).filter(f => f.endsWith(".json") && f !== "index.json").map(f => path.join(dir, f)) } catch { return [] }
100
- const entries = files.map(f => readJSON(f, null)).filter(Boolean).map(r => buildEntry(cls, r))
90
+ const entries = files.map(f => readJson(f, null)).filter(Boolean).map(r => buildEntry(cls, r))
101
91
  return sortRecent(filterActive(entries)).slice(0, limit)
102
92
  }
103
93
 
104
94
  function queryGet(cls, id) {
105
- if (cls === "mistake") return readJSONL(path.join(classDir(cls), "mistakes.jsonl")).find(e => e?.id === id) ?? null
106
- return readJSON(path.join(classDir(cls), `${id}.json`), null)
95
+ if (cls === "mistake") return readJsonl(path.join(classDir(cls), "mistakes.jsonl")).find(e => e?.id === id) ?? null
96
+ return readJson(path.join(classDir(cls), `${id}.json`), null)
107
97
  }
108
98
 
109
99
  function scoreRelevance(r, query, project) {
@@ -132,61 +122,67 @@ function enforceAuditEvidence(record) {
132
122
  return ["db_refs", "file_refs", "log_refs"].some(k => Array.isArray(prov[k]) && prov[k].some(i => typeof i === "string" && i.trim()))
133
123
  }
134
124
 
135
- function setToolTitle(context, title, metadata = {}) {
125
+ function setToolTitle(context, title) {
136
126
  if (!context || typeof context.metadata !== "function") return
137
- context.metadata({ title, metadata })
138
- }
139
-
140
- function classifyPutTitle(cls, id) {
141
- const label = String(id || "").trim() || "unnamed"
142
- if (cls === "checkpoint") return `Save checkpoint: ${label}`
143
- if (cls === "verification_receipt") return `Save verification receipt: ${label}`
144
- return `Save ${cls}: ${label}`
127
+ context.metadata({ title })
145
128
  }
146
129
 
147
- function handlePut(cls, id, dataStr) {
130
+ function handleAdd(cls, id, dataStr) {
148
131
  let parsed
149
- try { parsed = JSON.parse(dataStr) } catch (e) { return `data must be valid JSON: ${e.message}` }
132
+ try { parsed = JSON.parse(dataStr) } catch (e) { return `invalid JSON: ${e.message}` }
150
133
  if (!isPlainObject(parsed)) return "data must be a JSON object"
151
134
  if (!id?.trim()) return "non-blank id is required"
152
135
 
153
136
  const now = new Date().toISOString()
154
- const record = { ...parsed, id, class: cls, source: parsed.source ?? "agent", status: parsed.status ?? "active", created_at: parsed.created_at ?? now, updated_at: now }
137
+ const existing = queryGet(cls, id.trim())
138
+ const record = { ...parsed, id, class: cls, source: parsed.source ?? "agent", status: parsed.status ?? "active", created_at: parsed.created_at ?? (existing?.created_at ?? now), updated_at: now }
155
139
 
156
- const schema = readJSON(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
140
+ const schema = readJson(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
157
141
  if (schema) {
142
+ const required = Array.isArray(schema.required) ? schema.required : []
143
+ if (required.includes("provenance")) {
144
+ if (!record.provenance) record.provenance = { session_id: `auto-${now}` }
145
+ else if (!record.provenance.session_id) record.provenance.session_id = `auto-${now}`
146
+ }
147
+ if (required.includes("trigger") && !record.trigger) record.trigger = "manual"
148
+ if (required.includes("success_count") && record.success_count == null) record.success_count = 0
149
+ if (required.includes("failure_count") && record.failure_count == null) record.failure_count = 0
150
+ if (required.includes("promotion_state") && !record.promotion_state) record.promotion_state = "project"
151
+
158
152
  const unsupported = findUnsupportedSchemaKeywords(schema)
159
- if (unsupported.length) return `Unsupported schema keywords: ${unsupported.join(", ")}`
153
+ if (unsupported.length) return `unsupported schema: ${unsupported.join(", ")}`
160
154
  const errs = validateSchema(schema, record, "$")
161
155
  if (cls === "audit" && !enforceAuditEvidence(record)) errs.push("$.provenance must include at least one non-empty evidence ref")
162
- if (errs.length) return `Validation errors: ${errs.join("; ")}`
156
+ if (errs.length) return `validation: ${errs.join("; ")}`
163
157
  }
164
158
 
165
159
  if (cls === "mistake") upsertMistake(record)
166
160
  else writeObject(cls, record)
167
161
 
168
- return stableStringify({ ok: true, id })
162
+ const action = existing ? "updated" : "saved"
163
+ return `${action}: ${id.trim()}`
169
164
  }
170
165
 
171
- function handleGet(cls, id) {
166
+ function handleFetch(cls, id) {
172
167
  if (!id?.trim()) return "non-blank id is required"
173
168
  const record = queryGet(cls, id.trim())
174
- if (!record) return stableStringify({ ok: false, found: false })
175
- return stableStringify({ ok: true, record })
169
+ if (!record) return `not found: ${id}`
170
+ return stableStringify(record)
176
171
  }
177
172
 
178
173
  function handleList(cls, limit = 10) {
179
174
  const entries = queryList(cls, Math.min(limit, 100))
180
- return stableStringify({ ok: true, count: entries.length, entries })
175
+ const lines = entries.map(e => ` ${e.id}: ${truncateText(e.summary || "", 80)}`)
176
+ return `${entries.length} ${cls} record${entries.length === 1 ? "" : "s"}\n` + lines.join("\n")
181
177
  }
182
178
 
183
179
  function handleLatest(cls) {
184
180
  const list = queryList(cls, 100)
185
181
  const active = filterActive(list)
186
- if (!active[0]?.id) return stableStringify({ ok: false, found: false })
182
+ if (!active[0]?.id) return "no active records"
187
183
  const record = queryGet(cls, active[0].id)
188
- if (!record) return stableStringify({ ok: false, found: false })
189
- return stableStringify({ ok: true, record })
184
+ if (!record) return "no active records"
185
+ return stableStringify(record)
190
186
  }
191
187
 
192
188
  function handleSearch(query, scope, classes, project, limit) {
@@ -197,7 +193,7 @@ function handleSearch(query, scope, classes, project, limit) {
197
193
  let records = []
198
194
  for (const cls of clsList) {
199
195
  if (cls === "mistake") {
200
- for (const m of readJSONL(path.join(classDir(cls), "mistakes.jsonl"))) {
196
+ for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
201
197
  if (!hasExpired(m)) records.push(m)
202
198
  }
203
199
  } else {
@@ -205,7 +201,7 @@ function handleSearch(query, scope, classes, project, limit) {
205
201
  let files = []
206
202
  try { files = fs.readdirSync(dir).filter(f => f.endsWith(".json") && f !== "index.json") } catch { continue }
207
203
  for (const f of files) {
208
- const r = readJSON(path.join(dir, f), null)
204
+ const r = readJson(path.join(dir, f), null)
209
205
  if (r && !hasExpired(r)) records.push(r)
210
206
  }
211
207
  }
@@ -216,7 +212,19 @@ function handleSearch(query, scope, classes, project, limit) {
216
212
  .filter(e => e.score > 0)
217
213
  .sort((a, b) => b.score - a.score)
218
214
  .slice(0, lim)
219
- return stableStringify({ ok: true, count: scored.length, query: q, results: scored })
215
+ const lines = scored.map(e => ` ${e.id} (${e.score}pt): ${truncateText(e.summary || "", 80)}`)
216
+ return `${scored.length} result${scored.length === 1 ? "" : "s"} for '${q}'\n` + lines.join("\n")
217
+ }
218
+
219
+ function handleArchive(cls, id) {
220
+ if (!id?.trim()) return "non-blank id is required"
221
+ const record = queryGet(cls, id.trim())
222
+ if (!record) return `not found: ${id}`
223
+ record.status = "expired"
224
+ record.updated_at = new Date().toISOString()
225
+ if (cls === "mistake") upsertMistake(record)
226
+ else writeObject(cls, record)
227
+ return `archived: ${id.trim()}`
220
228
  }
221
229
 
222
230
  export const MemoryToolsPlugin = async () => {
@@ -225,56 +233,56 @@ export const MemoryToolsPlugin = async () => {
225
233
 
226
234
  return {
227
235
  tool: {
228
- hm_put: tool({
229
- description: "Create or update an OpenHermes memory record",
236
+ add_memory: tool({
237
+ description: "Save a new memory record or update an existing one by class and id",
230
238
  args: {
231
239
  class: tool.schema.enum(CLASSES),
232
240
  id: tool.schema.string(),
233
241
  data: tool.schema.string(),
234
242
  },
235
243
  async execute(args, context) {
236
- setToolTitle(context, classifyPutTitle(args.class, args.id), { action: "put", class: args.class, id: args.id })
237
- return handlePut(args.class, args.id, args.data)
244
+ setToolTitle(context, `save ${args.class}: ${args.id}`)
245
+ return handleAdd(args.class, args.id, args.data)
238
246
  },
239
247
  }),
240
248
 
241
- hm_get: tool({
242
- description: "Get a specific OpenHermes memory record by ID",
249
+ fetch_memory: tool({
250
+ description: "Get a specific memory record by class and id",
243
251
  args: {
244
252
  class: tool.schema.enum(CLASSES).describe("Memory class"),
245
253
  id: tool.schema.string().describe("Record ID"),
246
254
  },
247
255
  async execute(args, context) {
248
- setToolTitle(context, `Open ${args.class}: ${args.id}`, { action: "get", class: args.class, id: args.id })
249
- return handleGet(args.class, args.id)
256
+ setToolTitle(context, `fetch ${args.class}: ${args.id}`)
257
+ return handleFetch(args.class, args.id)
250
258
  },
251
259
  }),
252
260
 
253
- hm_list: tool({
254
- description: "List OpenHermes memory records by class, sorted by recency",
261
+ list_memory: tool({
262
+ description: "List recent memory records by class, sorted by recency",
255
263
  args: {
256
264
  class: tool.schema.enum(CLASSES).describe("Memory class"),
257
265
  limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
258
266
  },
259
267
  async execute(args, context) {
260
- setToolTitle(context, `List ${args.class} records`, { action: "list", class: args.class, limit: args.limit })
268
+ setToolTitle(context, `list ${args.class}`)
261
269
  return handleList(args.class, args.limit)
262
270
  },
263
271
  }),
264
272
 
265
- hm_latest: tool({
266
- description: "Get the latest active OpenHermes memory record by class",
273
+ latest_memory: tool({
274
+ description: "Get the latest active memory record by class",
267
275
  args: {
268
276
  class: tool.schema.enum(CLASSES).describe("Memory class"),
269
277
  },
270
278
  async execute(args, context) {
271
- setToolTitle(context, `Latest ${args.class}`, { action: "latest", class: args.class })
279
+ setToolTitle(context, `latest ${args.class}`)
272
280
  return handleLatest(args.class)
273
281
  },
274
282
  }),
275
283
 
276
- hm_search: tool({
277
- description: "Search OpenHermes memory records with keyword matching and relevance ranking",
284
+ search_memory: tool({
285
+ description: "Search memory records with keyword matching and relevance ranking across all classes",
278
286
  args: {
279
287
  query: tool.schema.string().describe("Search query string"),
280
288
  scope: tool.schema.enum(["global", "local", "auto"]).optional().default("auto").describe("Search scope"),
@@ -283,17 +291,22 @@ export const MemoryToolsPlugin = async () => {
283
291
  limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
284
292
  },
285
293
  async execute(args, context) {
286
- setToolTitle(context, `Search memory: ${truncateText(args.query, 48)}`, {
287
- action: "search",
288
- scope: args.scope,
289
- classes: args.classes?.length ? args.classes : CLASSES,
290
- project: args.project ?? null,
291
- limit: args.limit,
292
- })
294
+ setToolTitle(context, `search: ${truncateText(args.query, 48)}`)
293
295
  return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
294
296
  },
295
297
  }),
296
298
 
299
+ archive_memory: tool({
300
+ description: "Soft-delete a memory record by setting its status to expired",
301
+ args: {
302
+ class: tool.schema.enum(CLASSES),
303
+ id: tool.schema.string(),
304
+ },
305
+ async execute(args, context) {
306
+ setToolTitle(context, `archive ${args.class}: ${args.id}`)
307
+ return handleArchive(args.class, args.id)
308
+ },
309
+ }),
297
310
  },
298
311
  }
299
312
  }
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import os from "node:os"
4
+
5
+ const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "ohc.json")
6
+
7
+ const DEFAULTS = { enabled: true, max: 200000, min: 50000 }
8
+
9
+ function writeDefaults() {
10
+ try {
11
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true })
12
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULTS, null, 2) + "\n", "utf8")
13
+ } catch {}
14
+ }
15
+
16
+ export function loadConfig() {
17
+ let raw = {}
18
+ try {
19
+ raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"))
20
+ } catch {
21
+ writeDefaults()
22
+ return { ...DEFAULTS }
23
+ }
24
+
25
+ return {
26
+ enabled: raw.enabled !== false,
27
+ max: typeof raw.max === "number" && raw.max > 0 ? raw.max : DEFAULTS.max,
28
+ min: typeof raw.min === "number" ? Math.max(10000, raw.min) : DEFAULTS.min,
29
+ }
30
+ }