openhermes 1.5.2 → 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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -157
  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 +101 -54
  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
  71. package/lib/tools/_memory.mjs +0 -230
  72. package/lib/tools/hm_get.mjs +0 -13
  73. package/lib/tools/hm_latest.mjs +0 -12
  74. package/lib/tools/hm_list.mjs +0 -13
  75. package/lib/tools/hm_put.mjs +0 -14
  76. package/lib/tools/hm_search.mjs +0 -16
@@ -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,49 +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 handlePut(cls, id, dataStr) {
125
+ function setToolTitle(context, title) {
126
+ if (!context || typeof context.metadata !== "function") return
127
+ context.metadata({ title })
128
+ }
129
+
130
+ function handleAdd(cls, id, dataStr) {
136
131
  let parsed
137
- 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}` }
138
133
  if (!isPlainObject(parsed)) return "data must be a JSON object"
139
134
  if (!id?.trim()) return "non-blank id is required"
140
135
 
141
136
  const now = new Date().toISOString()
142
- 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 }
143
139
 
144
- const schema = readJSON(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
140
+ const schema = readJson(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
145
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
+
146
152
  const unsupported = findUnsupportedSchemaKeywords(schema)
147
- if (unsupported.length) return `Unsupported schema keywords: ${unsupported.join(", ")}`
153
+ if (unsupported.length) return `unsupported schema: ${unsupported.join(", ")}`
148
154
  const errs = validateSchema(schema, record, "$")
149
155
  if (cls === "audit" && !enforceAuditEvidence(record)) errs.push("$.provenance must include at least one non-empty evidence ref")
150
- if (errs.length) return `Validation errors: ${errs.join("; ")}`
156
+ if (errs.length) return `validation: ${errs.join("; ")}`
151
157
  }
152
158
 
153
159
  if (cls === "mistake") upsertMistake(record)
154
160
  else writeObject(cls, record)
155
161
 
156
- return stableStringify({ ok: true, id })
162
+ const action = existing ? "updated" : "saved"
163
+ return `${action}: ${id.trim()}`
157
164
  }
158
165
 
159
- function handleGet(cls, id) {
166
+ function handleFetch(cls, id) {
160
167
  if (!id?.trim()) return "non-blank id is required"
161
168
  const record = queryGet(cls, id.trim())
162
- if (!record) return stableStringify({ ok: false, found: false })
163
- return stableStringify({ ok: true, record })
169
+ if (!record) return `not found: ${id}`
170
+ return stableStringify(record)
164
171
  }
165
172
 
166
173
  function handleList(cls, limit = 10) {
167
174
  const entries = queryList(cls, Math.min(limit, 100))
168
- 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")
169
177
  }
170
178
 
171
179
  function handleLatest(cls) {
172
180
  const list = queryList(cls, 100)
173
181
  const active = filterActive(list)
174
- if (!active[0]?.id) return stableStringify({ ok: false, found: false })
182
+ if (!active[0]?.id) return "no active records"
175
183
  const record = queryGet(cls, active[0].id)
176
- if (!record) return stableStringify({ ok: false, found: false })
177
- return stableStringify({ ok: true, record })
184
+ if (!record) return "no active records"
185
+ return stableStringify(record)
178
186
  }
179
187
 
180
188
  function handleSearch(query, scope, classes, project, limit) {
@@ -185,7 +193,7 @@ function handleSearch(query, scope, classes, project, limit) {
185
193
  let records = []
186
194
  for (const cls of clsList) {
187
195
  if (cls === "mistake") {
188
- for (const m of readJSONL(path.join(classDir(cls), "mistakes.jsonl"))) {
196
+ for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
189
197
  if (!hasExpired(m)) records.push(m)
190
198
  }
191
199
  } else {
@@ -193,7 +201,7 @@ function handleSearch(query, scope, classes, project, limit) {
193
201
  let files = []
194
202
  try { files = fs.readdirSync(dir).filter(f => f.endsWith(".json") && f !== "index.json") } catch { continue }
195
203
  for (const f of files) {
196
- const r = readJSON(path.join(dir, f), null)
204
+ const r = readJson(path.join(dir, f), null)
197
205
  if (r && !hasExpired(r)) records.push(r)
198
206
  }
199
207
  }
@@ -204,7 +212,19 @@ function handleSearch(query, scope, classes, project, limit) {
204
212
  .filter(e => e.score > 0)
205
213
  .sort((a, b) => b.score - a.score)
206
214
  .slice(0, lim)
207
- 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()}`
208
228
  }
