@pragma-sh/kimi-plugin 0.1.0-alpha.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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pragma-sh/kimi-plugin",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Kimi Code CLI plugin that reports agent status to Pragma via native hooks.",
5
+ "license": "AGPL-3.0-only",
6
+ "files": [
7
+ "assets",
8
+ "dist",
9
+ "hooks",
10
+ "kimi.plugin.json",
11
+ "pragma-plugin.json",
12
+ "scripts",
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ "./pragma-plugin": "./src/pragma-plugin.ts"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "bun build src/pragma-plugin.ts --outdir dist --format esm --target node --packages bundle && mv dist/pragma-plugin.js dist/pragma-plugin.mjs && bun build scripts/install-local.ts --outfile dist/install.mjs --format esm --target node --packages bundle",
24
+ "install:local": "bun run build && bun run scripts/install-local.ts",
25
+ "prepack": "bun run build",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "bun --bun vitest run",
28
+ "lint": "oxlint ."
29
+ },
30
+ "devDependencies": {
31
+ "@pragma/plugin": "workspace:*",
32
+ "@pragma/watcher-kit": "workspace:*",
33
+ "@types/node": "^24.12.3",
34
+ "bunup": "^0.16.32",
35
+ "typescript": "^6.0.3",
36
+ "vitest": "^4.1.8"
37
+ },
38
+ "pragma": {
39
+ "pluginId": "pragma.kimi",
40
+ "main": "./dist/pragma-plugin.mjs"
41
+ }
42
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/pragma-sh/pragma/main/packages/plugin-registry/pragma-plugin.schema.json",
3
+ "name": "Kimi Code CLI",
4
+ "description": "Launch Kimi Code CLI in Pragma with live status, approvals, questions, subagents, and session naming.",
5
+ "categories": ["agent-plugin"],
6
+ "images": [
7
+ {
8
+ "url": "https://raw.githubusercontent.com/pragma-sh/pragma/main/packages/kimi-plugin/assets/kimi.png",
9
+ "alt": "Kimi logo"
10
+ }
11
+ ],
12
+ "install": { "command": "node", "args": ["dist/install.mjs"] },
13
+ "agentBinary": "kimi"
14
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ interface InstalledPlugin extends Record<string, unknown> {
16
+ id: string;
17
+ root: string;
18
+ source: "local-path";
19
+ enabled: boolean;
20
+ installedAt: string;
21
+ updatedAt: string;
22
+ originalSource: string;
23
+ }
24
+
25
+ interface InstalledFile {
26
+ version: 1;
27
+ plugins: InstalledPlugin[];
28
+ }
29
+
30
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
+ const RUNTIME_PATHS = ["assets", "dist", "hooks", "kimi.plugin.json", "package.json"];
32
+
33
+ /** Installs a fresh package snapshot into Kimi's managed plugin store. */
34
+ export function installLocal(
35
+ kimiHome = process.env.KIMI_CODE_HOME ?? join(homedir(), ".kimi-code"),
36
+ packageRoot = PACKAGE_ROOT,
37
+ ): string {
38
+ const manifest = JSON.parse(readFileSync(join(packageRoot, "kimi.plugin.json"), "utf8")) as {
39
+ name?: unknown;
40
+ };
41
+ if (typeof manifest.name !== "string" || manifest.name.length === 0) {
42
+ throw new Error('kimi.plugin.json must contain a non-empty "name"');
43
+ }
44
+ for (const relativePath of RUNTIME_PATHS) {
45
+ if (!existsSync(join(packageRoot, relativePath))) {
46
+ throw new Error(`Missing runtime path: ${relativePath}`);
47
+ }
48
+ }
49
+
50
+ const id = manifest.name.toLowerCase();
51
+ const pluginsDir = join(kimiHome, "plugins");
52
+ const managedDir = join(pluginsDir, "managed");
53
+ const target = join(managedDir, id);
54
+ mkdirSync(managedDir, { recursive: true });
55
+ const staging = mkdtempSync(join(managedDir, `${id}-`));
56
+ try {
57
+ for (const relativePath of RUNTIME_PATHS) {
58
+ cpSync(join(packageRoot, relativePath), join(staging, relativePath), { recursive: true });
59
+ }
60
+ rmSync(target, { recursive: true, force: true });
61
+ renameSync(staging, target);
62
+ } catch (error) {
63
+ rmSync(staging, { recursive: true, force: true });
64
+ throw error;
65
+ }
66
+
67
+ const installedPath = join(pluginsDir, "installed.json");
68
+ const installed = readInstalled(installedPath);
69
+ const existingIndex = installed.plugins.findIndex((plugin) => plugin.id === id);
70
+ const existing = existingIndex < 0 ? undefined : installed.plugins[existingIndex];
71
+ const now = new Date().toISOString();
72
+ const record: InstalledPlugin = {
73
+ ...existing,
74
+ id,
75
+ root: target,
76
+ source: "local-path",
77
+ enabled: existing?.enabled ?? true,
78
+ installedAt:
79
+ typeof existing?.installedAt === "string" && existing.installedAt.length > 0
80
+ ? existing.installedAt
81
+ : now,
82
+ updatedAt: now,
83
+ originalSource: packageRoot,
84
+ };
85
+ if (existingIndex < 0) installed.plugins.push(record);
86
+ else installed.plugins[existingIndex] = record;
87
+
88
+ const tempPath = `${installedPath}.${process.pid}.tmp`;
89
+ writeFileSync(tempPath, `${JSON.stringify(installed, null, 2)}\n`);
90
+ renameSync(tempPath, installedPath);
91
+ return target;
92
+ }
93
+
94
+ function readInstalled(path: string): InstalledFile {
95
+ if (!existsSync(path)) return { version: 1, plugins: [] };
96
+ const value = JSON.parse(readFileSync(path, "utf8")) as {
97
+ version?: unknown;
98
+ plugins?: unknown;
99
+ };
100
+ if (value.version !== 1 || !Array.isArray(value.plugins)) {
101
+ throw new Error(`${path} is not a valid Kimi installed.json file`);
102
+ }
103
+ return { version: 1, plugins: value.plugins as InstalledPlugin[] };
104
+ }
105
+
106
+ const entryPath = process.argv[1];
107
+ if (entryPath !== undefined && resolve(entryPath) === fileURLToPath(import.meta.url)) {
108
+ const target = installLocal();
109
+ process.stdout.write(`Installed Pragma Kimi plugin to ${target}.\n`);
110
+ process.stdout.write("Run `/plugins reload` or start a new Kimi session to load it.\n");
111
+ }
package/src/models.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type { AgentModelEntry, PluginContext } from "@pragma/plugin/catalog";
2
+
3
+ // The official Kimi Code installer puts the CLI in `~/.kimi-code/bin`, which
4
+ // GUI-launched Pragma hosts do not put on the subprocess PATH (see
5
+ // `process_env` in pragma-core). The `||` fallback keeps the loader working on
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`;
9
+ const KIMI_DEFAULT_MODEL_COMMAND = `kimi provider list || "${KIMI_BIN_DIR}/kimi" provider list`;
10
+
11
+ interface KimiConfigModel {
12
+ provider: string;
13
+ model: string;
14
+ displayName?: string;
15
+ }
16
+
17
+ /** Reads Kimi's model aliases through its supported host CLI. */
18
+ export async function loadKimiModels(ctx: PluginContext): Promise<AgentModelEntry[]> {
19
+ try {
20
+ const results = await ctx.sdk.exec.run({
21
+ cwd: ctx.project?.path ?? "/tmp",
22
+ commands: [KIMI_MODELS_COMMAND, KIMI_DEFAULT_MODEL_COMMAND],
23
+ });
24
+ return kimiModelsFromConfig(
25
+ parseKimiProviderModels(results[0]?.stdout ?? ""),
26
+ parseKimiDefaultModel(results[1]?.stdout ?? ""),
27
+ );
28
+ } catch {
29
+ return [];
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Maps Kimi's config record to launcher model entries, preferring the
35
+ * configured default and otherwise preserving declaration order.
36
+ */
37
+ export function kimiModelsFromConfig(
38
+ config: Record<string, KimiConfigModel>,
39
+ defaultModel?: string,
40
+ ): AgentModelEntry[] {
41
+ const entries = Object.entries(config).map(([id, model]) => ({
42
+ id,
43
+ name: model.displayName ?? model.model,
44
+ }));
45
+ const defaultIndex = entries.findIndex((entry) => entry.id === defaultModel);
46
+ if (defaultIndex > 0) {
47
+ const [entry] = entries.splice(defaultIndex, 1);
48
+ if (entry !== undefined) entries.unshift(entry);
49
+ }
50
+ return entries;
51
+ }
52
+
53
+ /**
54
+ * Parses `kimi provider list --json` without retaining provider credentials,
55
+ * skipping aliases Kimi reports as disabled.
56
+ */
57
+ export function parseKimiProviderModels(output: string): Record<string, KimiConfigModel> {
58
+ let value: unknown;
59
+ try {
60
+ value = JSON.parse(output);
61
+ } catch {
62
+ return {};
63
+ }
64
+ const models = recordValue(recordValue(value)?.models);
65
+ if (models === undefined) return {};
66
+
67
+ const parsed: Record<string, KimiConfigModel> = {};
68
+ for (const [alias, candidate] of Object.entries(models)) {
69
+ const model = recordValue(candidate);
70
+ if (model === undefined || typeof model.model !== "string" || model.model.length === 0) {
71
+ continue;
72
+ }
73
+ // A disabled alias is rejected by Kimi at launch, so it must never reach the picker.
74
+ if (model.disabled === true) continue;
75
+ parsed[alias] = {
76
+ provider: typeof model.provider === "string" ? model.provider : "",
77
+ model: model.model,
78
+ ...(typeof model.displayName === "string" ? { displayName: model.displayName } : {}),
79
+ };
80
+ }
81
+ return parsed;
82
+ }
83
+
84
+ /** Reads the optional default alias from human-readable `kimi provider list`. */
85
+ export function parseKimiDefaultModel(output: string): string | undefined {
86
+ const match = /^Default model:\s*(.+)$/m.exec(output);
87
+ return match?.[1]?.trim() || undefined;
88
+ }
89
+
90
+ function recordValue(value: unknown): Record<string, unknown> | undefined {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value)
92
+ ? (value as Record<string, unknown>)
93
+ : undefined;
94
+ }
@@ -0,0 +1,103 @@
1
+ import { defineAgent, definePlugin, type PluginDefinition } from "@pragma/plugin/catalog";
2
+ import { createTuiWatcher } from "@pragma/watcher-kit";
3
+
4
+ import { loadKimiModels } from "./models";
5
+
6
+ /** Lets Kimi's paste-aware composer commit interjected text before Enter. */
7
+ const INTERJECT_SUBMIT_DELAY_MS = 200;
8
+ /** Kimi paints its TUI within a couple of seconds of launch; type after it. */
9
+ const PREFILL_DELAY_MS = 2500;
10
+
11
+ const baseWatcher = createTuiWatcher({
12
+ agent: "kimi",
13
+ // Kimi command approvals cannot be brokered, but AskUserQuestion is reported
14
+ // by the hook and answered through Kimi's native question dialog.
15
+ handleDecisions: false,
16
+ handleQuestionAnswers: true,
17
+ questionFinalizeKeys: "1",
18
+ interjectSubmitDelayMs: INTERJECT_SUBMIT_DELAY_MS,
19
+ });
20
+
21
+ /**
22
+ * Pragma plugin for the Kimi Code CLI, bundled to `dist/pragma-plugin.mjs`.
23
+ *
24
+ * Lifecycle reporting is a declarative hook bundle (`kimi.plugin.json` ->
25
+ * `hooks/report.sh`) installed into Kimi through its own `/plugins install`
26
+ * mechanism, because Kimi loads no in-process JavaScript plugin and its only
27
+ * live extension point is shell-command hooks. This module contributes only
28
+ * the Pragma-side launcher, model provider and watcher.
29
+ */
30
+ export const kimiAgentPlugin: PluginDefinition = definePlugin({
31
+ name: "Kimi Code",
32
+ description: "Launch Kimi Code from Pragma.",
33
+ watchers: [
34
+ {
35
+ agent: "kimi",
36
+ async watch(ctx) {
37
+ try {
38
+ await baseWatcher.watch(ctx);
39
+ } finally {
40
+ // `SessionEnd` already clears a graceful exit; this covers a session
41
+ // killed hard enough that no hook runs.
42
+ try {
43
+ await ctx.sdk.agents.report({
44
+ agent: ctx.agentId,
45
+ tabId: ctx.session.tabId,
46
+ worktreeId: ctx.session.worktreeId,
47
+ status: "cleared",
48
+ attentionKind: null,
49
+ });
50
+ } catch {
51
+ // Session-exit cleanup must never disrupt watcher shutdown.
52
+ }
53
+ }
54
+ },
55
+ },
56
+ ],
57
+ agents: [
58
+ defineAgent({
59
+ id: "kimi",
60
+ name: "Kimi Code",
61
+ icon: () => null,
62
+ 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"] },
71
+ prefillDelayMs: PREFILL_DELAY_MS,
72
+ prefillMode: "plain",
73
+ prefillSubmit: "\r",
74
+ 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.
77
+ permissionModes: [
78
+ { id: "yolo", name: "Auto-approve tools" },
79
+ { id: "default", name: "Ask for approval" },
80
+ { id: "auto", name: "Fully autonomous" },
81
+ { id: "plan", name: "Plan mode" },
82
+ ],
83
+ // `commandApproval`: Kimi's permission-request hook is fire-and-forget,
84
+ // so Pragma cannot approve on the agent's behalf.
85
+ excludeFeatures: ["commandApproval"],
86
+ args: {
87
+ model: (modelId: string) => ["-m", modelId],
88
+ reasoning: () => [],
89
+ permissionMode: (permissionModeId: string) => {
90
+ if (permissionModeId === "yolo") {
91
+ return [];
92
+ }
93
+ if (permissionModeId === "auto") {
94
+ return ["--auto"];
95
+ }
96
+ return permissionModeId === "plan" ? ["--plan"] : [];
97
+ },
98
+ },
99
+ }),
100
+ ],
101
+ });
102
+
103
+ export default kimiAgentPlugin;