@pragma-sh/kimi-plugin 1.0.2 → 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.
- package/dist/pragma-plugin.mjs +576 -14
- package/kimi.plugin.json +1 -1
- package/package.json +1 -1
- package/src/accounts.ts +186 -0
- package/src/launch-options.test.ts +34 -0
- package/src/models.ts +6 -3
- package/src/pragma-plugin.ts +62 -12
package/dist/pragma-plugin.mjs
CHANGED
|
@@ -1,13 +1,423 @@
|
|
|
1
|
-
// ../plugin/dist/shared/chunk-
|
|
2
|
-
var PLUGIN_API_VERSION = "1.
|
|
1
|
+
// ../plugin/dist/shared/chunk-b8hkp0vs.js
|
|
2
|
+
var PLUGIN_API_VERSION = "1.2.0";
|
|
3
3
|
|
|
4
|
-
// ../plugin/dist/shared/chunk-
|
|
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,
|
|
211
|
-
const input = mode === "plain" ?
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
247
|
-
if (typeof
|
|
656
|
+
const record2 = value;
|
|
657
|
+
if (typeof record2.question !== "string" || !record2.question.trim()) {
|
|
248
658
|
return [];
|
|
249
659
|
}
|
|
250
|
-
const options = Array.isArray(
|
|
251
|
-
return [{ question:
|
|
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"
|
|
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
package/package.json
CHANGED
package/src/accounts.ts
ADDED
|
@@ -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
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
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;
|
package/src/pragma-plugin.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
76
|
-
//
|
|
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"];
|