209
229
 
210
230
  export const MemoryToolsPlugin = async () => {
@@ -213,44 +233,56 @@ export const MemoryToolsPlugin = async () => {
213
233
 
214
234
  return {
215
235
  tool: {
216
- hm_put: tool({
217
- 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",
218
238
  args: {
219
239
  class: tool.schema.enum(CLASSES),
220
240
  id: tool.schema.string(),
221
241
  data: tool.schema.string(),
222
242
  },
223
- async execute(args) { return handlePut(args.class, args.id, args.data) },
243
+ async execute(args, context) {
244
+ setToolTitle(context, `save ${args.class}: ${args.id}`)
245
+ return handleAdd(args.class, args.id, args.data)
246
+ },
224
247
  }),
225
248
 
226
- hm_get: tool({
227
- description: "Get a specific OpenHermes memory record by ID",
249
+ fetch_memory: tool({
250
+ description: "Get a specific memory record by class and id",
228
251
  args: {
229
252
  class: tool.schema.enum(CLASSES).describe("Memory class"),
230
253
  id: tool.schema.string().describe("Record ID"),
231
254
  },
232
- async execute(args) { return handleGet(args.class, args.id) },
255
+ async execute(args, context) {
256
+ setToolTitle(context, `fetch ${args.class}: ${args.id}`)
257
+ return handleFetch(args.class, args.id)
258
+ },
233
259
  }),
234
260
 
235
- hm_list: tool({
236
- 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",
237
263
  args: {
238
264
  class: tool.schema.enum(CLASSES).describe("Memory class"),
239
265
  limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
240
266
  },
241
- async execute(args) { return handleList(args.class, args.limit) },
267
+ async execute(args, context) {
268
+ setToolTitle(context, `list ${args.class}`)
269
+ return handleList(args.class, args.limit)
270
+ },
242
271
  }),
243
272
 
244
- hm_latest: tool({
245
- description: "Get the latest active OpenHermes memory record by class",
273
+ latest_memory: tool({
274
+ description: "Get the latest active memory record by class",
246
275
  args: {
247
276
  class: tool.schema.enum(CLASSES).describe("Memory class"),
248
277
  },
249
- async execute(args) { return handleLatest(args.class) },
278
+ async execute(args, context) {
279
+ setToolTitle(context, `latest ${args.class}`)
280
+ return handleLatest(args.class)
281
+ },
250
282
  }),
251
283
 
252
- hm_search: tool({
253
- 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",
254
286
  args: {
255
287
  query: tool.schema.string().describe("Search query string"),
256
288
  scope: tool.schema.enum(["global", "local", "auto"]).optional().default("auto").describe("Search scope"),
@@ -258,7 +290,22 @@ export const MemoryToolsPlugin = async () => {
258
290
  project: tool.schema.string().optional().describe("Project filter"),
259
291
  limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
260
292
  },
261
- async execute(args) { return handleSearch(args.query, args.scope, args.classes, args.project, args.limit) },
293
+ async execute(args, context) {
294
+ setToolTitle(context, `search: ${truncateText(args.query, 48)}`)
295
+ return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
296
+ },
297
+ }),
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
+ },
262
309
  }),
263
310
  },
264
311
  }
@@ -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
+ }