@mutmutco/kilo-plugin 3.79.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 (40) hide show
  1. package/agent/reviewer.md +108 -0
  2. package/package.json +23 -0
  3. package/scripts/command-ladder-core.mjs +334 -0
  4. package/scripts/command-ladder-gate.mjs +126 -0
  5. package/scripts/deny-gate-crash.mjs +179 -0
  6. package/scripts/edit-tool-paths.mjs +113 -0
  7. package/scripts/env-write-lint.mjs +137 -0
  8. package/scripts/hook-io.mjs +17 -0
  9. package/scripts/hook-policy.mjs +73 -0
  10. package/scripts/hook-run.mjs +170 -0
  11. package/scripts/hook-trace.mjs +108 -0
  12. package/scripts/pretooluse-shell-gates.mjs +420 -0
  13. package/scripts/secret-echo-lint.mjs +170 -0
  14. package/scripts/secret-redact.mjs +537 -0
  15. package/scripts/throttle-core.mjs +324 -0
  16. package/scripts/validate-hook.mjs +156 -0
  17. package/scripts/vault-edit-gate.mjs +94 -0
  18. package/server.mjs +237 -0
  19. package/skills/bootstrap/SKILL.md +493 -0
  20. package/skills/bootstrap/seeds/Dockerfile.template +30 -0
  21. package/skills/bootstrap/seeds/README.template.md +36 -0
  22. package/skills/bootstrap/seeds/architecture.template.md +34 -0
  23. package/skills/bootstrap/seeds/decisions-readme.template.md +46 -0
  24. package/skills/bootstrap/seeds/docker-compose.template.yml +26 -0
  25. package/skills/bootstrap/seeds/gate.template.yml +90 -0
  26. package/skills/bootstrap/seeds/google-login.template.md +33 -0
  27. package/skills/bootstrap/seeds/manifest.json +26 -0
  28. package/skills/bootstrap/seeds/mmi-product-required-checks.template.json +23 -0
  29. package/skills/browser-automation/SKILL.md +93 -0
  30. package/skills/doctor/SKILL.md +76 -0
  31. package/skills/epic/SKILL.md +87 -0
  32. package/skills/hotfix/SKILL.md +113 -0
  33. package/skills/mmi/SKILL.md +400 -0
  34. package/skills/onboard/SKILL.md +70 -0
  35. package/skills/rcand/SKILL.md +194 -0
  36. package/skills/release/SKILL.md +546 -0
  37. package/skills/resume/SKILL.md +68 -0
  38. package/skills/secrets/SKILL.md +157 -0
  39. package/skills/stage/SKILL.md +151 -0
  40. package/skills/worktree/SKILL.md +86 -0
