@ken-jo/agent-connector 0.2.0 → 0.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.
- package/README.md +9 -2
- package/dist/chunk-44HUXXC6.js +284 -0
- package/dist/chunk-44HUXXC6.js.map +1 -0
- package/dist/{chunk-AKJGNTU3.js → chunk-DS3LUFVN.js} +21 -21
- package/dist/chunk-DS3LUFVN.js.map +1 -0
- package/dist/{chunk-KHO5WNTP.js → chunk-KVAAFYRR.js} +18 -2
- package/dist/chunk-KVAAFYRR.js.map +1 -0
- package/dist/{chunk-O2SBA5KR.js → chunk-NJU275US.js} +9 -9
- package/dist/{chunk-WCETQWWK.js → chunk-OP4KBXUO.js} +1 -17
- package/dist/chunk-OP4KBXUO.js.map +1 -0
- package/dist/chunk-QADSG2JY.js +1989 -0
- package/dist/chunk-QADSG2JY.js.map +1 -0
- package/dist/{package-O63K4S7T.js → chunk-WNREHWKL.js} +20 -114
- package/dist/chunk-WNREHWKL.js.map +1 -0
- package/dist/{chunk-4ML7SZ7R.js → chunk-ZAM4M7LI.js} +7 -7
- package/dist/cli/sdk.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/{detect-JK7T46N6.js → detect-MNDHGLSV.js} +3 -3
- package/dist/{doctor-62BHIXOR.js → doctor-N5KZDD55.js} +35 -5
- package/dist/doctor-N5KZDD55.js.map +1 -0
- package/dist/{hook-AC7APC66.js → hook-7LUOKIXH.js} +6 -6
- package/dist/{install-DMW2C7SV.js → install-TQTT4F3I.js} +31 -7
- package/dist/install-TQTT4F3I.js.map +1 -0
- package/dist/kilo-PKAJCWPX.js +852 -0
- package/dist/kilo-PKAJCWPX.js.map +1 -0
- package/dist/{kilo-cli-2RNODHWY.js → kilo-cli-QWUVXG2O.js} +157 -6
- package/dist/kilo-cli-QWUVXG2O.js.map +1 -0
- package/dist/{kimi-Q5RG7CYT.js → kimi-EVD7YKXS.js} +79 -4
- package/dist/kimi-EVD7YKXS.js.map +1 -0
- package/dist/{kiro-EKTRW424.js → kiro-UQRY6EIO.js} +68 -3
- package/dist/kiro-UQRY6EIO.js.map +1 -0
- package/dist/{leaderboard-R5HRJHNU.js → leaderboard-MUJ7DURM.js} +2 -2
- package/dist/package-SYPA5POO.js +130 -0
- package/dist/package-SYPA5POO.js.map +1 -0
- package/dist/{pi-SJPIL4VE.js → pi-WMTUX6ES.js} +102 -23
- package/dist/pi-WMTUX6ES.js.map +1 -0
- package/dist/{qwen-code-IFCM6BJY.js → qwen-code-MRRIUQ3P.js} +78 -5
- package/dist/qwen-code-MRRIUQ3P.js.map +1 -0
- package/dist/runtime/index.js +4 -4
- package/dist/{serve-LFOUSZAD.js → serve-XJOSA7BY.js} +6 -6
- package/dist/{status-M7JLZVLC.js → status-DYSS6BOR.js} +16 -7
- package/dist/status-DYSS6BOR.js.map +1 -0
- package/dist/{telemetry-3NVEXPYX.js → telemetry-IJ4GP2Q3.js} +2 -2
- package/dist/uninstall-3DXIBRCV.js +162 -0
- package/dist/uninstall-3DXIBRCV.js.map +1 -0
- package/dist/{upgrade-76BQZBMA.js → upgrade-GJEB3ITY.js} +26 -8
- package/dist/upgrade-GJEB3ITY.js.map +1 -0
- package/dist/{usage-G3GSCX23.js → usage-2BJLM3GJ.js} +2 -2
- package/dist/{usage-event-QUTMNBS5.js → usage-event-472X3DOY.js} +5 -5
- package/dist/{warp-LLOSXR7A.js → warp-X6MB67NT.js} +81 -3
- package/dist/warp-X6MB67NT.js.map +1 -0
- package/dist/{zed-HYOITK7F.js → zed-NTCYW4V2.js} +68 -3
- package/dist/zed-NTCYW4V2.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-AKJGNTU3.js.map +0 -1
- package/dist/chunk-KHO5WNTP.js.map +0 -1
- package/dist/chunk-WCETQWWK.js.map +0 -1
- package/dist/chunk-Z2BVYKZL.js +0 -369
- package/dist/chunk-Z2BVYKZL.js.map +0 -1
- package/dist/doctor-62BHIXOR.js.map +0 -1
- package/dist/install-DMW2C7SV.js.map +0 -1
- package/dist/kilo-ISR5FBBJ.js +0 -408
- package/dist/kilo-ISR5FBBJ.js.map +0 -1
- package/dist/kilo-cli-2RNODHWY.js.map +0 -1
- package/dist/kimi-Q5RG7CYT.js.map +0 -1
- package/dist/kiro-EKTRW424.js.map +0 -1
- package/dist/package-O63K4S7T.js.map +0 -1
- package/dist/pi-SJPIL4VE.js.map +0 -1
- package/dist/qwen-code-IFCM6BJY.js.map +0 -1
- package/dist/status-M7JLZVLC.js.map +0 -1
- package/dist/uninstall-H6FGMIHN.js +0 -73
- package/dist/uninstall-H6FGMIHN.js.map +0 -1
- package/dist/upgrade-76BQZBMA.js.map +0 -1
- package/dist/warp-LLOSXR7A.js.map +0 -1
- package/dist/zed-HYOITK7F.js.map +0 -1
- /package/dist/{chunk-O2SBA5KR.js.map → chunk-NJU275US.js.map} +0 -0
- /package/dist/{chunk-4ML7SZ7R.js.map → chunk-ZAM4M7LI.js.map} +0 -0
- /package/dist/{detect-JK7T46N6.js.map → detect-MNDHGLSV.js.map} +0 -0
- /package/dist/{hook-AC7APC66.js.map → hook-7LUOKIXH.js.map} +0 -0
- /package/dist/{leaderboard-R5HRJHNU.js.map → leaderboard-MUJ7DURM.js.map} +0 -0
- /package/dist/{serve-LFOUSZAD.js.map → serve-XJOSA7BY.js.map} +0 -0
- /package/dist/{telemetry-3NVEXPYX.js.map → telemetry-IJ4GP2Q3.js.map} +0 -0
- /package/dist/{usage-G3GSCX23.js.map → usage-2BJLM3GJ.js.map} +0 -0
- /package/dist/{usage-event-QUTMNBS5.js.map → usage-event-472X3DOY.js.map} +0 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveEnvRefsDeep
|
|
4
|
+
} from "./chunk-DBQMX75U.js";
|
|
5
|
+
import {
|
|
6
|
+
BaseAdapter
|
|
7
|
+
} from "./chunk-PD4VFEBY.js";
|
|
8
|
+
import "./chunk-7L6ITC4T.js";
|
|
9
|
+
import {
|
|
10
|
+
buildServeWrapperCommand,
|
|
11
|
+
shouldWrapForTelemetry
|
|
12
|
+
} from "./chunk-MEH7GC7R.js";
|
|
13
|
+
import "./chunk-N6HH7K6P.js";
|
|
14
|
+
import "./chunk-TRQKHP7D.js";
|
|
15
|
+
|
|
16
|
+
// src/adapters/kilo/index.ts
|
|
17
|
+
import {
|
|
18
|
+
chmodSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
writeFileSync
|
|
24
|
+
} from "fs";
|
|
25
|
+
import { homedir, platform as osPlatform } from "os";
|
|
26
|
+
import { dirname, join } from "path";
|
|
27
|
+
var HOST = "kilo";
|
|
28
|
+
var MCP_ROOT_KEY = "mcp";
|
|
29
|
+
var PLUGIN_ARRAY_KEY = "plugin";
|
|
30
|
+
var CONFIG_FILE = "kilo.json";
|
|
31
|
+
var KILO_EXTENSION_ID = "kilocode.kilo-code";
|
|
32
|
+
var LEGACY_MCP_SETTINGS_FILE = "mcp_settings.json";
|
|
33
|
+
var LEGACY_CLINE_SETTINGS_FILE = "cline_mcp_settings.json";
|
|
34
|
+
var EVENT_TO_KILO = {
|
|
35
|
+
PreToolUse: "tool.execute.before",
|
|
36
|
+
PostToolUse: "tool.execute.after",
|
|
37
|
+
SessionStart: "experimental.chat.system.transform"
|
|
38
|
+
};
|
|
39
|
+
function kiloConfigDir() {
|
|
40
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
41
|
+
const base = xdg && xdg.trim() !== "" ? xdg : join(homedir(), ".config");
|
|
42
|
+
return join(base, "kilo");
|
|
43
|
+
}
|
|
44
|
+
function vscodeUserDir() {
|
|
45
|
+
const home = homedir();
|
|
46
|
+
switch (osPlatform()) {
|
|
47
|
+
case "darwin":
|
|
48
|
+
return join(home, "Library", "Application Support", "Code", "User");
|
|
49
|
+
case "win32": {
|
|
50
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
51
|
+
return join(appData, "Code", "User");
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return join(home, ".config", "Code", "User");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
var KiloAdapter = class extends BaseAdapter {
|
|
58
|
+
id = HOST;
|
|
59
|
+
name = "Kilo Code";
|
|
60
|
+
paradigm = "ts-plugin";
|
|
61
|
+
capabilities = {
|
|
62
|
+
// Memory surface: AGENTS.md-first managed block (project <projectDir>/AGENTS.md
|
|
63
|
+
// via the base default; user scope → ~/.kilocode/rules/agent-connector.md below).
|
|
64
|
+
supportsMemory: true,
|
|
65
|
+
// Kilo Code 7.x shares the ts-plugin layer with the Kilo CLI — same events.
|
|
66
|
+
preToolUse: true,
|
|
67
|
+
postToolUse: true,
|
|
68
|
+
preCompact: false,
|
|
69
|
+
sessionStart: true,
|
|
70
|
+
sessionEnd: false,
|
|
71
|
+
userPromptSubmit: false,
|
|
72
|
+
stop: false,
|
|
73
|
+
notification: false,
|
|
74
|
+
canModifyArgs: true,
|
|
75
|
+
canModifyOutput: true,
|
|
76
|
+
canInjectSessionContext: true,
|
|
77
|
+
// Kilo Code registers stdio, SSE, and Streamable HTTP MCP servers.
|
|
78
|
+
transports: ["stdio", "sse", "http"],
|
|
79
|
+
// Content surfaces: commands, skills (since 7.x rebuild), and subagents.
|
|
80
|
+
supportsCommands: true,
|
|
81
|
+
supportsSkills: true,
|
|
82
|
+
supportsSubagents: true
|
|
83
|
+
};
|
|
84
|
+
// ── Detection ────────────────────────────────────────────────────────────
|
|
85
|
+
detectInstalled(projectDir) {
|
|
86
|
+
const userConfig = this.userConfigPath();
|
|
87
|
+
const userBackendDir = kiloConfigDir();
|
|
88
|
+
const legacyExtDir = join(vscodeUserDir(), "globalStorage", KILO_EXTENSION_ID);
|
|
89
|
+
const legacySettings = join(legacyExtDir, "settings", LEGACY_MCP_SETTINGS_FILE);
|
|
90
|
+
const legacyClineSettings = join(
|
|
91
|
+
legacyExtDir,
|
|
92
|
+
"settings",
|
|
93
|
+
LEGACY_CLINE_SETTINGS_FILE
|
|
94
|
+
);
|
|
95
|
+
const projectConfig = join(projectDir, ".kilo", CONFIG_FILE);
|
|
96
|
+
const userMatch = existsSync(userConfig) || existsSync(userBackendDir) || existsSync(legacySettings) || existsSync(legacyClineSettings) || existsSync(legacyExtDir);
|
|
97
|
+
const projectMatch = existsSync(projectConfig);
|
|
98
|
+
const installed = userMatch || projectMatch;
|
|
99
|
+
const scope = userMatch || !projectMatch ? "user" : "project";
|
|
100
|
+
const configPath = scope === "user" ? userConfig : projectConfig;
|
|
101
|
+
const reason = installed ? userMatch ? `found Kilo Code config under ${userBackendDir}` : `found Kilo Code project config at ${projectConfig}` : `no Kilo Code config at ${userBackendDir} or ${projectConfig}`;
|
|
102
|
+
return {
|
|
103
|
+
id: this.id,
|
|
104
|
+
name: this.name,
|
|
105
|
+
installed,
|
|
106
|
+
paradigm: this.paradigm,
|
|
107
|
+
capabilities: this.capabilities,
|
|
108
|
+
configPath,
|
|
109
|
+
scope,
|
|
110
|
+
reason,
|
|
111
|
+
confidence: installed ? "high" : "low"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ── Native paths ─────────────────────────────────────────────────────────
|
|
115
|
+
/** Absolute path to the user-scope kilo.json (kilo backend, XDG dir). */
|
|
116
|
+
userConfigPath() {
|
|
117
|
+
return join(kiloConfigDir(), CONFIG_FILE);
|
|
118
|
+
}
|
|
119
|
+
// ── Memory surface: global rules dir at user scope ──────────────────────
|
|
120
|
+
// Project scope stays on the AGENTS.md base default. User scope targets a
|
|
121
|
+
// dedicated agent-connector.md in the legacy-compatible ~/.kilocode/rules/
|
|
122
|
+
// dir (always loaded; avoids JSONC `instructions` edits in kilo.jsonc; the
|
|
123
|
+
// AC-created file is cleanly deletable on uninstall). Shared backend with
|
|
124
|
+
// kilo-cli — the convergent write dedupes via the content hash.
|
|
125
|
+
memoryTargets(ctx) {
|
|
126
|
+
if (this.memoryOverride(ctx)?.path || ctx.scope !== "user") {
|
|
127
|
+
return super.memoryTargets(ctx);
|
|
128
|
+
}
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
path: join(homedir(), ".kilocode", "rules", "agent-connector.md"),
|
|
132
|
+
reason: "kilo global rules dir (~/.kilocode/rules; agent-connector-owned file)"
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* MCP config dir. Note the project surface is `.kilo/` (kilo backend), NOT the
|
|
138
|
+
* extension's `.kilocode/` content tree (which still hosts commands/subagents).
|
|
139
|
+
*/
|
|
140
|
+
getConfigDir(ctx) {
|
|
141
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilo") : kiloConfigDir();
|
|
142
|
+
}
|
|
143
|
+
getServerConfigPath(ctx) {
|
|
144
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilo", CONFIG_FILE) : this.userConfigPath();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* For ts-plugin hosts the "hook config path" is the generated plugin FILE.
|
|
148
|
+
* The backend also supports auto-discovery from the plugin dir, but we ALSO
|
|
149
|
+
* register the path in kilo.json's `plugin` array for explicit load (idempotent).
|
|
150
|
+
*/
|
|
151
|
+
getHookConfigPath(ctx) {
|
|
152
|
+
return join(this.pluginDir(ctx), this.pluginFileName(ctx));
|
|
153
|
+
}
|
|
154
|
+
/** Plugin directory the synthesized module is written into, per scope. */
|
|
155
|
+
pluginDir(ctx) {
|
|
156
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilo", "plugin") : join(kiloConfigDir(), "plugin");
|
|
157
|
+
}
|
|
158
|
+
/** Plugin module file name (one per connector, kebab-case id). */
|
|
159
|
+
pluginFileName(ctx) {
|
|
160
|
+
return `${ctx.connector.id}.js`;
|
|
161
|
+
}
|
|
162
|
+
// ── MCP server install / uninstall ───────────────────────────────────────
|
|
163
|
+
installServer(ctx) {
|
|
164
|
+
const { connector } = ctx;
|
|
165
|
+
const override = connector.platforms[HOST]?.server;
|
|
166
|
+
if (!connector.server || override === false) {
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
platform: this.id,
|
|
170
|
+
action: "skip",
|
|
171
|
+
detail: connector.server ? "server registration disabled for kilo" : "connector declares no MCP server"
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
const server = override && typeof override === "object" ? { ...connector.server, ...override } : connector.server;
|
|
176
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
177
|
+
const entry = this.renderServerEntry(ctx, server);
|
|
178
|
+
return [
|
|
179
|
+
this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun)
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
uninstallServer(ctx) {
|
|
183
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
184
|
+
return [
|
|
185
|
+
this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun)
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Render a normalized ServerDef into kilo.json's native `mcp` entry. Local
|
|
190
|
+
* (stdio) servers fold the executable + args into a SINGLE `command` array and
|
|
191
|
+
* carry env under `environment`; remote servers use a `type:"remote"` URL entry.
|
|
192
|
+
*/
|
|
193
|
+
renderServerEntry(ctx, server) {
|
|
194
|
+
const transport = server.transport;
|
|
195
|
+
if (transport === "stdio") {
|
|
196
|
+
let command = server.command ?? "";
|
|
197
|
+
let args = [...server.args ?? []];
|
|
198
|
+
if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {
|
|
199
|
+
const wrapped = buildServeWrapperCommand(
|
|
200
|
+
ctx.homeBinPath,
|
|
201
|
+
ctx.connector.id,
|
|
202
|
+
command,
|
|
203
|
+
args,
|
|
204
|
+
ctx.scope,
|
|
205
|
+
this.id
|
|
206
|
+
);
|
|
207
|
+
command = wrapped.command;
|
|
208
|
+
args = wrapped.args;
|
|
209
|
+
}
|
|
210
|
+
const argv = [
|
|
211
|
+
resolveEnvRefsDeep(command),
|
|
212
|
+
...resolveEnvRefsDeep(args)
|
|
213
|
+
];
|
|
214
|
+
const entry2 = {
|
|
215
|
+
type: "local",
|
|
216
|
+
command: argv,
|
|
217
|
+
environment: this.renderEnv(server.env) ?? {}
|
|
218
|
+
};
|
|
219
|
+
return entry2;
|
|
220
|
+
}
|
|
221
|
+
const entry = {
|
|
222
|
+
type: "remote",
|
|
223
|
+
url: resolveEnvRefsDeep(server.url ?? "")
|
|
224
|
+
};
|
|
225
|
+
const headers = this.renderEnv(server.headers);
|
|
226
|
+
if (headers) entry.headers = headers;
|
|
227
|
+
return entry;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Render env/header values. Resolve `${env:VAR}` references to literals at
|
|
231
|
+
* install time for consistency with kilo-cli.
|
|
232
|
+
*/
|
|
233
|
+
renderEnv(env) {
|
|
234
|
+
if (!env || Object.keys(env).length === 0) return void 0;
|
|
235
|
+
return resolveEnvRefsDeep({ ...env });
|
|
236
|
+
}
|
|
237
|
+
// ── Hook install / uninstall (ts-plugin + explicit `plugin` array) ────────
|
|
238
|
+
installHooks(ctx) {
|
|
239
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
240
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
241
|
+
if (ctx.connector.platforms[HOST]?.hooks === false) {
|
|
242
|
+
return [
|
|
243
|
+
{ platform: this.id, action: "skip", path: pluginPath, detail: "hooks disabled for kilo" }
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
if (ctx.connector.hookEvents.length === 0) {
|
|
247
|
+
return [
|
|
248
|
+
{ platform: this.id, action: "skip", path: pluginPath, detail: "connector declares no hooks" }
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
const changes = [];
|
|
252
|
+
for (const file of this.synthesizePlugin(ctx)) {
|
|
253
|
+
const before = existsSync(file.path) ? this.safeRead(file.path) : void 0;
|
|
254
|
+
let action;
|
|
255
|
+
if (before === void 0) action = "create";
|
|
256
|
+
else if (before === file.contents) action = "skip";
|
|
257
|
+
else action = "update";
|
|
258
|
+
if (action !== "skip" && !ctx.dryRun) {
|
|
259
|
+
ensureDir(dirname(file.path));
|
|
260
|
+
writeFileSync(file.path, file.contents, "utf8");
|
|
261
|
+
chmodSync(file.path, file.executable ? 493 : 420);
|
|
262
|
+
}
|
|
263
|
+
changes.push({
|
|
264
|
+
platform: this.id,
|
|
265
|
+
action,
|
|
266
|
+
path: file.path,
|
|
267
|
+
detail: `kilo plugin module (${this.hookDetail(ctx)})`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
changes.push(this.upsertPluginInArray(configPath, pluginPath, ctx.dryRun));
|
|
271
|
+
return changes;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Human-facing summary of which declared events the synthesized module ACTUALLY
|
|
275
|
+
* wires. Only events present in EVENT_TO_KILO are mapped/wired; any declared
|
|
276
|
+
* event with no Kilo mapping is reported separately as "unsupported here".
|
|
277
|
+
*/
|
|
278
|
+
hookDetail(ctx) {
|
|
279
|
+
const declared = ctx.connector.hookEvents;
|
|
280
|
+
const mapped = declared.filter((e) => EVENT_TO_KILO[e] !== void 0);
|
|
281
|
+
const unsupported = declared.filter((e) => EVENT_TO_KILO[e] === void 0);
|
|
282
|
+
const base = mapped.join(",");
|
|
283
|
+
return unsupported.length > 0 ? `${base}; unsupported here: ${unsupported.join(",")}` : base;
|
|
284
|
+
}
|
|
285
|
+
uninstallHooks(ctx) {
|
|
286
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
287
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
288
|
+
const changes = [];
|
|
289
|
+
changes.push(this.removePluginFromArray(configPath, pluginPath, ctx.dryRun));
|
|
290
|
+
if (existsSync(pluginPath)) {
|
|
291
|
+
if (!ctx.dryRun) rmSync(pluginPath, { force: true });
|
|
292
|
+
changes.push({
|
|
293
|
+
platform: this.id,
|
|
294
|
+
action: "remove",
|
|
295
|
+
path: pluginPath,
|
|
296
|
+
detail: "kilo plugin module"
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
changes.push({
|
|
300
|
+
platform: this.id,
|
|
301
|
+
action: "skip",
|
|
302
|
+
path: pluginPath,
|
|
303
|
+
detail: "no kilo plugin module present"
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
changes.push(this.removeDirIfEmpty(this.pluginDir(ctx), ctx.dryRun));
|
|
307
|
+
return changes;
|
|
308
|
+
}
|
|
309
|
+
/** Upsert the module path into kilo.json's top-level `plugin` array (idempotent). */
|
|
310
|
+
upsertPluginInArray(configPath, modulePath, dryRun) {
|
|
311
|
+
if (this.isPresentButUnparseable(configPath)) {
|
|
312
|
+
return {
|
|
313
|
+
platform: this.id,
|
|
314
|
+
action: "warn",
|
|
315
|
+
path: configPath,
|
|
316
|
+
detail: `existing ${configPath} is not parseable; left untouched (back it up / fix it, then re-run)`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const cfg = this.readJson(configPath) ?? {};
|
|
320
|
+
const plugins = this.pluginArrayBucket(cfg);
|
|
321
|
+
if (plugins.includes(modulePath)) {
|
|
322
|
+
return {
|
|
323
|
+
platform: this.id,
|
|
324
|
+
action: "skip",
|
|
325
|
+
path: configPath,
|
|
326
|
+
detail: `${PLUGIN_ARRAY_KEY}[] already includes module`
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
plugins.push(modulePath);
|
|
330
|
+
this.writeJson(configPath, cfg, dryRun);
|
|
331
|
+
return {
|
|
332
|
+
platform: this.id,
|
|
333
|
+
action: "create",
|
|
334
|
+
path: configPath,
|
|
335
|
+
detail: `${PLUGIN_ARRAY_KEY}[] += module`
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Remove the module path from kilo.json's `plugin` array. */
|
|
339
|
+
removePluginFromArray(configPath, modulePath, dryRun) {
|
|
340
|
+
if (this.isPresentButUnparseable(configPath)) {
|
|
341
|
+
return {
|
|
342
|
+
platform: this.id,
|
|
343
|
+
action: "warn",
|
|
344
|
+
path: configPath,
|
|
345
|
+
detail: `existing ${configPath} is not parseable; left untouched (back it up / fix it, then re-run)`
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const cfg = this.readJson(configPath);
|
|
349
|
+
const raw = cfg?.[PLUGIN_ARRAY_KEY];
|
|
350
|
+
const plugins = Array.isArray(raw) ? raw : void 0;
|
|
351
|
+
const idx = plugins ? plugins.indexOf(modulePath) : -1;
|
|
352
|
+
if (!cfg || !plugins || idx < 0) {
|
|
353
|
+
return {
|
|
354
|
+
platform: this.id,
|
|
355
|
+
action: "skip",
|
|
356
|
+
path: configPath,
|
|
357
|
+
detail: `${PLUGIN_ARRAY_KEY}[] does not include module`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
plugins.splice(idx, 1);
|
|
361
|
+
this.writeJson(configPath, cfg, dryRun);
|
|
362
|
+
return {
|
|
363
|
+
platform: this.id,
|
|
364
|
+
action: "remove",
|
|
365
|
+
path: configPath,
|
|
366
|
+
detail: `${PLUGIN_ARRAY_KEY}[] -= module`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** Get-or-create the top-level `plugin` array as a mutable string array. */
|
|
370
|
+
pluginArrayBucket(cfg) {
|
|
371
|
+
const existing = cfg[PLUGIN_ARRAY_KEY];
|
|
372
|
+
const plugins = Array.isArray(existing) ? existing.filter((p) => typeof p === "string") : [];
|
|
373
|
+
cfg[PLUGIN_ARRAY_KEY] = plugins;
|
|
374
|
+
return plugins;
|
|
375
|
+
}
|
|
376
|
+
// ── ts-plugin synthesis ────────────────────────────────────────────────
|
|
377
|
+
/**
|
|
378
|
+
* Build ONE self-contained ESM plugin module for Kilo Code (the VS Code
|
|
379
|
+
* extension). Structurally identical to the kilo-cli plugin but bridges to
|
|
380
|
+
* "kilo" (not "kilo-cli") so the dispatcher routes to the correct adapter.
|
|
381
|
+
*/
|
|
382
|
+
synthesizePlugin(ctx) {
|
|
383
|
+
const path = this.getHookConfigPath(ctx);
|
|
384
|
+
const contents = this.buildPluginSource(ctx);
|
|
385
|
+
return [{ path, contents, executable: false }];
|
|
386
|
+
}
|
|
387
|
+
/** Compose the generated plugin source with plain string concatenation. */
|
|
388
|
+
buildPluginSource(ctx) {
|
|
389
|
+
const homeBin = JSON.stringify(ctx.homeBinPath);
|
|
390
|
+
const connectorId = JSON.stringify(ctx.connector.id);
|
|
391
|
+
const events = ctx.connector.hookEvents.filter(
|
|
392
|
+
(e) => EVENT_TO_KILO[e] !== void 0
|
|
393
|
+
);
|
|
394
|
+
const has = (e) => events.includes(e);
|
|
395
|
+
const header = `/**
|
|
396
|
+
* AUTO-GENERATED by agent-connector \u2014 DO NOT EDIT.
|
|
397
|
+
*
|
|
398
|
+
* Self-contained Kilo Code (VS Code extension) plugin bridge for connector ${ctx.connector.id}.
|
|
399
|
+
* It imports nothing from agent-connector: every hook invocation shells out to
|
|
400
|
+
* the stable home binary's universal entrypoint and JSON-parses the normalized
|
|
401
|
+
* response. Fail-open: any bridge error degrades to "allow".
|
|
402
|
+
*
|
|
403
|
+
* Default export is the @kilocode/plugin PluginModule shape
|
|
404
|
+
* ({ id, server: async (input) => Hooks }); the extension loads it via the
|
|
405
|
+
* plugin dir auto-discovery and/or the "plugin" array in kilo.json.
|
|
406
|
+
*/
|
|
407
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
408
|
+
|
|
409
|
+
const HOME_BIN = ${homeBin};
|
|
410
|
+
const CONNECTOR_ID = ${connectorId};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Invoke the universal hook entrypoint for one event.
|
|
414
|
+
* @param {string} event canonical event name (PreToolUse|PostToolUse|SessionStart)
|
|
415
|
+
* @param {object} payload Kilo-shaped payload posted on stdin
|
|
416
|
+
* @returns {object|null} normalized HookResponse, or null on any failure
|
|
417
|
+
*/
|
|
418
|
+
function bridge(event, payload) {
|
|
419
|
+
try {
|
|
420
|
+
// On Windows HOME_BIN is the agent-connector.cmd launcher: Node cannot
|
|
421
|
+
// execFile a batch file, and shell+args is deprecated (DEP0190), so run one
|
|
422
|
+
// quoted command line via a shell. POSIX keeps the direct execFile (no shell).
|
|
423
|
+
const args = ["hook", "kilo", event, "--connector", CONNECTOR_ID];
|
|
424
|
+
const opts = { input: JSON.stringify(payload), encoding: "utf8" };
|
|
425
|
+
const stdout =
|
|
426
|
+
process.platform === "win32"
|
|
427
|
+
? execSync([HOME_BIN, ...args].map((a) => '"' + a + '"').join(" "), opts)
|
|
428
|
+
: execFileSync(HOME_BIN, args, opts);
|
|
429
|
+
const text = (stdout || "").trim();
|
|
430
|
+
if (text === "") return { decision: "allow" };
|
|
431
|
+
return JSON.parse(text);
|
|
432
|
+
} catch {
|
|
433
|
+
// Fail-open \u2014 never wedge a tool call on a bridge error.
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`;
|
|
438
|
+
const handlers = [];
|
|
439
|
+
if (has("PreToolUse")) {
|
|
440
|
+
handlers.push(` // PreToolUse \u2192 block (throw) / rewrite args (mutate output.args).
|
|
441
|
+
"tool.execute.before": async (input, output) => {
|
|
442
|
+
const payload = {
|
|
443
|
+
toolName: input.tool ?? "",
|
|
444
|
+
toolInput: (output && output.args) ?? {},
|
|
445
|
+
sessionId: input.sessionID ?? "",
|
|
446
|
+
projectDir: PROJECT_DIR,
|
|
447
|
+
};
|
|
448
|
+
const res = bridge("PreToolUse", payload);
|
|
449
|
+
if (!res) return;
|
|
450
|
+
// Kilo has no "ask" gate \u2014 degrade "ask" to a block (safe direction).
|
|
451
|
+
if (res.decision === "deny" || res.decision === "ask") {
|
|
452
|
+
throw new Error(res.reason || "Blocked by ${ctx.connector.id}");
|
|
453
|
+
}
|
|
454
|
+
if (res.updatedInput && output && output.args) {
|
|
455
|
+
Object.assign(output.args, res.updatedInput);
|
|
456
|
+
}
|
|
457
|
+
},`);
|
|
458
|
+
}
|
|
459
|
+
if (has("PostToolUse")) {
|
|
460
|
+
handlers.push(` // PostToolUse \u2192 observe / rewrite tool output (mutate output.output).
|
|
461
|
+
"tool.execute.after": async (input, output) => {
|
|
462
|
+
const payload = {
|
|
463
|
+
toolName: input.tool ?? "",
|
|
464
|
+
toolInput: input.args ?? {},
|
|
465
|
+
toolOutput: output ? output.output : undefined,
|
|
466
|
+
sessionId: input.sessionID ?? "",
|
|
467
|
+
projectDir: PROJECT_DIR,
|
|
468
|
+
};
|
|
469
|
+
const res = bridge("PostToolUse", payload);
|
|
470
|
+
if (!res) return;
|
|
471
|
+
if (typeof res.updatedOutput === "string" && output) {
|
|
472
|
+
output.output = res.updatedOutput;
|
|
473
|
+
}
|
|
474
|
+
},`);
|
|
475
|
+
}
|
|
476
|
+
if (has("SessionStart")) {
|
|
477
|
+
handlers.push(` // SessionStart surrogate \u2192 inject context into the system prompt.
|
|
478
|
+
// The extension has no real SessionStart hook; the system-prompt transform is
|
|
479
|
+
// the verified injection point (mirrors OpenCode and kilo-cli).
|
|
480
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
481
|
+
const payload = {
|
|
482
|
+
sessionId: (input && input.sessionID) ?? "",
|
|
483
|
+
projectDir: PROJECT_DIR,
|
|
484
|
+
};
|
|
485
|
+
const res = bridge("SessionStart", payload);
|
|
486
|
+
if (!res) return;
|
|
487
|
+
if (res.additionalContext && output && Array.isArray(output.system)) {
|
|
488
|
+
// Insert at index 1 (after the header) to preserve the prompt-cache fold
|
|
489
|
+
// (header must remain system[0]).
|
|
490
|
+
output.system.splice(1, 0, res.additionalContext);
|
|
491
|
+
}
|
|
492
|
+
},`);
|
|
493
|
+
}
|
|
494
|
+
const definition = `
|
|
495
|
+
const plugin = {
|
|
496
|
+
id: CONNECTOR_ID,
|
|
497
|
+
// @kilocode/plugin PluginModule: server(input) returns the Hooks object keyed
|
|
498
|
+
// by the fork's event names. ctx.directory is the project root; fall back to cwd.
|
|
499
|
+
server: async (input) => {
|
|
500
|
+
const PROJECT_DIR =
|
|
501
|
+
(input && (input.directory || (input.worktree && input.worktree.path))) ||
|
|
502
|
+
process.cwd();
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
${handlers.join("\n")}
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export default plugin;
|
|
511
|
+
`;
|
|
512
|
+
return header + definition;
|
|
513
|
+
}
|
|
514
|
+
// ── Runtime: parse OUR bridge payload → normalized event ───────────────
|
|
515
|
+
/**
|
|
516
|
+
* `raw` is the payload OUR generated plugin posts (NOT a host-native shape):
|
|
517
|
+
* { toolName, toolInput, toolOutput?, isError?, sessionId, projectDir }
|
|
518
|
+
* so this maps straight through.
|
|
519
|
+
*/
|
|
520
|
+
parseEvent(event, raw) {
|
|
521
|
+
const input = raw ?? {};
|
|
522
|
+
const base = {
|
|
523
|
+
hostPlatform: HOST,
|
|
524
|
+
connectorId: "",
|
|
525
|
+
sessionId: typeof input.sessionId === "string" ? input.sessionId : "",
|
|
526
|
+
raw,
|
|
527
|
+
...typeof input.projectDir === "string" ? { projectDir: input.projectDir } : {}
|
|
528
|
+
};
|
|
529
|
+
switch (event) {
|
|
530
|
+
case "PreToolUse": {
|
|
531
|
+
const ev = {
|
|
532
|
+
...base,
|
|
533
|
+
toolName: input.toolName ?? "",
|
|
534
|
+
toolInput: input.toolInput ?? {}
|
|
535
|
+
};
|
|
536
|
+
return ev;
|
|
537
|
+
}
|
|
538
|
+
case "PostToolUse": {
|
|
539
|
+
const ev = {
|
|
540
|
+
...base,
|
|
541
|
+
toolName: input.toolName ?? "",
|
|
542
|
+
toolInput: input.toolInput ?? {},
|
|
543
|
+
...typeof input.toolOutput === "string" ? { toolOutput: input.toolOutput } : {},
|
|
544
|
+
...typeof input.isError === "boolean" ? { isError: input.isError } : {}
|
|
545
|
+
};
|
|
546
|
+
return ev;
|
|
547
|
+
}
|
|
548
|
+
case "SessionStart": {
|
|
549
|
+
const ev = { ...base, source: "startup" };
|
|
550
|
+
return ev;
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
return { ...base, source: "startup" };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ── Runtime: normalized response → reply the generated bridge parses ───
|
|
557
|
+
/**
|
|
558
|
+
* OUR generated bridge consumes this stdout directly. The reply body IS the
|
|
559
|
+
* normalized HookResponse itself — the bridge JSON.parses it and reads
|
|
560
|
+
* decision / updatedInput / updatedOutput / additionalContext.
|
|
561
|
+
*/
|
|
562
|
+
formatReply(_event, response) {
|
|
563
|
+
return {
|
|
564
|
+
exitCode: 0,
|
|
565
|
+
stdout: JSON.stringify(response ?? { decision: "allow" })
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// ── Content surfaces: commands / skills / subagents ───────────────────
|
|
569
|
+
// CONTENT-ONLY: pure native-file writers. No runtime dispatch, no home-bin
|
|
570
|
+
// pointer, no telemetry wrap. Each method is idempotent (byte-identical →
|
|
571
|
+
// skip) via BaseAdapter.writeContentFile and reversible via removeContentFile.
|
|
572
|
+
// Honors platforms["kilo"] per-surface false to skip.
|
|
573
|
+
//
|
|
574
|
+
// NOTE on dirs: the .kilocode/ tree is the EXTENSION's legacy content surface
|
|
575
|
+
// — kept SEPARATE from the MCP/hooks backend dir (~/.config/kilo). Commands
|
|
576
|
+
// live under .kilocode/commands (project) / ~/.kilocode/commands (user);
|
|
577
|
+
// subagents live under .kilocode/agents (project) / ~/.kilocode/agents (user).
|
|
578
|
+
// Skills use the newer .kilo/skills tree (project) / ~/.kilo/skills (user).
|
|
579
|
+
/** The extension's user-scope legacy content home (~/.kilocode). */
|
|
580
|
+
userContentDir() {
|
|
581
|
+
return join(homedir(), ".kilocode");
|
|
582
|
+
}
|
|
583
|
+
/** Skills root: project → <projectDir>/.kilo/skills; user → ~/.kilo/skills. */
|
|
584
|
+
skillsRootDir(ctx) {
|
|
585
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilo", "skills") : join(homedir(), ".kilo", "skills");
|
|
586
|
+
}
|
|
587
|
+
/** Commands root: project → <projectDir>/.kilocode/commands; user → ~/.kilocode/commands. */
|
|
588
|
+
commandsDir(ctx) {
|
|
589
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilocode", "commands") : join(this.userContentDir(), "commands");
|
|
590
|
+
}
|
|
591
|
+
/** Subagents root: project → <projectDir>/.kilocode/agents; user → ~/.kilocode/agents. */
|
|
592
|
+
agentsDir(ctx) {
|
|
593
|
+
return ctx.scope === "project" ? join(ctx.projectDir, ".kilocode", "agents") : join(this.userContentDir(), "agents");
|
|
594
|
+
}
|
|
595
|
+
/** Native command file path: <commandsDir>/<name>.md. */
|
|
596
|
+
commandPath(ctx, name) {
|
|
597
|
+
return join(this.commandsDir(ctx), `${name}.md`);
|
|
598
|
+
}
|
|
599
|
+
/** Native skill dir: <skillsRootDir>/<name>. */
|
|
600
|
+
skillDir(ctx, name) {
|
|
601
|
+
return join(this.skillsRootDir(ctx), name);
|
|
602
|
+
}
|
|
603
|
+
/** Native subagent file path: <agentsDir>/<name>.md. */
|
|
604
|
+
subagentPath(ctx, name) {
|
|
605
|
+
return join(this.agentsDir(ctx), `${name}.md`);
|
|
606
|
+
}
|
|
607
|
+
// ── Commands ──────────────────────────────────────────────────────────────
|
|
608
|
+
installCommands(ctx) {
|
|
609
|
+
const { connector } = ctx;
|
|
610
|
+
if (connector.platforms[HOST]?.commands === false) {
|
|
611
|
+
return [{ platform: this.id, action: "skip", detail: "commands disabled for kilo" }];
|
|
612
|
+
}
|
|
613
|
+
if (connector.commands.length === 0) {
|
|
614
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no commands" }];
|
|
615
|
+
}
|
|
616
|
+
return connector.commands.map(
|
|
617
|
+
(cmd) => this.writeContentFile(
|
|
618
|
+
this.commandPath(ctx, cmd.name),
|
|
619
|
+
this.renderCommand(cmd),
|
|
620
|
+
ctx.dryRun
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
uninstallCommands(ctx) {
|
|
625
|
+
const { connector } = ctx;
|
|
626
|
+
if (connector.commands.length === 0) {
|
|
627
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no commands" }];
|
|
628
|
+
}
|
|
629
|
+
return connector.commands.map(
|
|
630
|
+
(cmd) => this.removeContentFile(this.commandPath(ctx, cmd.name), ctx.dryRun)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
/** Render a command to md+frontmatter (description, argument-hint, mode, model). */
|
|
634
|
+
renderCommand(cmd) {
|
|
635
|
+
const frontmatter = {};
|
|
636
|
+
if (cmd.description !== void 0) frontmatter.description = cmd.description;
|
|
637
|
+
if (cmd.argumentHint !== void 0) frontmatter["argument-hint"] = cmd.argumentHint;
|
|
638
|
+
if (cmd.model !== void 0) frontmatter.model = cmd.model;
|
|
639
|
+
if (cmd.extra) Object.assign(frontmatter, cmd.extra);
|
|
640
|
+
return this.renderFrontmatterMd(frontmatter, cmd.prompt);
|
|
641
|
+
}
|
|
642
|
+
// ── Skills ────────────────────────────────────────────────────────────────
|
|
643
|
+
installSkills(ctx) {
|
|
644
|
+
const { connector } = ctx;
|
|
645
|
+
if (connector.platforms[HOST]?.skills === false) {
|
|
646
|
+
return [{ platform: this.id, action: "skip", detail: "skills disabled for kilo" }];
|
|
647
|
+
}
|
|
648
|
+
if (connector.skills.length === 0) {
|
|
649
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
|
|
650
|
+
}
|
|
651
|
+
const changes = [];
|
|
652
|
+
for (const skill of connector.skills) {
|
|
653
|
+
const dir = this.skillDir(ctx, skill.name);
|
|
654
|
+
changes.push(
|
|
655
|
+
this.writeContentFile(join(dir, "SKILL.md"), this.renderSkill(skill), ctx.dryRun)
|
|
656
|
+
);
|
|
657
|
+
for (const [rel, contents] of Object.entries(skill.resources ?? {})) {
|
|
658
|
+
const target = this.resolveWithin(dir, rel);
|
|
659
|
+
if (target === null) {
|
|
660
|
+
changes.push({
|
|
661
|
+
platform: this.id,
|
|
662
|
+
action: "warn",
|
|
663
|
+
detail: `skill resource "${rel}" escapes the skill dir; skipped`
|
|
664
|
+
});
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
changes.push(this.writeContentFile(target, contents, ctx.dryRun));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return changes;
|
|
671
|
+
}
|
|
672
|
+
uninstallSkills(ctx) {
|
|
673
|
+
const { connector } = ctx;
|
|
674
|
+
if (connector.skills.length === 0) {
|
|
675
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no skills" }];
|
|
676
|
+
}
|
|
677
|
+
const changes = [];
|
|
678
|
+
for (const skill of connector.skills) {
|
|
679
|
+
const dir = this.skillDir(ctx, skill.name);
|
|
680
|
+
changes.push(this.removeContentFile(join(dir, "SKILL.md"), ctx.dryRun));
|
|
681
|
+
for (const rel of Object.keys(skill.resources ?? {})) {
|
|
682
|
+
const target = this.resolveWithin(dir, rel);
|
|
683
|
+
if (target === null) continue;
|
|
684
|
+
changes.push(this.removeContentFile(target, ctx.dryRun));
|
|
685
|
+
}
|
|
686
|
+
changes.push(this.removeDirIfEmpty(dir, ctx.dryRun));
|
|
687
|
+
}
|
|
688
|
+
return changes;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Render a skill's SKILL.md: frontmatter (name, description + optional model,
|
|
692
|
+
* allowed-tools, disable-model-invocation) + markdown body.
|
|
693
|
+
*/
|
|
694
|
+
renderSkill(skill) {
|
|
695
|
+
const frontmatter = {
|
|
696
|
+
name: skill.name,
|
|
697
|
+
description: skill.description
|
|
698
|
+
};
|
|
699
|
+
if (skill.model !== void 0) frontmatter.model = skill.model;
|
|
700
|
+
const allow = skill.tools?.allow;
|
|
701
|
+
if (allow && allow.length > 0) frontmatter["allowed-tools"] = allow.join(", ");
|
|
702
|
+
if (skill.disableModelInvocation !== void 0) {
|
|
703
|
+
frontmatter["disable-model-invocation"] = skill.disableModelInvocation;
|
|
704
|
+
}
|
|
705
|
+
if (skill.extra) Object.assign(frontmatter, skill.extra);
|
|
706
|
+
return this.renderFrontmatterMd(frontmatter, skill.body);
|
|
707
|
+
}
|
|
708
|
+
// ── Subagents ───────────────────────────────────────────────────────────────
|
|
709
|
+
installSubagents(ctx) {
|
|
710
|
+
const { connector } = ctx;
|
|
711
|
+
if (connector.platforms[HOST]?.subagents === false) {
|
|
712
|
+
return [{ platform: this.id, action: "skip", detail: "subagents disabled for kilo" }];
|
|
713
|
+
}
|
|
714
|
+
if (connector.subagents.length === 0) {
|
|
715
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no subagents" }];
|
|
716
|
+
}
|
|
717
|
+
return connector.subagents.map(
|
|
718
|
+
(agent) => this.writeContentFile(
|
|
719
|
+
this.subagentPath(ctx, agent.name),
|
|
720
|
+
this.renderSubagent(agent),
|
|
721
|
+
ctx.dryRun
|
|
722
|
+
)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
uninstallSubagents(ctx) {
|
|
726
|
+
const { connector } = ctx;
|
|
727
|
+
if (connector.subagents.length === 0) {
|
|
728
|
+
return [{ platform: this.id, action: "skip", detail: "connector declares no subagents" }];
|
|
729
|
+
}
|
|
730
|
+
return connector.subagents.map(
|
|
731
|
+
(agent) => this.removeContentFile(this.subagentPath(ctx, agent.name), ctx.dryRun)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Render a subagent to md+frontmatter. Kilo Code's shape is
|
|
736
|
+
* (description, mode:"subagent", model, permission) with the system prompt as
|
|
737
|
+
* the body. `name` is NOT a frontmatter field — it comes from the filename.
|
|
738
|
+
* `permission` is derived from the coarse `readonly` knob: a readonly agent
|
|
739
|
+
* gets a per-tool deny map (edit/bash) so it cannot mutate the workspace.
|
|
740
|
+
*/
|
|
741
|
+
renderSubagent(agent) {
|
|
742
|
+
const frontmatter = {
|
|
743
|
+
description: agent.description,
|
|
744
|
+
mode: "subagent"
|
|
745
|
+
};
|
|
746
|
+
if (agent.model !== void 0) frontmatter.model = agent.model;
|
|
747
|
+
if (agent.readonly === true) {
|
|
748
|
+
frontmatter.permission = { edit: "deny", bash: "deny" };
|
|
749
|
+
}
|
|
750
|
+
if (agent.extra) Object.assign(frontmatter, agent.extra);
|
|
751
|
+
return this.renderFrontmatterMd(frontmatter, agent.prompt);
|
|
752
|
+
}
|
|
753
|
+
// ── Diagnostics ──────────────────────────────────────────────────────────
|
|
754
|
+
getHealthChecks(ctx) {
|
|
755
|
+
const mcpPath = this.getServerConfigPath(ctx);
|
|
756
|
+
const pluginPath = this.getHookConfigPath(ctx);
|
|
757
|
+
const connectorId = ctx.connector.id;
|
|
758
|
+
const hasHooks = ctx.connector.hookEvents.length > 0;
|
|
759
|
+
const checks = [
|
|
760
|
+
{
|
|
761
|
+
name: `${this.name}: kilo.json present`,
|
|
762
|
+
check: () => existsSync(mcpPath) ? { status: "OK", detail: mcpPath } : { status: "FAIL", detail: `not found: ${mcpPath}` }
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
// The backend merges kilo.json AND kilo.jsonc at user/project/.kilo/ levels.
|
|
766
|
+
name: `${this.name}: MCP delegated to kilo backend`,
|
|
767
|
+
check: () => ({
|
|
768
|
+
status: "OK",
|
|
769
|
+
detail: ctx.scope === "user" ? `using ${CONFIG_FILE} under ${kiloConfigDir()} (merged with kilo.jsonc by shared backend)` : `project scope uses .kilo/${CONFIG_FILE} (merged with kilo.jsonc by shared backend)`
|
|
770
|
+
})
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: `${this.name}: server entry registered`,
|
|
774
|
+
check: () => {
|
|
775
|
+
if (!ctx.connector.server) {
|
|
776
|
+
return { status: "OK", detail: "no MCP server declared" };
|
|
777
|
+
}
|
|
778
|
+
const cfg = this.readJson(mcpPath);
|
|
779
|
+
const bucket = cfg?.[MCP_ROOT_KEY];
|
|
780
|
+
if (!cfg || !bucket) {
|
|
781
|
+
return { status: "FAIL", detail: `no ${MCP_ROOT_KEY} in ${mcpPath}` };
|
|
782
|
+
}
|
|
783
|
+
return connectorId in bucket ? { status: "OK", detail: `${MCP_ROOT_KEY}.${connectorId} present` } : {
|
|
784
|
+
status: "FAIL",
|
|
785
|
+
detail: `no ${MCP_ROOT_KEY}.${connectorId} in ${mcpPath}`
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: `${this.name}: plugin module present`,
|
|
791
|
+
check: () => {
|
|
792
|
+
if (!hasHooks) return { status: "OK", detail: "no hooks declared" };
|
|
793
|
+
return existsSync(pluginPath) ? { status: "OK", detail: pluginPath } : { status: "FAIL", detail: `not found: ${pluginPath}` };
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: `${this.name}: plugin registered in ${PLUGIN_ARRAY_KEY}[]`,
|
|
798
|
+
check: () => {
|
|
799
|
+
if (!hasHooks) return { status: "OK", detail: "no hooks declared" };
|
|
800
|
+
const cfg = this.readJson(mcpPath);
|
|
801
|
+
const raw = cfg?.[PLUGIN_ARRAY_KEY];
|
|
802
|
+
const plugins = Array.isArray(raw) ? raw : void 0;
|
|
803
|
+
return plugins && plugins.includes(pluginPath) ? { status: "OK", detail: `${PLUGIN_ARRAY_KEY}[] includes module` } : {
|
|
804
|
+
status: "FAIL",
|
|
805
|
+
detail: `module not in ${PLUGIN_ARRAY_KEY}[] of ${mcpPath}`
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
];
|
|
810
|
+
for (const cmd of ctx.connector.commands) {
|
|
811
|
+
const p = this.commandPath(ctx, cmd.name);
|
|
812
|
+
checks.push({
|
|
813
|
+
name: `${this.name}: command ${cmd.name} present`,
|
|
814
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
for (const skill of ctx.connector.skills) {
|
|
818
|
+
const p = join(this.skillDir(ctx, skill.name), "SKILL.md");
|
|
819
|
+
checks.push({
|
|
820
|
+
name: `${this.name}: skill ${skill.name} present`,
|
|
821
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
for (const agent of ctx.connector.subagents) {
|
|
825
|
+
const p = this.subagentPath(ctx, agent.name);
|
|
826
|
+
checks.push({
|
|
827
|
+
name: `${this.name}: subagent ${agent.name} present`,
|
|
828
|
+
check: () => existsSync(p) ? { status: "OK", detail: p } : { status: "FAIL", detail: `not found: ${p}` }
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
return checks;
|
|
832
|
+
}
|
|
833
|
+
/** Read a file, returning undefined on any error (idempotency compare). */
|
|
834
|
+
safeRead(path) {
|
|
835
|
+
try {
|
|
836
|
+
return readFileSync(path, "utf8");
|
|
837
|
+
} catch {
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
function ensureDir(dir) {
|
|
843
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
844
|
+
}
|
|
845
|
+
var adapter = new KiloAdapter();
|
|
846
|
+
var kilo_default = adapter;
|
|
847
|
+
export {
|
|
848
|
+
KiloAdapter,
|
|
849
|
+
adapter,
|
|
850
|
+
kilo_default as default
|
|
851
|
+
};
|
|
852
|
+
//# sourceMappingURL=kilo-PKAJCWPX.js.map
|