kibi-opencode 0.15.2 → 0.16.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/README.md +33 -5
- package/dist/auto-update.d.ts +31 -0
- package/dist/auto-update.js +396 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +3 -0
- package/dist/init-kibi-alias.js +3 -1
- package/dist/plugin.js +36 -5
- package/dist/version-metadata.json +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
OpenCode plugin for Kibi - repo-local, per-branch, queryable knowledge base.
|
|
4
4
|
|
|
5
|
+
`kibi-opencode` is optional. It provides OpenCode-specific guidance, startup UX,
|
|
6
|
+
and background sync/check maintenance, but it does not ship a replacement `kibi`
|
|
7
|
+
CLI or `kibi-mcp` server binary. Install and configure the base `kibi-cli`,
|
|
8
|
+
`kibi-mcp`, and `kibi-core` packages in the project first; add this plugin only
|
|
9
|
+
when you want OpenCode-specific behavior.
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -16,6 +22,28 @@ Or via OpenCode's plugin system in `opencode.json`:
|
|
|
16
22
|
}
|
|
17
23
|
```
|
|
18
24
|
|
|
25
|
+
By default, the plugin checks npm on OpenCode startup and refreshes only the
|
|
26
|
+
cached `kibi-opencode` plugin package when a newer version is available. The
|
|
27
|
+
base `kibi-cli`, `kibi-mcp`, and `kibi-core` project dependencies are never
|
|
28
|
+
updated by this plugin updater.
|
|
29
|
+
|
|
30
|
+
Use an exact semver entry when you want a fixed plugin version:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"plugin": ["kibi-opencode@0.15.0"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Exact semver pins are respected: the updater will notify that a newer plugin is
|
|
39
|
+
available, but it will not replace the pinned version until you change the
|
|
40
|
+
plugin array. Dist-tags such as `kibi-opencode@latest` remain auto-updatable.
|
|
41
|
+
|
|
42
|
+
Configure the MCP server separately with the project-local `kibi-mcp` binary.
|
|
43
|
+
The plugin may run internal maintenance commands such as `kibi sync` and
|
|
44
|
+
`kibi check`, so the project-local `kibi` CLI should resolve from the OpenCode
|
|
45
|
+
process environment.
|
|
46
|
+
|
|
19
47
|
## Features
|
|
20
48
|
|
|
21
49
|
### Smart Enforcement
|
|
@@ -127,6 +155,8 @@ The plugin injects guidance into OpenCode sessions to improve agent grounding. U
|
|
|
127
155
|
|
|
128
156
|
OpenCode exposes Kibi MCP prompts as slash commands. The \`/init-kibi\` command triggers the \`kb_autopilot_generate\` workflow to assist in retroactive bootstrap using only public MCP tools.
|
|
129
157
|
|
|
158
|
+
OpenCode may show Kibi MCP tools with the configured server prefix. For example, canonical MCP names such as `kb_autopilot_generate`, `kb_upsert`, and `kb_check` can appear to agents as `kibi_kb_autopilot_generate`, `kibi_kb_upsert`, and `kibi_kb_check`. Use the visible `kibi_kb_*` identifier when OpenCode requires an exact tool name; it maps to the same MCP operation.
|
|
159
|
+
|
|
130
160
|
### Discovery-first MCP guidance
|
|
131
161
|
|
|
132
162
|
Agent-visible guidance is intentionally limited to the curated public MCP surface:
|
|
@@ -134,6 +164,8 @@ Agent-visible guidance is intentionally limited to the curated public MCP surfac
|
|
|
134
164
|
- Discovery/reporting: `kb_search`, `kb_query`, `kb_status`, `kb_find_gaps`, `kb_coverage`, `kb_graph`
|
|
135
165
|
- Mutation/validation: `kb_upsert`, `kb_delete`, `kb_check`
|
|
136
166
|
|
|
167
|
+
In OpenCode tool pickers or logs, these may be displayed with the `kibi_` server prefix, for example `kibi_kb_search` or `kibi_kb_upsert`.
|
|
168
|
+
|
|
137
169
|
The plugin guidance prefers `kb_search` for broad discovery, then `kb_query` for exact/source-linked follow-up.
|
|
138
170
|
|
|
139
171
|
### Background Sync Operations
|
|
@@ -162,6 +194,7 @@ Config files (project overrides global):
|
|
|
162
194
|
| Key | Type | Default | Description |
|
|
163
195
|
|-----|------|---------|-------------|
|
|
164
196
|
| `enabled` | boolean | `true` | Enable/disable all plugin features |
|
|
197
|
+
| `autoUpdate` | boolean | `true` | Automatically refresh the cached `kibi-opencode` plugin package on OpenCode startup unless the plugin array uses an exact semver pin |
|
|
165
198
|
| `prompt.enabled` | boolean | `true` | Enable prompt guidance injection |
|
|
166
199
|
| `prompt.hookMode` | string | `"auto"` | Hook mode: `auto`, `chat-params`, `system-transform`, `compat` |
|
|
167
200
|
| `sync.enabled` | boolean | `true` | Enable automatic sync |
|
|
@@ -172,7 +205,6 @@ Config files (project overrides global):
|
|
|
172
205
|
| `ux.toastFailures` | boolean | `true` | Show failure toasts for sync/check issues |
|
|
173
206
|
| `ux.toastSuccesses` | boolean | `false` | Show success toasts for sync/check completion |
|
|
174
207
|
| `ux.toastCooldownMs` | number | `10000` | Cooldown between repeated UX toasts |
|
|
175
|
-
PP|| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
176
208
|
| `guidance.dynamic` | boolean | `true` | Enable dynamic contextual guidance |
|
|
177
209
|
| `guidance.warnOnKbEdits` | boolean | `true` | Enable loud warnings for .kb/** edits |
|
|
178
210
|
| `guidance.factFirstDomainRouting` | boolean | `true` | Enable FACT-first domain routing suggestions |
|
|
@@ -265,10 +297,6 @@ Disable specific features while keeping others:
|
|
|
265
297
|
}
|
|
266
298
|
```
|
|
267
299
|
|
|
268
|
-
## Dogfooding
|
|
269
|
-
|
|
270
|
-
This repository's OpenCode setup dogfoods local built artifacts. `opencode.json` starts the local `kibi-mcp` server, `.opencode/plugins/kibi.ts` re-exports `packages/opencode/dist/index.js`, and the published npm package (`kibi-opencode`) remains the distribution artifact for external consumers. See [DEV.md](DEV.md) for the repo-local workflow and rebuild rule.
|
|
271
|
-
|
|
272
300
|
## Troubleshooting
|
|
273
301
|
|
|
274
302
|
If you see a false "workspace needs Kibi bootstrap" warning even though your workspace is already initialized with `.kb/config.json` pointing at relocated `kibi-docs/*` paths, this indicates a stale plugin cache. See [the main troubleshooting docs](../../docs/troubleshooting.md#opencode-shows-workspace-needs-kibi-bootstrap-before-the-tui) for recovery steps.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface KibiOpencodePluginEntry {
|
|
2
|
+
entry: string;
|
|
3
|
+
isPinned: boolean;
|
|
4
|
+
requestedVersion: string | null;
|
|
5
|
+
configPath: string;
|
|
6
|
+
}
|
|
7
|
+
export type AutoUpdateStatus = "disabled" | "plugin-not-found" | "current-version-unknown" | "latest-version-unknown" | "up-to-date" | "pinned" | "updated" | "install-failed";
|
|
8
|
+
export interface AutoUpdateResult {
|
|
9
|
+
status: AutoUpdateStatus;
|
|
10
|
+
currentVersion?: string;
|
|
11
|
+
latestVersion?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AutoUpdateInput {
|
|
14
|
+
directory: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface AutoUpdateRunnerDeps {
|
|
18
|
+
getCurrentVersion: () => string | null;
|
|
19
|
+
getLatestVersion: (channel?: string) => Promise<string | null>;
|
|
20
|
+
invalidatePackage: () => boolean;
|
|
21
|
+
runInstall: () => Promise<boolean>;
|
|
22
|
+
notify: (message: string) => Promise<void>;
|
|
23
|
+
log: (message: string, metadata?: Record<string, unknown>) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function findKibiOpencodePluginEntry(directory: string): KibiOpencodePluginEntry | null;
|
|
26
|
+
export declare function getCachedPluginVersion(): string | null;
|
|
27
|
+
export declare function getLatestPluginVersion(channel?: string): Promise<string | null>;
|
|
28
|
+
export declare function invalidateKibiOpencodePackage(): boolean;
|
|
29
|
+
export declare function runBunInstallForOpenCodePlugin(): Promise<boolean>;
|
|
30
|
+
export declare function createAutoUpdateRunner(deps: AutoUpdateRunnerDeps): (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
|
|
31
|
+
export declare const runKibiOpencodeAutoUpdate: (input: AutoUpdateInput) => Promise<AutoUpdateResult>;
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as logger from "./logger.js";
|
|
7
|
+
const PACKAGE_NAME = "kibi-opencode";
|
|
8
|
+
const NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
9
|
+
const NPM_FETCH_TIMEOUT_MS = 5000;
|
|
10
|
+
const EXACT_SEMVER_REGEX = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
11
|
+
function parseJsonObject(raw) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
logger.info("auto-update: failed to parse JSON", {
|
|
21
|
+
error: err instanceof Error ? err.message : String(err),
|
|
22
|
+
});
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function stripJsonComments(raw) {
|
|
27
|
+
return raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
|
|
28
|
+
}
|
|
29
|
+
function configPaths(directory) {
|
|
30
|
+
const home = os.homedir();
|
|
31
|
+
return [
|
|
32
|
+
path.join(directory, "opencode.json"),
|
|
33
|
+
path.join(directory, "opencode.jsonc"),
|
|
34
|
+
path.join(directory, ".opencode", "opencode.json"),
|
|
35
|
+
path.join(directory, ".opencode", "opencode.jsonc"),
|
|
36
|
+
path.join(home, ".config", "opencode", "opencode.json"),
|
|
37
|
+
path.join(home, ".config", "opencode", "opencode.jsonc"),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
export function findKibiOpencodePluginEntry(directory) {
|
|
41
|
+
for (const configPath of configPaths(directory)) {
|
|
42
|
+
if (!fs.existsSync(configPath)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const parsed = parseJsonObject(stripJsonComments(fs.readFileSync(configPath, "utf8")));
|
|
46
|
+
const plugins = parsed?.plugin;
|
|
47
|
+
if (!Array.isArray(plugins)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const candidate of plugins) {
|
|
51
|
+
if (typeof candidate !== "string") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (candidate === PACKAGE_NAME) {
|
|
55
|
+
return {
|
|
56
|
+
entry: candidate,
|
|
57
|
+
isPinned: false,
|
|
58
|
+
requestedVersion: null,
|
|
59
|
+
configPath,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (candidate.startsWith(`${PACKAGE_NAME}@`)) {
|
|
63
|
+
const requestedVersion = candidate
|
|
64
|
+
.slice(PACKAGE_NAME.length + 1)
|
|
65
|
+
.trim();
|
|
66
|
+
return {
|
|
67
|
+
entry: candidate,
|
|
68
|
+
isPinned: EXACT_SEMVER_REGEX.test(requestedVersion),
|
|
69
|
+
requestedVersion,
|
|
70
|
+
configPath,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (candidate.startsWith("file://") && candidate.includes(PACKAGE_NAME)) {
|
|
74
|
+
return {
|
|
75
|
+
entry: candidate,
|
|
76
|
+
isPinned: true,
|
|
77
|
+
requestedVersion: null,
|
|
78
|
+
configPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function getOpenCodeConfigDir() {
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
const appData = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
88
|
+
return path.join(appData, "opencode");
|
|
89
|
+
}
|
|
90
|
+
return path.join(os.homedir(), ".config", "opencode");
|
|
91
|
+
}
|
|
92
|
+
function getOpenCodeCacheRoot() {
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
95
|
+
return path.join(localAppData, "opencode");
|
|
96
|
+
}
|
|
97
|
+
if (process.platform === "darwin") {
|
|
98
|
+
return path.join(os.homedir(), "Library", "Caches", "opencode");
|
|
99
|
+
}
|
|
100
|
+
return path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"), "opencode");
|
|
101
|
+
}
|
|
102
|
+
function getOpenCodePackagesCacheDir() {
|
|
103
|
+
return path.join(getOpenCodeCacheRoot(), "packages");
|
|
104
|
+
}
|
|
105
|
+
function parseSemver(version) {
|
|
106
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
107
|
+
if (!match) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const major = Number(match[1]);
|
|
111
|
+
const minor = Number(match[2]);
|
|
112
|
+
const patch = Number(match[3]);
|
|
113
|
+
if (!Number.isInteger(major) ||
|
|
114
|
+
!Number.isInteger(minor) ||
|
|
115
|
+
!Number.isInteger(patch)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
core: [major, minor, patch],
|
|
120
|
+
prerelease: match[4]?.split(".") ?? [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function isNumericPrereleaseIdentifier(value) {
|
|
124
|
+
return /^(0|[1-9]\d*)$/.test(value);
|
|
125
|
+
}
|
|
126
|
+
function comparePrereleaseIdentifiers(left, right) {
|
|
127
|
+
const leftIsNumeric = isNumericPrereleaseIdentifier(left);
|
|
128
|
+
const rightIsNumeric = isNumericPrereleaseIdentifier(right);
|
|
129
|
+
if (leftIsNumeric && rightIsNumeric) {
|
|
130
|
+
return Number(left) - Number(right);
|
|
131
|
+
}
|
|
132
|
+
if (leftIsNumeric) {
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
if (rightIsNumeric) {
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
return left.localeCompare(right, "en", { sensitivity: "variant" });
|
|
139
|
+
}
|
|
140
|
+
function comparePrerelease(left, right) {
|
|
141
|
+
if (left.length === 0 && right.length === 0) {
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (left.length === 0) {
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
if (right.length === 0) {
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
const maxLength = Math.max(left.length, right.length);
|
|
151
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
152
|
+
const leftPart = left[index];
|
|
153
|
+
const rightPart = right[index];
|
|
154
|
+
if (leftPart === undefined) {
|
|
155
|
+
return -1;
|
|
156
|
+
}
|
|
157
|
+
if (rightPart === undefined) {
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
const comparison = comparePrereleaseIdentifiers(leftPart, rightPart);
|
|
161
|
+
if (comparison !== 0) {
|
|
162
|
+
return comparison;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
function isSemverGreaterThan(candidate, current) {
|
|
168
|
+
const candidateVersion = parseSemver(candidate);
|
|
169
|
+
const currentVersion = parseSemver(current);
|
|
170
|
+
if (!candidateVersion || !currentVersion) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
for (const index of [0, 1, 2]) {
|
|
174
|
+
if (candidateVersion.core[index] > currentVersion.core[index]) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (candidateVersion.core[index] < currentVersion.core[index]) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return (comparePrerelease(candidateVersion.prerelease, currentVersion.prerelease) >
|
|
182
|
+
0);
|
|
183
|
+
}
|
|
184
|
+
function readVersionFromPackageJson(packageJsonPath) {
|
|
185
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const parsed = parseJsonObject(fs.readFileSync(packageJsonPath, "utf8"));
|
|
189
|
+
return typeof parsed?.version === "string" ? parsed.version : null;
|
|
190
|
+
}
|
|
191
|
+
function findPackageJsonUp(startUrl) {
|
|
192
|
+
let current = path.dirname(fileURLToPath(startUrl));
|
|
193
|
+
while (true) {
|
|
194
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
195
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
196
|
+
const parsed = parseJsonObject(fs.readFileSync(packageJsonPath, "utf8"));
|
|
197
|
+
if (parsed?.name === PACKAGE_NAME) {
|
|
198
|
+
return packageJsonPath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const parent = path.dirname(current);
|
|
202
|
+
if (parent === current) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
current = parent;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function getCachedPluginVersion() {
|
|
209
|
+
const packageJsonCandidates = [
|
|
210
|
+
path.join(getOpenCodeConfigDir(), "node_modules", PACKAGE_NAME, "package.json"),
|
|
211
|
+
path.join(getOpenCodeCacheRoot(), "node_modules", PACKAGE_NAME, "package.json"),
|
|
212
|
+
path.join(getOpenCodePackagesCacheDir(), "node_modules", PACKAGE_NAME, "package.json"),
|
|
213
|
+
];
|
|
214
|
+
for (const candidate of packageJsonCandidates) {
|
|
215
|
+
const version = readVersionFromPackageJson(candidate);
|
|
216
|
+
if (version) {
|
|
217
|
+
return version;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const runningPackageJson = findPackageJsonUp(import.meta.url);
|
|
221
|
+
return runningPackageJson
|
|
222
|
+
? readVersionFromPackageJson(runningPackageJson)
|
|
223
|
+
: null;
|
|
224
|
+
}
|
|
225
|
+
export async function getLatestPluginVersion(channel = "latest") {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timeout = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT_MS);
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(NPM_DIST_TAGS_URL, {
|
|
230
|
+
signal: controller.signal,
|
|
231
|
+
headers: { Accept: "application/json" },
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const tags = (await response.json());
|
|
237
|
+
const selected = tags[channel] ?? tags.latest;
|
|
238
|
+
return typeof selected === "string" ? selected : null;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
logger.info("auto-update: failed to fetch latest version", {
|
|
242
|
+
error: err instanceof Error ? err.message : String(err),
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function removePackageDir(dir) {
|
|
251
|
+
if (!fs.existsSync(dir)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
function removeSpecifierCacheDirs(cacheDir) {
|
|
258
|
+
if (!fs.existsSync(cacheDir)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
let removed = false;
|
|
262
|
+
for (const entry of fs.readdirSync(cacheDir, { withFileTypes: true })) {
|
|
263
|
+
if (entry.isDirectory() && entry.name.startsWith(`${PACKAGE_NAME}@`)) {
|
|
264
|
+
fs.rmSync(path.join(cacheDir, entry.name), {
|
|
265
|
+
recursive: true,
|
|
266
|
+
force: true,
|
|
267
|
+
});
|
|
268
|
+
removed = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return removed;
|
|
272
|
+
}
|
|
273
|
+
function removeEveryPath(paths) {
|
|
274
|
+
let removed = false;
|
|
275
|
+
for (const targetPath of paths) {
|
|
276
|
+
if (removePackageDir(targetPath)) {
|
|
277
|
+
removed = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return removed;
|
|
281
|
+
}
|
|
282
|
+
function removeSpecifiersFromEveryDir(dirs) {
|
|
283
|
+
let removed = false;
|
|
284
|
+
for (const dir of dirs) {
|
|
285
|
+
if (removeSpecifierCacheDirs(dir)) {
|
|
286
|
+
removed = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return removed;
|
|
290
|
+
}
|
|
291
|
+
export function invalidateKibiOpencodePackage() {
|
|
292
|
+
const configDir = getOpenCodeConfigDir();
|
|
293
|
+
const cacheRoot = getOpenCodeCacheRoot();
|
|
294
|
+
const packagesCacheDir = getOpenCodePackagesCacheDir();
|
|
295
|
+
const removedPackageDirs = removeEveryPath([
|
|
296
|
+
path.join(configDir, "node_modules", PACKAGE_NAME),
|
|
297
|
+
path.join(cacheRoot, "node_modules", PACKAGE_NAME),
|
|
298
|
+
path.join(packagesCacheDir, "node_modules", PACKAGE_NAME),
|
|
299
|
+
]);
|
|
300
|
+
const removedSpecifierDirs = removeSpecifiersFromEveryDir([
|
|
301
|
+
cacheRoot,
|
|
302
|
+
packagesCacheDir,
|
|
303
|
+
]);
|
|
304
|
+
const removedLock = removeEveryPath([
|
|
305
|
+
path.join(cacheRoot, "bun.lock"),
|
|
306
|
+
path.join(cacheRoot, "bun.lockb"),
|
|
307
|
+
path.join(packagesCacheDir, "bun.lock"),
|
|
308
|
+
path.join(packagesCacheDir, "bun.lockb"),
|
|
309
|
+
]);
|
|
310
|
+
return removedPackageDirs || removedSpecifierDirs || removedLock;
|
|
311
|
+
}
|
|
312
|
+
function activeInstallWorkspace() {
|
|
313
|
+
const configDir = getOpenCodeConfigDir();
|
|
314
|
+
const cacheRoot = getOpenCodeCacheRoot();
|
|
315
|
+
const packagesCacheDir = getOpenCodePackagesCacheDir();
|
|
316
|
+
if (fs.existsSync(path.join(configDir, "node_modules", PACKAGE_NAME, "package.json"))) {
|
|
317
|
+
return configDir;
|
|
318
|
+
}
|
|
319
|
+
if (fs.existsSync(path.join(cacheRoot, "node_modules", PACKAGE_NAME, "package.json"))) {
|
|
320
|
+
return cacheRoot;
|
|
321
|
+
}
|
|
322
|
+
return packagesCacheDir;
|
|
323
|
+
}
|
|
324
|
+
export function runBunInstallForOpenCodePlugin() {
|
|
325
|
+
return new Promise((resolve) => {
|
|
326
|
+
const workspace = activeInstallWorkspace();
|
|
327
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
328
|
+
execFile("bun", ["install"], { cwd: workspace }, (error) => {
|
|
329
|
+
resolve(error === null);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function channelForEntry(entry) {
|
|
334
|
+
if (!entry.requestedVersion || entry.isPinned) {
|
|
335
|
+
return "latest";
|
|
336
|
+
}
|
|
337
|
+
return entry.requestedVersion;
|
|
338
|
+
}
|
|
339
|
+
export function createAutoUpdateRunner(deps) {
|
|
340
|
+
return async function runAutoUpdate(input) {
|
|
341
|
+
if (!input.enabled) {
|
|
342
|
+
deps.log("auto-update: disabled");
|
|
343
|
+
return { status: "disabled" };
|
|
344
|
+
}
|
|
345
|
+
const pluginEntry = findKibiOpencodePluginEntry(input.directory);
|
|
346
|
+
if (!pluginEntry) {
|
|
347
|
+
deps.log("auto-update: kibi-opencode plugin entry not found");
|
|
348
|
+
return { status: "plugin-not-found" };
|
|
349
|
+
}
|
|
350
|
+
const currentVersion = deps.getCurrentVersion() ?? pluginEntry.requestedVersion;
|
|
351
|
+
if (!currentVersion) {
|
|
352
|
+
deps.log("auto-update: current version unknown");
|
|
353
|
+
return { status: "current-version-unknown" };
|
|
354
|
+
}
|
|
355
|
+
const latestVersion = await deps.getLatestVersion(channelForEntry(pluginEntry));
|
|
356
|
+
if (!latestVersion) {
|
|
357
|
+
deps.log("auto-update: latest version unknown");
|
|
358
|
+
return { status: "latest-version-unknown", currentVersion };
|
|
359
|
+
}
|
|
360
|
+
if (currentVersion === latestVersion ||
|
|
361
|
+
!isSemverGreaterThan(latestVersion, currentVersion)) {
|
|
362
|
+
deps.log("auto-update: plugin already up to date", {
|
|
363
|
+
currentVersion,
|
|
364
|
+
latestVersion,
|
|
365
|
+
});
|
|
366
|
+
return { status: "up-to-date", currentVersion, latestVersion };
|
|
367
|
+
}
|
|
368
|
+
if (pluginEntry.isPinned) {
|
|
369
|
+
await deps.notify(`${PACKAGE_NAME} ${latestVersion} is available; current plugin entry is pinned to ${currentVersion}.`);
|
|
370
|
+
deps.log("auto-update: exact semver pin detected, skipping install", {
|
|
371
|
+
currentVersion,
|
|
372
|
+
latestVersion,
|
|
373
|
+
entry: pluginEntry.entry,
|
|
374
|
+
});
|
|
375
|
+
return { status: "pinned", currentVersion, latestVersion };
|
|
376
|
+
}
|
|
377
|
+
deps.invalidatePackage();
|
|
378
|
+
const installed = await deps.runInstall();
|
|
379
|
+
if (!installed) {
|
|
380
|
+
await deps.notify(`${PACKAGE_NAME} ${latestVersion} is available, but automatic install failed.`);
|
|
381
|
+
return { status: "install-failed", currentVersion, latestVersion };
|
|
382
|
+
}
|
|
383
|
+
await deps.notify(`${PACKAGE_NAME} updated from ${currentVersion} to ${latestVersion}. Restart OpenCode to apply.`);
|
|
384
|
+
return { status: "updated", currentVersion, latestVersion };
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
export const runKibiOpencodeAutoUpdate = createAutoUpdateRunner({
|
|
388
|
+
getCurrentVersion: getCachedPluginVersion,
|
|
389
|
+
getLatestVersion: getLatestPluginVersion,
|
|
390
|
+
invalidatePackage: invalidateKibiOpencodePackage,
|
|
391
|
+
runInstall: runBunInstallForOpenCodePlugin,
|
|
392
|
+
notify: async (message) => {
|
|
393
|
+
logger.info("auto-update.notification", { message });
|
|
394
|
+
},
|
|
395
|
+
log: (message, metadata) => logger.info(message, metadata),
|
|
396
|
+
});
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import * as logger from "./logger.js";
|
|
5
5
|
const DEFAULTS = {
|
|
6
6
|
enabled: true,
|
|
7
|
+
autoUpdate: true,
|
|
7
8
|
prompt: { enabled: true, hookMode: "auto" },
|
|
8
9
|
sync: { enabled: true, debounceMs: 2000, ignore: [], relevant: [] },
|
|
9
10
|
ux: {
|
|
@@ -64,6 +65,8 @@ export function validateAndMerge(obj) {
|
|
|
64
65
|
const out = { ...DEFAULTS };
|
|
65
66
|
if (typeof src.enabled === "boolean")
|
|
66
67
|
out.enabled = src.enabled;
|
|
68
|
+
if (typeof src.autoUpdate === "boolean")
|
|
69
|
+
out.autoUpdate = src.autoUpdate;
|
|
67
70
|
if (src.prompt && typeof src.prompt === "object") {
|
|
68
71
|
const p = src.prompt;
|
|
69
72
|
out.prompt = { ...DEFAULTS.prompt };
|
package/dist/init-kibi-alias.js
CHANGED
|
@@ -22,9 +22,11 @@ export function buildInitKibiAlias() {
|
|
|
22
22
|
"",
|
|
23
23
|
"## 2. Synthesize Candidates",
|
|
24
24
|
"Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities. This tool is **read-only**.",
|
|
25
|
+
"In OpenCode, Kibi MCP tools are host-prefixed: canonical `kb_autopilot_generate`, `kb_upsert`, and `kb_check` may appear as `kibi_kb_autopilot_generate`, `kibi_kb_upsert`, and `kibi_kb_check`.",
|
|
26
|
+
"OpenCode prefixes Kibi MCP tools with `kibi_`; use the visible prefixed name when the host requires exact tool identifiers.",
|
|
25
27
|
"",
|
|
26
28
|
"## 3. Preview and Approval",
|
|
27
|
-
"Present the `promptBlock
|
|
29
|
+
"Present the `promptBlock`, a summary of synthesized `candidates`, and the exact `applyPlan` payloads to the user. **Wait for explicit approval** before proceeding to writes.",
|
|
28
30
|
"",
|
|
29
31
|
"## 4. Apply Approved Candidates",
|
|
30
32
|
"Apply approved candidates sequentially using `kb_upsert` (following ascending phase order). Confirm success of each write before moving to the next. Run `kb_check` after the batch to verify KB integrity.",
|
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { createAutoUpdateRunner, getLatestPluginVersion, invalidateKibiOpencodePackage, runBunInstallForOpenCodePlugin, } from "./auto-update.js";
|
|
2
3
|
import { analyzeCodeFile, } from "./comment-analysis.js";
|
|
3
4
|
import { getE2eCoverageSignal } from "./e2e-coverage-signals.js"; // implements REQ-opencode-file-context-guidance-v1
|
|
4
5
|
import { buildDirtyRelevantFingerprint, buildEnforcementScopeKey, } from "./enforcement-scope.js";
|
|
@@ -20,6 +21,7 @@ import { createSyncScheduler } from "./scheduler.js";
|
|
|
20
21
|
import { createSessionEditState, } from "./session-edit-state.js";
|
|
21
22
|
import { getSessionTracker } from "./session-tracker.js";
|
|
22
23
|
import { notifyStartup, } from "./startup-notifier.js";
|
|
24
|
+
import { sendToast, } from "./toast.js";
|
|
23
25
|
import { readKibiPackageVersions } from "./version-metadata.js";
|
|
24
26
|
import { resolveWorkContext, } from "./work-context-resolver.js";
|
|
25
27
|
import * as fs from "node:fs";
|
|
@@ -1044,7 +1046,7 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1044
1046
|
}
|
|
1045
1047
|
}
|
|
1046
1048
|
logger.info("kibi-opencode: setup complete");
|
|
1047
|
-
if (input.client
|
|
1049
|
+
if (input.client) {
|
|
1048
1050
|
const client = input.client;
|
|
1049
1051
|
const scheduleStartupNotify = startupNotifyGlobals.__kibi_test_schedule_startup_notify ??
|
|
1050
1052
|
((callback, delayMs) => {
|
|
@@ -1057,11 +1059,40 @@ const kibiOpencodePlugin = async (input) => {
|
|
|
1057
1059
|
if (meta[key] !== "unknown")
|
|
1058
1060
|
versions[key] = meta[key];
|
|
1059
1061
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
+
if (!maintenanceDegraded) {
|
|
1063
|
+
notifyStartup(makeStartupClient(client), {
|
|
1064
|
+
suppressToast: cfg.ux.toastStartup === false,
|
|
1065
|
+
directory: input.directory,
|
|
1066
|
+
...(Object.keys(versions).length > 0 ? { versions } : {}),
|
|
1067
|
+
versionMetadataSource: meta.source,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
const autoUpdateRunner = createAutoUpdateRunner({
|
|
1071
|
+
getCurrentVersion: () => meta.opencode === "unknown" ? null : meta.opencode,
|
|
1072
|
+
getLatestVersion: getLatestPluginVersion,
|
|
1073
|
+
invalidatePackage: invalidateKibiOpencodePackage,
|
|
1074
|
+
runInstall: runBunInstallForOpenCodePlugin,
|
|
1075
|
+
notify: async (message) => {
|
|
1076
|
+
if (cfg.ux.toastStartup !== false) {
|
|
1077
|
+
await sendToast(makeToastClient(client), {
|
|
1078
|
+
variant: "info",
|
|
1079
|
+
title: "Kibi OpenCode",
|
|
1080
|
+
message,
|
|
1081
|
+
duration: 8000,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
logger.info("auto-update.notification", { message });
|
|
1085
|
+
},
|
|
1086
|
+
log: (message, metadata) => logger.info(message, metadata),
|
|
1087
|
+
});
|
|
1088
|
+
const runAutoUpdate = startupNotifyGlobals.__kibi_test_auto_update_runner ?? autoUpdateRunner;
|
|
1089
|
+
void runAutoUpdate({
|
|
1062
1090
|
directory: input.directory,
|
|
1063
|
-
|
|
1064
|
-
|
|
1091
|
+
enabled: cfg.autoUpdate,
|
|
1092
|
+
}).catch((err) => {
|
|
1093
|
+
logger.info("auto-update: background update check failed", {
|
|
1094
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1095
|
+
});
|
|
1065
1096
|
});
|
|
1066
1097
|
}, 2000);
|
|
1067
1098
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"opencode":"0.
|
|
1
|
+
{"opencode":"0.16.0","mcp":"0.16.0","cli":"0.12.4","core":"0.6.0"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Kibi OpenCode plugin - thin adapter to integrate Kibi with OpenCode sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"default": "./dist/tui.js"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"files": [
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
41
43
|
"engines": {
|
|
42
44
|
"node": ">=18"
|
|
43
45
|
},
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
"@opencode-ai/plugin": "^1.4.7",
|
|
61
63
|
"@opentui/core": "^0.1.99",
|
|
62
64
|
"@opentui/solid": "^0.1.99",
|
|
63
|
-
"kibi-cli": "^0.12.
|
|
65
|
+
"kibi-cli": "^0.12.3"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@types/node": "latest",
|