package/server.mjs ADDED
@@ -0,0 +1,237 @@
1
+ // Kilo Code plugin module (kilo-p1). Loaded in-process by the Kilo plugin host.
2
+ //
3
+ // The shared gate scripts read a Claude/Kimi snake_case stdin payload (`tool_name`, `tool_input`,
4
+ // `tool_response`) and never grew a second vocabulary — the Codex and Kimi launchers already established
5
+ // that translation lives in a thin per-host shell, not in the gates. server.mjs is that shell for Kilo:
6
+ // every hook translates the Kilo camelCase hook input into the snake_case shape and replays it through
7
+ // scripts/hook-run.mjs, the same named-policy launcher every active host uses. The launcher stamps
8
+ // MMI_HOOK_SURFACE=kilo (which also engages the shared redactor's rewrite channel) and owns the
9
+ // fail-closed fallback for crashed deny gates.
10
+ //
11
+ // Kilo's contract differences, all confirmed on a real host (see plans/2026-07-31-kilo-code-plugin-port.md):
12
+ // - blocking a tool = throwing from `tool.execute.before`
13
+ // - rewriting output = mutating `output.output` in `tool.execute.after`, and `output.text` in
14
+ // `experimental.text.complete` — a WORKING redactor, unlike Codex/Kimi
15
+ // - tool names are lowercase (`bash`, `write`, `edit`, `patch`, `glob`, `grep`, `read`) and the bash
16
+ // args carry `command`, the write args carry `filePath`
17
+ // - npm lifecycle scripts are disabled, so first-run provisioning lives here, is idempotent and
18
+ // version-stamped, and NEVER throws (a broken provision reports, it does not brick the session)
19
+ // - the package ships NO command wrappers: Kilo lists skills in the `/` picker natively, so a
20
+ // same-named command would duplicate the skill (kilo-p1 user feedback on /mmi)
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { dirname, join } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const HERE = dirname(fileURLToPath(import.meta.url));
29
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'));
30
+ const VERSION = typeof PACKAGE_JSON.version === 'string' ? PACKAGE_JSON.version : '0.0.0';
31
+ const KILO_HOME_DIR = join(homedir(), '.kilo');
32
+ const VERSION_STAMP = join(KILO_HOME_DIR, '.mmi-kilo-version');
33
+ const LAUNCHER = join(HERE, 'scripts', 'hook-run.mjs');
34
+ const SPAWN_TIMEOUT_MS = 30_000;
35
+
36
+ /** The Kilo host runs plugins in-process under a Bun-compiled binary, so `process.execPath` is the kilo
37
+ * binary, NOT node. The launcher must run under real Node — that is what the shared gate scripts are. */
38
+ export function nodeBin() {
39
+ const base = String(process.execPath).replace(/\\/g, '/').split('/').pop()?.toLowerCase() ?? '';
40
+ if (base === 'node' || base === 'node.exe') return process.execPath;
41
+ return 'node';
42
+ }
43
+
44
+ // Kilo tool names → the Claude vocabulary the shared gates already understand.
45
+ const SHELL_TOOLS = new Set(['bash', 'shell', 'powershell']);
46
+ const EDIT_TOOLS = new Set(['write', 'edit', 'patch', 'apply_patch']);
47
+ const REDACT_TOOL_NAME = {
48
+ bash: 'Bash', shell: 'Bash', powershell: 'Bash',
49
+ write: 'Write', edit: 'Write', patch: 'Write', apply_patch: 'apply_patch',
50
+ read: 'Read', grep: 'Grep', glob: 'Glob',
51
+ };
52
+
53
+ /** Idempotent first-run provisioning of the non-plugin payload (~/.kilo/{skills,command,agent}) behind
54
+ * the version stamp. Never throws: a broken provision reports and lets the session proceed. */
55
+ export function provisionKiloPayload(root = HERE, version = VERSION) {
56
+ try {
57
+ const stamp = VERSION_STAMP;
58
+ if (existsSync(stamp) && readFileSync(stamp, 'utf8') === version) {
59
+ return { provisioned: false, detail: `MMI Kilo payload current (${version})` };
60
+ }
61
+ const skillsSource = join(root, 'skills');
62
+ if (existsSync(skillsSource)) {
63
+ for (const entry of readdirSync(skillsSource, { withFileTypes: true })) {
64
+ if (!entry.isDirectory() || entry.name.startsWith('_')) continue;
65
+ cpSync(join(skillsSource, entry.name), join(KILO_HOME_DIR, 'skills', entry.name), {
66
+ recursive: true,
67
+ });
68
+ }
69
+ }
70
+ // Kilo lists skills in the `/` picker natively, so the package ships NO command wrappers — a
71
+ // same-named command would duplicate the skill in the picker (kilo-p1 user feedback). Only the
72
+ // agent payload is copied; an empty/absent source dir is left alone (and a stale provisioned
73
+ // command from an older install is NOT resurrected).
74
+ for (const from of [join(root, 'agent')]) {
75
+ if (!existsSync(from)) continue;
76
+ const files = readdirSync(from);
77
+ if (files.length === 0) continue;
78
+ const to = join(KILO_HOME_DIR, 'agent');
79
+ mkdirSync(to, { recursive: true });
80
+ for (const file of files) {
81
+ cpSync(join(from, file), join(to, file), { recursive: true });
82
+ }
83
+ }
84
+ mkdirSync(KILO_HOME_DIR, { recursive: true });
85
+ writeFileSync(stamp, version, 'utf8');
86
+ return { provisioned: true, detail: `provisioned MMI Kilo payload ${version} into ~/.kilo` };
87
+ } catch (err) {
88
+ return { provisioned: false, detail: `MMI Kilo provisioning failed: ${err && err.message}` };
89
+ }
90
+ }
91
+
92
+ function hookPayload(event, input, toolName, toolInput, toolResponse) {
93
+ const payload = {
94
+ hook_event_name: event,
95
+ session_id: typeof input.sessionID === 'string' ? input.sessionID : '',
96
+ cwd: typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd(),
97
+ };
98
+ if (toolName !== undefined) payload.tool_name = toolName;
99
+ if (toolInput !== undefined) payload.tool_input = toolInput;
100
+ if (toolResponse !== undefined) payload.tool_response = toolResponse;
101
+ return payload;
102
+ }
103
+
104
+ /** Run one named shared-policy gate with a buffered snake_case payload. Returns the deny
105
+ * decision parsed from the launcher's stdout envelope (exit 2 is the shared block signal too), plus the
106
+ * raw stdout so redaction hooks can read the rewrite. */
107
+ export function runGate(gate, payload) {
108
+ const args = ['--surface', 'kilo', '--gate', gate];
109
+ let result;
110
+ try {
111
+ result = spawnSync(nodeBin(), [LAUNCHER, ...args], {
112
+ input: JSON.stringify(payload),
113
+ encoding: 'utf8',
114
+ timeout: SPAWN_TIMEOUT_MS,
115
+ // The Kilo host is a GUI process (VS Code extension) with no console; without this flag every
116
+ // gate spawn flashes a headed terminal window on Windows. The shared runner hides its child too.
117
+ windowsHide: true,
118
+ });
119
+ } catch (err) {
120
+ return { denied: false, reason: '', stdout: '', error: String(err && err.message) };
121
+ }
122
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
123
+ const denyLine = stdout.split(/\r?\n/).find((line) => line.trim().startsWith('{'));
124
+ if (denyLine) {
125
+ try {
126
+ const decision = JSON.parse(denyLine).hookSpecificOutput;
127
+ if (decision?.permissionDecision === 'deny') {
128
+ return { denied: true, reason: decision.permissionDecisionReason || `${gate} denied the tool call`, stdout };
129
+ }
130
+ } catch {
131
+ // malformed envelope — treat as no decision, fall through to the exit-code signal
132
+ }
133
+ }
134
+ if (result.status === 2) {
135
+ return { denied: true, reason: `${gate} exited 2 (deny)`, stdout };
136
+ }
137
+ return { denied: false, reason: '', stdout, error: result.error ? String(result.error) : '' };
138
+ }
139
+
140
+ function denyError(gate, reason) {
141
+ return new Error(`MMI ${gate} deny: ${reason}`);
142
+ }
143
+
144
+ function bashToolInput(args) {
145
+ const command = typeof args?.command === 'string' ? args.command : '';
146
+ return { command };
147
+ }
148
+
149
+ function editToolInput(args) {
150
+ const filePath = typeof args?.filePath === 'string' ? args.filePath : '';
151
+ const input = {};
152
+ if (filePath) {
153
+ input.file_path = filePath;
154
+ input.path = filePath;
155
+ }
156
+ if (typeof args?.command === 'string' && args.command) input.command = args.command;
157
+ if (typeof args?.patch === 'string' && args.patch) input.patch = args.patch;
158
+ if (typeof args?.content === 'string') input.content = args.content;
159
+ return input;
160
+ }
161
+
162
+ /** The redactor's stdout is `{"hookSpecificOutput":{"updatedToolOutput": <redacted>}}` when it changed
163
+ * something, and nothing when clean. Returns the rewrite, or undefined when there is none. */
164
+ function readRedactorRewrite(stdout) {
165
+ const line = String(stdout ?? '').split(/\r?\n/).find((l) => l.trim().startsWith('{'));
166
+ if (!line) return undefined;
167
+ try {
168
+ const updated = JSON.parse(line).hookSpecificOutput?.updatedToolOutput;
169
+ return typeof updated === 'string' || updated !== undefined ? updated : undefined;
170
+ } catch {
171
+ return undefined;
172
+ }
173
+ }
174
+
175
+ export default {
176
+ id: 'mmi',
177
+ server: async () => {
178
+ const provisioned = provisionKiloPayload();
179
+ return {
180
+ // Stamps the session env so every gate this session spawns inherits the real surface, the real
181
+ // session key (deny-gate-crash breaker), and the real project cwd (activity trace placement).
182
+ 'shell.env': async (input, output) => {
183
+ output.env = {
184
+ ...output.env,
185
+ MMI_HOOK_SURFACE: 'kilo',
186
+ KILO_PLUGIN_ROOT: HERE,
187
+ ...(typeof input.sessionID === 'string' && input.sessionID
188
+ ? { MMI_GATE_SESSION_ID: input.sessionID }
189
+ : {}),
190
+ ...(typeof input.cwd === 'string' && input.cwd ? { MMI_HOOK_ACTIVITY_CWD: input.cwd } : {}),
191
+ };
192
+ },
193
+ // PreToolUse equivalent: run the shared shell + vault deny gates; throwing blocks the tool.
194
+ 'tool.execute.before': async (input, output) => {
195
+ const tool = typeof input.tool === 'string' ? input.tool : '';
196
+ const args = output.args ?? {};
197
+ if (SHELL_TOOLS.has(tool)) {
198
+ const payload = hookPayload('PreToolUse', input, 'Bash', bashToolInput(args));
199
+ const decision = runGate('command-ladder', payload);
200
+ if (decision.denied) throw denyError('command-ladder', decision.reason);
201
+ return;
202
+ }
203
+ if (EDIT_TOOLS.has(tool)) {
204
+ const payload = hookPayload('PreToolUse', input, 'Write', editToolInput(args));
205
+ const decision = runGate('vault-edit', payload);
206
+ if (decision.denied) throw denyError('vault-edit', decision.reason);
207
+ }
208
+ },
209
+ // PostToolUse equivalent with a WORKING rewrite channel: secret-redact.mjs's updatedToolOutput is
210
+ // applied to output.output. Write/Read/WebFetch are not rewriteable (same harness limit as Claude).
211
+ 'tool.execute.after': async (input, output) => {
212
+ const tool = typeof input.tool === 'string' ? input.tool : '';
213
+ const translated = REDACT_TOOL_NAME[tool];
214
+ if (!translated) return;
215
+ const payload = hookPayload('PostToolUse', input, translated, input.args, output.output ?? '');
216
+ const gate = runGate('secret-output', payload);
217
+ const rewrite = readRedactorRewrite(gate.stdout);
218
+ if (rewrite !== undefined) output.output = rewrite;
219
+ },
220
+ // Final-text redaction: the same redactor, run against the completed text part.
221
+ 'experimental.text.complete': async (input, output) => {
222
+ const text = typeof output.text === 'string' ? output.text : '';
223
+ if (!text) return;
224
+ const payload = hookPayload('PostToolUse', input, 'Bash', undefined, text);
225
+ const gate = runGate('secret-output', payload);
226
+ const rewrite = readRedactorRewrite(gate.stdout);
227
+ if (rewrite !== undefined) output.text = rewrite;
228
+ },
229
+ // A failed provision must be audible but never fatal.
230
+ dispose: async () => {
231
+ if (!provisioned.provisioned && provisioned.detail.startsWith('MMI Kilo provisioning failed')) {
232
+ process.stderr.write(`${provisioned.detail}\n`);
233
+ }
234
+ },
235
+ };
236
+ },
237
+ };