openhermes 2.8.0 → 4.0.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/CONTEXT.md +18 -0
- package/ETHOS.md +15 -0
- package/README.md +135 -292
- package/bootstrap.mjs +174 -512
- package/harness/agents/openhermes.md +87 -0
- package/harness/codex/CONSTITUTION.md +70 -148
- package/harness/codex/ROUTING.md +126 -0
- package/harness/commands/oh-doctor.md +26 -0
- package/harness/instructions/CONVENTIONS.md +206 -206
- package/harness/instructions/RUNTIME.md +54 -31
- package/harness/skills/oh-builder/SKILL.md +98 -0
- package/harness/skills/oh-caveman/SKILL.md +33 -0
- package/harness/skills/oh-expert/SKILL.md +121 -0
- package/harness/skills/oh-freeze/SKILL.md +28 -0
- package/harness/skills/oh-gauntlet/SKILL.md +119 -0
- package/harness/skills/oh-grill/SKILL.md +77 -0
- package/harness/skills/oh-guard/SKILL.md +33 -0
- package/harness/skills/oh-handoff/SKILL.md +33 -0
- package/harness/skills/oh-health/SKILL.md +90 -0
- package/harness/skills/oh-init/SKILL.md +78 -0
- package/harness/skills/oh-investigate/SKILL.md +35 -0
- package/harness/skills/oh-issue/SKILL.md +36 -0
- package/harness/skills/oh-learn/SKILL.md +28 -0
- package/harness/skills/oh-manifest/SKILL.md +84 -0
- package/harness/skills/oh-plan-review/SKILL.md +128 -0
- package/harness/skills/oh-planner/SKILL.md +159 -0
- package/harness/skills/oh-prd/SKILL.md +35 -0
- package/harness/skills/oh-retro/SKILL.md +33 -0
- package/harness/skills/oh-review/SKILL.md +110 -0
- package/harness/skills/oh-security/SKILL.md +110 -0
- package/harness/skills/oh-ship/SKILL.md +39 -0
- package/harness/skills/oh-skill-craft/SKILL.md +107 -0
- package/harness/skills/oh-skills-link/SKILL.md +29 -0
- package/harness/skills/oh-skills-list/SKILL.md +31 -0
- package/harness/skills/oh-triage/SKILL.md +36 -0
- package/index.mjs +3 -60
- package/lib/harness-resolver.mjs +77 -0
- package/lib/logger.mjs +62 -0
- package/package.json +49 -53
- package/test/plugins-behavioral.test.mjs +64 -0
- package/test/plugins.test.mjs +62 -0
- package/autorecall.mjs +0 -237
- package/curator.mjs +0 -482
- package/harness/commands/build-fix.md +0 -60
- package/harness/commands/checkpoint.md +0 -68
- package/harness/commands/code-review.md +0 -71
- package/harness/commands/doctor.md +0 -42
- package/harness/commands/eval.md +0 -89
- package/harness/commands/go-build.md +0 -87
- package/harness/commands/go-review.md +0 -71
- package/harness/commands/harness-audit.md +0 -90
- package/harness/commands/learn.md +0 -37
- package/harness/commands/loop-start.md +0 -38
- package/harness/commands/loop-status.md +0 -30
- package/harness/commands/memory-search.md +0 -37
- package/harness/commands/model-route.md +0 -32
- package/harness/commands/ohc.md +0 -13
- package/harness/commands/orchestrate.md +0 -88
- package/harness/commands/plan.md +0 -53
- package/harness/commands/quality-gate.md +0 -35
- package/harness/commands/refactor-clean.md +0 -102
- package/harness/commands/rust-build.md +0 -78
- package/harness/commands/rust-review.md +0 -65
- package/harness/commands/security.md +0 -93
- package/harness/commands/setup-pm.md +0 -65
- package/harness/commands/skill-create.md +0 -99
- package/harness/commands/test-coverage.md +0 -80
- package/harness/commands/update-codemaps.md +0 -81
- package/harness/commands/update-docs.md +0 -67
- package/harness/commands/verify.md +0 -68
- package/harness/prompts/architect.txt +0 -189
- package/harness/prompts/build-cpp.md +0 -98
- package/harness/prompts/build-error-resolver.md +0 -44
- package/harness/prompts/build-go.md +0 -340
- package/harness/prompts/build-java.md +0 -140
- package/harness/prompts/build-kotlin.md +0 -137
- package/harness/prompts/build-rust.md +0 -108
- package/harness/prompts/code-reviewer.md +0 -40
- package/harness/prompts/doc-updater.md +0 -206
- package/harness/prompts/docs-lookup.md +0 -71
- package/harness/prompts/e2e-runner.txt +0 -317
- package/harness/prompts/explore.md +0 -42
- package/harness/prompts/harness-optimizer.md +0 -42
- package/harness/prompts/loop-operator.md +0 -53
- package/harness/prompts/planner.md +0 -37
- package/harness/prompts/refactor-cleaner.md +0 -256
- package/harness/prompts/review-cpp.md +0 -81
- package/harness/prompts/review-database.md +0 -261
- package/harness/prompts/review-go.md +0 -257
- package/harness/prompts/review-java.md +0 -113
- package/harness/prompts/review-kotlin.md +0 -143
- package/harness/prompts/review-python.md +0 -101
- package/harness/prompts/review-rust.md +0 -77
- package/harness/prompts/security-reviewer.md +0 -42
- package/harness/prompts/tdd-guide.md +0 -228
- package/harness/rules/audit.md +0 -84
- package/harness/rules/checkpointing.md +0 -75
- package/harness/rules/context-loading.md +0 -33
- package/harness/rules/credential-exposure.md +0 -0
- package/harness/rules/delegation.md +0 -80
- package/harness/rules/handoff.md +0 -267
- package/harness/rules/memory-management.md +0 -28
- package/harness/rules/precedence.md +0 -52
- package/harness/rules/promotion.md +0 -46
- package/harness/rules/ranking.md +0 -64
- package/harness/rules/retrieval.md +0 -94
- package/harness/rules/runtime-guards.md +0 -196
- package/harness/rules/self-heal.md +0 -79
- package/harness/rules/session-start.md +0 -34
- package/harness/rules/skills-management.md +0 -165
- package/harness/rules/state-drift.md +0 -192
- package/harness/rules/verification.md +0 -88
- package/harness/scripts/sync-commands.mjs +0 -259
- package/harness/skills/.bundled_manifest +0 -17
- package/harness/skills/.usage.json +0 -6
- package/harness/skills/api-design/SKILL.md +0 -523
- package/harness/skills/backend-patterns/SKILL.md +0 -598
- package/harness/skills/coding-standards/SKILL.md +0 -549
- package/harness/skills/e2e-testing/SKILL.md +0 -326
- package/harness/skills/frontend-patterns/SKILL.md +0 -642
- package/harness/skills/frontend-slides/SKILL.md +0 -184
- package/harness/skills/security-review/SKILL.md +0 -495
- package/harness/skills/strategic-compact/SKILL.md +0 -131
- package/harness/skills/tdd-workflow/SKILL.md +0 -463
- package/harness/skills/verification-loop/SKILL.md +0 -126
- package/lib/ambient-memory.mjs +0 -167
- package/lib/handoff.mjs +0 -171
- package/lib/hardening.mjs +0 -146
- package/lib/memory-tools-plugin.mjs +0 -368
- package/lib/ohc/block-sync.mjs +0 -69
- package/lib/ohc/compress/search.mjs +0 -152
- package/lib/ohc/compress/state.mjs +0 -76
- package/lib/ohc/config.mjs +0 -185
- package/lib/ohc/message-ids.mjs +0 -178
- package/lib/ohc/notify.mjs +0 -135
- package/lib/ohc/protected-patterns.mjs +0 -55
- package/lib/ohc/prune-apply.mjs +0 -134
- package/lib/ohc/pruner.mjs +0 -608
- package/lib/ohc/reaper.mjs +0 -70
- package/lib/ohc/state.mjs +0 -265
- package/lib/ohc/strategies/deduplication.mjs +0 -72
- package/lib/ohc/strategies/index.mjs +0 -2
- package/lib/ohc/strategies/purge-errors.mjs +0 -43
- package/lib/ohc/token-utils.mjs +0 -26
- package/lib/ohc/updater.mjs +0 -132
- package/lib/paths.mjs +0 -49
- package/lib/schema-validator.mjs +0 -79
- package/lib/search.mjs +0 -48
- package/schemas/audit.schema.json +0 -82
- package/schemas/backlog.schema.json +0 -63
- package/schemas/checkpoint.schema.json +0 -65
- package/schemas/constraint.schema.json +0 -62
- package/schemas/decision.schema.json +0 -63
- package/schemas/instinct.schema.json +0 -63
- package/schemas/loop-state.schema.json +0 -33
- package/schemas/mistake.schema.json +0 -64
- package/schemas/verification_receipt.schema.json +0 -88
- package/skill-builder.mjs +0 -88
package/lib/ohc/state.mjs
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import os from "node:os"
|
|
4
|
-
|
|
5
|
-
const STATE_DIR = path.join(os.homedir(), ".local", "share", "opencode", "ohc")
|
|
6
|
-
const LEGACY_FILE = path.join(os.homedir(), ".local", "share", "opencode", "ohc-state.json")
|
|
7
|
-
|
|
8
|
-
function sessionPath(sessionId) {
|
|
9
|
-
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
10
|
-
return path.join(STATE_DIR, `${safe}.json`)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function ensureDir() {
|
|
14
|
-
fs.mkdirSync(STATE_DIR, { recursive: true })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function migrateLegacy() {
|
|
18
|
-
try {
|
|
19
|
-
if (!fs.existsSync(LEGACY_FILE)) return
|
|
20
|
-
const raw = JSON.parse(fs.readFileSync(LEGACY_FILE, "utf8"))
|
|
21
|
-
if (typeof raw !== "object") return
|
|
22
|
-
ensureDir()
|
|
23
|
-
for (const [sid, data] of Object.entries(raw)) {
|
|
24
|
-
const sp = sessionPath(sid)
|
|
25
|
-
if (!fs.existsSync(sp)) {
|
|
26
|
-
fs.writeFileSync(sp, JSON.stringify({ ...data, migratedFrom: "legacy" }, null, 2), "utf8")
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
fs.renameSync(LEGACY_FILE, LEGACY_FILE + ".bak")
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
migrateLegacy()
|
|
34
|
-
|
|
35
|
-
export function loadOhcState(sessionId) {
|
|
36
|
-
if (!sessionId) return null
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(fs.readFileSync(sessionPath(sessionId), "utf8"))
|
|
39
|
-
} catch {
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function saveOhcState(sessionId, data) {
|
|
45
|
-
if (!sessionId) return
|
|
46
|
-
ensureDir()
|
|
47
|
-
fs.writeFileSync(sessionPath(sessionId), JSON.stringify({ ...data, updatedAt: new Date().toISOString() }, null, 2), "utf8")
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export function createSessionState() {
|
|
52
|
-
return {
|
|
53
|
-
sessionId: null,
|
|
54
|
-
isSubAgent: false,
|
|
55
|
-
manualMode: false,
|
|
56
|
-
pendingManualTrigger: null,
|
|
57
|
-
prune: {
|
|
58
|
-
tools: new Map(),
|
|
59
|
-
messages: {
|
|
60
|
-
byMessageId: new Map(),
|
|
61
|
-
blocksById: new Map(),
|
|
62
|
-
activeBlockIds: new Set(),
|
|
63
|
-
activeByAnchorMessageId: new Map(),
|
|
64
|
-
nextBlockId: 1,
|
|
65
|
-
nextRunId: 1,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
nudges: {
|
|
69
|
-
contextLimitAnchors: new Set(),
|
|
70
|
-
turnNudgeAnchors: new Set(),
|
|
71
|
-
iterationNudgeAnchors: new Set(),
|
|
72
|
-
},
|
|
73
|
-
stats: {
|
|
74
|
-
pruneTokenCounter: 0,
|
|
75
|
-
totalPruneTokens: 0,
|
|
76
|
-
},
|
|
77
|
-
toolParameters: new Map(),
|
|
78
|
-
toolIdList: [],
|
|
79
|
-
messageIds: { byRawId: new Map(), byRef: new Map(), nextRef: 1 },
|
|
80
|
-
lastCompaction: 0,
|
|
81
|
-
currentTurn: 0,
|
|
82
|
-
modelContextLimit: undefined,
|
|
83
|
-
systemPromptTokens: undefined,
|
|
84
|
-
protectedTurns: { enabled: false, turns: 0 },
|
|
85
|
-
compressionTiming: { starts: new Map(), pendingByCallId: new Map(), lastDurationMs: 0, totalDurationMs: 0 },
|
|
86
|
-
lastNudgePct: 0,
|
|
87
|
-
lastAutoPruneAt: null,
|
|
88
|
-
prunedIds: new Set(),
|
|
89
|
-
summary: null,
|
|
90
|
-
anchorMessageId: null,
|
|
91
|
-
totalTokensSaved: 0,
|
|
92
|
-
totalMessagesRemoved: 0,
|
|
93
|
-
blockCount: 0,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function pruneMapToObj(map) {
|
|
98
|
-
return Object.fromEntries(map)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function pruneMapFromObj(obj) {
|
|
102
|
-
if (!obj || typeof obj !== "object") return new Map()
|
|
103
|
-
return new Map(Object.entries(obj))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function setToArr(s) {
|
|
107
|
-
return [...s]
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function setFromArr(a) {
|
|
111
|
-
return new Set(Array.isArray(a) ? a : [])
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function serializeState(state) {
|
|
115
|
-
return {
|
|
116
|
-
sessionId: state.sessionId,
|
|
117
|
-
manualMode: state.manualMode,
|
|
118
|
-
lastCompaction: state.lastCompaction,
|
|
119
|
-
currentTurn: state.currentTurn,
|
|
120
|
-
modelContextLimit: state.modelContextLimit,
|
|
121
|
-
systemPromptTokens: state.systemPromptTokens,
|
|
122
|
-
stats: { ...state.stats },
|
|
123
|
-
nudges: {
|
|
124
|
-
contextLimitAnchors: setToArr(state.nudges.contextLimitAnchors),
|
|
125
|
-
turnNudgeAnchors: setToArr(state.nudges.turnNudgeAnchors),
|
|
126
|
-
iterationNudgeAnchors: setToArr(state.nudges.iterationNudgeAnchors),
|
|
127
|
-
},
|
|
128
|
-
prune: {
|
|
129
|
-
tools: pruneMapToObj(state.prune.tools),
|
|
130
|
-
messages: {
|
|
131
|
-
nextBlockId: state.prune?.messages?.nextBlockId || 1,
|
|
132
|
-
nextRunId: state.prune?.messages?.nextRunId || 1,
|
|
133
|
-
blocksById: pruneMapToObj(state.prune?.messages?.blocksById),
|
|
134
|
-
byMessageId: pruneMapToObj(state.prune?.messages?.byMessageId),
|
|
135
|
-
activeBlockIds: setToArr(state.prune?.messages?.activeBlockIds),
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
lastAutoPruneAt: state.lastAutoPruneAt,
|
|
139
|
-
totalTokensSaved: state.totalTokensSaved,
|
|
140
|
-
totalMessagesRemoved: state.totalMessagesRemoved,
|
|
141
|
-
blockCount: state.blockCount,
|
|
142
|
-
summary: state.summary,
|
|
143
|
-
anchorMessageId: state.anchorMessageId,
|
|
144
|
-
prunedIds: setToArr(state.prunedIds),
|
|
145
|
-
isSubAgent: state.isSubAgent || false,
|
|
146
|
-
messageIds: {
|
|
147
|
-
byRawId: pruneMapToObj(state.messageIds?.byRawId),
|
|
148
|
-
byRef: pruneMapToObj(state.messageIds?.byRef),
|
|
149
|
-
nextRef: state.messageIds?.nextRef || 1,
|
|
150
|
-
},
|
|
151
|
-
compressionTiming: {
|
|
152
|
-
starts: Object.fromEntries(state.compressionTiming?.starts || new Map()),
|
|
153
|
-
pendingByCallId: pruneMapToObj(state.compressionTiming?.pendingByCallId || new Map()),
|
|
154
|
-
lastDurationMs: state.compressionTiming?.lastDurationMs || 0,
|
|
155
|
-
totalDurationMs: state.compressionTiming?.totalDurationMs || 0,
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function deserializeState(saved) {
|
|
161
|
-
const state = createSessionState()
|
|
162
|
-
if (!saved) return state
|
|
163
|
-
state.sessionId = saved.sessionId || null
|
|
164
|
-
state.manualMode = saved.manualMode || false
|
|
165
|
-
state.lastCompaction = saved.lastCompaction || 0
|
|
166
|
-
state.currentTurn = saved.currentTurn || 0
|
|
167
|
-
state.modelContextLimit = saved.modelContextLimit
|
|
168
|
-
state.systemPromptTokens = saved.systemPromptTokens
|
|
169
|
-
if (saved.stats) Object.assign(state.stats, saved.stats)
|
|
170
|
-
if (saved.nudges) {
|
|
171
|
-
state.nudges.contextLimitAnchors = setFromArr(saved.nudges.contextLimitAnchors)
|
|
172
|
-
state.nudges.turnNudgeAnchors = setFromArr(saved.nudges.turnNudgeAnchors)
|
|
173
|
-
state.nudges.iterationNudgeAnchors = setFromArr(saved.nudges.iterationNudgeAnchors)
|
|
174
|
-
}
|
|
175
|
-
if (saved.prune?.tools) state.prune.tools = pruneMapFromObj(saved.prune.tools)
|
|
176
|
-
if (saved.prune?.messages) {
|
|
177
|
-
const pm = saved.prune.messages
|
|
178
|
-
state.prune.messages.nextBlockId = pm.nextBlockId || 1
|
|
179
|
-
state.prune.messages.nextRunId = pm.nextRunId || 1
|
|
180
|
-
state.prune.messages.blocksById = pruneMapFromObj(pm.blocksById)
|
|
181
|
-
state.prune.messages.byMessageId = pruneMapFromObj(pm.byMessageId)
|
|
182
|
-
state.prune.messages.activeBlockIds = setFromArr(pm.activeBlockIds)
|
|
183
|
-
}
|
|
184
|
-
state.lastAutoPruneAt = saved.lastAutoPruneAt || null
|
|
185
|
-
state.totalTokensSaved = saved.totalTokensSaved || 0
|
|
186
|
-
state.totalMessagesRemoved = saved.totalMessagesRemoved || 0
|
|
187
|
-
state.blockCount = saved.blockCount || 0
|
|
188
|
-
state.summary = saved.summary || null
|
|
189
|
-
state.anchorMessageId = saved.anchorMessageId || null
|
|
190
|
-
state.prunedIds = setFromArr(saved.prunedIds)
|
|
191
|
-
if (saved.compressionTiming) {
|
|
192
|
-
state.compressionTiming.starts = pruneMapFromObj(saved.compressionTiming.starts)
|
|
193
|
-
state.compressionTiming.lastDurationMs = saved.compressionTiming.lastDurationMs || 0
|
|
194
|
-
state.compressionTiming.totalDurationMs = saved.compressionTiming.totalDurationMs || 0
|
|
195
|
-
}
|
|
196
|
-
if (saved.messageIds) {
|
|
197
|
-
state.messageIds.byRawId = pruneMapFromObj(saved.messageIds.byRawId)
|
|
198
|
-
state.messageIds.byRef = pruneMapFromObj(saved.messageIds.byRef)
|
|
199
|
-
state.messageIds.nextRef = saved.messageIds.nextRef || 1
|
|
200
|
-
}
|
|
201
|
-
if (saved.isSubAgent !== undefined) state.isSubAgent = saved.isSubAgent
|
|
202
|
-
return state
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function buildToolIdList(state, messages) {
|
|
206
|
-
const ids = []
|
|
207
|
-
for (const msg of messages) {
|
|
208
|
-
if (!Array.isArray(msg.parts)) continue
|
|
209
|
-
for (const part of msg.parts) {
|
|
210
|
-
if (part.type === "tool" && part.callID) {
|
|
211
|
-
ids.push(part.callID)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
state.toolIdList = ids
|
|
216
|
-
return ids
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function syncToolCache(state, messages) {
|
|
220
|
-
let userTurn = 0
|
|
221
|
-
for (const msg of messages) {
|
|
222
|
-
const hasUserText = msg.info?.role === "user" && msg.parts?.some(p => p.type === "text" && p.text?.trim())
|
|
223
|
-
if (hasUserText) userTurn++
|
|
224
|
-
if (!Array.isArray(msg.parts)) continue
|
|
225
|
-
for (const part of msg.parts) {
|
|
226
|
-
if (part.type !== "tool" || !part.callID) continue
|
|
227
|
-
const existing = state.toolParameters.get(part.callID)
|
|
228
|
-
if (existing) {
|
|
229
|
-
existing.status = part.state?.status || existing.status
|
|
230
|
-
continue
|
|
231
|
-
}
|
|
232
|
-
state.toolParameters.set(part.callID, {
|
|
233
|
-
tool: part.tool || "unknown",
|
|
234
|
-
parameters: part.state?.input || {},
|
|
235
|
-
status: part.state?.status || "pending",
|
|
236
|
-
turn: userTurn,
|
|
237
|
-
tokenCount: estimateToolTokens(part),
|
|
238
|
-
lastSeen: Date.now(),
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
state.currentTurn = Math.max(state.currentTurn, userTurn)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function estimateToolTokens(part) {
|
|
246
|
-
if (!part.state) return 0
|
|
247
|
-
let t = 0
|
|
248
|
-
if (part.state.input) t += JSON.stringify(part.state.input).length / 4
|
|
249
|
-
if (part.state.output) {
|
|
250
|
-
t += (typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output ?? "")).length / 4
|
|
251
|
-
}
|
|
252
|
-
return Math.ceil(t)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export function countTurns(state, messages) {
|
|
256
|
-
let userCount = 0
|
|
257
|
-
for (const msg of messages) {
|
|
258
|
-
if (msg.info?.role === "user") {
|
|
259
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
260
|
-
const hasText = parts.some(p => p.type === "text" && p.text?.trim())
|
|
261
|
-
if (hasText) userCount++
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return userCount
|
|
265
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
|
|
2
|
-
import { getTotalToolTokens } from "../token-utils.mjs"
|
|
3
|
-
|
|
4
|
-
export function deduplicate(state, config, messages) {
|
|
5
|
-
if (state.manualMode && !config.manualMode?.automaticStrategies) return
|
|
6
|
-
|
|
7
|
-
if (!config.strategies?.deduplication?.enabled) return
|
|
8
|
-
|
|
9
|
-
const allIds = state.toolIdList
|
|
10
|
-
if (!allIds?.length) return
|
|
11
|
-
|
|
12
|
-
const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
|
|
13
|
-
if (!unprunedIds.length) return
|
|
14
|
-
|
|
15
|
-
const protectedTools = config.strategies.deduplication.protectedTools || []
|
|
16
|
-
|
|
17
|
-
const sigMap = new Map()
|
|
18
|
-
|
|
19
|
-
for (const id of unprunedIds) {
|
|
20
|
-
const meta = state.toolParameters.get(id)
|
|
21
|
-
if (!meta) continue
|
|
22
|
-
|
|
23
|
-
if (isToolNameProtected(meta.tool, protectedTools)) continue
|
|
24
|
-
|
|
25
|
-
const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
|
|
26
|
-
if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
|
|
27
|
-
|
|
28
|
-
const sig = createToolSignature(meta.tool, meta.parameters)
|
|
29
|
-
if (!sigMap.has(sig)) sigMap.set(sig, [])
|
|
30
|
-
sigMap.get(sig).push(id)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const toPrune = []
|
|
34
|
-
for (const ids of sigMap.values()) {
|
|
35
|
-
if (ids.length > 1) {
|
|
36
|
-
toPrune.push(...ids.slice(0, -1))
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!toPrune.length) return
|
|
41
|
-
|
|
42
|
-
state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
|
|
43
|
-
for (const id of toPrune) {
|
|
44
|
-
const entry = state.toolParameters.get(id)
|
|
45
|
-
state.prune.tools.set(id, entry?.tokenCount ?? 0)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function createToolSignature(tool, params) {
|
|
50
|
-
if (!params) return tool
|
|
51
|
-
const norm = normalizeParams(params)
|
|
52
|
-
const sorted = sortKeys(norm)
|
|
53
|
-
return `${tool}::${JSON.stringify(sorted)}`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeParams(p) {
|
|
57
|
-
if (typeof p !== "object" || p === null) return p
|
|
58
|
-
if (Array.isArray(p)) return p
|
|
59
|
-
const n = {}
|
|
60
|
-
for (const [k, v] of Object.entries(p)) {
|
|
61
|
-
if (v !== undefined && v !== null) n[k] = v
|
|
62
|
-
}
|
|
63
|
-
return n
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sortKeys(o) {
|
|
67
|
-
if (typeof o !== "object" || o === null) return o
|
|
68
|
-
if (Array.isArray(o)) return o.map(sortKeys)
|
|
69
|
-
const s = {}
|
|
70
|
-
for (const k of Object.keys(o).sort()) s[k] = sortKeys(o[k])
|
|
71
|
-
return s
|
|
72
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
|
|
2
|
-
import { getTotalToolTokens } from "../token-utils.mjs"
|
|
3
|
-
|
|
4
|
-
export function purgeErrors(state, config, messages) {
|
|
5
|
-
if (state.manualMode && !config.manualMode?.automaticStrategies) return
|
|
6
|
-
|
|
7
|
-
if (!config.strategies?.purgeErrors?.enabled) return
|
|
8
|
-
|
|
9
|
-
const allIds = state.toolIdList
|
|
10
|
-
if (!allIds?.length) return
|
|
11
|
-
|
|
12
|
-
const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
|
|
13
|
-
if (!unprunedIds.length) return
|
|
14
|
-
|
|
15
|
-
const protectedTools = config.strategies.purgeErrors.protectedTools || []
|
|
16
|
-
const threshold = Math.max(1, config.strategies.purgeErrors.turns ?? 4)
|
|
17
|
-
|
|
18
|
-
const toPrune = []
|
|
19
|
-
for (const id of unprunedIds) {
|
|
20
|
-
const meta = state.toolParameters.get(id)
|
|
21
|
-
if (!meta) continue
|
|
22
|
-
|
|
23
|
-
if (isToolNameProtected(meta.tool, protectedTools)) continue
|
|
24
|
-
|
|
25
|
-
const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
|
|
26
|
-
if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
|
|
27
|
-
|
|
28
|
-
if (meta.status !== "error") continue
|
|
29
|
-
|
|
30
|
-
const turnAge = state.currentTurn - meta.turn
|
|
31
|
-
if (turnAge >= threshold) {
|
|
32
|
-
toPrune.push(id)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (!toPrune.length) return
|
|
37
|
-
|
|
38
|
-
state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
|
|
39
|
-
for (const id of toPrune) {
|
|
40
|
-
const entry = state.toolParameters.get(id)
|
|
41
|
-
state.prune.tools.set(id, entry?.tokenCount ?? 0)
|
|
42
|
-
}
|
|
43
|
-
}
|
package/lib/ohc/token-utils.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { totalTokens } from "./reaper.mjs"
|
|
2
|
-
|
|
3
|
-
export { totalTokens }
|
|
4
|
-
|
|
5
|
-
export function countTokens(value) {
|
|
6
|
-
if (typeof value === "string") return Math.ceil(value.length / 4)
|
|
7
|
-
if (typeof value === "object" && value !== null) return Math.ceil(JSON.stringify(value).length / 4)
|
|
8
|
-
return 0
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getTotalToolTokens(state, toolIds) {
|
|
12
|
-
let total = 0
|
|
13
|
-
for (const id of toolIds) {
|
|
14
|
-
const entry = state.toolParameters.get(id)
|
|
15
|
-
if (entry?.tokenCount) total += entry.tokenCount
|
|
16
|
-
}
|
|
17
|
-
return total
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function estimateToolTokenCost(part) {
|
|
21
|
-
if (part.type !== "tool") return 0
|
|
22
|
-
let t = 0
|
|
23
|
-
if (part.state?.input) t += countTokens(part.state.input)
|
|
24
|
-
if (part.state?.output) t += countTokens(part.state.output)
|
|
25
|
-
return t
|
|
26
|
-
}
|
package/lib/ohc/updater.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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", "opencode.json")
|
|
6
|
-
const CACHE_ROOTS = [
|
|
7
|
-
path.join(os.homedir(), ".cache", "opencode", "packages"),
|
|
8
|
-
path.join(os.homedir(), ".cache", "opencode", "node_modules"),
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
function detectInstallMethod() {
|
|
12
|
-
let raw = {}
|
|
13
|
-
try {
|
|
14
|
-
raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"))
|
|
15
|
-
} catch {
|
|
16
|
-
return { method: "unknown", entry: null, reason: `cannot read ${CONFIG_PATH}` }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const plugins = Array.isArray(raw.plugin) ? raw.plugin : []
|
|
20
|
-
for (const p of plugins) {
|
|
21
|
-
const entry = typeof p === "string" ? p : (Array.isArray(p) && typeof p[0] === "string" ? p[0] : "")
|
|
22
|
-
if (!entry.startsWith("openhermes")) continue
|
|
23
|
-
|
|
24
|
-
if (entry.includes("@git+https://") || entry.includes("@git+ssh://")) {
|
|
25
|
-
const repoUrl = entry.replace(/^openhermes@/, "")
|
|
26
|
-
return { method: "git", entry, repoUrl }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (entry === "openhermes") {
|
|
30
|
-
return { method: "npm", entry }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { method: "other", entry, reason: `unrecognized format: ${entry}` }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { method: "unknown", entry: null, reason: "openhermes not found in plugin config" }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function walkCacheDirs(root, results) {
|
|
40
|
-
if (!fs.existsSync(root)) return
|
|
41
|
-
if (path.basename(root).startsWith("openhermes")) {
|
|
42
|
-
results.push({ name: path.basename(root), path: root })
|
|
43
|
-
}
|
|
44
|
-
let entries = []
|
|
45
|
-
try {
|
|
46
|
-
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
47
|
-
} catch {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (!entry.isDirectory()) continue
|
|
53
|
-
const full = path.join(root, entry.name)
|
|
54
|
-
if (entry.name.startsWith("openhermes")) {
|
|
55
|
-
results.push({ name: entry.name, path: full })
|
|
56
|
-
}
|
|
57
|
-
walkCacheDirs(full, results)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function findCacheDirs({ cacheRoots = CACHE_ROOTS } = {}) {
|
|
62
|
-
const results = []
|
|
63
|
-
for (const root of cacheRoots) walkCacheDirs(root, results)
|
|
64
|
-
const seen = new Set()
|
|
65
|
-
return results.filter(dir => {
|
|
66
|
-
if (seen.has(dir.path)) return false
|
|
67
|
-
seen.add(dir.path)
|
|
68
|
-
return true
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function handleUpdateMe(ctx, input, output) {
|
|
73
|
-
const info = detectInstallMethod()
|
|
74
|
-
|
|
75
|
-
if (info.method === "unknown") {
|
|
76
|
-
output.parts.length = 0
|
|
77
|
-
output.parts.push({
|
|
78
|
-
type: "text",
|
|
79
|
-
text: `[Update-Me] Could not detect installation method.\n${info.reason}\n\nEnsure 'openhermes' is in your opencode.json plugin list:\n https://github.com/nathwn12/openhermes#setup`,
|
|
80
|
-
})
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const cacheDirs = findCacheDirs()
|
|
85
|
-
|
|
86
|
-
if (cacheDirs.length === 0) {
|
|
87
|
-
output.parts.length = 0
|
|
88
|
-
output.parts.push({
|
|
89
|
-
type: "text",
|
|
90
|
-
text: `[Update-Me] No OpenHermes plugin cache found.\nRestart OpenCode to redownload from ${info.method === "git" ? "git HEAD" : "npm registry"}.`,
|
|
91
|
-
})
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let clearedCount = 0
|
|
96
|
-
let failedCount = 0
|
|
97
|
-
let firstError = null
|
|
98
|
-
for (const dir of cacheDirs) {
|
|
99
|
-
try {
|
|
100
|
-
fs.rmSync(dir.path, { recursive: true, force: true })
|
|
101
|
-
clearedCount++
|
|
102
|
-
} catch (e) {
|
|
103
|
-
failedCount++
|
|
104
|
-
if (!firstError) firstError = e.message
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
output.parts.length = 0
|
|
109
|
-
let msg = `[Update-Me] OpenHermes update (${info.method})\n`
|
|
110
|
-
|
|
111
|
-
if (clearedCount > 0) {
|
|
112
|
-
msg += `\n ✓ Cleared OpenHermes plugin cache (${clearedCount} entr${clearedCount === 1 ? "y" : "ies"}).`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (failedCount > 0) {
|
|
116
|
-
msg += `\n ⚠ ${failedCount} entr${failedCount === 1 ? "y" : "ies"} could not be removed (may be locked).`
|
|
117
|
-
if (firstError) msg += `\n ${firstError}`
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
msg += `\n\nRestart OpenCode to load the latest OpenHermes from ${info.method === "git" ? "git HEAD" : "npm registry"}.`
|
|
121
|
-
|
|
122
|
-
output.parts.push({ type: "text", text: msg })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function UpdaterPlugin(ctx) {
|
|
126
|
-
return {
|
|
127
|
-
"command.execute.before": async (input, output) => {
|
|
128
|
-
if (input.command !== "update-me") return
|
|
129
|
-
await handleUpdateMe(ctx, input, output)
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
}
|
package/lib/paths.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import os from "node:os"
|
|
3
|
-
import fs from "node:fs"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
|
-
import { isTruthy } from "./hardening.mjs"
|
|
6
|
-
|
|
7
|
-
const HOME = process.env.USERPROFILE || os.homedir()
|
|
8
|
-
const DATA_ROOT = path.join(HOME, ".local", "share", "opencode", "openhermes")
|
|
9
|
-
const CACHE_ROOT = path.join(HOME, ".cache", "opencode", "openhermes")
|
|
10
|
-
const PKG_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
-
|
|
12
|
-
function resolveRoot(envVar, fallback) {
|
|
13
|
-
if (!isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) return fallback
|
|
14
|
-
const cwd = process.cwd()
|
|
15
|
-
const project = path.join(cwd, ".opencode", "openhermes")
|
|
16
|
-
try { fs.accessSync(path.join(project, "memory")); return project } catch {}
|
|
17
|
-
return fallback
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export function getDataRoot() {
|
|
22
|
-
return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", DATA_ROOT)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getCacheRoot() {
|
|
26
|
-
return CACHE_ROOT
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getMemoryRoot() {
|
|
30
|
-
return path.join(getDataRoot(), "memory")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function getRuntimeRoot() {
|
|
34
|
-
return path.join(getDataRoot(), "runtime")
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getRecallRoot() {
|
|
38
|
-
return path.join(getCacheRoot(), "recall")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function getArchiveRoot() {
|
|
42
|
-
return path.join(getDataRoot(), "archive")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function getSchemaRoot() {
|
|
46
|
-
return path.resolve(PKG_DIR, "..", "schemas")
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
package/lib/schema-validator.mjs
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from "./hardening.mjs"
|
|
2
|
-
|
|
3
|
-
const SUPPORTED_SCHEMA_KEYS = new Set(["type", "const", "enum", "format", "minimum", "maximum", "required", "properties", "items"])
|
|
4
|
-
const SCHEMA_METADATA_KEYS = new Set(["$schema", "title", "description", "default"])
|
|
5
|
-
|
|
6
|
-
function matchesType(type, value) {
|
|
7
|
-
if (type === "null") return value === null
|
|
8
|
-
if (type === "array") return Array.isArray(value)
|
|
9
|
-
if (type === "object") return isPlainObject(value)
|
|
10
|
-
if (type === "integer") return Number.isInteger(value)
|
|
11
|
-
if (type === "number") return typeof value === "number" && Number.isFinite(value)
|
|
12
|
-
return typeof value === type
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function validateNode(schema, value, at, errors) {
|
|
16
|
-
if (!isPlainObject(schema) || value === undefined) return
|
|
17
|
-
if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const)) errors.push(`${at} must equal ${JSON.stringify(schema.const)}`)
|
|
18
|
-
if (Array.isArray(schema.enum) && !schema.enum.some(option => JSON.stringify(option) === JSON.stringify(value))) errors.push(`${at} must be one of ${schema.enum.map(option => JSON.stringify(option)).join(", ")}`)
|
|
19
|
-
|
|
20
|
-
const types = Array.isArray(schema.type) ? schema.type : (schema.type ? [schema.type] : [])
|
|
21
|
-
if (types.length && !types.some(type => matchesType(type, value))) {
|
|
22
|
-
errors.push(`${at} must be ${types.join(" or ")}`)
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (typeof value === "string" && schema.format === "date-time" && Number.isNaN(Date.parse(value))) errors.push(`${at} must be a valid date-time string`)
|
|
27
|
-
if (typeof value === "number") {
|
|
28
|
-
if (schema.minimum !== undefined && value < schema.minimum) errors.push(`${at} must be >= ${schema.minimum}`)
|
|
29
|
-
if (schema.maximum !== undefined && value > schema.maximum) errors.push(`${at} must be <= ${schema.maximum}`)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (Array.isArray(value)) {
|
|
33
|
-
value.forEach((item, index) => validateNode(schema.items, item, `${at}[${index}]`, errors))
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (isPlainObject(value)) {
|
|
38
|
-
for (const key of Array.isArray(schema.required) ? schema.required : []) {
|
|
39
|
-
if (value[key] === undefined) errors.push(`${at}.${key} is required`)
|
|
40
|
-
}
|
|
41
|
-
for (const [key, child] of Object.entries(isPlainObject(schema.properties) ? schema.properties : {})) {
|
|
42
|
-
if (value[key] !== undefined) validateNode(child, value[key], `${at}.${key}`, errors)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validateSchema(schema, value, at = "$") {
|
|
48
|
-
const errors = []
|
|
49
|
-
validateNode(schema, value, at, errors)
|
|
50
|
-
return errors
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function visitSchemaNode(schema, at, unsupported) {
|
|
54
|
-
if (!isPlainObject(schema)) return
|
|
55
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
56
|
-
if (key === "properties") {
|
|
57
|
-
if (!isPlainObject(value)) unsupported.push(`${at}.properties must be an object`)
|
|
58
|
-
else for (const [propertyName, propertySchema] of Object.entries(value)) visitSchemaNode(propertySchema, `${at}.properties.${propertyName}`, unsupported)
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
61
|
-
if (key === "items") {
|
|
62
|
-
visitSchemaNode(value, `${at}.items`, unsupported)
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
if (key === "$defs" || key === "definitions") {
|
|
66
|
-
if (isPlainObject(value)) for (const defName of Object.keys(value)) visitSchemaNode(value[defName], `${at}.${key}.${defName}`, unsupported)
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
if (!SUPPORTED_SCHEMA_KEYS.has(key) && !SCHEMA_METADATA_KEYS.has(key)) unsupported.push(`${at} uses unsupported schema keyword "${key}"`)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function findUnsupportedSchemaKeywords(schema) {
|
|
74
|
-
const unsupported = []
|
|
75
|
-
visitSchemaNode(schema, "$", unsupported)
|
|
76
|
-
return unsupported
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export { findUnsupportedSchemaKeywords, isPlainObject, validateSchema }
|