akm-opencode 0.9.20202610010009 → 0.9.21202610012143
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/README.md +2 -2
- package/dist/index.js +90 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# akm-opencode
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for [AKM](https://github.com/itlackey/akm) `0.9.
|
|
3
|
+
OpenCode plugin for [AKM](https://github.com/itlackey/akm) `0.9.21`. It exposes exactly five public tools and uses lifecycle hooks to bring relevant AKM context into a session. The dependency is exact-pinned because the plugin calls AKM's in-process `dist/` APIs.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -56,7 +56,7 @@ The plugin subscribes to OpenCode lifecycle events. Hook failures are logged thr
|
|
|
56
56
|
|
|
57
57
|
The session observation buffer that retrospective feedback reads from survives every non-terminal event: it is bounded by `AKM_SESSION_BUFFER_MAX_ENTRIES` and dropped only on `session.deleted`. Discarding it at `session.idle` would empty it between turns, so "thanks, that worked" would credit nothing in exactly the sessions that used the most assets.
|
|
58
58
|
|
|
59
|
-
Automatic feedback skips references that AKM reports as ineligible, and no `akm_*` tool call submits feedback on its own. A *successful* `akm_show` / `akm_search` / `akm_curate` only inspected a concept, which is not evidence that it helped, so on OpenCode the positive signal comes from a retrospective confirmation instead. A *failed* call (an error result, a timeout, an ambiguous ref) is not feedback on the asset either, and is only logged. Same rules as the Claude plugin, which applies them to the `akm` subcommand of a Bash invocation.
|
|
59
|
+
Automatic feedback skips references that AKM reports as ineligible, and no `akm_*` tool call submits feedback on its own. A *successful* `akm_show` / `akm_search` / `akm_curate` only inspected a concept, which is not evidence that it helped, so on OpenCode the positive signal comes from a retrospective confirmation instead. A *failed* call (an error result, a timeout, an ambiguous ref) is not feedback on the asset either, and is only logged: its ref is not remembered as used, so a later "thanks, that worked" does not credit it and a later correction does not blame it. Same rules as the Claude plugin, which applies them to the `akm` subcommand of a Bash invocation.
|
|
60
60
|
|
|
61
61
|
### Automatic learning proposals
|
|
62
62
|
|
package/dist/index.js
CHANGED
|
@@ -119,6 +119,7 @@ function shouldSubmitAutomaticFeedback(signal) {
|
|
|
119
119
|
|
|
120
120
|
// ../claude/shared/memory-events.ts
|
|
121
121
|
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
122
|
+
import { homedir } from "os";
|
|
122
123
|
import path from "path";
|
|
123
124
|
|
|
124
125
|
// ../claude/shared/redaction.ts
|
|
@@ -376,7 +377,7 @@ function rotateIfOversized(filePath) {
|
|
|
376
377
|
|
|
377
378
|
// ../claude/shared/memory-events.ts
|
|
378
379
|
function getHarnessStateDir(harness) {
|
|
379
|
-
const root = process.env.XDG_STATE_HOME ?? path.join(
|
|
380
|
+
const root = process.env.XDG_STATE_HOME ?? path.join(homedir(), ".local", "state");
|
|
380
381
|
return path.join(root, harness === "claude-code" ? "akm-claude" : "akm-opencode");
|
|
381
382
|
}
|
|
382
383
|
function getEventLogPath(harness) {
|
|
@@ -1008,8 +1009,45 @@ function isFile(file) {
|
|
|
1008
1009
|
return false;
|
|
1009
1010
|
}
|
|
1010
1011
|
}
|
|
1012
|
+
var SCRIPT_EXTENSIONS = new Set([
|
|
1013
|
+
".sh",
|
|
1014
|
+
".ts",
|
|
1015
|
+
".js",
|
|
1016
|
+
".ps1",
|
|
1017
|
+
".cmd",
|
|
1018
|
+
".bat",
|
|
1019
|
+
".py",
|
|
1020
|
+
".rb",
|
|
1021
|
+
".go",
|
|
1022
|
+
".pl",
|
|
1023
|
+
".php",
|
|
1024
|
+
".lua",
|
|
1025
|
+
".r",
|
|
1026
|
+
".swift",
|
|
1027
|
+
".kt",
|
|
1028
|
+
".kts"
|
|
1029
|
+
]);
|
|
1030
|
+
function markdownFile(directPath) {
|
|
1031
|
+
return isFile(`${directPath}.md`) || directPath.endsWith(".md") && isFile(directPath);
|
|
1032
|
+
}
|
|
1033
|
+
function isAsset(type, directPath) {
|
|
1034
|
+
if (type === "skills")
|
|
1035
|
+
return isFile(path3.join(directPath, "SKILL.md"));
|
|
1036
|
+
if (type === "scripts")
|
|
1037
|
+
return SCRIPT_EXTENSIONS.has(path3.extname(directPath).toLowerCase()) && isFile(directPath);
|
|
1038
|
+
if (type === "secrets")
|
|
1039
|
+
return !/\.(lock|sensitive)$/.test(directPath) && isFile(directPath);
|
|
1040
|
+
if (type === "tasks")
|
|
1041
|
+
return isFile(`${directPath}.yml`);
|
|
1042
|
+
if (type === "env") {
|
|
1043
|
+
return isFile(`${directPath}.env`) || path3.basename(directPath) === "default" && isFile(path3.join(path3.dirname(directPath), ".env"));
|
|
1044
|
+
}
|
|
1045
|
+
return markdownFile(directPath) || type === "workflows" && isFile(`${directPath}.yml`);
|
|
1046
|
+
}
|
|
1047
|
+
var OUTSIDE_THEIR_DIRECTORY = new Set(["scripts", "agents", "commands", "facts", "instructions", "knowledge", "lessons", "memories", "sessions"]);
|
|
1011
1048
|
function conceptExistsInAnyBundle(conceptId, bundleRoots) {
|
|
1012
1049
|
const type = conceptId.split("/", 1)[0];
|
|
1050
|
+
const rest = conceptId.slice(type.length + 1);
|
|
1013
1051
|
for (const root of bundleRoots) {
|
|
1014
1052
|
if (!root)
|
|
1015
1053
|
continue;
|
|
@@ -1017,16 +1055,10 @@ function conceptExistsInAnyBundle(conceptId, bundleRoots) {
|
|
|
1017
1055
|
const directPath = path3.resolve(resolvedRoot, conceptId);
|
|
1018
1056
|
if (directPath !== resolvedRoot && !directPath.startsWith(`${resolvedRoot}${path3.sep}`))
|
|
1019
1057
|
continue;
|
|
1020
|
-
if (type
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (isFile(directPath))
|
|
1025
|
-
return true;
|
|
1026
|
-
} else {
|
|
1027
|
-
if (isFile(`${directPath}.md`) || directPath.endsWith(".md") && isFile(directPath))
|
|
1028
|
-
return true;
|
|
1029
|
-
if (type === "memories" && isFile(`${directPath}.derived.md`))
|
|
1058
|
+
if (isAsset(type, directPath))
|
|
1059
|
+
return true;
|
|
1060
|
+
if (OUTSIDE_THEIR_DIRECTORY.has(type) && rest.includes("/") && !rest.startsWith(`${type}/`)) {
|
|
1061
|
+
if (isAsset(type, path3.resolve(resolvedRoot, rest)))
|
|
1030
1062
|
return true;
|
|
1031
1063
|
}
|
|
1032
1064
|
}
|
|
@@ -1050,14 +1082,39 @@ function validateRefCandidates(candidates, bundleRoots) {
|
|
|
1050
1082
|
// ../claude/shared/recall-policy.ts
|
|
1051
1083
|
var STASH_BOILERPLATE = "This is an **AKM stash** \u2014 a structured knowledge repository that stores reusable";
|
|
1052
1084
|
var COMPACTION_CONTINUATION = "This session is being continued from a previous conversation";
|
|
1085
|
+
var TAG = /<(\/?)([A-Za-z][\w:.-]*)(?:\s[^<>]*)?>/g;
|
|
1086
|
+
function leadingBlockEnd(text) {
|
|
1087
|
+
const tags = new RegExp(TAG);
|
|
1088
|
+
let name;
|
|
1089
|
+
let depth = 0;
|
|
1090
|
+
for (let tag = tags.exec(text);tag; tag = tags.exec(text)) {
|
|
1091
|
+
if (name === undefined) {
|
|
1092
|
+
if (tag.index !== 0 || tag[1])
|
|
1093
|
+
return -1;
|
|
1094
|
+
name = tag[2];
|
|
1095
|
+
} else if (tag[2] !== name)
|
|
1096
|
+
continue;
|
|
1097
|
+
depth += tag[1] ? -1 : 1;
|
|
1098
|
+
if (depth === 0)
|
|
1099
|
+
return tag.index + tag[0].length;
|
|
1100
|
+
}
|
|
1101
|
+
return -1;
|
|
1102
|
+
}
|
|
1103
|
+
function stripLeadingEnvelopes(text) {
|
|
1104
|
+
let rest = text.trimStart();
|
|
1105
|
+
for (let end = leadingBlockEnd(rest);end > 0; end = leadingBlockEnd(rest))
|
|
1106
|
+
rest = rest.slice(end).trimStart();
|
|
1107
|
+
return rest;
|
|
1108
|
+
}
|
|
1053
1109
|
function isNonTaskPrompt(text) {
|
|
1054
|
-
const
|
|
1055
|
-
if (
|
|
1110
|
+
const typed = stripLeadingEnvelopes(text);
|
|
1111
|
+
if (typed === STASH_BOILERPLATE || typed.startsWith(COMPACTION_CONTINUATION))
|
|
1056
1112
|
return true;
|
|
1057
|
-
return
|
|
1113
|
+
return typed === "" ? text.trim() !== "" : typed.startsWith("<") && typed.includes("</");
|
|
1058
1114
|
}
|
|
1059
1115
|
function shouldRecall(prompt, options) {
|
|
1060
|
-
const
|
|
1116
|
+
const trimmed = prompt.trim();
|
|
1117
|
+
const text = stripLeadingEnvelopes(trimmed) || trimmed;
|
|
1061
1118
|
const lower = text.toLowerCase();
|
|
1062
1119
|
const scopeHints = [];
|
|
1063
1120
|
if (!text)
|
|
@@ -1219,7 +1276,7 @@ var retrospectiveState = new Map;
|
|
|
1219
1276
|
var cachedAkmBundleDir;
|
|
1220
1277
|
var PROPOSED_QUALITY_WARNING = "Do not treat proposed assets as curated until accepted.";
|
|
1221
1278
|
var AKM_WORKFLOW_INSTRUCTION = [
|
|
1222
|
-
"# AKM workflow (v0.9.
|
|
1279
|
+
"# AKM workflow (v0.9.21)",
|
|
1223
1280
|
"",
|
|
1224
1281
|
"Use AKM as a reusable knowledge and workflow bundle.",
|
|
1225
1282
|
"",
|
|
@@ -2373,12 +2430,12 @@ var AKM_HINTS_PREFIX = [
|
|
|
2373
2430
|
'- **`akm_search` (known name)** \u2014 use ONLY when you already know an asset exists (e.g. after `akm_show` returned "not found") and need to locate its exact ref. Do not use as a discovery tool.',
|
|
2374
2431
|
"- **`akm_show <bundle>//meta`** \u2014 when working in or with an unfamiliar bundle, read its optional `.meta/` orientation (purpose, key assets, conventions, maintainer) before diving in. `akm_show meta` reads your working bundle's `.meta/index.md`; `akm_show meta:<name>` reads other `.meta/` docs (e.g. `meta:about`). These docs are direct-read and never appear in `akm_search`.",
|
|
2375
2432
|
"",
|
|
2376
|
-
"
|
|
2433
|
+
"When an asset's content is wrong, stale or incomplete, call `akm_feedback <ref> negative` with a note saying <what is wrong and what should change>: negative feedback is what triggers a review and fix of the asset, so make the note specific. Call it with `positive` when an asset materially helped (it only improves ranking). A failed akm call is not feedback on the asset. Use `akm_remember` to persist durable learnings so future sessions inherit them.",
|
|
2377
2434
|
"",
|
|
2378
2435
|
AKM_WORKFLOW_INSTRUCTION
|
|
2379
2436
|
].join(`
|
|
2380
2437
|
`);
|
|
2381
|
-
var AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content,
|
|
2438
|
+
var AKM_CURATED_TAIL = "\n\nTip: call `akm_show <ref>` to fetch full content. If an asset is wrong, stale or incomplete, call `akm_feedback <ref> negative` with a note saying <what is wrong and what should change>: that queues it for review and a fix. Use `positive` when it helped.";
|
|
2382
2439
|
var AKM_CONTEXT_TRUNCATED_MARKER = `
|
|
2383
2440
|
|
|
2384
2441
|
[truncated for context]`;
|
|
@@ -3276,6 +3333,7 @@ ${hint}` : hint;
|
|
|
3276
3333
|
const candidateRefs = [...new Set([...allArgRefs, ...allOutputRefs])];
|
|
3277
3334
|
const parsedForRefs = isAkmTool ? parseToolOutput(output.output) : null;
|
|
3278
3335
|
const allRefs = isAkmTool && parsedForRefs ? extractToolRefs(input.tool, input.args, parsedForRefs) : validateRefCandidates(candidateRefs, [await getAkmBundleDir(logClient) ?? ""]);
|
|
3336
|
+
const failedCall = classifyToolFeedback(parsedForRefs) === "negative";
|
|
3279
3337
|
if (allRefs.length > 0) {
|
|
3280
3338
|
writeStructuredEvent({
|
|
3281
3339
|
event: "tool_ref_observed",
|
|
@@ -3285,13 +3343,15 @@ ${hint}` : hint;
|
|
|
3285
3343
|
refs: allRefs,
|
|
3286
3344
|
outcome: { status: "ok" }
|
|
3287
3345
|
});
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3346
|
+
if (!failedCall) {
|
|
3347
|
+
for (const ref of allRefs) {
|
|
3348
|
+
addBufferEntry(input.sessionID, {
|
|
3349
|
+
kind: "tool-ref",
|
|
3350
|
+
toolName: input.tool,
|
|
3351
|
+
ref,
|
|
3352
|
+
status: "unknown"
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3295
3355
|
}
|
|
3296
3356
|
}
|
|
3297
3357
|
if (input.tool === "read") {
|
|
@@ -3328,7 +3388,8 @@ ${hint}` : hint;
|
|
|
3328
3388
|
if ((input.tool === "akm_show" || input.tool === "akm_curate") && feedback !== "negative") {
|
|
3329
3389
|
noteShownRefs(input.sessionID, toolRefs);
|
|
3330
3390
|
}
|
|
3331
|
-
|
|
3391
|
+
if (!failedCall)
|
|
3392
|
+
noteRecentRefs(input.sessionID, toolRefs);
|
|
3332
3393
|
writeStructuredEvent({
|
|
3333
3394
|
event: "tool_observation",
|
|
3334
3395
|
sessionId: input.sessionID,
|
|
@@ -3337,7 +3398,7 @@ ${hint}` : hint;
|
|
|
3337
3398
|
refs: toolRefs,
|
|
3338
3399
|
outcome: { status: feedback === "negative" ? "failed" : "ok" }
|
|
3339
3400
|
});
|
|
3340
|
-
if (toolRefs.length > 0 && input.sessionID) {
|
|
3401
|
+
if (toolRefs.length > 0 && input.sessionID && !failedCall) {
|
|
3341
3402
|
for (const ref of toolRefs) {
|
|
3342
3403
|
addBufferEntry(input.sessionID, {
|
|
3343
3404
|
kind: "tool-ref",
|
|
@@ -3416,11 +3477,11 @@ ${hint}` : hint;
|
|
|
3416
3477
|
}
|
|
3417
3478
|
}),
|
|
3418
3479
|
akm_feedback: tool({
|
|
3419
|
-
description: "Record
|
|
3480
|
+
description: "Record feedback for a bundle asset. Negative feedback with a note queues the asset for review: the next improve run proposes a fix based on the note, so say what is wrong and what should change. Positive feedback raises the asset's ranking and never triggers a rewrite. Call it after akm_show when the asset's content materially helped, or proved wrong, stale or incomplete. A failed akm call is not feedback on the asset.",
|
|
3420
3481
|
args: {
|
|
3421
3482
|
ref: tool.schema.string().describe("Asset ref to record feedback for."),
|
|
3422
3483
|
sentiment: tool.schema.enum(["positive", "negative"]).describe("Whether the feedback is positive or negative."),
|
|
3423
|
-
note: tool.schema.string().optional().describe("
|
|
3484
|
+
note: tool.schema.string().optional().describe("What is wrong and what should change. Required for negative feedback; it guides the fix.")
|
|
3424
3485
|
},
|
|
3425
3486
|
async execute({ ref, sentiment, note }, context) {
|
|
3426
3487
|
const args = ["feedback", ref, sentiment === "positive" ? "--positive" : "--negative"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21202610012143",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for AKM with discovery tools, feedback, memory, and automatic learning proposals from corrections and recurring workflows.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@opencode-ai/plugin": "^1.18.14",
|
|
41
|
-
"akm-cli": "0.9.
|
|
41
|
+
"akm-cli": "0.9.21"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^26.1.1",
|