@pragma-sh/kimi-plugin 1.0.1 → 1.1.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.
@@ -1,13 +1,423 @@
1
- // ../plugin/dist/shared/chunk-e222xk1w.js
2
- var PLUGIN_API_VERSION = "1.0.1";
1
+ // ../plugin/dist/shared/chunk-b8hkp0vs.js
2
+ var PLUGIN_API_VERSION = "1.2.0";
3
3
 
4
- // ../plugin/dist/shared/chunk-6qx2xj9p.js
4
+ // ../plugin/dist/shared/chunk-whqa76q3.js
5
5
  function defineAgent(input) {
6
6
  return input;
7
7
  }
8
+ function dedupeSlashCommands(commands) {
9
+ const seen = new Set;
10
+ return commands.filter((command) => {
11
+ if (!command.name || seen.has(command.name))
12
+ return false;
13
+ seen.add(command.name);
14
+ return true;
15
+ });
16
+ }
17
+ var DEFAULT_TIMEOUT_SECONDS = 30;
18
+ var ACP_COMMANDS_TTL_MS = 5 * 60000;
19
+ var MAX_CONCURRENT_PROBES = 2;
20
+ var SETTLE_SECONDS = "1.5";
21
+ var UPDATE_MARKER = "available_commands_update";
22
+ var STATE_KEY = Symbol.for("pragma.acpDiscovery");
23
+ var state = globalThis[STATE_KEY] ??= { cache: new Map, inflight: new Map, running: 0, waiting: [] };
24
+ var { cache, inflight } = state;
25
+ async function discoverAcpSlashCommands(ctx, source) {
26
+ const cwd = ctx.project?.path ?? "/tmp";
27
+ const key = `${cwd}\x00${source.command.join("\x00")}`;
28
+ const cached = cache.get(key);
29
+ if (cached && Date.now() - cached.at < ACP_COMMANDS_TTL_MS)
30
+ return cached.commands;
31
+ const probe = inflight.get(key) ?? startProbe(ctx, source, cwd, key);
32
+ return cached ? cached.commands : probe;
33
+ }
34
+ function startProbe(ctx, source, cwd, key) {
35
+ const probe = withProbeSlot(async () => {
36
+ const [result] = await ctx.sdk.exec.run({ cwd, commands: [acpCommandsScript(source)] });
37
+ return result?.status === 0 ? parseAcpCommands(result.stdout) : [];
38
+ }).catch(() => []).then((commands) => {
39
+ if (commands.length > 0)
40
+ cache.set(key, { commands, at: Date.now() });
41
+ return commands;
42
+ }).finally(() => inflight.delete(key));
43
+ inflight.set(key, probe);
44
+ return probe;
45
+ }
46
+ async function withProbeSlot(work) {
47
+ if (state.running >= MAX_CONCURRENT_PROBES) {
48
+ await new Promise((resolve) => state.waiting.push(resolve));
49
+ }
50
+ state.running += 1;
51
+ try {
52
+ return await work();
53
+ } finally {
54
+ state.running -= 1;
55
+ state.waiting.shift()?.();
56
+ }
57
+ }
58
+ function acpCommandsScript(source) {
59
+ const ticks = Math.round((source.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 10);
60
+ const [binary] = source.command;
61
+ const initialize = JSON.stringify({
62
+ jsonrpc: "2.0",
63
+ id: 1,
64
+ method: "initialize",
65
+ params: { protocolVersion: 1, clientCapabilities: {} }
66
+ });
67
+ return [
68
+ `command -v ${shellQuote(binary ?? "")} >/dev/null 2>&1 || exit 0`,
69
+ "work=$(mktemp -d) || exit 0",
70
+ `trap 'rm -rf "$work"' 0`,
71
+ 'mkfifo "$work/in" || exit 0',
72
+ ': >"$work/out"',
73
+ "{",
74
+ ` printf '%s\\n' ${shellQuote(initialize)}`,
75
+ " n=0",
76
+ ` while ! grep -q '"id":1' "$work/out" && [ ! -e "$work/done" ] && [ "$n" -lt ${ticks} ]; do sleep 0.1; n=$((n + 1)); done`,
77
+ ` printf '{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"%s","mcpServers":[]}}\\n' "$PWD"`,
78
+ ` while [ ! -e "$work/done" ] && [ "$n" -lt ${ticks * 2} ]; do sleep 0.1; n=$((n + 1)); done`,
79
+ '} >"$work/in" &',
80
+ "writer=$!",
81
+ `${source.command.map(shellArg).join(" ")} <"$work/in" >"$work/out" 2>/dev/null &`,
82
+ "agent=$!",
83
+ "n=0",
84
+ `while [ "$n" -lt ${ticks} ]; do`,
85
+ ` if grep -q ${UPDATE_MARKER} "$work/out"; then sleep ${SETTLE_SECONDS}; break; fi`,
86
+ " sleep 0.1; n=$((n + 1))",
87
+ "done",
88
+ ': >"$work/done"',
89
+ 'kill "$agent" 2>/dev/null',
90
+ 'wait "$writer" 2>/dev/null',
91
+ `grep ${UPDATE_MARKER} "$work/out" | tail -n 1`,
92
+ "exit 0"
93
+ ].join(`
94
+ `);
95
+ }
96
+ function parseAcpCommands(stdout) {
97
+ let commands = [];
98
+ for (const line of stdout.split(`
99
+ `)) {
100
+ if (!line.includes(UPDATE_MARKER))
101
+ continue;
102
+ const parsed = updateCommands(line);
103
+ if (parsed)
104
+ commands = parsed;
105
+ }
106
+ return commands;
107
+ }
108
+ function updateCommands(line) {
109
+ let message;
110
+ try {
111
+ message = JSON.parse(line);
112
+ } catch {
113
+ return null;
114
+ }
115
+ const update = record(record(record(message)?.params)?.update);
116
+ if (update?.sessionUpdate !== UPDATE_MARKER || !Array.isArray(update.availableCommands)) {
117
+ return null;
118
+ }
119
+ return update.availableCommands.flatMap((entry) => {
120
+ const command = acpCommand(entry);
121
+ return command ? [command] : [];
122
+ });
123
+ }
124
+ function acpCommand(entry) {
125
+ const command = record(entry);
126
+ const name = text(command?.name).replace(/^\//, "");
127
+ if (!name)
128
+ return null;
129
+ const description = text(command?.description);
130
+ const argumentHint = text(record(command?.input)?.hint);
131
+ return {
132
+ name,
133
+ ...description ? { description } : {},
134
+ ...argumentHint ? { argumentHint } : {}
135
+ };
136
+ }
137
+ function text(value) {
138
+ return typeof value === "string" ? value : "";
139
+ }
140
+ function record(value) {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
142
+ }
143
+ function shellArg(value) {
144
+ return value.split("{tmp}").map((part) => part ? shellQuote(part) : "").join('"$work/tmp"');
145
+ }
146
+ function shellQuote(value) {
147
+ return `'${value.replaceAll("'", "'\\''")}'`;
148
+ }
149
+ var RECORD_SEPARATOR = "\x1E";
150
+ var HEAD_LINES = 40;
151
+ var MAX_DEPTH = 8;
152
+ async function discoverMarkdownEntries(ctx, sources) {
153
+ if (sources.length === 0)
154
+ return [];
155
+ const [result] = await ctx.sdk.exec.run({
156
+ cwd: ctx.project?.path ?? "/tmp",
157
+ commands: [markdownScanScript(sources)]
158
+ });
159
+ if (!result || result.status !== 0)
160
+ return [];
161
+ return parseMarkdownScan(result.stdout, sources);
162
+ }
163
+ async function discoverSlashCommands(ctx, sources) {
164
+ return markdownSlashCommands(await discoverMarkdownEntries(ctx, sources));
165
+ }
166
+ function markdownSlashCommands(entries) {
167
+ const seen = new Set;
168
+ const commands = [];
169
+ for (const entry of entries) {
170
+ if (entry.frontmatter["user-invocable"] === "false" || seen.has(entry.name))
171
+ continue;
172
+ seen.add(entry.name);
173
+ const description = entry.frontmatter.description;
174
+ const argumentHint = entry.frontmatter["argument-hint"];
175
+ commands.push({
176
+ name: entry.name,
177
+ ...description ? { description } : {},
178
+ ...argumentHint ? { argumentHint } : {}
179
+ });
180
+ }
181
+ return commands;
182
+ }
183
+ function markdownModes(entries) {
184
+ const seen = new Set;
185
+ const modes = [];
186
+ for (const entry of entries) {
187
+ if (seen.has(entry.name))
188
+ continue;
189
+ seen.add(entry.name);
190
+ const description = entry.frontmatter.description;
191
+ modes.push({
192
+ id: entry.name,
193
+ name: titleCase(entry.name),
194
+ ...description ? { description } : {}
195
+ });
196
+ }
197
+ return modes;
198
+ }
199
+ var noCommands = () => [];
200
+ function slashCommandProvider(builtins, sources, discovery = {}) {
201
+ return async (ctx) => {
202
+ const [acp, loaded, files] = await Promise.all([
203
+ discovery.acp ? discoverAcpSlashCommands(ctx, discovery.acp).catch(noCommands) : [],
204
+ discovery.load ? discovery.load(ctx).catch(noCommands) : [],
205
+ discoverSlashCommands(ctx, sources).catch(noCommands)
206
+ ]);
207
+ return dedupeSlashCommands([...builtins, ...acp, ...loaded, ...files]);
208
+ };
209
+ }
210
+ function modeProvider(builtins, sources, keep = () => true) {
211
+ return async (ctx) => {
212
+ const entries = await discoverMarkdownEntries(ctx, sources).catch(() => []);
213
+ const discovered = markdownModes(entries.filter(keep));
214
+ const ids = new Set(builtins.map((mode) => mode.id));
215
+ return [...builtins, ...discovered.filter((mode) => !ids.has(mode.id))];
216
+ };
217
+ }
218
+ function markdownScanScript(sources) {
219
+ const lines = [
220
+ "pragma_scan() {",
221
+ ' [ -d "$2" ] || return 0',
222
+ ' find -L "$2" -maxdepth "$3" -type f -name "$4" 2>/dev/null | sort | while IFS= read -r f; do',
223
+ ` printf '\\036%s\\t%s\\n' "$1" "\${f#"$2"/}"; head -n ${HEAD_LINES} "$f" 2>/dev/null; printf '\\n'`,
224
+ " done",
225
+ "}"
226
+ ];
227
+ sources.forEach((source, index) => {
228
+ const skills = source.layout === "skills";
229
+ const depth = skills ? 2 : source.recursive === false ? 1 : MAX_DEPTH;
230
+ const pattern = skills ? "SKILL.md" : `*${source.suffix ?? ".md"}`;
231
+ lines.push(`pragma_scan ${index} ${shellDir(source.dir)} ${depth} ${shellQuote2(pattern)}`);
232
+ });
233
+ lines.push("exit 0");
234
+ return lines.join(`
235
+ `);
236
+ }
237
+ function parseMarkdownScan(stdout, sources) {
238
+ const entries = [];
239
+ for (const record2 of stdout.split(RECORD_SEPARATOR).slice(1)) {
240
+ const newline = record2.indexOf(`
241
+ `);
242
+ const header = newline === -1 ? record2 : record2.slice(0, newline);
243
+ const [indexText, relativePath] = header.split("\t");
244
+ const source = sources[Number(indexText)];
245
+ if (!source || !relativePath)
246
+ continue;
247
+ const frontmatter = parseFrontmatter(newline === -1 ? "" : record2.slice(newline + 1));
248
+ const name = entryName(source, relativePath, frontmatter);
249
+ if (name)
250
+ entries.push({ name: `${source.prefix ?? ""}${name}`, frontmatter, source });
251
+ }
252
+ return entries;
253
+ }
254
+ function parseFrontmatter(content) {
255
+ const lines = content.split(/\r?\n/);
256
+ if (lines[0]?.trim() !== "---")
257
+ return {};
258
+ const result = {};
259
+ let blockKey = null;
260
+ for (const line of lines.slice(1)) {
261
+ if (line.trim() === "---")
262
+ break;
263
+ if (blockKey && /^\s+\S/.test(line)) {
264
+ const previous = result[blockKey];
265
+ result[blockKey] = previous ? `${previous} ${line.trim()}` : line.trim();
266
+ continue;
267
+ }
268
+ blockKey = null;
269
+ const match = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
270
+ if (!match)
271
+ continue;
272
+ const value = unquote(match[2].trim());
273
+ if (/^[|>][+-]?$/.test(value)) {
274
+ blockKey = match[1];
275
+ } else if (value) {
276
+ result[match[1]] = value;
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ function entryName(source, relativePath, frontmatter) {
282
+ if (source.layout === "skills") {
283
+ const directory = relativePath.split("/").slice(0, -1).join("/");
284
+ return frontmatter.name ?? (directory || null);
285
+ }
286
+ if (source.nameFromFrontmatter && frontmatter.name)
287
+ return frontmatter.name;
288
+ const suffix = source.suffix ?? ".md";
289
+ if (!relativePath.endsWith(suffix))
290
+ return null;
291
+ return relativePath.slice(0, -suffix.length).split("/").join(":") || null;
292
+ }
293
+ function unquote(value) {
294
+ const quoted = /^(["'])(.*)\1$/.exec(value);
295
+ return quoted ? quoted[2] : value;
296
+ }
297
+ function titleCase(value) {
298
+ return value.split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
299
+ }
300
+ function shellDir(dir) {
301
+ if (dir === "~")
302
+ return '"$HOME"';
303
+ if (dir.startsWith("~/"))
304
+ return `"$HOME"/${shellQuote2(dir.slice(2))}`;
305
+ return shellQuote2(dir);
306
+ }
307
+ function shellQuote2(value) {
308
+ return `'${value.replaceAll("'", "'\\''")}'`;
309
+ }
8
310
  function definePlugin(input) {
9
311
  return { ...input, __apiVersion: PLUGIN_API_VERSION };
10
312
  }
313
+ var ACCOUNT_PROVIDERS = {
314
+ anthropic: { title: "Anthropic", modelsDevIds: ["anthropic"] },
315
+ openai: { title: "OpenAI", modelsDevIds: ["openai"] },
316
+ cursor: { title: "Cursor" },
317
+ "github-copilot": { title: "GitHub Copilot" },
318
+ xai: { title: "xAI", dashboardUrl: "https://console.x.ai", modelsDevIds: ["xai"] },
319
+ jetbrains: { title: "JetBrains" },
320
+ "opencode-go": { title: "OpenCode Go", modelsDevIds: ["opencode-go"] },
321
+ "opencode-zen": {
322
+ title: "OpenCode Zen",
323
+ dashboardUrl: "https://opencode.ai/auth",
324
+ modelsDevIds: ["opencode"]
325
+ },
326
+ moonshot: { title: "Moonshot", modelsDevIds: ["kimi-code-plan-global", "kimi-code-plan-cn"] },
327
+ "moonshot-platform": {
328
+ title: "Moonshot AI Platform",
329
+ dashboardUrl: "https://platform.moonshot.ai/console/api-keys",
330
+ modelsDevIds: ["moonshotai", "moonshotai-cn"]
331
+ },
332
+ google: {
333
+ title: "Google Gemini API",
334
+ dashboardUrl: "https://aistudio.google.com/apikey",
335
+ modelsDevIds: ["google"]
336
+ },
337
+ openrouter: {
338
+ title: "OpenRouter",
339
+ dashboardUrl: "https://openrouter.ai/settings/keys",
340
+ modelsDevIds: ["openrouter"]
341
+ },
342
+ deepseek: {
343
+ title: "DeepSeek",
344
+ dashboardUrl: "https://platform.deepseek.com/api_keys",
345
+ modelsDevIds: ["deepseek"]
346
+ },
347
+ groq: { title: "Groq", dashboardUrl: "https://console.groq.com/keys", modelsDevIds: ["groq"] },
348
+ mistral: {
349
+ title: "Mistral",
350
+ dashboardUrl: "https://console.mistral.ai/api-keys",
351
+ modelsDevIds: ["mistral"]
352
+ },
353
+ cerebras: {
354
+ title: "Cerebras",
355
+ dashboardUrl: "https://cloud.cerebras.ai",
356
+ modelsDevIds: ["cerebras"]
357
+ },
358
+ fireworks: {
359
+ title: "Fireworks AI",
360
+ dashboardUrl: "https://fireworks.ai/account/api-keys",
361
+ modelsDevIds: ["fireworks-ai"]
362
+ },
363
+ together: {
364
+ title: "Together AI",
365
+ dashboardUrl: "https://api.together.ai/settings/api-keys",
366
+ modelsDevIds: ["togetherai"]
367
+ },
368
+ huggingface: {
369
+ title: "Hugging Face",
370
+ dashboardUrl: "https://huggingface.co/settings/tokens",
371
+ modelsDevIds: ["huggingface"]
372
+ },
373
+ nvidia: { title: "NVIDIA", dashboardUrl: "https://build.nvidia.com", modelsDevIds: ["nvidia"] },
374
+ "vercel-ai-gateway": {
375
+ title: "Vercel AI Gateway",
376
+ dashboardUrl: "https://vercel.com/dashboard/ai-gateway",
377
+ modelsDevIds: ["vercel"]
378
+ },
379
+ zai: {
380
+ title: "Z.ai",
381
+ dashboardUrl: "https://z.ai/manage-apikey/apikey-list",
382
+ modelsDevIds: ["zai-coding-plan", "zai", "zhipuai"]
383
+ },
384
+ minimax: {
385
+ title: "MiniMax",
386
+ dashboardUrl: "https://platform.minimax.io",
387
+ modelsDevIds: ["minimax-coding-plan", "minimax", "minimax-cn-coding-plan", "minimax-cn"]
388
+ },
389
+ xiaomi: {
390
+ title: "Xiaomi MiMo",
391
+ dashboardUrl: "https://platform.xiaomimimo.com",
392
+ modelsDevIds: [
393
+ "xiaomi",
394
+ "xiaomi-token-plan-ams",
395
+ "xiaomi-token-plan-sgp",
396
+ "xiaomi-token-plan-cn"
397
+ ]
398
+ }
399
+ };
400
+ function modelsDevAccountProviders(except = []) {
401
+ return Object.keys(ACCOUNT_PROVIDERS).flatMap((provider) => {
402
+ const { modelsDevIds } = ACCOUNT_PROVIDERS[provider];
403
+ return modelsDevIds && !except.includes(provider) ? [{ provider, modelsDevIds }] : [];
404
+ });
405
+ }
406
+ function apiKeyTokenKind(provider) {
407
+ return `key:${provider}`;
408
+ }
409
+ function defineAccounts(providers) {
410
+ return providers;
411
+ }
412
+ var API_KEY_DIGEST_LENGTH = 16;
413
+ async function apiKeyIdentity(key) {
414
+ const trimmed = key.trim();
415
+ if (!trimmed)
416
+ return null;
417
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(trimmed));
418
+ const hex = [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, API_KEY_DIGEST_LENGTH);
419
+ return { id: `key:${hex}`, name: `API key …${trimmed.slice(-4)}` };
420
+ }
11
421
 
12
422
  // ../watcher-kit/src/index.ts
13
423
  var DEFAULT_APPROVE_KEYS = "\r";
@@ -207,8 +617,8 @@ async function finalizeQuestionAnswers(ctx, runtime) {
207
617
  if (!ctx.signal.aborted)
208
618
  await writeKeys(ctx, runtime.questionFinalizeKeys);
209
619
  }
210
- async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text) {
211
- const input = mode === "plain" ? text : `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
620
+ async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text2) {
621
+ const input = mode === "plain" ? text2 : `${BRACKETED_PASTE_START}${text2}${BRACKETED_PASTE_END}`;
212
622
  await writeKeys(ctx, input);
213
623
  if (!submitKeys) {
214
624
  return;
@@ -226,15 +636,15 @@ function rememberCommandAttention(runtime, event) {
226
636
  runtime.outstandingCommandRequestId = null;
227
637
  }
228
638
  }
229
- function rememberQuestion(cache, event) {
639
+ function rememberQuestion(cache2, event) {
230
640
  if (event.status === "attention" && event.attentionKind === "question" && typeof event.requestId === "string" && event.requestId.length > 0) {
231
641
  const entries = (event.questions ?? []).flatMap(questionEntry);
232
642
  if (entries.length > 0) {
233
- cache.set(event.requestId, { questions: entries });
643
+ cache2.set(event.requestId, { questions: entries });
234
644
  return;
235
645
  }
236
646
  const options = (event.options ?? []).map((option) => option.label).filter((option) => option.trim() !== "");
237
- cache.set(event.requestId, {
647
+ cache2.set(event.requestId, {
238
648
  questions: [{ question: event.question ?? "", options }]
239
649
  });
240
650
  }
@@ -243,12 +653,12 @@ function questionEntry(value) {
243
653
  if (typeof value !== "object" || value === null || !("question" in value)) {
244
654
  return [];
245
655
  }
246
- const record = value;
247
- if (typeof record.question !== "string" || !record.question.trim()) {
656
+ const record2 = value;
657
+ if (typeof record2.question !== "string" || !record2.question.trim()) {
248
658
  return [];
249
659
  }
250
- const options = Array.isArray(record.options) ? record.options.filter((option) => Boolean(option) && typeof option === "object").map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== "") : [];
251
- return [{ question: record.question, options }];
660
+ const options = Array.isArray(record2.options) ? record2.options.filter((option) => Boolean(option) && typeof option === "object").map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== "") : [];
661
+ return [{ question: record2.question, options }];
252
662
  }
253
663
  function questionAnswerKeys(input) {
254
664
  if (input.dismissed || input.reply === null) {
@@ -363,9 +773,142 @@ function recordValue(value) {
363
773
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
364
774
  }
365
775
 
776
+ // src/accounts.ts
777
+ var PROVIDER_LIST_TTL_MS = 5000;
778
+ var CATALOG_TTL_MS = 10 * 60000;
779
+ var CATALOG_TIMEOUT_MS = 1e4;
780
+ var KIMI_CATALOG_COMMAND = `kimi provider catalog list --json || "${KIMI_BIN_DIR}/kimi" provider catalog list --json`;
781
+ var KIMI_API_KEY_PROVIDERS = modelsDevAccountProviders([
782
+ "moonshot"
783
+ ]);
784
+ var providerLists = new Map;
785
+ var catalog;
786
+ function kimiApiKeyProviders() {
787
+ return KIMI_API_KEY_PROVIDERS.map(({ provider, modelsDevIds }) => ({
788
+ provider,
789
+ agent: "kimi",
790
+ credentialPath: () => "$KIMI_CODE_HOME or ~/.kimi-code/config.toml",
791
+ available: (ctx) => kimiOffers(ctx, modelsDevIds),
792
+ identify: (ctx) => identifyKimiProvider(ctx, modelsDevIds),
793
+ sharedToken: {
794
+ kind: apiKeyTokenKind(provider),
795
+ read: async (ctx) => {
796
+ const key = await kimiProviderKey(ctx, modelsDevIds);
797
+ return key ? { type: "api", key } : null;
798
+ }
799
+ }
800
+ }));
801
+ }
802
+ async function kimiOffers(ctx, modelsDevIds) {
803
+ const ids = await catalogIds(ctx);
804
+ return ids === null || modelsDevIds.some((id) => ids.has(id));
805
+ }
806
+ function catalogIds(ctx) {
807
+ if (catalog && Date.now() - catalog.at < CATALOG_TTL_MS)
808
+ return catalog.ids;
809
+ const ids = listCatalogIds(ctx);
810
+ const entry = { at: Date.now(), ids };
811
+ catalog = entry;
812
+ ids.then((result) => {
813
+ if (result === null && catalog === entry)
814
+ catalog = undefined;
815
+ return;
816
+ });
817
+ return ids;
818
+ }
819
+ async function listCatalogIds(ctx) {
820
+ let timer;
821
+ const timeout = new Promise((resolve) => {
822
+ timer = setTimeout(() => resolve(null), CATALOG_TIMEOUT_MS);
823
+ });
824
+ const listing = ctx.sdk.exec.run({ cwd: ctx.project?.path ?? "/tmp", commands: [KIMI_CATALOG_COMMAND] }).then(([result]) => parseKimiCatalogIds(result?.stdout ?? "")).catch(() => null);
825
+ try {
826
+ return await Promise.race([listing, timeout]);
827
+ } finally {
828
+ clearTimeout(timer);
829
+ }
830
+ }
831
+ function parseKimiCatalogIds(output) {
832
+ try {
833
+ const providers = recordValue(JSON.parse(output));
834
+ return providers && Object.keys(providers).length > 0 ? new Set(Object.keys(providers)) : null;
835
+ } catch {
836
+ return null;
837
+ }
838
+ }
839
+ async function identifyKimiProvider(ctx, modelsDevIds) {
840
+ const key = await kimiProviderKey(ctx, modelsDevIds);
841
+ return key ? apiKeyIdentity(key) : null;
842
+ }
843
+ async function kimiProviderKey(ctx, modelsDevIds) {
844
+ const keys = await providerKeys(ctx);
845
+ for (const id of modelsDevIds) {
846
+ const key = keys.get(id);
847
+ if (key)
848
+ return key;
849
+ }
850
+ return null;
851
+ }
852
+ function providerKeys(ctx) {
853
+ const cacheKey = JSON.stringify([ctx.project?.path ?? null, ctx.account.env]);
854
+ const cached = providerLists.get(cacheKey);
855
+ if (cached && Date.now() - cached.at < PROVIDER_LIST_TTL_MS)
856
+ return cached.keys;
857
+ const keys = listProviderKeys(ctx);
858
+ providerLists.set(cacheKey, { at: Date.now(), keys });
859
+ return keys;
860
+ }
861
+ async function listProviderKeys(ctx) {
862
+ try {
863
+ const [result] = await ctx.sdk.exec.run({
864
+ cwd: ctx.project?.path ?? "/tmp",
865
+ commands: [KIMI_MODELS_COMMAND]
866
+ });
867
+ return parseKimiProviderKeys(result?.stdout ?? "");
868
+ } catch {
869
+ return new Map;
870
+ }
871
+ }
872
+ function parseKimiProviderKeys(output) {
873
+ let value;
874
+ try {
875
+ value = JSON.parse(output);
876
+ } catch {
877
+ return new Map;
878
+ }
879
+ const keys = new Map;
880
+ for (const [id, candidate] of Object.entries(recordValue(recordValue(value)?.providers) ?? {})) {
881
+ const apiKey = recordValue(candidate)?.apiKey;
882
+ if (typeof apiKey === "string" && apiKey.trim())
883
+ keys.set(id, apiKey.trim());
884
+ }
885
+ return keys;
886
+ }
887
+
366
888
  // src/pragma-plugin.ts
367
889
  var INTERJECT_SUBMIT_DELAY_MS = 200;
368
890
  var PREFILL_DELAY_MS = 2500;
891
+ var BUILTIN_SLASH_COMMANDS = [
892
+ { name: "init", description: "Analyze the project and write AGENTS.md" },
893
+ { name: "compact", description: "Compact the conversation context" }
894
+ ];
895
+ var SLASH_COMMAND_SOURCES = [
896
+ { dir: ".kimi-code/skills", layout: "skills", prefix: "skill:" },
897
+ { dir: ".agents/skills", layout: "skills", prefix: "skill:" },
898
+ { dir: ".claude/skills", layout: "skills", prefix: "skill:" },
899
+ { dir: ".claude/commands", layout: "files" },
900
+ { dir: "~/.kimi-code/skills", layout: "skills", prefix: "skill:" },
901
+ { dir: "~/.agents/skills", layout: "skills", prefix: "skill:" },
902
+ { dir: "~/.claude/skills", layout: "skills", prefix: "skill:" },
903
+ { dir: "~/.claude/commands", layout: "files" }
904
+ ];
905
+ var AGENT_SOURCES = [
906
+ { dir: ".kimi-code/agents", layout: "files", nameFromFrontmatter: true },
907
+ { dir: ".agents/agents", layout: "files", nameFromFrontmatter: true },
908
+ { dir: "~/.kimi-code/agents", layout: "files", nameFromFrontmatter: true },
909
+ { dir: "~/.agents/agents", layout: "files", nameFromFrontmatter: true }
910
+ ];
911
+ var DEFAULT_MODE = "default";
369
912
  var baseWatcher = createTuiWatcher({
370
913
  agent: "kimi",
371
914
  handleDecisions: false,
@@ -376,6 +919,20 @@ var baseWatcher = createTuiWatcher({
376
919
  var kimiAgentPlugin = definePlugin({
377
920
  name: "Kimi Code",
378
921
  description: "Launch Kimi Code from Pragma.",
922
+ accounts: defineAccounts([
923
+ {
924
+ provider: "moonshot",
925
+ agent: "kimi",
926
+ iconPath: "assets/kimi.png",
927
+ login: {
928
+ command: ["kimi", "login"],
929
+ instructions: "Open the link, sign in to Kimi, and enter the code shown here."
930
+ },
931
+ env: (home) => ({ KIMI_CODE_HOME: home }),
932
+ credentialPath: (home) => home ?? "~/.kimi-code"
933
+ },
934
+ ...kimiApiKeyProviders()
935
+ ]),
379
936
  watchers: [
380
937
  {
381
938
  agent: "kimi",
@@ -402,7 +959,7 @@ var kimiAgentPlugin = definePlugin({
402
959
  name: "Kimi Code",
403
960
  icon: () => null,
404
961
  iconPath: "assets/kimi.png",
405
- launch: { command: ["kimi", "-y"] },
962
+ launch: { command: ["kimi"] },
406
963
  prefillDelayMs: PREFILL_DELAY_MS,
407
964
  prefillMode: "plain",
408
965
  prefillSubmit: "\r",
@@ -413,13 +970,18 @@ var kimiAgentPlugin = definePlugin({
413
970
  { id: "auto", name: "Fully autonomous" },
414
971
  { id: "plan", name: "Plan mode" }
415
972
  ],
973
+ modes: modeProvider([{ id: DEFAULT_MODE, name: "Default" }], AGENT_SOURCES),
974
+ slashCommands: slashCommandProvider(BUILTIN_SLASH_COMMANDS, SLASH_COMMAND_SOURCES, {
975
+ acp: { command: ["kimi", "acp"] }
976
+ }),
416
977
  excludeFeatures: ["commandApproval"],
417
978
  args: {
418
979
  model: (modelId) => ["-m", modelId],
419
980
  reasoning: () => [],
981
+ mode: (modeId) => modeId === DEFAULT_MODE ? [] : ["--agent", modeId],
420
982
  permissionMode: (permissionModeId) => {
421
983
  if (permissionModeId === "yolo") {
422
- return [];
984
+ return ["-y"];
423
985
  }
424
986
  if (permissionModeId === "auto") {
425
987
  return ["--auto"];
package/kimi.plugin.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "pragma-kimi",
3
- "version": "0.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Report Kimi Code agent status into Pragma via pragma-cli.",
5
5
  "author": {
6
6
  "name": "Pragma"
7
7
  },
8
8
  "license": "MIT",
9
- "keywords": ["pragma", "agent"],
9
+ "keywords": [
10
+ "pragma",
11
+ "agent"
12
+ ],
10
13
  "hooks": [
11
14
  {
12
15
  "event": "SessionStart",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pragma-sh/kimi-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Kimi Code CLI plugin that reports agent status to Pragma via native hooks.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -0,0 +1,186 @@
1
+ import {
2
+ apiKeyIdentity,
3
+ apiKeyTokenKind,
4
+ modelsDevAccountProviders,
5
+ type AccountContext,
6
+ type AccountIdentity,
7
+ type AccountProviderDefinition,
8
+ type ModelsDevAccountProvider,
9
+ type PluginContext,
10
+ } from "@pragma-sh/plugin/catalog";
11
+
12
+ import { KIMI_BIN_DIR, KIMI_MODELS_COMMAND, recordValue } from "./models";
13
+
14
+ /** How long one `kimi provider list` answers every provider's `identify`. */
15
+ const PROVIDER_LIST_TTL_MS = 5_000;
16
+ /** The catalog is models.dev, fetched over the network: ask rarely. */
17
+ const CATALOG_TTL_MS = 10 * 60_000;
18
+ /** Past this, an offline or slow catalog fetch stops holding up the account list. */
19
+ const CATALOG_TIMEOUT_MS = 10_000;
20
+ /** Every provider `kimi provider catalog add` can import, as JSON keyed by models.dev id. */
21
+ const KIMI_CATALOG_COMMAND = `kimi provider catalog list --json || "${KIMI_BIN_DIR}/kimi" provider catalog list --json`;
22
+
23
+ /**
24
+ * API-key providers Kimi Code can hold, besides its own Kimi sign-in
25
+ * (`moonshot`, declared with `kimi login`). `kimi provider catalog add <id>`
26
+ * imports them from models.dev under that id, so they are matched the same
27
+ * way OpenCode's are.
28
+ */
29
+ export const KIMI_API_KEY_PROVIDERS: ModelsDevAccountProvider[] = modelsDevAccountProviders([
30
+ "moonshot",
31
+ ]);
32
+
33
+ /** Kimi Code's configured providers: id → API key. Never logged or returned. */
34
+ type ProviderKeys = ReadonlyMap<string, string>;
35
+
36
+ const providerLists = new Map<string, { at: number; keys: Promise<ProviderKeys> }>();
37
+ let catalog: { at: number; ids: Promise<ReadonlySet<string> | null> } | undefined;
38
+
39
+ /**
40
+ * One account provider per API-key provider in Kimi Code's `config.toml`.
41
+ *
42
+ * Each follows Kimi's own config (no `env`): only `moonshot` may relocate
43
+ * `KIMI_CODE_HOME`, and two providers setting it would fight at launch. Keys
44
+ * are added with `kimi provider catalog add <id> --api-key …`, so there is no
45
+ * login command. `identify` digests the key — never the key itself. A
46
+ * provider is listed only while Kimi's own catalog still offers one of its ids.
47
+ */
48
+ export function kimiApiKeyProviders(): AccountProviderDefinition[] {
49
+ return KIMI_API_KEY_PROVIDERS.map(({ provider, modelsDevIds }) => ({
50
+ provider,
51
+ agent: "kimi",
52
+ credentialPath: () => "$KIMI_CODE_HOME or ~/.kimi-code/config.toml",
53
+ available: (ctx) => kimiOffers(ctx, modelsDevIds),
54
+ identify: (ctx) => identifyKimiProvider(ctx, modelsDevIds),
55
+ // Kimi lends its keys to other harnesses but takes none: its config is
56
+ // written through `kimi provider catalog add`, which would put the key on
57
+ // a command line, and it has no per-account slot to put a copy in.
58
+ sharedToken: {
59
+ kind: apiKeyTokenKind(provider),
60
+ read: async (ctx) => {
61
+ const key = await kimiProviderKey(ctx, modelsDevIds);
62
+ return key ? { type: "api", key } : null;
63
+ },
64
+ },
65
+ }));
66
+ }
67
+
68
+ /**
69
+ * Whether `kimi provider catalog add` can import any of these ids. Kimi's
70
+ * answer when it gives one; when it cannot (offline, not installed), the
71
+ * declaration stands.
72
+ */
73
+ async function kimiOffers(ctx: PluginContext, modelsDevIds: readonly string[]): Promise<boolean> {
74
+ const ids = await catalogIds(ctx);
75
+ return ids === null || modelsDevIds.some((id) => ids.has(id));
76
+ }
77
+
78
+ /**
79
+ * Kimi's catalog ids, one listing shared by every provider for a while. A
80
+ * failed listing is not kept, so the next account refresh asks again.
81
+ */
82
+ function catalogIds(ctx: PluginContext): Promise<ReadonlySet<string> | null> {
83
+ if (catalog && Date.now() - catalog.at < CATALOG_TTL_MS) return catalog.ids;
84
+ const ids = listCatalogIds(ctx);
85
+ const entry = { at: Date.now(), ids };
86
+ catalog = entry;
87
+ void ids.then((result) => {
88
+ if (result === null && catalog === entry) catalog = undefined;
89
+ return undefined;
90
+ });
91
+ return ids;
92
+ }
93
+
94
+ /** Forgets the cached catalog; for tests. */
95
+ export function resetKimiCatalogCache(): void {
96
+ catalog = undefined;
97
+ }
98
+
99
+ async function listCatalogIds(ctx: PluginContext): Promise<ReadonlySet<string> | null> {
100
+ let timer: ReturnType<typeof setTimeout> | undefined;
101
+ const timeout = new Promise<null>((resolve) => {
102
+ timer = setTimeout(() => resolve(null), CATALOG_TIMEOUT_MS);
103
+ });
104
+ const listing = ctx.sdk.exec
105
+ .run({ cwd: ctx.project?.path ?? "/tmp", commands: [KIMI_CATALOG_COMMAND] })
106
+ .then(([result]) => parseKimiCatalogIds(result?.stdout ?? ""))
107
+ .catch(() => null);
108
+ try {
109
+ return await Promise.race([listing, timeout]);
110
+ } finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+
115
+ /** The provider ids in `kimi provider catalog list --json`, or null when unreadable. */
116
+ export function parseKimiCatalogIds(output: string): ReadonlySet<string> | null {
117
+ try {
118
+ const providers = recordValue(JSON.parse(output));
119
+ return providers && Object.keys(providers).length > 0 ? new Set(Object.keys(providers)) : null;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ async function identifyKimiProvider(
126
+ ctx: AccountContext,
127
+ modelsDevIds: readonly string[],
128
+ ): Promise<AccountIdentity | null> {
129
+ const key = await kimiProviderKey(ctx, modelsDevIds);
130
+ return key ? apiKeyIdentity(key) : null;
131
+ }
132
+
133
+ /** The first of a provider's models.dev ids Kimi holds a key for. Never logged. */
134
+ async function kimiProviderKey(
135
+ ctx: AccountContext,
136
+ modelsDevIds: readonly string[],
137
+ ): Promise<string | null> {
138
+ const keys = await providerKeys(ctx);
139
+ for (const id of modelsDevIds) {
140
+ const key = keys.get(id);
141
+ if (key) return key;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Kimi's providers through its supported CLI, which applies `KIMI_CODE_HOME`
148
+ * and its own config rules. Every provider identifies at once, so one listing
149
+ * per login env is shared for a few seconds instead of running Kimi per row.
150
+ */
151
+ function providerKeys(ctx: AccountContext): Promise<ProviderKeys> {
152
+ const cacheKey = JSON.stringify([ctx.project?.path ?? null, ctx.account.env]);
153
+ const cached = providerLists.get(cacheKey);
154
+ if (cached && Date.now() - cached.at < PROVIDER_LIST_TTL_MS) return cached.keys;
155
+ const keys = listProviderKeys(ctx);
156
+ providerLists.set(cacheKey, { at: Date.now(), keys });
157
+ return keys;
158
+ }
159
+
160
+ async function listProviderKeys(ctx: AccountContext): Promise<ProviderKeys> {
161
+ try {
162
+ const [result] = await ctx.sdk.exec.run({
163
+ cwd: ctx.project?.path ?? "/tmp",
164
+ commands: [KIMI_MODELS_COMMAND],
165
+ });
166
+ return parseKimiProviderKeys(result?.stdout ?? "");
167
+ } catch {
168
+ return new Map();
169
+ }
170
+ }
171
+
172
+ /** Reads each provider's API key from `kimi provider list --json`. */
173
+ export function parseKimiProviderKeys(output: string): ProviderKeys {
174
+ let value: unknown;
175
+ try {
176
+ value = JSON.parse(output);
177
+ } catch {
178
+ return new Map();
179
+ }
180
+ const keys = new Map<string, string>();
181
+ for (const [id, candidate] of Object.entries(recordValue(recordValue(value)?.providers) ?? {})) {
182
+ const apiKey = recordValue(candidate)?.apiKey;
183
+ if (typeof apiKey === "string" && apiKey.trim()) keys.set(id, apiKey.trim());
184
+ }
185
+ return keys;
186
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ agentLaunchArgs,
5
+ resolveAgentOptions,
6
+ type PluginContext,
7
+ } from "@pragma-sh/plugin/catalog";
8
+
9
+ import { kimiAgentPlugin } from "./pragma-plugin";
10
+
11
+ /** A context whose host shell finds no command or agent files. */
12
+ const emptyHost = {
13
+ project: { id: "p", name: "p", path: "/repo" },
14
+ sdk: { exec: { run: async () => [{ status: 0, stdout: "", stderr: "" }] } },
15
+ } as unknown as PluginContext;
16
+
17
+ const agent = kimiAgentPlugin.agents![0]!;
18
+
19
+ describe("Kimi launch options", () => {
20
+ it("exposes built-in slash commands", async () => {
21
+ const { slashCommands } = await resolveAgentOptions(agent, emptyHost);
22
+ expect(slashCommands.map((command) => command.name)).toEqual(["init", "compact"]);
23
+ });
24
+
25
+ it("launches in yolo mode by default", () => {
26
+ expect(agent.launch.command).toEqual(["kimi"]);
27
+ expect(agentLaunchArgs(agent, null)).toEqual(["-y"]);
28
+ expect(agentLaunchArgs(agent, { modeId: "okabe", permissionModeId: "plan" })).toEqual([
29
+ "--agent",
30
+ "okabe",
31
+ "--plan",
32
+ ]);
33
+ });
34
+ });
package/src/models.ts CHANGED
@@ -4,8 +4,10 @@ import type { AgentModelEntry, PluginContext } from "@pragma-sh/plugin/catalog";
4
4
  // GUI-launched Pragma hosts do not put on the subprocess PATH (see
5
5
  // `process_env` in pragma-core). The `||` fallback keeps the loader working on
6
6
  // such a host while still preferring a `kimi` the user installed on PATH.
7
- const KIMI_BIN_DIR = "$HOME/.kimi-code/bin";
8
- const KIMI_MODELS_COMMAND = `kimi provider list --json || "${KIMI_BIN_DIR}/kimi" provider list --json`;
7
+ /** Where the official Kimi Code installer puts the CLI. */
8
+ export const KIMI_BIN_DIR = "$HOME/.kimi-code/bin";
9
+ /** Kimi Code's configured providers and model aliases, as JSON. */
10
+ export const KIMI_MODELS_COMMAND = `kimi provider list --json || "${KIMI_BIN_DIR}/kimi" provider list --json`;
9
11
  const KIMI_DEFAULT_MODEL_COMMAND = `kimi provider list || "${KIMI_BIN_DIR}/kimi" provider list`;
10
12
 
11
13
  interface KimiConfigModel {
@@ -87,7 +89,8 @@ export function parseKimiDefaultModel(output: string): string | undefined {
87
89
  return match?.[1]?.trim() || undefined;
88
90
  }
89
91
 
90
- function recordValue(value: unknown): Record<string, unknown> | undefined {
92
+ /** `value` as a plain object, or undefined. */
93
+ export function recordValue(value: unknown): Record<string, unknown> | undefined {
91
94
  return typeof value === "object" && value !== null && !Array.isArray(value)
92
95
  ? (value as Record<string, unknown>)
93
96
  : undefined;
@@ -1,6 +1,14 @@
1
- import { defineAgent, definePlugin, type PluginDefinition } from "@pragma-sh/plugin/catalog";
1
+ import {
2
+ defineAccounts,
3
+ defineAgent,
4
+ definePlugin,
5
+ modeProvider,
6
+ slashCommandProvider,
7
+ type PluginDefinition,
8
+ } from "@pragma-sh/plugin/catalog";
2
9
  import { createTuiWatcher } from "@pragma-sh/watcher-kit";
3
10
 
11
+ import { kimiApiKeyProviders } from "./accounts";
4
12
  import { loadKimiModels } from "./models";
5
13
 
6
14
  /** Lets Kimi's paste-aware composer commit interjected text before Enter. */
@@ -8,6 +16,31 @@ const INTERJECT_SUBMIT_DELAY_MS = 200;
8
16
  /** Kimi paints its TUI within a couple of seconds of launch; type after it. */
9
17
  const PREFILL_DELAY_MS = 2500;
10
18
 
19
+ /** Kimi built-ins worth starting a session with; skills and commands are discovered. */
20
+ const BUILTIN_SLASH_COMMANDS = [
21
+ { name: "init", description: "Analyze the project and write AGENTS.md" },
22
+ { name: "compact", description: "Compact the conversation context" },
23
+ ];
24
+ /** Kimi invokes skills as `/skill:<name>`; Claude Code commands keep their name. */
25
+ const SLASH_COMMAND_SOURCES = [
26
+ { dir: ".kimi-code/skills", layout: "skills" as const, prefix: "skill:" },
27
+ { dir: ".agents/skills", layout: "skills" as const, prefix: "skill:" },
28
+ { dir: ".claude/skills", layout: "skills" as const, prefix: "skill:" },
29
+ { dir: ".claude/commands", layout: "files" as const },
30
+ { dir: "~/.kimi-code/skills", layout: "skills" as const, prefix: "skill:" },
31
+ { dir: "~/.agents/skills", layout: "skills" as const, prefix: "skill:" },
32
+ { dir: "~/.claude/skills", layout: "skills" as const, prefix: "skill:" },
33
+ { dir: "~/.claude/commands", layout: "files" as const },
34
+ ];
35
+ /** Agent profiles selected with `--agent <name>`. */
36
+ const AGENT_SOURCES = [
37
+ { dir: ".kimi-code/agents", layout: "files" as const, nameFromFrontmatter: true },
38
+ { dir: ".agents/agents", layout: "files" as const, nameFromFrontmatter: true },
39
+ { dir: "~/.kimi-code/agents", layout: "files" as const, nameFromFrontmatter: true },
40
+ { dir: "~/.agents/agents", layout: "files" as const, nameFromFrontmatter: true },
41
+ ];
42
+ const DEFAULT_MODE = "default";
43
+
11
44
  const baseWatcher = createTuiWatcher({
12
45
  agent: "kimi",
13
46
  // Kimi command approvals cannot be brokered, but AskUserQuestion is reported
@@ -30,6 +63,22 @@ const baseWatcher = createTuiWatcher({
30
63
  export const kimiAgentPlugin: PluginDefinition = definePlugin({
31
64
  name: "Kimi Code",
32
65
  description: "Launch Kimi Code from Pragma.",
66
+ accounts: defineAccounts([
67
+ {
68
+ provider: "moonshot",
69
+ agent: "kimi",
70
+ iconPath: "assets/kimi.png",
71
+ login: {
72
+ command: ["kimi", "login"],
73
+ instructions: "Open the link, sign in to Kimi, and enter the code shown here.",
74
+ },
75
+ // `KIMI_CODE_HOME` is Kimi Code's whole data root: config, sessions,
76
+ // and OAuth tokens.
77
+ env: (home) => ({ KIMI_CODE_HOME: home }),
78
+ credentialPath: (home) => home ?? "~/.kimi-code",
79
+ },
80
+ ...kimiApiKeyProviders(),
81
+ ]),
33
82
  watchers: [
34
83
  {
35
84
  agent: "kimi",
@@ -60,35 +109,36 @@ export const kimiAgentPlugin: PluginDefinition = definePlugin({
60
109
  name: "Kimi Code",
61
110
  icon: () => null,
62
111
  iconPath: "assets/kimi.png",
63
- // `-y` (yolo) is the default launch: Kimi's manual mode gates Bash behind
64
- // a TUI approval prompt, so a plain `kimi` never completes a safe shell
65
- // command headlessly (`pragma-cli agent verify` `command-no-permission`).
66
- // Baking `-y` into the base command mirrors Claude Code's
67
- // `--permission-mode auto`; the mode selector below is declared for when
68
- // the host wires it up (then this base and the per-mode args must be
69
- // reconciled).
70
- launch: { command: ["kimi", "-y"] },
112
+ launch: { command: ["kimi"] },
71
113
  prefillDelayMs: PREFILL_DELAY_MS,
72
114
  prefillMode: "plain",
73
115
  prefillSubmit: "\r",
74
116
  models: loadKimiModels,
75
- // First entry is the default; it matches the `-y` baked into `launch`.
76
- // These are the real launch flags that change how much Kimi asks for.
117
+ // First entry is the default: `-y` (yolo), because Kimi's manual mode
118
+ // gates Bash behind a TUI approval prompt, so a plain `kimi` never
119
+ // completes a safe shell command headlessly (`pragma-cli agent verify`
120
+ // `command-no-permission`). These are the real launch flags that change
121
+ // how much Kimi asks for.
77
122
  permissionModes: [
78
123
  { id: "yolo", name: "Auto-approve tools" },
79
124
  { id: "default", name: "Ask for approval" },
80
125
  { id: "auto", name: "Fully autonomous" },
81
126
  { id: "plan", name: "Plan mode" },
82
127
  ],
128
+ modes: modeProvider([{ id: DEFAULT_MODE, name: "Default" }], AGENT_SOURCES),
129
+ slashCommands: slashCommandProvider(BUILTIN_SLASH_COMMANDS, SLASH_COMMAND_SOURCES, {
130
+ acp: { command: ["kimi", "acp"] },
131
+ }),
83
132
  // `commandApproval`: Kimi's permission-request hook is fire-and-forget,
84
133
  // so Pragma cannot approve on the agent's behalf.
85
134
  excludeFeatures: ["commandApproval"],
86
135
  args: {
87
136
  model: (modelId: string) => ["-m", modelId],
88
137
  reasoning: () => [],
138
+ mode: (modeId: string) => (modeId === DEFAULT_MODE ? [] : ["--agent", modeId]),
89
139
  permissionMode: (permissionModeId: string) => {
90
140
  if (permissionModeId === "yolo") {
91
- return [];
141
+ return ["-y"];
92
142
  }
93
143
  if (permissionModeId === "auto") {
94
144
  return ["--auto"];