@jmtrin/opencode-kevin 1.2.0 → 1.3.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.
Files changed (141) hide show
  1. package/dist/plugin/index.d.ts +4 -3
  2. package/dist/plugin/index.js +122 -113
  3. package/dist/plugin/native.d.ts +2 -2
  4. package/dist/plugin/native.js +2 -2
  5. package/package.json +56 -68
  6. package/LICENSE +0 -21
  7. package/README.md +0 -605
  8. package/dist/migrations/001_initial.sql +0 -92
  9. package/dist/migrations/002_indexes.sql +0 -14
  10. package/dist/migrations/003_v02_signal.sql +0 -58
  11. package/dist/migrations/004_v03_knowledge.sql +0 -138
  12. package/dist/migrations/005_v04_signal.sql +0 -57
  13. package/dist/migrations/006_v05_glassbox.sql +0 -118
  14. package/dist/migrations/007_v06_pull.sql +0 -145
  15. package/dist/migrations/008_v07_truth.sql +0 -124
  16. package/dist/migrations/009_v08_team.sql +0 -100
  17. package/dist/migrations/010_v09_native.sql +0 -78
  18. package/dist/migrations/011_v10_proven.sql +0 -69
  19. package/dist/migrations/012_v11_drift.sql +0 -24
  20. package/dist/plugin/Archiver.d.ts +0 -42
  21. package/dist/plugin/Archiver.js +0 -83
  22. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  23. package/dist/plugin/ArtifactWriter.js +0 -291
  24. package/dist/plugin/CausalChain.d.ts +0 -22
  25. package/dist/plugin/CausalChain.js +0 -197
  26. package/dist/plugin/ChatBridge.d.ts +0 -41
  27. package/dist/plugin/ChatBridge.js +0 -103
  28. package/dist/plugin/ConflictDetector.d.ts +0 -35
  29. package/dist/plugin/ConflictDetector.js +0 -260
  30. package/dist/plugin/ContextInjector.d.ts +0 -222
  31. package/dist/plugin/ContextInjector.js +0 -533
  32. package/dist/plugin/ConventionMiner.d.ts +0 -35
  33. package/dist/plugin/ConventionMiner.js +0 -242
  34. package/dist/plugin/Curator.d.ts +0 -116
  35. package/dist/plugin/Curator.js +0 -324
  36. package/dist/plugin/DashboardHtml.d.ts +0 -5
  37. package/dist/plugin/DashboardHtml.js +0 -180
  38. package/dist/plugin/Feedback.d.ts +0 -67
  39. package/dist/plugin/Feedback.js +0 -120
  40. package/dist/plugin/HookLiveness.d.ts +0 -88
  41. package/dist/plugin/HookLiveness.js +0 -322
  42. package/dist/plugin/InjectionLedger.d.ts +0 -98
  43. package/dist/plugin/InjectionLedger.js +0 -295
  44. package/dist/plugin/LessonFixer.d.ts +0 -44
  45. package/dist/plugin/LessonFixer.js +0 -45
  46. package/dist/plugin/Materializer.d.ts +0 -83
  47. package/dist/plugin/Materializer.js +0 -181
  48. package/dist/plugin/MemoryService.d.ts +0 -360
  49. package/dist/plugin/MemoryService.js +0 -1242
  50. package/dist/plugin/Migrate.d.ts +0 -17
  51. package/dist/plugin/Migrate.js +0 -271
  52. package/dist/plugin/PatternMiner.d.ts +0 -49
  53. package/dist/plugin/PatternMiner.js +0 -132
  54. package/dist/plugin/QualityGate.d.ts +0 -124
  55. package/dist/plugin/QualityGate.js +0 -119
  56. package/dist/plugin/Reflector.d.ts +0 -91
  57. package/dist/plugin/Reflector.js +0 -307
  58. package/dist/plugin/RepoIdentity.d.ts +0 -124
  59. package/dist/plugin/RepoIdentity.js +0 -300
  60. package/dist/plugin/RepoTruth.d.ts +0 -80
  61. package/dist/plugin/RepoTruth.js +0 -599
  62. package/dist/plugin/Retrospective.d.ts +0 -17
  63. package/dist/plugin/Retrospective.js +0 -216
  64. package/dist/plugin/SharedLayer.d.ts +0 -159
  65. package/dist/plugin/SharedLayer.js +0 -462
  66. package/dist/plugin/Store.d.ts +0 -29
  67. package/dist/plugin/Store.js +0 -50
  68. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  69. package/dist/plugin/ToolCallObserver.js +0 -201
  70. package/dist/plugin/TuiActions.d.ts +0 -43
  71. package/dist/plugin/TuiActions.js +0 -181
  72. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  73. package/dist/plugin/TuiSnapshots.js +0 -158
  74. package/dist/plugin/columns.d.ts +0 -11
  75. package/dist/plugin/columns.js +0 -54
  76. package/dist/plugin/confidence.d.ts +0 -8
  77. package/dist/plugin/confidence.js +0 -34
  78. package/dist/plugin/contract.d.ts +0 -54
  79. package/dist/plugin/contract.js +0 -386
  80. package/dist/plugin/diff.d.ts +0 -8
  81. package/dist/plugin/diff.js +0 -182
  82. package/dist/plugin/escape.d.ts +0 -39
  83. package/dist/plugin/escape.js +0 -60
  84. package/dist/plugin/fingerprint.d.ts +0 -27
  85. package/dist/plugin/fingerprint.js +0 -73
  86. package/dist/plugin/inferability.d.ts +0 -32
  87. package/dist/plugin/inferability.js +0 -88
  88. package/dist/plugin/kevin_approve.d.ts +0 -34
  89. package/dist/plugin/kevin_approve.js +0 -52
  90. package/dist/plugin/kevin_audit.d.ts +0 -229
  91. package/dist/plugin/kevin_audit.js +0 -631
  92. package/dist/plugin/kevin_bench.d.ts +0 -10
  93. package/dist/plugin/kevin_bench.js +0 -82
  94. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  95. package/dist/plugin/kevin_conflicts.js +0 -50
  96. package/dist/plugin/kevin_contract.d.ts +0 -8
  97. package/dist/plugin/kevin_contract.js +0 -59
  98. package/dist/plugin/kevin_doctor.d.ts +0 -62
  99. package/dist/plugin/kevin_doctor.js +0 -196
  100. package/dist/plugin/kevin_facts.d.ts +0 -42
  101. package/dist/plugin/kevin_facts.js +0 -36
  102. package/dist/plugin/kevin_forget.d.ts +0 -33
  103. package/dist/plugin/kevin_forget.js +0 -260
  104. package/dist/plugin/kevin_native.d.ts +0 -29
  105. package/dist/plugin/kevin_native.js +0 -79
  106. package/dist/plugin/kevin_propose.d.ts +0 -23
  107. package/dist/plugin/kevin_propose.js +0 -14
  108. package/dist/plugin/kevin_publish.d.ts +0 -38
  109. package/dist/plugin/kevin_publish.js +0 -18
  110. package/dist/plugin/kevin_why.d.ts +0 -23
  111. package/dist/plugin/kevin_why.js +0 -111
  112. package/dist/plugin/memory-format.d.ts +0 -20
  113. package/dist/plugin/memory-format.js +0 -59
  114. package/dist/plugin/metrics.d.ts +0 -97
  115. package/dist/plugin/metrics.js +0 -288
  116. package/dist/plugin/okf-export.d.ts +0 -3
  117. package/dist/plugin/okf-export.js +0 -134
  118. package/dist/plugin/okf-import.d.ts +0 -76
  119. package/dist/plugin/okf-import.js +0 -271
  120. package/dist/plugin/okf.d.ts +0 -107
  121. package/dist/plugin/okf.js +0 -303
  122. package/dist/plugin/perf.d.ts +0 -35
  123. package/dist/plugin/perf.js +0 -174
  124. package/dist/plugin/query-tokenizer.d.ts +0 -13
  125. package/dist/plugin/query-tokenizer.js +0 -133
  126. package/dist/plugin/redact.d.ts +0 -2
  127. package/dist/plugin/redact.js +0 -18
  128. package/dist/plugin/replay-types.d.ts +0 -69
  129. package/dist/plugin/replay-types.js +0 -156
  130. package/dist/plugin/replay.d.ts +0 -36
  131. package/dist/plugin/replay.js +0 -202
  132. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  133. package/dist/plugin/sqlite-adapter.js +0 -81
  134. package/dist/plugin/time-ms.d.ts +0 -1
  135. package/dist/plugin/time-ms.js +0 -16
  136. package/dist/plugin/tui-types.d.ts +0 -59
  137. package/dist/plugin/tui-types.js +0 -4
  138. package/dist/plugin/tui.d.ts +0 -18
  139. package/dist/plugin/tui.js +0 -198
  140. package/dist/plugin/uuid.d.ts +0 -1
  141. package/dist/plugin/uuid.js +0 -50
