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.
- package/LICENSE +21 -0
- package/README.md +256 -157
- package/autorecall.mjs +2 -12
- package/bootstrap.mjs +158 -8
- package/curator.mjs +1 -5
- package/harness/commands/checkpoint.md +68 -0
- package/harness/commands/eval.md +89 -0
- package/harness/commands/go-build.md +87 -0
- package/harness/commands/go-review.md +71 -0
- package/harness/commands/harness-audit.md +90 -0
- package/harness/commands/learn.md +2 -2
- package/harness/commands/loop-start.md +38 -0
- package/harness/commands/loop-status.md +30 -0
- package/harness/commands/memory-search.md +2 -2
- package/harness/commands/model-route.md +32 -0
- package/harness/commands/orchestrate.md +88 -0
- package/harness/commands/quality-gate.md +35 -0
- package/harness/commands/refactor-clean.md +102 -0
- package/harness/commands/rust-build.md +78 -0
- package/harness/commands/rust-review.md +65 -0
- package/harness/commands/setup-pm.md +65 -0
- package/harness/commands/skill-create.md +99 -0
- package/harness/commands/test-coverage.md +80 -0
- package/harness/commands/update-codemaps.md +81 -0
- package/harness/commands/update-docs.md +67 -0
- package/harness/commands/verify.md +68 -0
- package/harness/instructions/CONVENTIONS.md +206 -0
- package/harness/instructions/RUNTIME.md +8 -1
- package/harness/prompts/build-cpp.md +84 -0
- package/harness/prompts/build-error-resolver.md +2 -1
- package/harness/prompts/build-go.md +326 -0
- package/harness/prompts/build-java.md +126 -0
- package/harness/prompts/build-kotlin.md +123 -0
- package/harness/prompts/build-rust.md +94 -0
- package/harness/prompts/code-reviewer.md +2 -1
- package/harness/prompts/doc-updater.md +193 -0
- package/harness/prompts/docs-lookup.md +60 -0
- package/harness/prompts/explore.md +1 -0
- package/harness/prompts/harness-optimizer.md +30 -0
- package/harness/prompts/loop-operator.md +42 -0
- package/harness/prompts/planner.md +3 -2
- package/harness/prompts/refactor-cleaner.md +242 -0
- package/harness/prompts/review-cpp.md +68 -0
- package/harness/prompts/review-database.md +248 -0
- package/harness/prompts/review-go.md +244 -0
- package/harness/prompts/review-java.md +100 -0
- package/harness/prompts/review-kotlin.md +130 -0
- package/harness/prompts/review-python.md +88 -0
- package/harness/prompts/review-rust.md +64 -0
- package/harness/prompts/security-reviewer.md +3 -2
- package/harness/prompts/tdd-guide.md +214 -0
- package/harness/rules/delegation.md +28 -22
- package/harness/rules/memory-management.md +4 -4
- package/harness/rules/retrieval.md +5 -5
- package/harness/rules/runtime-guards.md +1 -1
- package/harness/rules/session-start.md +4 -4
- package/harness/rules/skills-management.md +2 -2
- package/harness/rules/state-drift.md +1 -1
- package/harness/rules/verification.md +4 -4
- package/harness/skills/coding-standards/SKILL.md +1 -1
- package/index.mjs +25 -4
- package/lib/hardening.mjs +11 -1
- package/lib/memory-tools-plugin.mjs +101 -54
- package/lib/ohc/config.mjs +30 -0
- package/lib/ohc/pruner.mjs +239 -0
- package/lib/ohc/reaper.mjs +61 -0
- package/lib/ohc/state.mjs +32 -0
- package/lib/ohc/updater.mjs +110 -0
- package/package.json +1 -1
- package/skill-builder.mjs +2 -6
- package/lib/tools/_memory.mjs +0 -230
- package/lib/tools/hm_get.mjs +0 -13
- package/lib/tools/hm_latest.mjs +0 -12
- package/lib/tools/hm_list.mjs +0 -13
- package/lib/tools/hm_put.mjs +0 -14
- 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 `
|
|
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 `
|
|
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
|
|
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 `
|
|
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 (`
|
|
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 `
|
|
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** — `
|
|
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
|
|
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
|
-
|
|
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.
|
|
28
|
-
const handler =
|
|
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
|
-
|
|
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 "
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =>
|
|
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
|
|
106
|
-
return
|
|
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
|
|
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 `
|
|
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
|
|
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 =
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
162
|
+
const action = existing ? "updated" : "saved"
|
|
163
|
+
return `${action}: ${id.trim()}`
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
function
|
|
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
|
|
163
|
-
return stableStringify(
|
|
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
|
-
|
|
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
|
|
182
|
+
if (!active[0]?.id) return "no active records"
|
|
175
183
|
const record = queryGet(cls, active[0].id)
|
|
176
|
-
if (!record) return
|
|
177
|
-
return stableStringify(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
description: "
|
|
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
|
|
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
|
-
|
|
227
|
-
description: "Get a specific
|
|
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
|
|
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
|
-
|
|
236
|
-
description: "List
|
|
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
|
|
267
|
+
async execute(args, context) {
|
|
268
|
+
setToolTitle(context, `list ${args.class}`)
|
|
269
|
+
return handleList(args.class, args.limit)
|
|
270
|
+
},
|
|
242
271
|
}),
|
|
243
272
|
|
|
244
|
-
|
|
245
|
-
description: "Get the latest active
|
|
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) {
|
|
278
|
+
async execute(args, context) {
|
|
279
|
+
setToolTitle(context, `latest ${args.class}`)
|
|
280
|
+
return handleLatest(args.class)
|
|
281
|
+
},
|
|
250
282
|
}),
|
|
251
283
|
|
|
252
|
-
|
|
253
|
-
description: "Search
|
|
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
|
|
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
|
+
}
|