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.
- package/LICENSE +21 -0
- package/README.md +217 -111
- 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 +84 -71
- 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
|
@@ -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,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
|
|
125
|
+
function setToolTitle(context, title) {
|
|
136
126
|
if (!context || typeof context.metadata !== "function") return
|
|
137
|
-
context.metadata({ title
|
|
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
|
|
130
|
+
function handleAdd(cls, id, dataStr) {
|
|
148
131
|
let parsed
|
|
149
|
-
try { parsed = JSON.parse(dataStr) } catch (e) { return `
|
|
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
|
|
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 =
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
162
|
+
const action = existing ? "updated" : "saved"
|
|
163
|
+
return `${action}: ${id.trim()}`
|
|
169
164
|
}
|
|
170
165
|
|
|
171
|
-
function
|
|
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
|
|
175
|
-
return stableStringify(
|
|
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
|
-
|
|
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
|
|
182
|
+
if (!active[0]?.id) return "no active records"
|
|
187
183
|
const record = queryGet(cls, active[0].id)
|
|
188
|
-
if (!record) return
|
|
189
|
-
return stableStringify(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
description: "
|
|
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,
|
|
237
|
-
return
|
|
244
|
+
setToolTitle(context, `save ${args.class}: ${args.id}`)
|
|
245
|
+
return handleAdd(args.class, args.id, args.data)
|
|
238
246
|
},
|
|
239
247
|
}),
|
|
240
248
|
|
|
241
|
-
|
|
242
|
-
description: "Get a specific
|
|
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, `
|
|
249
|
-
return
|
|
256
|
+
setToolTitle(context, `fetch ${args.class}: ${args.id}`)
|
|
257
|
+
return handleFetch(args.class, args.id)
|
|
250
258
|
},
|
|
251
259
|
}),
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
description: "List
|
|
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, `
|
|
268
|
+
setToolTitle(context, `list ${args.class}`)
|
|
261
269
|
return handleList(args.class, args.limit)
|
|
262
270
|
},
|
|
263
271
|
}),
|
|
264
272
|
|
|
265
|
-
|
|
266
|
-
description: "Get the latest active
|
|
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, `
|
|
279
|
+
setToolTitle(context, `latest ${args.class}`)
|
|
272
280
|
return handleLatest(args.class)
|
|
273
281
|
},
|
|
274
282
|
}),
|
|
275
283
|
|
|
276
|
-
|
|
277
|
-
description: "Search
|
|
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, `
|
|
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
|
+
}
|