@@ -1,202 +0,0 @@
1
- import { dirname, join } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { Archiver } from "./Archiver.js";
4
- import { CausalChain } from "./CausalChain.js";
5
- import { ContextInjector } from "./ContextInjector.js";
6
- import { InjectionLedger } from "./InjectionLedger.js";
7
- import { MemoryService } from "./MemoryService.js";
8
- import { Migrate } from "./Migrate.js";
9
- import { Reflector } from "./Reflector.js";
10
- import { Store } from "./Store.js";
11
- import { ToolCallObserver } from "./ToolCallObserver.js";
12
- import { Metrics } from "./metrics.js";
13
- const MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "migrations");
14
- export async function replay(transcript, opts) {
15
- const store = new Store({ path: opts?.dbPath ?? ":memory:" });
16
- await new Migrate(store, MIGRATIONS_DIR).run();
17
- // Freeze retrieval BEFORE any query happens (K5-008): deterministic
18
- // mode uses the DATE_NOW sentinel, ignores recency and never bumps.
19
- store
20
- .prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('deterministic_retrieval', '1')")
21
- .run();
22
- store
23
- .prepare("UPDATE kevin_settings SET value = '1' WHERE key = 'deterministic_retrieval'")
24
- .run();
25
- // v0.6.0 (K6-022) — the release floor (0.6) blocks every
26
- // single-observation memory the v0.5-era fixture exercises; the replay
27
- // harness preserves the v0.5 push semantics it was recorded under, the
28
- // same opt-out the legacy plugin harnesses apply.
29
- store
30
- .prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('injection_confidence_floor', '0')")
31
- .run();
32
- store
33
- .prepare("UPDATE kevin_settings SET value = '0' WHERE key = 'injection_confidence_floor'")
34
- .run();
35
- const metrics = new Metrics(store);
36
- const memoryService = new MemoryService(store, metrics);
37
- const observer = new ToolCallObserver(store, metrics);
38
- // v0.3.0 fix (mirrors index.ts) — the Reflector stamps
39
- // tool_calls.error_fingerprint with the stderr-based fingerprint the
40
- // error memory uses; without it the recurrence queries in settle and
41
- // the feedback loop never match (tool_calls.fingerprint is keyed on
42
- // `${tool}|${args}|${success}`, memories on a hash of stderr/stdout).
43
- const linkErrorStmt = store.prepare("UPDATE tool_calls SET error_fingerprint = ? WHERE id = ?");
44
- const reflector = new Reflector(memoryService, {
45
- throttleMs: 0,
46
- onLinkError: (callID, fp) => {
47
- linkErrorStmt.run(fp, callID);
48
- },
49
- }, metrics);
50
- const ledger = new InjectionLedger(store, metrics);
51
- const injector = new ContextInjector(memoryService, metrics, ledger);
52
- const causalChain = new CausalChain(store, memoryService, metrics);
53
- // The Archiver reads the clock through this accessor, which is updated
54
- // to the current event's `at` before each event is processed.
55
- let currentAt = new Date(transcript.events[0]?.at ?? Date.now());
56
- const archiver = new Archiver(store, memoryService, metrics, () => currentAt);
57
- const lastUserQueryBySession = new Map();
58
- const callCache = new Map();
59
- function resolveQuery(sessionId) {
60
- return lastUserQueryBySession.get(sessionId) ?? "";
61
- }
62
- for (const event of transcript.events) {
63
- currentAt = new Date(event.at);
64
- await handleEvent(event);
65
- }
66
- metrics.flush();
67
- const outcomeRows = store
68
- .prepare("SELECT outcome, COUNT(*) AS n FROM kevin_injections GROUP BY outcome")
69
- .all();
70
- const counts = {
71
- unmeasured: 0,
72
- effective: 0,
73
- ineffective: 0,
74
- inconclusive: 0,
75
- };
76
- for (const r of outcomeRows) {
77
- counts[r.outcome] = r.n;
78
- }
79
- const total = outcomeRows.reduce((acc, r) => acc + r.n, 0);
80
- const tokenValue = (key) => {
81
- const row = store
82
- .prepare("SELECT value FROM kevin_metrics WHERE key = ?")
83
- .get(key);
84
- return row?.value ?? 0;
85
- };
86
- return {
87
- transcript: transcript.name,
88
- memoriesCreated: totalMemories(store),
89
- injections: {
90
- total,
91
- effective: counts.effective,
92
- ineffective: counts.ineffective,
93
- inconclusive: counts.inconclusive,
94
- unmeasured: counts.unmeasured,
95
- },
96
- precisionRate: metrics.precisionRate(),
97
- coverageRate: metrics.coverageRate(),
98
- tokensInjected: {
99
- prePrompt: tokenValue("tokens_injected_pre_prompt"),
100
- compacting: tokenValue("tokens_injected_compacting"),
101
- },
102
- blocked: metrics.blockedSnapshot(),
103
- };
104
- async function handleEvent(event) {
105
- switch (event.kind) {
106
- case "session.created":
107
- // Mirror of the live wiring (index.ts): a fresh session must
108
- // not inherit the previous session's injection seen-set
109
- // (plan §5.1 rule 3). Without this, a multi-session
110
- // transcript would block every memory in session 2 with
111
- // `seen_this_session` while the live plugin would admit it.
112
- injector.onSessionCreated(event.sessionId);
113
- return;
114
- case "chat.message": {
115
- const derived = injector.deriveQuery([
116
- { role: "user", content: event.text },
117
- ]);
118
- if (derived.length > 0) {
119
- lastUserQueryBySession.set(event.sessionId, derived);
120
- }
121
- return;
122
- }
123
- case "tool.before": {
124
- callCache.set(event.callId, {
125
- tool: event.tool,
126
- args: event.args,
127
- });
128
- observer.onBefore({
129
- tool: event.tool,
130
- args: event.args,
131
- sessionId: event.sessionId,
132
- callID: event.callId,
133
- projectId: null,
134
- }, {});
135
- return;
136
- }
137
- case "tool.after": {
138
- const cached = callCache.get(event.callId);
139
- const tool = cached?.tool ?? "unknown";
140
- const args = cached?.args ?? {};
141
- const stderr = event.stderr ?? "";
142
- const stdout = event.stdout ?? "";
143
- observer.onAfter({
144
- tool,
145
- args,
146
- sessionId: event.sessionId,
147
- callID: event.callId,
148
- projectId: null,
149
- }, { success: event.success, stdout, stderr, exitCode: event.exitCode });
150
- if (!event.success) {
151
- await reflector.invoke({
152
- toolName: tool,
153
- argsSummary: observer.summarizeArgs(args),
154
- stderr,
155
- stdout,
156
- exitCode: event.exitCode,
157
- errorType: observer.inferErrorType(stderr, stdout, event.exitCode),
158
- sessionId: event.sessionId,
159
- callID: event.callId,
160
- projectId: null,
161
- });
162
- }
163
- else {
164
- causalChain.onSuccess(tool, args, null, event.sessionId);
165
- }
166
- return;
167
- }
168
- case "system.transform": {
169
- const query = resolveQuery(event.sessionId);
170
- if (!query)
171
- return;
172
- const output = { system: [] };
173
- injector.onSystemTransform({
174
- sessionID: event.sessionId,
175
- messages: [{ role: "user", content: query }],
176
- }, output);
177
- return;
178
- }
179
- case "compacting": {
180
- const query = resolveQuery(event.sessionId);
181
- if (!query)
182
- return;
183
- const output = { context: [] };
184
- injector.onCompacting({
185
- sessionID: event.sessionId,
186
- messages: [{ role: "user", content: query }],
187
- }, output);
188
- return;
189
- }
190
- case "session.idle": {
191
- ledger.settle(event.sessionId);
192
- archiver.run();
193
- metrics.flush();
194
- return;
195
- }
196
- }
197
- }
198
- }
199
- function totalMemories(store) {
200
- const row = store.prepare("SELECT COUNT(*) AS n FROM memories").get();
201
- return row.n ?? 0;
202
- }
@@ -1,12 +0,0 @@
1
- export interface SqliteStatement {
2
- get(...params: unknown[]): unknown;
3
- all(...params: unknown[]): unknown[];
4
- run(...params: unknown[]): void;
5
- }
6
- export interface SqliteAdapter {
7
- prepare(sql: string): SqliteStatement;
8
- transaction<T>(fn: () => T): () => T;
9
- exec(sql: string): void;
10
- close(): void;
11
- }
12
- export declare function createDatabase(path: string): SqliteAdapter;
@@ -1,81 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { fileURLToPath } from "node:url";
3
- const localRequire = createRequire(typeof import.meta.url === "string"
4
- ? import.meta.url
5
- : fileURLToPath(`file://${process.cwd()}/`));
6
- function detectRuntime() {
7
- if (typeof globalThis.Bun !== "undefined") {
8
- return "bun";
9
- }
10
- return "node";
11
- }
12
- function loadBunDatabase(path) {
13
- const mod = localRequire("bun:sqlite");
14
- return new mod.Database(path);
15
- }
16
- /**
17
- * Wrapper sobre `node:sqlite` (DatabaseSync, estable en Node 24+).
18
- * API equivalente a better-sqlite3 excepto `transaction`, que implementamos
19
- * manualmente con BEGIN/COMMIT/ROLLBACK.
20
- */
21
- class NodeSqliteAdapter {
22
- db;
23
- constructor(db) {
24
- this.db = db;
25
- }
26
- prepare(sql) {
27
- return this.db.prepare(sql);
28
- }
29
- exec(sql) {
30
- this.db.exec(sql);
31
- }
32
- close() {
33
- this.db.close();
34
- }
35
- transaction(fn) {
36
- return () => {
37
- this.db.exec("BEGIN");
38
- try {
39
- const result = fn();
40
- this.db.exec("COMMIT");
41
- return result;
42
- }
43
- catch (err) {
44
- try {
45
- this.db.exec("ROLLBACK");
46
- }
47
- catch {
48
- // ignore rollback errors
49
- }
50
- throw err;
51
- }
52
- };
53
- }
54
- }
55
- function loadNodeSqliteDatabase(path) {
56
- const mod = localRequire("node:sqlite");
57
- return new NodeSqliteAdapter(new mod.DatabaseSync(path));
58
- }
59
- function loadBetterSqliteDatabase(path) {
60
- const D = localRequire("better-sqlite3");
61
- const Ctor = "default" in D ? D.default : D;
62
- return new Ctor(path);
63
- }
64
- function loadNodeDatabase(path) {
65
- // 1) Prefer node:sqlite (built-in, sin binarios nativos que descargar).
66
- // Estable en Node 24+ sin flag; en Node 22/23 requiere --experimental-sqlite.
67
- // Si require falla (modulo ausente o flag deshabilitado), cae al fallback.
68
- try {
69
- return loadNodeSqliteDatabase(path);
70
- }
71
- catch {
72
- // 2) Fallback a better-sqlite3 (Node 20 o Node 22/23 sin flag).
73
- return loadBetterSqliteDatabase(path);
74
- }
75
- }
76
- export function createDatabase(path) {
77
- if (detectRuntime() === "bun") {
78
- return loadBunDatabase(path);
79
- }
80
- return loadNodeDatabase(path);
81
- }
@@ -1 +0,0 @@
1
- export declare function toMs(legacyValue: string | null | undefined, msValue: number | null | undefined): number | null;
@@ -1,16 +0,0 @@
1
- // v1.1.0 (K11-003/K11-004 / plan §5.2, D11-01) — millisecond helper.
2
- // Readers prefer _ms and fall back to legacy second-granularity column.
3
- // The helper is the single implementation imported by InjectionLedger and
4
- // CausalChain (moved here in K11-004 to avoid duplication).
5
- export function toMs(legacyValue, msValue) {
6
- if (typeof msValue === "number" && !Number.isNaN(msValue))
7
- return msValue;
8
- if (!legacyValue)
9
- return null;
10
- // SQLite datetime('now') is 'YYYY-MM-DD HH:MM:SS' UTC
11
- const iso = legacyValue.includes("T")
12
- ? legacyValue
13
- : `${legacyValue.replace(" ", "T")}Z`;
14
- const n = Date.parse(iso);
15
- return Number.isNaN(n) ? null : n;
16
- }
@@ -1,59 +0,0 @@
1
- export interface ProposalView {
2
- readonly id: string;
3
- readonly kind: string;
4
- readonly target_path: string;
5
- readonly diff: string;
6
- readonly memory_ids: readonly string[];
7
- readonly created_at: string;
8
- readonly truncated?: boolean;
9
- readonly token?: string;
10
- }
11
- export interface ConflictView {
12
- readonly id: string;
13
- readonly kind: string;
14
- readonly a_summary: string;
15
- readonly b_summary: string;
16
- readonly opened_at: string;
17
- }
18
- export interface HealthView {
19
- readonly verdict: string;
20
- readonly reason: string;
21
- readonly hooks: readonly {
22
- readonly hook: string;
23
- readonly state: string;
24
- readonly fire_count: number;
25
- readonly expected_count: number;
26
- }[];
27
- readonly perf: readonly {
28
- readonly scope: string;
29
- readonly p95: number;
30
- readonly budget_p95: number;
31
- readonly within_budget: boolean;
32
- }[];
33
- readonly contract_digest: string;
34
- readonly counters: Record<string, number>;
35
- }
36
- export interface TuiSnapshotSet {
37
- readonly generatedAt: string;
38
- readonly proposals: readonly ProposalView[];
39
- readonly conflicts: readonly ConflictView[];
40
- readonly health: HealthView;
41
- }
42
- export type TuiAction = {
43
- readonly type: "approve";
44
- readonly proposalId: string;
45
- readonly token: string;
46
- } | {
47
- readonly type: "reject";
48
- readonly proposalId: string;
49
- readonly token: string;
50
- readonly note?: string;
51
- } | {
52
- readonly type: "acknowledge";
53
- readonly conflictId: string;
54
- };
55
- export interface ActionResult {
56
- readonly action: TuiAction;
57
- readonly status: "applied" | "rejected" | "stale_skipped" | "error";
58
- readonly detail?: string;
59
- }
@@ -1,4 +0,0 @@
1
- // v1.2.0 (K12-002 / plan §4.2-§4.3) — shared view types (type-only module).
2
- // This file MUST contain only type/interface exports — zero runtime values.
3
- // The TUI module may import it ONLY as `import type`.
4
- export {};
@@ -1,18 +0,0 @@
1
- import type { TuiPlugin } from "@opencode-ai/plugin/tui";
2
- import type { ConflictView, HealthView, ProposalView } from "./tui-types.js";
3
- export declare function tuiRoot(): string;
4
- export declare function readJsonSafe(name: string): {
5
- data: unknown;
6
- } | {
7
- error: "missing" | "corrupt";
8
- };
9
- export declare function truncateSummary(text: string, max?: number): string;
10
- export declare function formatProposalRow(p: ProposalView): string;
11
- export declare function formatConflictRow(c: ConflictView): string;
12
- export declare function formatHealthVerdict(h: HealthView): string;
13
- export declare const tui: TuiPlugin;
14
- declare const _default: {
15
- id: string;
16
- tui: TuiPlugin;
17
- };
18
- export default _default;
@@ -1,198 +0,0 @@
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 };
@@ -1 +0,0 @@
1
- export declare function uuidv7(): string;
@@ -1,50 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- // UUID v7 generator — timestamp-ordered IDs.
3
- // Format: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
4
- // First 48 bits: Unix timestamp (ms). Version nibble set to 7.
5
- // Variant nibble starts with binary 10.
6
- let lastTs = 0;
7
- let lastRandom = Buffer.alloc(10);
8
- function toHex(bytes) {
9
- return bytes.toString("hex");
10
- }
11
- export function uuidv7() {
12
- let ts = Date.now();
13
- if (ts < lastTs)
14
- ts = lastTs;
15
- if (ts === lastTs) {
16
- for (let i = 9; i >= 0; i--) {
17
- if (lastRandom[i] === 0xff) {
18
- lastRandom[i] = 0;
19
- }
20
- else {
21
- lastRandom[i]++;
22
- break;
23
- }
24
- }
25
- }
26
- else {
27
- lastRandom = randomBytes(10);
28
- }
29
- lastTs = ts;
30
- const rand = lastRandom;
31
- const bytes = Buffer.alloc(16);
32
- bytes[0] = Math.floor(ts / 2 ** 40) & 0xff;
33
- bytes[1] = Math.floor(ts / 2 ** 32) & 0xff;
34
- bytes[2] = (ts >>> 24) & 0xff;
35
- bytes[3] = (ts >>> 16) & 0xff;
36
- bytes[4] = (ts >>> 8) & 0xff;
37
- bytes[5] = ts & 0xff;
38
- bytes[6] = 0x70 | (rand[0] & 0x0f);
39
- bytes[7] = rand[1];
40
- bytes[8] = 0x80 | (rand[2] & 0x3f);
41
- bytes[9] = rand[3];
42
- bytes[10] = rand[4];
43
- bytes[11] = rand[5];
44
- bytes[12] = rand[6];
45
- bytes[13] = rand[7];
46
- bytes[14] = rand[8];
47
- bytes[15] = rand[9];
48
- const hex = toHex(bytes);
49
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
50
- }