@jmtrin/opencode-kevin 1.0.0 → 1.2.0
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 +54 -8
- package/dist/migrations/001_initial.sql +91 -91
- package/dist/migrations/002_indexes.sql +13 -13
- package/dist/migrations/003_v02_signal.sql +57 -57
- package/dist/migrations/004_v03_knowledge.sql +138 -138
- package/dist/migrations/005_v04_signal.sql +57 -57
- package/dist/migrations/006_v05_glassbox.sql +118 -118
- package/dist/migrations/007_v06_pull.sql +144 -144
- package/dist/migrations/012_v11_drift.sql +24 -0
- package/dist/plugin/Archiver.js +2 -17
- package/dist/plugin/CausalChain.js +32 -13
- package/dist/plugin/ChatBridge.d.ts +41 -0
- package/dist/plugin/ChatBridge.js +103 -0
- package/dist/plugin/ConflictDetector.js +7 -29
- package/dist/plugin/DashboardHtml.d.ts +5 -0
- package/dist/plugin/DashboardHtml.js +180 -0
- package/dist/plugin/Feedback.js +2 -19
- package/dist/plugin/HookLiveness.d.ts +1 -0
- package/dist/plugin/HookLiveness.js +11 -27
- package/dist/plugin/InjectionLedger.js +104 -57
- package/dist/plugin/Materializer.js +1 -88
- package/dist/plugin/MemoryService.d.ts +59 -1
- package/dist/plugin/MemoryService.js +13 -106
- package/dist/plugin/Migrate.js +5 -0
- package/dist/plugin/Retrospective.js +7 -0
- package/dist/plugin/ToolCallObserver.js +18 -5
- package/dist/plugin/TuiActions.d.ts +43 -0
- package/dist/plugin/TuiActions.js +181 -0
- package/dist/plugin/TuiSnapshots.d.ts +24 -0
- package/dist/plugin/TuiSnapshots.js +158 -0
- package/dist/plugin/capabilities.d.ts +2 -0
- package/dist/plugin/capabilities.js +3 -0
- package/dist/plugin/columns.d.ts +11 -0
- package/dist/plugin/columns.js +54 -0
- package/dist/plugin/contract.d.ts +8 -0
- package/dist/plugin/contract.js +23 -5
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +315 -10
- package/dist/plugin/kevin_audit.d.ts +17 -1
- package/dist/plugin/kevin_audit.js +69 -1
- package/dist/plugin/kevin_forget.d.ts +33 -0
- package/dist/plugin/kevin_forget.js +260 -0
- package/dist/plugin/kevin_why.js +1 -18
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +8 -0
- package/dist/plugin/query-tokenizer.js +56 -8
- package/dist/plugin/time-ms.d.ts +1 -0
- package/dist/plugin/time-ms.js +16 -0
- package/dist/plugin/tui-types.d.ts +59 -0
- package/dist/plugin/tui-types.js +4 -0
- package/dist/plugin/tui.d.ts +18 -0
- package/dist/plugin/tui.js +198 -0
- package/package.json +8 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// v1.2.0 (K12-008 skeleton + K12-009 panels / plan §4.4 R1, D12-02) — TUI module (target-exclusive, conditional on K12-016 GO).
|
|
2
|
+
// Allowed imports ONLY: @opencode-ai/plugin/tui, node:fs, node:path, node:os, import type from ./tui-types.js
|
|
3
|
+
// No console.log; user feedback via host toast API.
|
|
4
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export function tuiRoot() {
|
|
8
|
+
return join(homedir(), ".opencode-kevin", "tui");
|
|
9
|
+
}
|
|
10
|
+
export function readJsonSafe(name) {
|
|
11
|
+
const path = join(tuiRoot(), name);
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(path, "utf8");
|
|
14
|
+
try {
|
|
15
|
+
return { data: JSON.parse(raw) };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { error: "corrupt" };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const code = err?.code;
|
|
23
|
+
if (code === "ENOENT")
|
|
24
|
+
return { error: "missing" };
|
|
25
|
+
return { error: "corrupt" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// --- Pure helpers (K12-009) — unit-testable, no host dependency ---
|
|
29
|
+
export function truncateSummary(text, max = 80) {
|
|
30
|
+
if (text.length <= max)
|
|
31
|
+
return text;
|
|
32
|
+
return `${text.slice(0, max - 1)}…`;
|
|
33
|
+
}
|
|
34
|
+
export function formatProposalRow(p) {
|
|
35
|
+
const trunc = p.truncated ? " [truncated]" : "";
|
|
36
|
+
const ids = p.memory_ids.length ? ` memories:${p.memory_ids.join(",")}` : "";
|
|
37
|
+
return `${p.id} ${p.kind} ${p.target_path} ${p.created_at}${trunc}${ids}`;
|
|
38
|
+
}
|
|
39
|
+
export function formatConflictRow(c) {
|
|
40
|
+
return `${c.kind} ${c.id} A:${truncateSummary(c.a_summary)} B:${truncateSummary(c.b_summary)}`;
|
|
41
|
+
}
|
|
42
|
+
export function formatHealthVerdict(h) {
|
|
43
|
+
return `${h.verdict} — ${h.reason} — ${h.contract_digest}`;
|
|
44
|
+
}
|
|
45
|
+
// Mailbox writer — atomic tmp+rename, append semantics (pure fs, no network)
|
|
46
|
+
function writeMailboxAction(action) {
|
|
47
|
+
const dir = tuiRoot();
|
|
48
|
+
mkdirSync(dir, { recursive: true });
|
|
49
|
+
const target = join(dir, "actions.json");
|
|
50
|
+
let existing = null;
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(target, "utf8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (parsed && Array.isArray(parsed.actions)) {
|
|
55
|
+
existing = {
|
|
56
|
+
issuedAt: String(parsed.issuedAt ?? new Date().toISOString()),
|
|
57
|
+
actions: parsed.actions,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// missing/corrupt → start fresh
|
|
63
|
+
}
|
|
64
|
+
const next = {
|
|
65
|
+
issuedAt: new Date().toISOString(),
|
|
66
|
+
actions: existing ? [...existing.actions, action] : [action],
|
|
67
|
+
};
|
|
68
|
+
const tmp = `${target}.tmp`;
|
|
69
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), "utf8");
|
|
70
|
+
renameSync(tmp, target);
|
|
71
|
+
}
|
|
72
|
+
function emptyState(reason) {
|
|
73
|
+
return reason;
|
|
74
|
+
}
|
|
75
|
+
export const tui = async (api) => {
|
|
76
|
+
// Helper to show toast via host
|
|
77
|
+
const toast = (message, variant = "info") => {
|
|
78
|
+
try {
|
|
79
|
+
api.ui.toast({ message, variant });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// best-effort
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
api.route.register([
|
|
86
|
+
{
|
|
87
|
+
name: "kevin",
|
|
88
|
+
render: () => {
|
|
89
|
+
// Re-read on focus (caller invokes render on focus)
|
|
90
|
+
const proposalsRes = readJsonSafe("proposals.json");
|
|
91
|
+
const conflictsRes = readJsonSafe("conflicts.json");
|
|
92
|
+
const healthRes = readJsonSafe("health.json");
|
|
93
|
+
if ("error" in proposalsRes ||
|
|
94
|
+
"error" in conflictsRes ||
|
|
95
|
+
"error" in healthRes) {
|
|
96
|
+
const reason = "error" in proposalsRes
|
|
97
|
+
? `proposals.json: ${proposalsRes.error}`
|
|
98
|
+
: "error" in conflictsRes
|
|
99
|
+
? `conflicts.json: ${conflictsRes.error}`
|
|
100
|
+
: `health.json: ${healthRes.error}`;
|
|
101
|
+
return emptyState(`no snapshots yet — open an opencode session with the plugin enabled (${reason})`);
|
|
102
|
+
}
|
|
103
|
+
const proposals = proposalsRes.data;
|
|
104
|
+
const conflicts = conflictsRes.data;
|
|
105
|
+
const health = healthRes.data;
|
|
106
|
+
// Skeleton counts + tabular summaries (full JSX rendering is host-driven;
|
|
107
|
+
// this string representation carries the same data for headless verification).
|
|
108
|
+
// Interactive flows (Enter→diff, a→approve, r→reject, x→acknowledge) are exposed as keymap commands below
|
|
109
|
+
// and via api.ui.Dialog* when a host renders the route with Solid JSX — the string fallback ensures degrade-to-empty discipline.
|
|
110
|
+
const proposalLines = Array.isArray(proposals) && proposals.length
|
|
111
|
+
? proposals
|
|
112
|
+
.map((p) => `· ${formatProposalRow(p)}\n diff: ${truncateSummary(p.diff, 120)}${p.truncated ? " [truncated]" : ""}`)
|
|
113
|
+
.join("\n")
|
|
114
|
+
: " (no pending proposals)";
|
|
115
|
+
const conflictLines = Array.isArray(conflicts) && conflicts.length
|
|
116
|
+
? conflicts.map((c) => `· ${formatConflictRow(c)}`).join("\n")
|
|
117
|
+
: " (no open conflicts)";
|
|
118
|
+
const healthLine = health ? formatHealthVerdict(health) : "unknown";
|
|
119
|
+
const hooksLine = health?.hooks?.length
|
|
120
|
+
? health.hooks
|
|
121
|
+
.map((h) => ` ${h.hook} ${h.state} ${h.fire_count}/${h.expected_count}`)
|
|
122
|
+
.join("\n")
|
|
123
|
+
: " (no hooks)";
|
|
124
|
+
const perfLine = health?.perf?.length
|
|
125
|
+
? health.perf
|
|
126
|
+
.map((p) => ` ${p.scope} p95:${p.p95} budget:${p.budget_p95} ${p.within_budget ? "ok" : "OVER"}`)
|
|
127
|
+
.join("\n")
|
|
128
|
+
: " (no perf)";
|
|
129
|
+
const countersLine = health?.counters
|
|
130
|
+
? Object.entries(health.counters)
|
|
131
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
132
|
+
.join(" ")
|
|
133
|
+
: "(no counters)";
|
|
134
|
+
const msg = [
|
|
135
|
+
`Kevin — Proposals (${Array.isArray(proposals) ? proposals.length : 0})`,
|
|
136
|
+
proposalLines,
|
|
137
|
+
"",
|
|
138
|
+
`Conflicts (${Array.isArray(conflicts) ? conflicts.length : 0})`,
|
|
139
|
+
conflictLines,
|
|
140
|
+
"",
|
|
141
|
+
`Health — ${healthLine}`,
|
|
142
|
+
"hooks:",
|
|
143
|
+
hooksLine,
|
|
144
|
+
"perf:",
|
|
145
|
+
perfLine,
|
|
146
|
+
`counters: ${countersLine}`,
|
|
147
|
+
"",
|
|
148
|
+
"Keys: Enter=diff · a=approve · r=reject · x=acknowledge (via command palette) · k=open",
|
|
149
|
+
].join("\n");
|
|
150
|
+
return emptyState(msg);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
// Keymap layer: `k` opens the kevin route; also expose approve/reject/acknowledge commands for palette.
|
|
155
|
+
try {
|
|
156
|
+
const km = api.keymap;
|
|
157
|
+
km.registerLayer?.({
|
|
158
|
+
commands: {
|
|
159
|
+
"kevin.open": {
|
|
160
|
+
title: "Kevin — open",
|
|
161
|
+
description: "Open the Kevin route",
|
|
162
|
+
},
|
|
163
|
+
"kevin.proposal.approve": { title: "Kevin — approve proposal" },
|
|
164
|
+
"kevin.proposal.reject": { title: "Kevin — reject proposal" },
|
|
165
|
+
"kevin.conflict.acknowledge": { title: "Kevin — acknowledge conflict" },
|
|
166
|
+
},
|
|
167
|
+
bindings: {
|
|
168
|
+
"kevin.open": "k",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// best-effort
|
|
174
|
+
}
|
|
175
|
+
// Expose mailbox writers via command handlers (invoked from palette or future JSX buttons).
|
|
176
|
+
// These are also callable from tests via exported helpers — the route render's interactive dialogs
|
|
177
|
+
// would call the same writeMailboxAction in a real host with DialogConfirm/Select.
|
|
178
|
+
void writeMailboxAction;
|
|
179
|
+
// Attach helper closures to api for potential solid JSX callbacks (not part of typed API — cast).
|
|
180
|
+
const extended = api;
|
|
181
|
+
extended.kevinTui = {
|
|
182
|
+
approve: (proposalId, token) => {
|
|
183
|
+
writeMailboxAction({ type: "approve", proposalId, token });
|
|
184
|
+
toast("queued — applies at session idle", "info");
|
|
185
|
+
},
|
|
186
|
+
reject: (proposalId, token, note) => {
|
|
187
|
+
writeMailboxAction(note !== undefined
|
|
188
|
+
? { type: "reject", proposalId, token, note }
|
|
189
|
+
: { type: "reject", proposalId, token });
|
|
190
|
+
toast("queued — applies at session idle", "info");
|
|
191
|
+
},
|
|
192
|
+
acknowledge: (conflictId) => {
|
|
193
|
+
writeMailboxAction({ type: "acknowledge", conflictId });
|
|
194
|
+
toast("queued — applies at session idle", "info");
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
export default { id: "opencode-kevin", tui };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmtrin/opencode-kevin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Kevin — Observa y aprende: capa de aprendizaje para OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/plugin/index.d.ts",
|
|
11
11
|
"import": "./dist/plugin/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./tui": {
|
|
14
|
+
"types": "./dist/plugin/tui.d.ts",
|
|
15
|
+
"import": "./dist/plugin/tui.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": ["dist/plugin", "dist/migrations"],
|
|
@@ -27,6 +31,7 @@
|
|
|
27
31
|
"bench": "node --import tsx scripts/bench.ts",
|
|
28
32
|
"gen:corpus": "node --import tsx scripts/gen-corpus.ts",
|
|
29
33
|
"bench:check": "node --import tsx scripts/bench-check.ts",
|
|
34
|
+
"bench:regress": "tsx scripts/bench-regress.ts",
|
|
30
35
|
"replay": "node --import tsx scripts/replay.ts",
|
|
31
36
|
"measure:mix": "node --import tsx scripts/measure-mix.ts"
|
|
32
37
|
},
|
|
@@ -45,7 +50,8 @@
|
|
|
45
50
|
"vitest": "^2.1.8"
|
|
46
51
|
},
|
|
47
52
|
"engines": {
|
|
48
|
-
"node": ">=22.5.0"
|
|
53
|
+
"node": ">=22.5.0",
|
|
54
|
+
"opencode": "^1.18.0"
|
|
49
55
|
},
|
|
50
56
|
"repository": {
|
|
51
57
|
"type": "git",
|